coolify/app/Livewire/Project/Shared/EnvironmentVariable/All.php

402 lines
14 KiB
PHP
Raw Normal View History

2023-08-07 20:14:21 +00:00
<?php
2023-12-07 18:06:32 +00:00
namespace App\Livewire\Project\Shared\EnvironmentVariable;
2023-08-07 20:14:21 +00:00
use App\Models\EnvironmentVariable;
use App\Traits\EnvironmentVariableProtection;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
2023-08-07 20:14:21 +00:00
use Livewire\Component;
class All extends Component
{
use AuthorizesRequests, EnvironmentVariableProtection;
2023-08-07 20:14:21 +00:00
public $resource;
2024-08-15 09:23:44 +00:00
public string $resourceClass;
2024-08-15 09:23:44 +00:00
2023-09-08 14:16:59 +00:00
public bool $showPreview = false;
2024-08-15 09:23:44 +00:00
2023-09-08 14:16:59 +00:00
public ?string $variables = null;
2024-08-15 09:23:44 +00:00
2023-09-08 14:16:59 +00:00
public ?string $variablesPreview = null;
2024-08-15 09:23:44 +00:00
2023-09-08 14:16:59 +00:00
public string $view = 'normal';
2024-06-10 20:43:34 +00:00
public bool $is_env_sorting_enabled = false;
public bool $use_build_secrets = false;
protected $listeners = [
'saveKey' => 'submit',
2024-09-04 12:33:16 +00:00
'refreshEnvs',
2024-08-12 12:46:30 +00:00
'environmentVariableDeleted' => 'refreshEnvs',
];
2024-06-10 20:43:34 +00:00
2023-08-07 20:14:21 +00:00
public function mount()
{
$this->is_env_sorting_enabled = data_get($this->resource, 'settings.is_env_sorting_enabled', false);
$this->use_build_secrets = data_get($this->resource, 'settings.use_build_secrets', false);
$this->resourceClass = get_class($this->resource);
$resourceWithPreviews = [\App\Models\Application::class];
$simpleDockerfile = filled(data_get($this->resource, 'dockerfile'));
2024-08-15 09:23:44 +00:00
if (str($this->resourceClass)->contains($resourceWithPreviews) && ! $simpleDockerfile) {
2023-09-08 14:16:59 +00:00
$this->showPreview = true;
}
$this->getDevView();
2023-09-08 14:16:59 +00:00
}
public function instantSave()
{
try {
$this->authorize('manageEnvironment', $this->resource);
$this->resource->settings->is_env_sorting_enabled = $this->is_env_sorting_enabled;
$this->resource->settings->use_build_secrets = $this->use_build_secrets;
$this->resource->settings->save();
$this->getDevView();
$this->dispatch('success', 'Environment variable settings updated.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
2024-06-10 20:43:34 +00:00
public function getEnvironmentVariablesProperty()
2024-08-12 12:46:30 +00:00
{
$query = $this->resource->environment_variables()
->orderByRaw("CASE WHEN is_required = true AND (value IS NULL OR value = '') THEN 0 ELSE 1 END");
if ($this->is_env_sorting_enabled) {
$query->orderBy('key');
} else {
$query->orderBy('order');
}
return $query->get();
}
public function getEnvironmentVariablesPreviewProperty()
{
$query = $this->resource->environment_variables_preview()
->orderByRaw("CASE WHEN is_required = true AND (value IS NULL OR value = '') THEN 0 ELSE 1 END");
if ($this->is_env_sorting_enabled) {
$query->orderBy('key');
} else {
$query->orderBy('order');
}
return $query->get();
}
2024-06-10 20:43:34 +00:00
public function getHardcodedEnvironmentVariablesProperty()
{
return $this->getHardcodedVariables(false);
}
public function getHardcodedEnvironmentVariablesPreviewProperty()
{
return $this->getHardcodedVariables(true);
}
protected function getHardcodedVariables(bool $isPreview)
{
// Only for services and docker-compose applications
if ($this->resource->type() !== 'service' &&
($this->resourceClass !== 'App\Models\Application' ||
($this->resourceClass === 'App\Models\Application' && $this->resource->build_pack !== 'dockercompose'))) {
return collect([]);
}
$dockerComposeRaw = $this->resource->docker_compose_raw ?? $this->resource->docker_compose;
if (blank($dockerComposeRaw)) {
return collect([]);
}
// Extract all hard-coded variables
$hardcodedVars = extractHardcodedEnvironmentVariables($dockerComposeRaw);
// Filter out magic variables (SERVICE_FQDN_*, SERVICE_URL_*, SERVICE_NAME_*)
$hardcodedVars = $hardcodedVars->filter(function ($var) {
$key = $var['key'];
return ! str($key)->startsWith(['SERVICE_FQDN_', 'SERVICE_URL_', 'SERVICE_NAME_']);
});
// Filter out variables that exist in database (user has overridden/managed them)
// For preview, check against preview variables; for production, check against production variables
if ($isPreview) {
$managedKeys = $this->resource->environment_variables_preview()->pluck('key')->toArray();
} else {
$managedKeys = $this->resource->environment_variables()->where('is_preview', false)->pluck('key')->toArray();
}
$hardcodedVars = $hardcodedVars->filter(function ($var) use ($managedKeys) {
return ! in_array($var['key'], $managedKeys);
});
// Apply sorting based on is_env_sorting_enabled
if ($this->is_env_sorting_enabled) {
$hardcodedVars = $hardcodedVars->sortBy('key')->values();
}
// Otherwise keep order from docker-compose file
return $hardcodedVars;
}
2023-09-08 14:16:59 +00:00
public function getDevView()
{
$this->variables = $this->formatEnvironmentVariables($this->environmentVariables);
if ($this->showPreview) {
$this->variablesPreview = $this->formatEnvironmentVariables($this->environmentVariablesPreview);
}
}
private function formatEnvironmentVariables($variables)
{
return $variables->map(function ($item) {
2023-10-24 13:41:21 +00:00
if ($item->is_shown_once) {
return "$item->key=(Locked Secret, delete and add again to change)";
2023-10-24 13:41:21 +00:00
}
2024-03-15 21:02:37 +00:00
if ($item->is_multiline) {
return "$item->key=(Multiline environment variable, edit in normal view)";
2024-03-15 21:02:37 +00:00
}
2024-08-15 09:23:44 +00:00
2023-09-08 14:16:59 +00:00
return "$item->key=$item->value";
})->join("\n");
2023-09-08 14:16:59 +00:00
}
2024-06-10 20:43:34 +00:00
2023-09-08 14:16:59 +00:00
public function switch()
{
$this->view = $this->view === 'normal' ? 'dev' : 'normal';
$this->getDevView();
2023-09-08 14:16:59 +00:00
}
2024-06-10 20:43:34 +00:00
public function submit($data = null)
2023-09-08 14:16:59 +00:00
{
2024-08-12 11:23:36 +00:00
try {
$this->authorize('manageEnvironment', $this->resource);
if ($data === null) {
2024-08-12 12:46:30 +00:00
$this->handleBulkSubmit();
} else {
2024-08-12 12:46:30 +00:00
$this->handleSingleSubmit($data);
2023-09-08 14:16:59 +00:00
}
2024-08-12 11:23:36 +00:00
$this->updateOrder();
$this->getDevView();
} catch (\Throwable $e) {
2024-08-12 11:23:36 +00:00
return handleError($e, $this);
} finally {
$this->refreshEnvs();
2023-09-08 14:16:59 +00:00
}
2024-08-12 11:23:36 +00:00
}
private function updateOrder()
{
$variables = parseEnvFormatToArray($this->variables);
$order = 1;
foreach ($variables as $key => $value) {
$env = $this->resource->environment_variables()->where('key', $key)->first();
if ($env) {
$env->order = $order;
$env->save();
}
$order++;
}
if ($this->showPreview) {
$previewVariables = parseEnvFormatToArray($this->variablesPreview);
$order = 1;
foreach ($previewVariables as $key => $value) {
$env = $this->resource->environment_variables_preview()->where('key', $key)->first();
if ($env) {
$env->order = $order;
$env->save();
}
$order++;
}
}
}
2024-08-12 12:46:30 +00:00
private function handleBulkSubmit()
{
$variables = parseEnvFormatToArray($this->variables);
$changesMade = false;
$errorOccurred = false;
// 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;
}
2024-08-12 12:46:30 +00:00
if ($this->showPreview) {
$previewVariables = parseEnvFormatToArray($this->variablesPreview);
// 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;
}
2024-08-12 12:46:30 +00:00
}
// Only show success message if changes were actually made and no errors occurred
if ($changesMade && ! $errorOccurred) {
$this->dispatch('success', 'Environment variables updated.');
}
2024-08-12 12:46:30 +00:00
}
private function handleSingleSubmit($data)
{
$found = $this->resource->environment_variables()->where('key', $data['key'])->first();
if ($found) {
$this->dispatch('error', 'Environment variable already exists.');
2024-08-15 09:23:44 +00:00
2024-08-12 12:46:30 +00:00
return;
}
$maxOrder = $this->resource->environment_variables()->max('order') ?? 0;
2024-08-12 12:46:30 +00:00
$environment = $this->createEnvironmentVariable($data);
$environment->order = $maxOrder + 1;
2024-08-12 12:46:30 +00:00
$environment->save();
// Clear computed property cache to force refresh
unset($this->environmentVariables);
unset($this->environmentVariablesPreview);
$this->dispatch('success', 'Environment variable added.');
2024-08-12 12:46:30 +00:00
}
private function createEnvironmentVariable($data)
{
$environment = new EnvironmentVariable;
$environment->key = $data['key'];
$environment->value = $data['value'];
$environment->is_multiline = $data['is_multiline'] ?? false;
$environment->is_literal = $data['is_literal'] ?? false;
$environment->is_runtime = $data['is_runtime'] ?? true;
$environment->is_buildtime = $data['is_buildtime'] ?? true;
$environment->is_preview = $data['is_preview'] ?? false;
$environment->comment = $data['comment'] ?? null;
$environment->resourceable_id = $this->resource->id;
$environment->resourceable_type = $this->resource->getMorphClass();
return $environment;
2024-08-12 12:46:30 +00:00
}
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 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}' <br><br>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 => $data) {
if (str($key)->startsWith('SERVICE_FQDN') || str($key)->startsWith('SERVICE_URL') || str($key)->startsWith('SERVICE_NAME')) {
continue;
}
// Extract value and comment from parsed data
// Handle both array format ['value' => ..., 'comment' => ...] and plain string values
$value = is_array($data) ? ($data['value'] ?? '') : $data;
$comment = is_array($data) ? ($data['comment'] ?? null) : null;
$method = $isPreview ? 'environment_variables_preview' : 'environment_variables';
$found = $this->resource->$method()->where('key', $key)->first();
if ($found) {
2024-08-15 09:23:44 +00:00
if (! $found->is_shown_once && ! $found->is_multiline) {
$changed = false;
// Update value if it changed
if ($found->value !== $value) {
$found->value = $value;
$changed = true;
}
fix: preserve existing comments in bulk update and always show save notification This commit fixes two UX issues with environment variable bulk updates: 1. Comment Preservation (High Priority Bug): - When bulk updating environment variables via Developer view, existing manually-entered comments are now preserved when no inline comment is provided - Only overwrites existing comments when an inline comment (#comment) is explicitly provided in the pasted content - Previously: pasting "KEY=value" would erase existing comment to null - Now: pasting "KEY=value" preserves existing comment, "KEY=value #new" overwrites it 2. Save Notification (UX Improvement): - "Save all Environment variables" button now always shows success notification - Previously: only showed notification when changes were detected - Now: provides feedback even when no changes were made - Consistent with other save operations in the codebase Changes: - Modified updateOrCreateVariables() to only update comment field when inline comment is provided (null check prevents overwriting existing comments) - Modified handleBulkSubmit() to always dispatch success notification unless error occurred - Added comprehensive test coverage for bulk update comment preservation scenarios Tests: - Added 4 new feature tests covering comment preservation edge cases - All 22 existing unit tests for parseEnvFormatToArray pass - Code formatted with Pint 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-18 21:26:07 +00:00
// Only update comment from inline comment if one is provided (overwrites existing)
// If $comment is null, don't touch existing comment field to preserve it
if ($comment !== null && $found->comment !== $comment) {
$found->comment = $comment;
$changed = true;
}
if ($changed) {
$found->save();
$count++;
}
}
} else {
$environment = new EnvironmentVariable;
$environment->key = $key;
$environment->value = $value;
$environment->comment = $comment; // Set comment from inline comment
$environment->is_multiline = false;
$environment->is_preview = $isPreview;
$environment->resourceable_id = $this->resource->id;
$environment->resourceable_type = $this->resource->getMorphClass();
$environment->save();
$count++;
}
}
return $count;
}
2023-08-07 20:14:21 +00:00
public function refreshEnvs()
{
$this->resource->refresh();
// Clear computed property cache to force refresh
unset($this->environmentVariables);
unset($this->environmentVariablesPreview);
2023-09-08 14:16:59 +00:00
$this->getDevView();
2023-08-07 20:14:21 +00:00
}
2024-08-15 09:23:44 +00:00
}