From e8d985211e19b881abe721d54a2d6df8e23b961a Mon Sep 17 00:00:00 2001 From: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com> Date: Wed, 24 Dec 2025 11:30:16 +0100 Subject: [PATCH] feat: shared server environment variables --- app/Jobs/ApplicationDeploymentJob.php | 24 +-- app/Livewire/SharedVariables/Server/Index.php | 22 +++ app/Livewire/SharedVariables/Server/Show.php | 169 ++++++++++++++++++ app/Models/EnvironmentVariable.php | 65 ++++++- app/Models/Server.php | 5 + app/Models/SharedEnvironmentVariable.php | 5 + app/View/Components/Forms/EnvVarInput.php | 4 + bootstrap/helpers/constants.php | 2 +- ..._to_shared_environment_variables_table.php | 35 ++++ .../livewire/shared-variables/index.blade.php | 48 ++--- .../shared-variables/server/index.blade.php | 25 +++ .../shared-variables/server/show.blade.php | 36 ++++ routes/web.php | 4 + 13 files changed, 411 insertions(+), 33 deletions(-) create mode 100644 app/Livewire/SharedVariables/Server/Index.php create mode 100644 app/Livewire/SharedVariables/Server/Show.php create mode 100644 database/migrations/2025_12_24_095507_add_server_to_shared_environment_variables_table.php create mode 100644 resources/views/livewire/shared-variables/server/index.blade.php create mode 100644 resources/views/livewire/shared-variables/server/show.blade.php diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 56a29276b..b37eb9833 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -1234,7 +1234,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->server)); } // Check for PORT environment variable mismatch with ports_exposes @@ -1300,7 +1300,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->server)); } // Add PORT if not exists, use the first port as default if ($this->build_pack !== 'dockercompose') { @@ -2304,14 +2304,16 @@ 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 !== '') { - $this->env_nixpacks_args->push("--env {$env->key}={$env->real_value}"); + $resolvedValue = $env->getResolvedValueWithServer($this->server); + if (! is_null($resolvedValue) && $resolvedValue !== '') { + $this->env_nixpacks_args->push("--env {$env->key}={$resolvedValue}"); } } } else { foreach ($this->application->nixpacks_environment_variables_preview as $env) { - if (! is_null($env->real_value) && $env->real_value !== '') { - $this->env_nixpacks_args->push("--env {$env->key}={$env->real_value}"); + $resolvedValue = $env->getResolvedValueWithServer($this->server); + if (! is_null($resolvedValue) && $resolvedValue !== '') { + $this->env_nixpacks_args->push("--env {$env->key}={$resolvedValue}"); } } } @@ -2447,8 +2449,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->server); + if (! is_null($resolvedValue)) { + $this->env_args->put($env->key, $resolvedValue); } } } else { @@ -2458,8 +2461,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->server); + if (! is_null($resolvedValue)) { + $this->env_args->put($env->key, $resolvedValue); } } } 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..6aa34f242 --- /dev/null +++ b/app/Livewire/SharedVariables/Server/Show.php @@ -0,0 +1,169 @@ + 'refreshEnvs', 'saveKey' => 'saveKey', 'environmentVariableDeleted' => 'refreshEnvs']; + + public function saveKey($data) + { + try { + $this->authorize('update', $this->server); + + $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'], + 'type' => 'server', + 'team_id' => currentTeam()->id, + ]); + $this->server->refresh(); + $this->getDevView(); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function mount() + { + $serverUuid = request()->route('server_uuid'); + $teamId = currentTeam()->id; + $server = Server::where('team_id', $teamId)->where('uuid', $serverUuid)->first(); + if (!$server) { + return redirect()->route('dashboard'); + } + $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->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))->get(); + + if ($variablesToDelete->isEmpty()) { + return 0; + } + + $this->server->environment_variables()->whereNotIn('key', array_keys($variables))->delete(); + + return $variablesToDelete->count(); + } + + private function updateOrCreateVariables($variables) + { + $count = 0; + foreach ($variables as $key => $value) { + $found = $this->server->environment_variables()->where('key', $key)->first(); + + if ($found) { + if (! $found->is_shown_once && ! $found->is_multiline) { + if ($found->value !== $value) { + $found->value = $value; + $found->save(); + $count++; + } + } + } else { + $this->server->environment_variables()->create([ + 'key' => $key, + 'value' => $value, + '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'); + } +} \ No newline at end of file diff --git a/app/Models/EnvironmentVariable.php b/app/Models/EnvironmentVariable.php index 895dc1c43..9308b9ce6 100644 --- a/app/Models/EnvironmentVariable.php +++ b/app/Models/EnvironmentVariable.php @@ -122,6 +122,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); if ($this->is_literal || $this->is_multiline) { $real_value = '\''.$real_value.'\''; @@ -181,7 +192,43 @@ protected function isShared(): Attribute ); } - private function get_real_environment_variables(?string $environment_variable = null, $resource = null) + 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); + 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_internal(?string $environment_variable = null, $resource = null, $serverOverride = null) { if ((is_null($environment_variable) && $environment_variable === '') || is_null($resource)) { return null; @@ -203,6 +250,17 @@ private function get_real_environment_variables(?string $environment_variable = $id = $resource->environment->project->id; } elseif ($type->value() === 'team') { $id = $resource->team()->id; + } elseif ($type->value() === 'server') { + // Use server override if provided (for deployment context), otherwise use resource's 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; + } else { + $id = null; + } } if (is_null($id)) { continue; @@ -216,6 +274,11 @@ private function get_real_environment_variables(?string $environment_variable = return str($environment_variable)->value(); } + private function get_real_environment_variables(?string $environment_variable = null, $resource = null) + { + return $this->get_real_environment_variables_internal($environment_variable, $resource); + } + private function get_environment_variables(?string $environment_variable = null): ?string { if (! $environment_variable) { diff --git a/app/Models/Server.php b/app/Models/Server.php index be39e3f8d..31d4f6440 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -1016,6 +1016,11 @@ public function team() return $this->belongsTo(Team::class); } + 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 7956f006a..7b68bbac9 100644 --- a/app/Models/SharedEnvironmentVariable.php +++ b/app/Models/SharedEnvironmentVariable.php @@ -27,4 +27,9 @@ public function environment() { return $this->belongsTo(Environment::class); } + + public function server() + { + return $this->belongsTo(Server::class); + } } 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 bbbe2bc05..9c103aaac 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..0207ed955 --- /dev/null +++ b/database/migrations/2025_12_24_095507_add_server_to_shared_environment_variables_table.php @@ -0,0 +1,35 @@ +foreignId('server_id')->nullable()->constrained()->onDelete('cascade'); + $table->unique(['key', 'server_id', 'team_id']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('shared_environment_variables', function (Blueprint $table) { + $table->dropUnique(['key', 'server_id', 'team_id']); + $table->dropForeign(['server_id']); + $table->dropColumn('server_id'); + }); + DB::statement("ALTER TABLE shared_environment_variables DROP CONSTRAINT 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/resources/views/livewire/shared-variables/index.blade.php b/resources/views/livewire/shared-variables/index.blade.php index 3e19e5f1a..5064b75ba 100644 --- a/resources/views/livewire/shared-variables/index.blade.php +++ b/resources/views/livewire/shared-variables/index.blade.php @@ -5,27 +5,33 @@

Shared Variables

-
Set Team / Project / Environment wide variables.
+
Set Team / Project / Environment / Server wide variables.
-
- -
-
Team wide
-
Usable for all resources in a team.
-
-
- -
-
Project wide
-
Usable for all resources in a project.
-
-
- -
-
Environment wide
-
Usable for all resources in an environment.
-
-
+
+ +
+
Team wide
+
Usable for all resources in a team.
+
+
+ +
+
Project wide
+
Usable for all resources in a project.
+
+
+ +
+
Environment wide
+
Usable for all resources in an environment.
+
+
+ +
+
Server wide
+
Usable for all resources in a server.
+
+
-
+
diff --git a/resources/views/livewire/shared-variables/server/index.blade.php b/resources/views/livewire/shared-variables/server/index.blade.php new file mode 100644 index 000000000..4183fee5b --- /dev/null +++ b/resources/views/livewire/shared-variables/server/index.blade.php @@ -0,0 +1,25 @@ +
+ + Server Variables | Coolify + +
+

Servers

+
+
List of your servers.
+
+ @forelse ($servers as $server) + +
+
{{ $server->name }}
+
+ {{ $server->description }}
+
+
+ @empty +
+
No server found.
+
+ @endforelse +
+
\ No newline at end of file diff --git a/resources/views/livewire/shared-variables/server/show.blade.php b/resources/views/livewire/shared-variables/server/show.blade.php new file mode 100644 index 000000000..44ceeae7f --- /dev/null +++ b/resources/views/livewire/shared-variables/server/show.blade.php @@ -0,0 +1,36 @@ +
+ + Server Variable | Coolify + +
+

Shared Variables for {{ data_get($server, 'name') }}

+ @can('update', $server) + + + + @endcan + {{ $view === 'normal' ? 'Developer view' : 'Normal view' }} +
+
+
You can use these variables anywhere with
+
@{{ server.VARIABLENAME }}
+ +
+ @if ($view === 'normal') +
+ @forelse ($server->environment_variables->sort()->sortBy('key') as $env) + + @empty +
No environment variables found.
+ @endforelse +
+ @else +
+ + Save All Environment Variables +
+ @endif +
\ No newline at end of file diff --git a/routes/web.php b/routes/web.php index 2a9072299..c2c9293fd 100644 --- a/routes/web.php +++ b/routes/web.php @@ -70,6 +70,8 @@ use App\Livewire\SharedVariables\Index as SharedVariablesIndex; use App\Livewire\SharedVariables\Project\Index as ProjectSharedVariablesIndex; use App\Livewire\SharedVariables\Project\Show as ProjectSharedVariablesShow; +use App\Livewire\SharedVariables\Server\Index as ServerSharedVariablesIndex; +use App\Livewire\SharedVariables\Server\Show as ServerSharedVariablesShow; use App\Livewire\SharedVariables\Team\Index as TeamSharedVariablesIndex; use App\Livewire\Source\Github\Change as GitHubChange; use App\Livewire\Storage\Index as StorageIndex; @@ -145,6 +147,8 @@ Route::get('/project/{project_uuid}', ProjectSharedVariablesShow::class)->name('shared-variables.project.show'); Route::get('/environments', EnvironmentSharedVariablesIndex::class)->name('shared-variables.environment.index'); Route::get('/environments/project/{project_uuid}/environment/{environment_uuid}', EnvironmentSharedVariablesShow::class)->name('shared-variables.environment.show'); + Route::get('/servers', ServerSharedVariablesIndex::class)->name('shared-variables.server.index'); + Route::get('/server/{server_uuid}', ServerSharedVariablesShow::class)->name('shared-variables.server.show'); }); Route::prefix('team')->group(function () {