mirror of
https://github.com/coollabsio/coolify.git
synced 2026-08-24 10:05:47 -05:00
Add secret manager integration links and API support, resolve referenced credentials in database startup commands, and improve environment variable handling and filtering.
496 lines
17 KiB
PHP
496 lines
17 KiB
PHP
<?php
|
|
|
|
namespace App\Livewire\Project\Shared\EnvironmentVariable;
|
|
|
|
use App\Models\Application;
|
|
use App\Models\Environment;
|
|
use App\Models\EnvironmentVariable as ModelsEnvironmentVariable;
|
|
use App\Models\Project;
|
|
use App\Models\Server;
|
|
use App\Models\Service;
|
|
use App\Models\SharedEnvironmentVariable;
|
|
use App\Support\ValidationPatterns;
|
|
use App\Traits\EnvironmentVariableAnalyzer;
|
|
use App\Traits\EnvironmentVariableProtection;
|
|
use App\Traits\HasSecretManagerAutocomplete;
|
|
use Illuminate\Auth\Access\AuthorizationException;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
|
use Livewire\Attributes\Computed;
|
|
use Livewire\Component;
|
|
|
|
class Show extends Component
|
|
{
|
|
public bool $showEnvironmentType = true;
|
|
|
|
use AuthorizesRequests, EnvironmentVariableAnalyzer, EnvironmentVariableProtection, HasSecretManagerAutocomplete;
|
|
|
|
protected function secretManagerResource(): ?Model
|
|
{
|
|
return $this->isSharedVariable ? null : $this->env->resourceable;
|
|
}
|
|
|
|
public $parameters;
|
|
|
|
public ModelsEnvironmentVariable|SharedEnvironmentVariable $env;
|
|
|
|
public bool $isDisabled = false;
|
|
|
|
public bool $isLocked = false;
|
|
|
|
public bool $isMagicVariable = false;
|
|
|
|
public bool $isSharedVariable = false;
|
|
|
|
public string $type;
|
|
|
|
public int $tableAlphabeticalOrder = 0;
|
|
|
|
public int $tableCreationOrder = 0;
|
|
|
|
public string $key;
|
|
|
|
public ?string $value = null;
|
|
|
|
public ?string $real_value = null;
|
|
|
|
public ?string $comment = null;
|
|
|
|
public bool $is_shared = false;
|
|
|
|
public bool $is_multiline = false;
|
|
|
|
public bool $is_literal = false;
|
|
|
|
public bool $is_shown_once = false;
|
|
|
|
public bool $is_runtime = true;
|
|
|
|
public bool $is_buildtime = true;
|
|
|
|
public bool $is_required = false;
|
|
|
|
public bool $is_really_required = false;
|
|
|
|
public bool $is_redis_credential = false;
|
|
|
|
public bool $isValueHidden = false;
|
|
|
|
/**
|
|
* Decrypted value / real_value are only needed in the edit modal (or after save).
|
|
* Keeping them unloaded for table rows avoids decrypting every visible env on each page change.
|
|
*/
|
|
public bool $valuesLoaded = false;
|
|
|
|
/**
|
|
* Entangled with the edit modal open state so the modal stays open across the
|
|
* async loadValues() re-render (open immediately, decrypt after).
|
|
*/
|
|
public bool $editorOpen = false;
|
|
|
|
public array $problematicVariables = [];
|
|
|
|
protected $listeners = [
|
|
'refreshEnvs' => 'refresh',
|
|
'refresh',
|
|
'compose_loaded' => '$refresh',
|
|
];
|
|
|
|
protected function rules(): array
|
|
{
|
|
return [
|
|
'key' => ValidationPatterns::environmentVariableKeyRules(),
|
|
'value' => 'nullable',
|
|
'comment' => 'nullable|string|max:256',
|
|
'is_multiline' => 'required|boolean',
|
|
'is_literal' => 'required|boolean',
|
|
'is_shown_once' => 'required|boolean',
|
|
'is_runtime' => 'required|boolean',
|
|
'is_buildtime' => 'required|boolean',
|
|
'real_value' => 'nullable',
|
|
'is_required' => 'required|boolean',
|
|
];
|
|
}
|
|
|
|
protected function messages(): array
|
|
{
|
|
return ValidationPatterns::environmentVariableKeyMessages('key');
|
|
}
|
|
|
|
public function mount()
|
|
{
|
|
$this->syncData();
|
|
if ($this->env->getMorphClass() === SharedEnvironmentVariable::class) {
|
|
$this->isSharedVariable = true;
|
|
}
|
|
$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;
|
|
}
|
|
$this->problematicVariables = self::getProblematicVariablesForFrontend();
|
|
}
|
|
|
|
public function getResourceProperty()
|
|
{
|
|
return $this->env->resourceable ?? $this->env;
|
|
}
|
|
|
|
public function refresh()
|
|
{
|
|
if (! $this->env->exists || ! $this->env->fresh()) {
|
|
return;
|
|
}
|
|
$this->valuesLoaded = false;
|
|
$this->syncData();
|
|
$this->checkEnvs();
|
|
}
|
|
|
|
/**
|
|
* Decrypt and resolve values only when the edit modal is opened.
|
|
*/
|
|
public function loadValues(): void
|
|
{
|
|
if ($this->valuesLoaded) {
|
|
return;
|
|
}
|
|
|
|
// List queries omit the encrypted value column; refresh so edit has a full model.
|
|
if ($this->env->exists) {
|
|
$fresh = $this->env->fresh();
|
|
if ($fresh) {
|
|
$fresh->setAppends([]);
|
|
$this->env = $fresh;
|
|
}
|
|
}
|
|
|
|
$this->hydrateValueFields();
|
|
$this->valuesLoaded = true;
|
|
}
|
|
|
|
public function copyValue(): ?string
|
|
{
|
|
if ($this->env->is_shown_once || (auth()->user()?->isMember() ?? true)) {
|
|
return null;
|
|
}
|
|
|
|
if (! $this->env instanceof ModelsEnvironmentVariable) {
|
|
return $this->env->value;
|
|
}
|
|
|
|
return $this->env->get_real_environment_variables_with_server(
|
|
$this->env->resolveReferencedValue(),
|
|
$this->env->resourceable,
|
|
);
|
|
}
|
|
|
|
public function syncData(bool $toModel = false)
|
|
{
|
|
if ($toModel) {
|
|
$this->key = ValidationPatterns::normalizeEnvironmentVariableKey($this->key);
|
|
|
|
if ($this->isSharedVariable) {
|
|
$this->validate([
|
|
'key' => ValidationPatterns::environmentVariableKeyRules(),
|
|
'value' => 'nullable',
|
|
'comment' => 'nullable|string|max:256',
|
|
'is_multiline' => 'required|boolean',
|
|
'is_literal' => 'required|boolean',
|
|
'is_shown_once' => 'required|boolean',
|
|
'real_value' => 'nullable',
|
|
]);
|
|
} else {
|
|
$this->validate();
|
|
$this->env->is_required = $this->is_required;
|
|
$this->env->is_runtime = $this->is_runtime;
|
|
$this->env->is_buildtime = $this->is_buildtime;
|
|
$this->env->is_shared = $this->is_shared;
|
|
}
|
|
$this->env->key = $this->key;
|
|
$this->env->value = $this->value;
|
|
$this->env->comment = $this->comment;
|
|
$this->env->is_multiline = $this->is_multiline;
|
|
$this->env->is_literal = $this->is_literal;
|
|
$this->env->is_shown_once = $this->is_shown_once;
|
|
$this->env->save();
|
|
$this->valuesLoaded = true;
|
|
} else {
|
|
// Table metadata only — never decrypt here. Values load via loadValues().
|
|
$this->env->setAppends([]);
|
|
$this->key = $this->env->key;
|
|
$this->comment = $this->env->comment;
|
|
$this->is_multiline = (bool) $this->env->is_multiline;
|
|
$this->is_literal = (bool) $this->env->is_literal;
|
|
$this->is_shown_once = (bool) $this->env->is_shown_once;
|
|
$this->is_runtime = (bool) ($this->env->is_runtime ?? true);
|
|
$this->is_buildtime = (bool) ($this->env->is_buildtime ?? true);
|
|
$this->is_required = (bool) ($this->env->is_required ?? false);
|
|
// Use the stored column, not the value-based accessor (that decrypts).
|
|
$this->is_shared = (bool) ($this->env->getAttributes()['is_shared'] ?? false);
|
|
$this->isValueHidden = auth()->user()?->isMember() ?? true;
|
|
|
|
if ($this->valuesLoaded) {
|
|
$this->hydrateValueFields();
|
|
} else {
|
|
$this->value = null;
|
|
$this->real_value = null;
|
|
// Required badge: without decrypting, show when flagged required.
|
|
// Exact empty-value state is refined when the edit modal opens.
|
|
$this->is_really_required = $this->is_required;
|
|
}
|
|
}
|
|
}
|
|
|
|
private function hydrateValueFields(): void
|
|
{
|
|
$this->value = $this->env->value;
|
|
$this->is_shared = (bool) ($this->env->is_shared ?? false);
|
|
|
|
if ($this->is_shared) {
|
|
$this->real_value = $this->env->real_value;
|
|
$this->is_really_required = $this->is_required && blank($this->real_value);
|
|
} else {
|
|
$this->real_value = null;
|
|
$this->is_really_required = $this->is_required && blank($this->value);
|
|
}
|
|
|
|
if ($this->env->is_shown_once || (auth()->user()?->isMember() ?? true)) {
|
|
$this->value = null;
|
|
$this->real_value = null;
|
|
}
|
|
|
|
$this->isValueHidden = auth()->user()?->isMember() ?? true;
|
|
}
|
|
|
|
public function checkEnvs()
|
|
{
|
|
$this->isDisabled = false;
|
|
$this->isMagicVariable = false;
|
|
|
|
if (str($this->env->key)->startsWith('SERVICE_FQDN') || str($this->env->key)->startsWith('SERVICE_URL') || str($this->env->key)->startsWith('SERVICE_NAME')) {
|
|
$this->isDisabled = true;
|
|
$this->isMagicVariable = true;
|
|
}
|
|
|
|
if ($this->env->is_shown_once) {
|
|
$this->isLocked = true;
|
|
}
|
|
}
|
|
|
|
public function serialize()
|
|
{
|
|
data_forget($this->env, 'real_value');
|
|
}
|
|
|
|
public function lock()
|
|
{
|
|
$this->authorize('update', $this->env);
|
|
|
|
$this->env->is_shown_once = true;
|
|
if ($this->isSharedVariable) {
|
|
unset($this->env->is_required);
|
|
}
|
|
$this->serialize();
|
|
$this->env->save();
|
|
$this->checkEnvs();
|
|
$this->dispatch('refreshEnvs');
|
|
}
|
|
|
|
public function instantSave()
|
|
{
|
|
$this->submit();
|
|
}
|
|
|
|
public function submit()
|
|
{
|
|
try {
|
|
$this->authorize('update', $this->env);
|
|
$this->loadValues();
|
|
|
|
if (! $this->isSharedVariable && $this->is_required && str($this->value)->isEmpty()) {
|
|
$oldValue = $this->env->getOriginal('value');
|
|
$this->value = $oldValue;
|
|
$this->dispatch('error', 'Required environment variables cannot be empty.');
|
|
|
|
return;
|
|
}
|
|
|
|
$this->serialize();
|
|
$this->syncData(true);
|
|
$this->syncData(false);
|
|
$this->dispatch('success', 'Environment variable updated.');
|
|
$this->dispatch('envsUpdated');
|
|
$this->dispatch('configurationChanged');
|
|
} catch (\Exception $e) {
|
|
return handleError($e);
|
|
}
|
|
}
|
|
|
|
#[Computed]
|
|
public function availableSharedVariables(): array
|
|
{
|
|
// Shared across all Show row components in the same request (edit modals).
|
|
static $requestCache = [];
|
|
|
|
$team = currentTeam();
|
|
$cacheKey = implode('|', [
|
|
$team?->id ?? 'none',
|
|
data_get($this->parameters, 'project_uuid', ''),
|
|
data_get($this->parameters, 'environment_uuid', ''),
|
|
data_get($this->parameters, 'server_uuid', ''),
|
|
data_get($this->parameters, 'application_uuid', ''),
|
|
data_get($this->parameters, 'service_uuid', ''),
|
|
]);
|
|
|
|
if (array_key_exists($cacheKey, $requestCache)) {
|
|
return $requestCache[$cacheKey];
|
|
}
|
|
|
|
$result = [
|
|
'team' => [],
|
|
'project' => [],
|
|
'environment' => [],
|
|
'server' => [],
|
|
];
|
|
|
|
// Early return if no team
|
|
if (! $team) {
|
|
return $requestCache[$cacheKey] = $result;
|
|
}
|
|
|
|
// Check if user can view team variables
|
|
try {
|
|
$this->authorize('view', $team);
|
|
$result['team'] = $team->environment_variables()
|
|
->pluck('key')
|
|
->toArray();
|
|
} catch (AuthorizationException $e) {
|
|
// User not authorized to view team variables
|
|
}
|
|
|
|
// Get project variables if we have a project_uuid in route
|
|
$projectUuid = data_get($this->parameters, 'project_uuid');
|
|
if ($projectUuid) {
|
|
$project = Project::where('team_id', $team->id)
|
|
->where('uuid', $projectUuid)
|
|
->first();
|
|
|
|
if ($project) {
|
|
try {
|
|
$this->authorize('view', $project);
|
|
$result['project'] = $project->environment_variables()
|
|
->pluck('key')
|
|
->toArray();
|
|
|
|
// Get environment variables if we have an environment_uuid in route
|
|
$environmentUuid = data_get($this->parameters, 'environment_uuid');
|
|
if ($environmentUuid) {
|
|
$environment = $project->environments()
|
|
->where('uuid', $environmentUuid)
|
|
->first();
|
|
|
|
if ($environment) {
|
|
try {
|
|
$this->authorize('view', $environment);
|
|
$result['environment'] = $environment->environment_variables()
|
|
->pluck('key')
|
|
->toArray();
|
|
} catch (AuthorizationException $e) {
|
|
// User not authorized to view environment variables
|
|
}
|
|
}
|
|
}
|
|
} catch (AuthorizationException $e) {
|
|
// User not authorized to view project variables
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 = 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 (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 = 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 (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 = 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 (AuthorizationException $e) {
|
|
// User not authorized to view server variables
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return $requestCache[$cacheKey] = $result;
|
|
}
|
|
|
|
public function delete()
|
|
{
|
|
try {
|
|
$this->authorize('delete', $this->env);
|
|
|
|
// Check if the variable is used in Docker Compose
|
|
if ($this->type === 'service' || $this->type === 'application' && $this->env->resourceable?->docker_compose) {
|
|
[$isUsed, $reason] = $this->isEnvironmentVariableUsedInDockerCompose($this->env->key, $this->env->resourceable?->docker_compose);
|
|
|
|
if ($isUsed) {
|
|
$this->dispatch('error', "Cannot delete environment variable '{$this->env->key}' <br><br>Please remove it from the Docker Compose file first.");
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
$this->env->delete();
|
|
$this->dispatch('environmentVariableDeleted');
|
|
$this->dispatch('success', 'Environment variable deleted successfully.');
|
|
} catch (\Exception $e) {
|
|
return handleError($e);
|
|
}
|
|
}
|
|
}
|