feat(EnvironmentVariable): implement protection for critical environment variables and enhance deletion logic
This commit is contained in:
parent
f31624c6eb
commit
f63415f027
3 changed files with 173 additions and 9 deletions
|
|
@ -3,10 +3,13 @@
|
||||||
namespace App\Livewire\Project\Shared\EnvironmentVariable;
|
namespace App\Livewire\Project\Shared\EnvironmentVariable;
|
||||||
|
|
||||||
use App\Models\EnvironmentVariable;
|
use App\Models\EnvironmentVariable;
|
||||||
|
use App\Traits\EnvironmentVariableProtection;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
class All extends Component
|
class All extends Component
|
||||||
{
|
{
|
||||||
|
use EnvironmentVariableProtection;
|
||||||
|
|
||||||
public $resource;
|
public $resource;
|
||||||
|
|
||||||
public string $resourceClass;
|
public string $resourceClass;
|
||||||
|
|
@ -138,17 +141,57 @@ private function updateOrder()
|
||||||
private function handleBulkSubmit()
|
private function handleBulkSubmit()
|
||||||
{
|
{
|
||||||
$variables = parseEnvFormatToArray($this->variables);
|
$variables = parseEnvFormatToArray($this->variables);
|
||||||
|
$changesMade = false;
|
||||||
|
$errorOccurred = false;
|
||||||
|
|
||||||
$this->deleteRemovedVariables(false, $variables);
|
// Try to delete removed variables
|
||||||
$this->updateOrCreateVariables(false, $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) {
|
if ($this->showPreview) {
|
||||||
$previewVariables = parseEnvFormatToArray($this->variablesPreview);
|
$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)
|
private function handleSingleSubmit($data)
|
||||||
|
|
@ -184,11 +227,46 @@ private function createEnvironmentVariable($data)
|
||||||
private function deleteRemovedVariables($isPreview, $variables)
|
private function deleteRemovedVariables($isPreview, $variables)
|
||||||
{
|
{
|
||||||
$method = $isPreview ? 'environment_variables_preview' : 'environment_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}' <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();
|
$this->resource->$method()->whereNotIn('key', array_keys($variables))->delete();
|
||||||
|
|
||||||
|
return $variablesToDelete->count();
|
||||||
}
|
}
|
||||||
|
|
||||||
private function updateOrCreateVariables($isPreview, $variables)
|
private function updateOrCreateVariables($isPreview, $variables)
|
||||||
{
|
{
|
||||||
|
$count = 0;
|
||||||
foreach ($variables as $key => $value) {
|
foreach ($variables as $key => $value) {
|
||||||
if (str($key)->startsWith('SERVICE_FQDN') || str($key)->startsWith('SERVICE_URL')) {
|
if (str($key)->startsWith('SERVICE_FQDN') || str($key)->startsWith('SERVICE_URL')) {
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -198,8 +276,12 @@ private function updateOrCreateVariables($isPreview, $variables)
|
||||||
|
|
||||||
if ($found) {
|
if ($found) {
|
||||||
if (! $found->is_shown_once && ! $found->is_multiline) {
|
if (! $found->is_shown_once && ! $found->is_multiline) {
|
||||||
$found->value = $value;
|
// Only count as a change if the value actually changed
|
||||||
$found->save();
|
if ($found->value !== $value) {
|
||||||
|
$found->value = $value;
|
||||||
|
$found->save();
|
||||||
|
$count++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$environment = new EnvironmentVariable;
|
$environment = new EnvironmentVariable;
|
||||||
|
|
@ -212,8 +294,11 @@ private function updateOrCreateVariables($isPreview, $variables)
|
||||||
$environment->resourceable_type = $this->resource->getMorphClass();
|
$environment->resourceable_type = $this->resource->getMorphClass();
|
||||||
|
|
||||||
$environment->save();
|
$environment->save();
|
||||||
|
$count++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return $count;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function refreshEnvs()
|
public function refreshEnvs()
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,13 @@
|
||||||
|
|
||||||
use App\Models\EnvironmentVariable as ModelsEnvironmentVariable;
|
use App\Models\EnvironmentVariable as ModelsEnvironmentVariable;
|
||||||
use App\Models\SharedEnvironmentVariable;
|
use App\Models\SharedEnvironmentVariable;
|
||||||
|
use App\Traits\EnvironmentVariableProtection;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
class Show extends Component
|
class Show extends Component
|
||||||
{
|
{
|
||||||
|
use EnvironmentVariableProtection;
|
||||||
|
|
||||||
public $parameters;
|
public $parameters;
|
||||||
|
|
||||||
public ModelsEnvironmentVariable|SharedEnvironmentVariable $env;
|
public ModelsEnvironmentVariable|SharedEnvironmentVariable $env;
|
||||||
|
|
@ -175,11 +178,24 @@ public function submit()
|
||||||
public function delete()
|
public function delete()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
if ($this->is_redis_credential) {
|
// Check if the variable is protected
|
||||||
$this->dispatch('error', 'Cannot delete Redis credentials.');
|
if ($this->isProtectedEnvironmentVariable($this->env->key)) {
|
||||||
|
$this->dispatch('error', "Cannot delete system environment variable '{$this->env->key}'.");
|
||||||
|
|
||||||
return;
|
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}' <br><br>Please remove it from the Docker Compose file first.");
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$this->env->delete();
|
$this->env->delete();
|
||||||
$this->dispatch('environmentVariableDeleted');
|
$this->dispatch('environmentVariableDeleted');
|
||||||
$this->dispatch('success', 'Environment variable deleted successfully.');
|
$this->dispatch('success', 'Environment variable deleted successfully.');
|
||||||
|
|
|
||||||
63
app/Traits/EnvironmentVariableProtection.php
Normal file
63
app/Traits/EnvironmentVariableProtection.php
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Traits;
|
||||||
|
|
||||||
|
use Symfony\Component\Yaml\Yaml;
|
||||||
|
|
||||||
|
trait EnvironmentVariableProtection
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Check if an environment variable is protected from deletion
|
||||||
|
*
|
||||||
|
* @param string $key The environment variable key to check
|
||||||
|
* @return bool True if the variable is protected, false otherwise
|
||||||
|
*/
|
||||||
|
protected function isProtectedEnvironmentVariable(string $key): bool
|
||||||
|
{
|
||||||
|
return str($key)->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, ''];
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue