mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
feat(env): support variable flag directives in developer view (#12349)
This commit is contained in:
@@ -987,27 +987,12 @@ class All extends Component
|
||||
|
||||
private function formatEnvironmentVariables($variables)
|
||||
{
|
||||
$hideValues = ! $this->canViewEnvironmentValues();
|
||||
$canViewValues = $this->canViewEnvironmentValues();
|
||||
|
||||
return $variables
|
||||
->reject(fn ($item): bool => $this->isProtectedEnvironmentVariable($item->key))
|
||||
->map(function ($item) use ($hideValues) {
|
||||
if ($hideValues) {
|
||||
return "$item->key=(Hidden, only admins can view)";
|
||||
}
|
||||
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)";
|
||||
}
|
||||
$value = formatEnvValueForDevView($item->value);
|
||||
if ($value === null) {
|
||||
return "$item->key=(Value cannot be shown in this view, edit in normal view)";
|
||||
}
|
||||
|
||||
return "$item->key=$value";
|
||||
})->join("\n");
|
||||
->map(fn ($item) => formatEnvVariableForDevView($item, $canViewValues))
|
||||
->join("\n");
|
||||
}
|
||||
|
||||
public function switch()
|
||||
@@ -1022,11 +1007,28 @@ class All extends Component
|
||||
|
||||
public function submit($data = null)
|
||||
{
|
||||
// Parse before the save, so that an invalid "# coolify:" line changes nothing and keeps the edited text.
|
||||
try {
|
||||
$this->authorize('manageEnvironment', $this->resource);
|
||||
$this->ensureEnvironmentVariablesLoaded();
|
||||
if ($data === null) {
|
||||
$this->handleBulkSubmit();
|
||||
$variables = $this->normalizeEnvironmentVariables(parseEnvDevView($this->variables, EnvironmentVariable::DEV_VIEW_FLAG_DEFAULTS));
|
||||
$previewVariables = [];
|
||||
if ($this->showPreview) {
|
||||
try {
|
||||
$previewVariables = $this->normalizeEnvironmentVariables(parseEnvDevView($this->variablesPreview, EnvironmentVariable::DEV_VIEW_FLAG_DEFAULTS));
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
throw new \InvalidArgumentException('Preview deployments: '.$e->getMessage(), previous: $e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
try {
|
||||
if ($data === null) {
|
||||
$this->handleBulkSubmit($variables, $previewVariables);
|
||||
} else {
|
||||
$this->handleSingleSubmit($data);
|
||||
}
|
||||
@@ -1076,9 +1078,8 @@ class All extends Component
|
||||
}
|
||||
}
|
||||
|
||||
private function handleBulkSubmit()
|
||||
private function handleBulkSubmit(array $variables, array $previewVariables)
|
||||
{
|
||||
$variables = $this->normalizeEnvironmentVariables(parseEnvFormatToArray($this->variables));
|
||||
$changesMade = false;
|
||||
$errorOccurred = false;
|
||||
|
||||
@@ -1097,8 +1098,6 @@ class All extends Component
|
||||
}
|
||||
|
||||
if ($this->showPreview) {
|
||||
$previewVariables = $this->normalizeEnvironmentVariables(parseEnvFormatToArray($this->variablesPreview));
|
||||
|
||||
// Try to delete removed preview variables
|
||||
$deletedPreviewCount = $this->deleteRemovedVariables(true, $previewVariables);
|
||||
if ($deletedPreviewCount > 0) {
|
||||
@@ -1244,7 +1243,10 @@ class All extends Component
|
||||
$changed = true;
|
||||
}
|
||||
|
||||
if ($changed) {
|
||||
// Flags change only when a "# coolify:" line sets them
|
||||
$found->fill($data['settings']);
|
||||
|
||||
if ($changed || $found->isDirty()) {
|
||||
$found->save();
|
||||
$count++;
|
||||
}
|
||||
@@ -1255,6 +1257,7 @@ class All extends Component
|
||||
$environment->value = $value;
|
||||
$environment->comment = $comment; // Set comment from inline comment
|
||||
$environment->is_multiline = false;
|
||||
$environment->fill($data['settings']);
|
||||
$environment->is_preview = $isPreview;
|
||||
$environment->resourceable_id = $this->resource->id;
|
||||
$environment->resourceable_type = $this->resource->getMorphClass();
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\Livewire\SharedVariables\Environment;
|
||||
use App\Livewire\Concerns\RemembersCanvasView;
|
||||
use App\Models\Application;
|
||||
use App\Models\Project;
|
||||
use App\Models\SharedEnvironmentVariable;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Livewire\Component;
|
||||
@@ -83,30 +84,21 @@ class Show extends Component
|
||||
{
|
||||
$canViewValues = auth()->user()?->isAdminOfTeam($this->project->team_id) ?? false;
|
||||
|
||||
return $variables->map(function ($item) use ($canViewValues) {
|
||||
if (! $canViewValues) {
|
||||
return "$item->key=(Hidden, only admins can view)";
|
||||
}
|
||||
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)";
|
||||
}
|
||||
$value = formatEnvValueForDevView($item->value);
|
||||
if ($value === null) {
|
||||
return "$item->key=(Value cannot be shown in this view, edit in normal view)";
|
||||
}
|
||||
|
||||
return "$item->key=$value";
|
||||
})->join("\n");
|
||||
return $variables->map(fn ($item) => formatEnvVariableForDevView($item, $canViewValues))->join("\n");
|
||||
}
|
||||
|
||||
public function submit()
|
||||
{
|
||||
// Parse before the save, so that an invalid "# coolify:" line changes nothing and keeps the edited text.
|
||||
try {
|
||||
$this->authorize('update', $this->environment);
|
||||
$this->handleBulkSubmit();
|
||||
$variables = parseEnvDevView($this->variables, SharedEnvironmentVariable::DEV_VIEW_FLAG_DEFAULTS);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->handleBulkSubmit($variables);
|
||||
$this->getDevView();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
@@ -115,9 +107,8 @@ class Show extends Component
|
||||
}
|
||||
}
|
||||
|
||||
private function handleBulkSubmit()
|
||||
private function handleBulkSubmit(array $variables)
|
||||
{
|
||||
$variables = parseEnvFormatToArray($this->variables);
|
||||
$changesMade = false;
|
||||
|
||||
DB::transaction(function () use ($variables, &$changesMade) {
|
||||
@@ -165,6 +156,9 @@ class Show extends Component
|
||||
if (! $found->is_shown_once && ! $found->is_multiline && formatEnvValueForDevView($found->value) !== null) {
|
||||
if ($found->value !== $value) {
|
||||
$found->value = $value;
|
||||
}
|
||||
$found->fill($data['settings']);
|
||||
if ($found->isDirty()) {
|
||||
$found->save();
|
||||
$count++;
|
||||
}
|
||||
@@ -175,6 +169,7 @@ class Show extends Component
|
||||
'value' => $value,
|
||||
'is_multiline' => false,
|
||||
'is_literal' => false,
|
||||
...$data['settings'],
|
||||
'type' => 'environment',
|
||||
'team_id' => $this->project->team_id,
|
||||
]);
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Livewire\SharedVariables\Project;
|
||||
|
||||
use App\Livewire\Concerns\RemembersCanvasView;
|
||||
use App\Models\Project;
|
||||
use App\Models\SharedEnvironmentVariable;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Livewire\Component;
|
||||
@@ -81,30 +82,21 @@ class Show extends Component
|
||||
{
|
||||
$canViewValues = auth()->user()?->isAdminOfTeam($this->project->team_id) ?? false;
|
||||
|
||||
return $variables->map(function ($item) use ($canViewValues) {
|
||||
if (! $canViewValues) {
|
||||
return "$item->key=(Hidden, only admins can view)";
|
||||
}
|
||||
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)";
|
||||
}
|
||||
$value = formatEnvValueForDevView($item->value);
|
||||
if ($value === null) {
|
||||
return "$item->key=(Value cannot be shown in this view, edit in normal view)";
|
||||
}
|
||||
|
||||
return "$item->key=$value";
|
||||
})->join("\n");
|
||||
return $variables->map(fn ($item) => formatEnvVariableForDevView($item, $canViewValues))->join("\n");
|
||||
}
|
||||
|
||||
public function submit()
|
||||
{
|
||||
// Parse before the save, so that an invalid "# coolify:" line changes nothing and keeps the edited text.
|
||||
try {
|
||||
$this->authorize('update', $this->project);
|
||||
$this->handleBulkSubmit();
|
||||
$variables = parseEnvDevView($this->variables, SharedEnvironmentVariable::DEV_VIEW_FLAG_DEFAULTS);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->handleBulkSubmit($variables);
|
||||
$this->getDevView();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
@@ -113,10 +105,8 @@ class Show extends Component
|
||||
}
|
||||
}
|
||||
|
||||
private function handleBulkSubmit()
|
||||
private function handleBulkSubmit(array $variables)
|
||||
{
|
||||
$variables = parseEnvFormatToArray($this->variables);
|
||||
|
||||
$changesMade = DB::transaction(function () use ($variables) {
|
||||
// Delete removed variables
|
||||
$deletedCount = $this->deleteRemovedVariables($variables);
|
||||
@@ -157,6 +147,9 @@ class Show extends Component
|
||||
if (! $found->is_shown_once && ! $found->is_multiline && formatEnvValueForDevView($found->value) !== null) {
|
||||
if ($found->value !== $value) {
|
||||
$found->value = $value;
|
||||
}
|
||||
$found->fill($data['settings']);
|
||||
if ($found->isDirty()) {
|
||||
$found->save();
|
||||
$count++;
|
||||
}
|
||||
@@ -167,6 +160,7 @@ class Show extends Component
|
||||
'value' => $value,
|
||||
'is_multiline' => false,
|
||||
'is_literal' => false,
|
||||
...$data['settings'],
|
||||
'type' => 'project',
|
||||
'team_id' => $this->project->team_id,
|
||||
]);
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Livewire\SharedVariables\Server;
|
||||
|
||||
use App\Livewire\Concerns\RemembersCanvasView;
|
||||
use App\Models\Server;
|
||||
use App\Models\SharedEnvironmentVariable;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Livewire\Component;
|
||||
@@ -82,30 +83,21 @@ class Show extends Component
|
||||
{
|
||||
$canViewValues = auth()->user()?->isAdminOfTeam($this->server->team_id) ?? false;
|
||||
|
||||
return $variables->map(function ($item) use ($canViewValues) {
|
||||
if (! $canViewValues) {
|
||||
return "$item->key=(Hidden, only admins can view)";
|
||||
}
|
||||
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)";
|
||||
}
|
||||
$value = formatEnvValueForDevView($item->value);
|
||||
if ($value === null) {
|
||||
return "$item->key=(Value cannot be shown in this view, edit in normal view)";
|
||||
}
|
||||
|
||||
return "$item->key=$value";
|
||||
})->join("\n");
|
||||
return $variables->map(fn ($item) => formatEnvVariableForDevView($item, $canViewValues))->join("\n");
|
||||
}
|
||||
|
||||
public function submit()
|
||||
{
|
||||
// Parse before the save, so that an invalid "# coolify:" line changes nothing and keeps the edited text.
|
||||
try {
|
||||
$this->authorize('update', $this->server);
|
||||
$this->handleBulkSubmit();
|
||||
$variables = parseEnvDevView($this->variables, SharedEnvironmentVariable::DEV_VIEW_FLAG_DEFAULTS);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->handleBulkSubmit($variables);
|
||||
$this->getDevView();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
@@ -114,10 +106,8 @@ class Show extends Component
|
||||
}
|
||||
}
|
||||
|
||||
private function handleBulkSubmit()
|
||||
private function handleBulkSubmit(array $variables)
|
||||
{
|
||||
$variables = parseEnvFormatToArray($this->variables);
|
||||
|
||||
$changesMade = DB::transaction(function () use ($variables) {
|
||||
// Delete removed variables
|
||||
$deletedCount = $this->deleteRemovedVariables($variables);
|
||||
@@ -167,9 +157,12 @@ class Show extends Component
|
||||
|
||||
if ($found) {
|
||||
if (! $found->is_shown_once && ! $found->is_multiline && formatEnvValueForDevView($found->value) !== null) {
|
||||
if ($found->value !== $value || $found->comment !== $comment) {
|
||||
if ($found->value !== $value) {
|
||||
$found->value = $value;
|
||||
$found->comment = $comment;
|
||||
}
|
||||
$found->comment = $comment;
|
||||
$found->fill($data['settings']);
|
||||
if ($found->isDirty()) {
|
||||
$found->save();
|
||||
$count++;
|
||||
}
|
||||
@@ -181,6 +174,7 @@ class Show extends Component
|
||||
'comment' => $comment,
|
||||
'is_multiline' => false,
|
||||
'is_literal' => false,
|
||||
...$data['settings'],
|
||||
'type' => 'server',
|
||||
'team_id' => $this->server->team_id,
|
||||
]);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Livewire\SharedVariables\Team;
|
||||
|
||||
use App\Livewire\Concerns\RemembersCanvasView;
|
||||
use App\Models\SharedEnvironmentVariable;
|
||||
use App\Models\Team;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
@@ -75,30 +76,21 @@ class Index extends Component
|
||||
{
|
||||
$canViewValues = auth()->user()?->isAdminOfTeam($this->team->id) ?? false;
|
||||
|
||||
return $variables->map(function ($item) use ($canViewValues) {
|
||||
if (! $canViewValues) {
|
||||
return "$item->key=(Hidden, only admins can view)";
|
||||
}
|
||||
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)";
|
||||
}
|
||||
$value = formatEnvValueForDevView($item->value);
|
||||
if ($value === null) {
|
||||
return "$item->key=(Value cannot be shown in this view, edit in normal view)";
|
||||
}
|
||||
|
||||
return "$item->key=$value";
|
||||
})->join("\n");
|
||||
return $variables->map(fn ($item) => formatEnvVariableForDevView($item, $canViewValues))->join("\n");
|
||||
}
|
||||
|
||||
public function submit()
|
||||
{
|
||||
// Parse before the save, so that an invalid "# coolify:" line changes nothing and keeps the edited text.
|
||||
try {
|
||||
$this->authorize('update', $this->team);
|
||||
$this->handleBulkSubmit();
|
||||
$variables = parseEnvDevView($this->variables, SharedEnvironmentVariable::DEV_VIEW_FLAG_DEFAULTS);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->handleBulkSubmit($variables);
|
||||
$this->getDevView();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
@@ -107,9 +99,8 @@ class Index extends Component
|
||||
}
|
||||
}
|
||||
|
||||
private function handleBulkSubmit()
|
||||
private function handleBulkSubmit(array $variables)
|
||||
{
|
||||
$variables = parseEnvFormatToArray($this->variables);
|
||||
$changesMade = false;
|
||||
|
||||
DB::transaction(function () use ($variables, &$changesMade) {
|
||||
@@ -156,6 +147,9 @@ class Index extends Component
|
||||
if (! $found->is_shown_once && ! $found->is_multiline && formatEnvValueForDevView($found->value) !== null) {
|
||||
if ($found->value !== $value) {
|
||||
$found->value = $value;
|
||||
}
|
||||
$found->fill($data['settings']);
|
||||
if ($found->isDirty()) {
|
||||
$found->save();
|
||||
$count++;
|
||||
}
|
||||
@@ -166,6 +160,7 @@ class Index extends Component
|
||||
'value' => $value,
|
||||
'is_multiline' => false,
|
||||
'is_literal' => false,
|
||||
...$data['settings'],
|
||||
'type' => 'team',
|
||||
'team_id' => $this->team->id,
|
||||
]);
|
||||
|
||||
@@ -42,6 +42,13 @@ class EnvironmentVariable extends BaseModel
|
||||
{
|
||||
use Auditable;
|
||||
|
||||
/**
|
||||
* Flags that a "# coolify:" line of the developer view can set, with their defaults.
|
||||
*
|
||||
* @var array<string, bool>
|
||||
*/
|
||||
public const DEV_VIEW_FLAG_DEFAULTS = ['literal' => false, 'buildtime' => true, 'runtime' => true];
|
||||
|
||||
public const BUILDPACK_CONTROL_VARIABLE_PREFIXES = ['NIXPACKS_', 'RAILPACK_'];
|
||||
|
||||
/**
|
||||
@@ -101,6 +108,7 @@ class EnvironmentVariable extends BaseModel
|
||||
'value' => 'encrypted',
|
||||
'preserve_compose_null' => 'boolean',
|
||||
'is_multiline' => 'boolean',
|
||||
'is_literal' => 'boolean',
|
||||
'is_preview' => 'boolean',
|
||||
'is_runtime' => 'boolean',
|
||||
'is_buildtime' => 'boolean',
|
||||
|
||||
@@ -11,6 +11,14 @@ class SharedEnvironmentVariable extends Model
|
||||
{
|
||||
use Auditable;
|
||||
|
||||
/**
|
||||
* Flags that a "# coolify:" line of the developer view can set, with their defaults.
|
||||
* Shared variables have no buildtime or runtime flags.
|
||||
*
|
||||
* @var array<string, bool>
|
||||
*/
|
||||
public const DEV_VIEW_FLAG_DEFAULTS = ['literal' => false];
|
||||
|
||||
protected $fillable = [
|
||||
// Core identification
|
||||
'key',
|
||||
@@ -40,6 +48,7 @@ class SharedEnvironmentVariable extends Model
|
||||
protected $casts = [
|
||||
'key' => 'string',
|
||||
'value' => 'encrypted',
|
||||
'is_literal' => 'boolean',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -1284,6 +1284,97 @@ function formatEnvValueForDevView(?string $value): ?string
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a variable for the developer view: its KEY=value line, with a "# coolify:" directive
|
||||
* line above it when a flag is not its default. A variable that the view cannot edit gets a
|
||||
* placeholder line and no directive.
|
||||
*/
|
||||
function formatEnvVariableForDevView(EnvironmentVariable|SharedEnvironmentVariable $variable, bool $canViewValue): string
|
||||
{
|
||||
if (! $canViewValue) {
|
||||
return "{$variable->key}=(Hidden, only admins can view)";
|
||||
}
|
||||
if ($variable->is_shown_once) {
|
||||
return "{$variable->key}=(Locked Secret, delete and add again to change)";
|
||||
}
|
||||
if ($variable->is_multiline) {
|
||||
return "{$variable->key}=(Multiline environment variable, edit in normal view)";
|
||||
}
|
||||
$value = formatEnvValueForDevView($variable->value);
|
||||
if ($value === null) {
|
||||
return "{$variable->key}=(Value cannot be shown in this view, edit in normal view)";
|
||||
}
|
||||
|
||||
$flags = [];
|
||||
foreach ($variable::DEV_VIEW_FLAG_DEFAULTS as $flag => $default) {
|
||||
if ((bool) $variable->{"is_{$flag}"} !== $default) {
|
||||
$flags[] = $default ? "{$flag}=false" : $flag;
|
||||
}
|
||||
}
|
||||
$line = "{$variable->key}={$value}";
|
||||
|
||||
return $flags === [] ? $line : '# coolify: '.implode(' ', $flags)."\n".$line;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse developer view text. Returns the result of parseEnvFormatToArray() with a 'settings'
|
||||
* entry per key: the flags of the "# coolify:" line directly above the key line, as model
|
||||
* attributes (for example ['is_literal' => true]). A key without a directive gets empty
|
||||
* settings, so its current flags stay.
|
||||
*
|
||||
* @param array<string, bool> $flagDefaults the allowed flags and their defaults
|
||||
* @return array<string, array{value: string, comment: ?string, settings: array<string, bool>}>
|
||||
*
|
||||
* @throws InvalidArgumentException when a directive is not directly above a KEY=value line, or has an unknown flag or value
|
||||
*/
|
||||
function parseEnvDevView(?string $contents, array $flagDefaults): array
|
||||
{
|
||||
$contents ??= '';
|
||||
$settingsByKey = [];
|
||||
$directive = null;
|
||||
$directiveLine = 0;
|
||||
|
||||
foreach (explode("\n", $contents) as $index => $line) {
|
||||
$isKeyLine = $line !== '' && ! str_starts_with($line, '#') && str_contains($line, '=');
|
||||
if ($directive !== null && ! $isKeyLine) {
|
||||
throw new InvalidArgumentException("Line {$directiveLine}: a \"# coolify:\" line must be directly above a KEY=value line.");
|
||||
}
|
||||
|
||||
if ($isKeyLine) {
|
||||
$settingsByKey[substr($line, 0, strpos($line, '='))] = $directive ?? [];
|
||||
$directive = null;
|
||||
} elseif (str_starts_with($line, '# coolify:')) {
|
||||
$directiveLine = $index + 1;
|
||||
$flags = preg_split('/\s+/', trim(substr($line, strlen('# coolify:'))), -1, PREG_SPLIT_NO_EMPTY);
|
||||
if ($flags === []) {
|
||||
throw new InvalidArgumentException("Line {$directiveLine}: the \"# coolify:\" line has no flags.");
|
||||
}
|
||||
|
||||
$directive = [];
|
||||
foreach ($flags as $flag) {
|
||||
[$name, $value] = array_pad(explode('=', $flag, 2), 2, 'true');
|
||||
if (! array_key_exists($name, $flagDefaults)) {
|
||||
throw new InvalidArgumentException("Line {$directiveLine}: unknown flag \"{$name}\". Allowed flags: ".implode(', ', array_keys($flagDefaults)).'.');
|
||||
}
|
||||
if ($value !== 'true' && $value !== 'false') {
|
||||
throw new InvalidArgumentException("Line {$directiveLine}: the value of \"{$name}\" must be true or false.");
|
||||
}
|
||||
$directive["is_{$name}"] = $value === 'true';
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($directive !== null) {
|
||||
throw new InvalidArgumentException("Line {$directiveLine}: a \"# coolify:\" line must be directly above a KEY=value line.");
|
||||
}
|
||||
|
||||
$variables = parseEnvFormatToArray($contents);
|
||||
foreach ($variables as $key => $data) {
|
||||
$variables[$key]['settings'] = $settingsByKey[$key] ?? [];
|
||||
}
|
||||
|
||||
return $variables;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract inline comments from environment variables in raw docker-compose YAML.
|
||||
*
|
||||
|
||||
@@ -156,8 +156,12 @@
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
<form wire:submit="submit" class="p-4">
|
||||
<form wire:submit="submit" class="flex flex-col gap-4 p-4">
|
||||
<x-unsaved-bar action="submit" />
|
||||
<x-callout type="info" title="Note">
|
||||
To make a variable literal, put <code class="font-mono"># coolify: literal</code> directly above
|
||||
its line. Without this line, the flag does not change.
|
||||
</x-callout>
|
||||
<x-forms.textarea canGate="update" :canResource="$resource" rows="20"
|
||||
class="whitespace-pre-wrap" id="variables" wire:model="variables" monospace
|
||||
:label="$variablesLabel" />
|
||||
|
||||
@@ -69,6 +69,8 @@
|
||||
@can('manageEnvironment', $resource)
|
||||
<x-callout type="info" title="Note">
|
||||
Inline comments with space before # (e.g., <code class="font-mono">KEY=value #comment</code>) are stripped.
|
||||
To set flags, put <code class="font-mono"># coolify: literal buildtime=false runtime=false</code>
|
||||
directly above a variable line. Without this line, the flags of a variable do not change.
|
||||
</x-callout>
|
||||
<x-forms.textarea rows="10" class="whitespace-pre-wrap font-sans" id="variables"
|
||||
wire:model="variables" label="Production"></x-forms.textarea>
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Project\Shared\EnvironmentVariable\All;
|
||||
use App\Livewire\SharedVariables\Team\Index;
|
||||
use App\Models\Application;
|
||||
use App\Models\Environment;
|
||||
use App\Models\EnvironmentVariable;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Project;
|
||||
use App\Models\SharedEnvironmentVariable;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
InstanceSettings::forceCreate(['id' => 0]);
|
||||
|
||||
$this->user = User::factory()->create();
|
||||
$this->team = Team::factory()->create();
|
||||
$this->team->members()->attach($this->user, ['role' => 'owner']);
|
||||
$this->project = Project::factory()->create(['team_id' => $this->team->id]);
|
||||
$this->environment = Environment::factory()->create(['project_id' => $this->project->id]);
|
||||
$this->application = Application::factory()->create(['environment_id' => $this->environment->id]);
|
||||
EnvironmentVariable::query()->delete();
|
||||
|
||||
$this->actingAs($this->user);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$this->createVariable = fn (string $key, array $attributes = []) => EnvironmentVariable::create([
|
||||
'key' => $key,
|
||||
'value' => 'value',
|
||||
'resourceable_type' => Application::class,
|
||||
'resourceable_id' => $this->application->id,
|
||||
...$attributes,
|
||||
]);
|
||||
$this->flags = fn (string $key) => $this->application->environment_variables()->where('key', $key)->firstOrFail()
|
||||
->only(['value', 'is_literal', 'is_buildtime', 'is_runtime']);
|
||||
$this->devView = fn () => Livewire::test(All::class, ['resource' => $this->application])
|
||||
->call('loadEnvironmentVariables')
|
||||
->call('switch');
|
||||
});
|
||||
|
||||
it('shows directives for non-default flags and keeps flags when saved without edits', function () {
|
||||
($this->createVariable)('CUSTOM', ['is_literal' => true, 'is_buildtime' => false]);
|
||||
($this->createVariable)('PLAIN');
|
||||
|
||||
$component = ($this->devView)();
|
||||
|
||||
expect($component->get('variables'))->toBe("# coolify: literal buildtime=false\nCUSTOM=value\nPLAIN=value");
|
||||
|
||||
$component->call('submit')->assertNotDispatched('error');
|
||||
|
||||
expect(($this->flags)('CUSTOM'))->toBe(['value' => 'value', 'is_literal' => true, 'is_buildtime' => false, 'is_runtime' => true])
|
||||
->and(($this->flags)('PLAIN'))->toBe(['value' => 'value', 'is_literal' => false, 'is_buildtime' => true, 'is_runtime' => true]);
|
||||
});
|
||||
|
||||
it('sets flags from an added directive and resets them with explicit values', function () {
|
||||
($this->createVariable)('A');
|
||||
($this->createVariable)('B', ['is_literal' => true, 'is_runtime' => false]);
|
||||
|
||||
($this->devView)()
|
||||
->set('variables', "# coolify: literal\nA=value\n# coolify: literal=false runtime=true\nB=value")
|
||||
->call('submit')
|
||||
->assertNotDispatched('error');
|
||||
|
||||
expect(($this->flags)('A'))->toBe(['value' => 'value', 'is_literal' => true, 'is_buildtime' => true, 'is_runtime' => true])
|
||||
->and(($this->flags)('B'))->toBe(['value' => 'value', 'is_literal' => false, 'is_buildtime' => true, 'is_runtime' => true]);
|
||||
});
|
||||
|
||||
it('keeps flags of existing variables when a plain env file without directives is saved', function () {
|
||||
($this->createVariable)('A', ['is_literal' => true, 'is_buildtime' => false, 'is_runtime' => false]);
|
||||
|
||||
($this->devView)()
|
||||
->set('variables', "A=changed\nNEW=new")
|
||||
->call('submit')
|
||||
->assertNotDispatched('error');
|
||||
|
||||
expect(($this->flags)('A'))->toBe(['value' => 'changed', 'is_literal' => true, 'is_buildtime' => false, 'is_runtime' => false])
|
||||
->and(($this->flags)('NEW'))->toBe(['value' => 'new', 'is_literal' => false, 'is_buildtime' => true, 'is_runtime' => true]);
|
||||
});
|
||||
|
||||
it('creates a new variable with the flags of its directive', function () {
|
||||
($this->devView)()
|
||||
->set('variables', "# coolify: literal buildtime=false\nNEW=new")
|
||||
->call('submit')
|
||||
->assertNotDispatched('error');
|
||||
|
||||
expect(($this->flags)('NEW'))->toBe(['value' => 'new', 'is_literal' => true, 'is_buildtime' => false, 'is_runtime' => true]);
|
||||
});
|
||||
|
||||
it('rejects an invalid directive, changes nothing, and keeps the text', function () {
|
||||
($this->createVariable)('A');
|
||||
$text = "A=changed\nNEW=new\n# coolify: literal";
|
||||
|
||||
$component = ($this->devView)()
|
||||
->set('variables', $text)
|
||||
->call('submit')
|
||||
->assertDispatched('error', 'Line 3: a "# coolify:" line must be directly above a KEY=value line.');
|
||||
|
||||
expect($component->get('variables'))->toBe($text)
|
||||
->and($this->application->environment_variables()->pluck('value', 'key')->all())->toBe(['A' => 'value']);
|
||||
});
|
||||
|
||||
it('ignores a directive above a locked variable', function () {
|
||||
($this->createVariable)('LOCKED', ['is_shown_once' => true]);
|
||||
|
||||
$component = ($this->devView)();
|
||||
expect($component->get('variables'))->toBe('LOCKED=(Locked Secret, delete and add again to change)');
|
||||
|
||||
$component->set('variables', "# coolify: literal\nLOCKED=(Locked Secret, delete and add again to change)")
|
||||
->call('submit')
|
||||
->assertNotDispatched('error');
|
||||
|
||||
expect(($this->flags)('LOCKED'))->toBe(['value' => 'value', 'is_literal' => false, 'is_buildtime' => true, 'is_runtime' => true]);
|
||||
});
|
||||
|
||||
it('applies the literal directive to team shared variables and rejects buildtime', function () {
|
||||
SharedEnvironmentVariable::create(['key' => 'A', 'value' => 'value', 'type' => 'team', 'team_id' => $this->team->id]);
|
||||
|
||||
$component = Livewire::test(Index::class)
|
||||
->call('switch')
|
||||
->set('variables', "# coolify: literal\nA=value\n# coolify: literal\nNEW=new")
|
||||
->call('submit')
|
||||
->assertNotDispatched('error');
|
||||
|
||||
expect($this->team->environment_variables()->pluck('is_literal', 'key')->all())->toBe(['A' => true, 'NEW' => true])
|
||||
->and($component->get('variables'))->toBe("# coolify: literal\nA=value\n# coolify: literal\nNEW=new");
|
||||
|
||||
$component->set('variables', "# coolify: buildtime=false\nA=value")
|
||||
->call('submit')
|
||||
->assertDispatched('error', 'Line 1: unknown flag "buildtime". Allowed flags: literal.');
|
||||
|
||||
expect($this->team->environment_variables()->count())->toBe(2);
|
||||
});
|
||||
|
||||
it('names the preview text area in a directive error', function () {
|
||||
expect($this->application->git_repository)->not->toBeEmpty();
|
||||
|
||||
($this->devView)()
|
||||
->set('variablesPreview', "A=1\n# coolify: literal")
|
||||
->call('submit')
|
||||
->assertDispatched('error', 'Preview deployments: Line 2: a "# coolify:" line must be directly above a KEY=value line.');
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
use App\Models\EnvironmentVariable;
|
||||
use App\Models\SharedEnvironmentVariable;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class);
|
||||
|
||||
function devViewVariable(array $attributes): EnvironmentVariable
|
||||
{
|
||||
return (new EnvironmentVariable)->forceFill(['key' => 'KEY', 'value' => 'value', ...$attributes]);
|
||||
}
|
||||
|
||||
it('formats a directive line only for flags that are not their default', function (array $flags, string $expected) {
|
||||
expect(formatEnvVariableForDevView(devViewVariable($flags), true))->toBe($expected);
|
||||
})->with([
|
||||
'defaults' => [['is_literal' => false, 'is_buildtime' => true, 'is_runtime' => true], 'KEY=value'],
|
||||
'literal' => [['is_literal' => true, 'is_buildtime' => true, 'is_runtime' => true], "# coolify: literal\nKEY=value"],
|
||||
'all non-default' => [['is_literal' => true, 'is_buildtime' => false, 'is_runtime' => false], "# coolify: literal buildtime=false runtime=false\nKEY=value"],
|
||||
]);
|
||||
|
||||
it('formats only the literal flag for shared variables', function () {
|
||||
$variable = (new SharedEnvironmentVariable)->forceFill(['key' => 'KEY', 'value' => 'value', 'is_literal' => 1]);
|
||||
|
||||
expect(formatEnvVariableForDevView($variable, true))->toBe("# coolify: literal\nKEY=value");
|
||||
});
|
||||
|
||||
it('formats no directive above placeholder lines', function (array $attributes, string $expected, bool $canViewValue = true) {
|
||||
expect(formatEnvVariableForDevView(devViewVariable(['is_literal' => true, ...$attributes]), $canViewValue))->toBe($expected);
|
||||
})->with([
|
||||
'hidden' => [[], 'KEY=(Hidden, only admins can view)', false],
|
||||
'locked' => [['is_shown_once' => true], 'KEY=(Locked Secret, delete and add again to change)'],
|
||||
'multiline' => [['is_multiline' => true], 'KEY=(Multiline environment variable, edit in normal view)'],
|
||||
'both quote types' => [['value' => '\'"a" b\''], 'KEY=(Value cannot be shown in this view, edit in normal view)'],
|
||||
]);
|
||||
|
||||
it('reads back the flags and values that it formats', function (string $value, array $flags) {
|
||||
$text = formatEnvVariableForDevView(devViewVariable(['value' => $value, ...$flags]), true)
|
||||
."\n".formatEnvVariableForDevView(devViewVariable(['key' => 'OTHER', 'value' => 'x']), true);
|
||||
|
||||
$parsed = parseEnvDevView($text, EnvironmentVariable::DEV_VIEW_FLAG_DEFAULTS);
|
||||
|
||||
expect($parsed['KEY']['value'])->toBe($value)
|
||||
->and([...$flags, ...$parsed['KEY']['settings']])->toBe($flags)
|
||||
->and($parsed['OTHER']['settings'])->toBe([]);
|
||||
})->with([
|
||||
'plain' => 'plain',
|
||||
'double quoted' => '"abc"',
|
||||
'single quoted' => "'\$FOO'",
|
||||
'hash' => 'value #not-a-comment',
|
||||
])->with([
|
||||
'defaults' => [['is_literal' => false, 'is_buildtime' => true, 'is_runtime' => true]],
|
||||
'literal' => [['is_literal' => true, 'is_buildtime' => true, 'is_runtime' => true]],
|
||||
'all non-default' => [['is_literal' => true, 'is_buildtime' => false, 'is_runtime' => false]],
|
||||
]);
|
||||
|
||||
it('parses explicit and bare directive values', function () {
|
||||
$parsed = parseEnvDevView(
|
||||
"# coolify: literal=false buildtime runtime=true\nA=1 # a comment\n# other comment\nB=2",
|
||||
EnvironmentVariable::DEV_VIEW_FLAG_DEFAULTS,
|
||||
);
|
||||
|
||||
expect($parsed)->toBe([
|
||||
'A' => ['value' => '1', 'comment' => 'a comment', 'settings' => ['is_literal' => false, 'is_buildtime' => true, 'is_runtime' => true]],
|
||||
'B' => ['value' => '2', 'comment' => null, 'settings' => []],
|
||||
]);
|
||||
});
|
||||
|
||||
it('rejects invalid directives', function (string $text, string $message) {
|
||||
expect(fn () => parseEnvDevView($text, EnvironmentVariable::DEV_VIEW_FLAG_DEFAULTS))
|
||||
->toThrow(InvalidArgumentException::class, $message);
|
||||
})->with([
|
||||
'last line' => ["A=1\n# coolify: literal", 'Line 2: a "# coolify:" line must be directly above a KEY=value line.'],
|
||||
'blank line before key' => ["# coolify: literal\n\nA=1", 'Line 1: a "# coolify:" line must be directly above a KEY=value line.'],
|
||||
'comment before key' => ["# coolify: literal\n# note\nA=1", 'Line 1: a "# coolify:" line must be directly above a KEY=value line.'],
|
||||
'two directives' => ["# coolify: literal\n# coolify: runtime=false\nA=1", 'Line 1: a "# coolify:" line must be directly above a KEY=value line.'],
|
||||
'no flags' => ["# coolify:\nA=1", 'Line 1: the "# coolify:" line has no flags.'],
|
||||
'unknown flag' => ["# coolify: multiline\nA=1", 'Line 1: unknown flag "multiline". Allowed flags: literal, buildtime, runtime.'],
|
||||
'bad value' => ["# coolify: literal=yes\nA=1", 'Line 1: the value of "literal" must be true or false.'],
|
||||
]);
|
||||
|
||||
it('accepts only the literal flag for shared variables', function () {
|
||||
expect(parseEnvDevView("# coolify: literal\nA=1", SharedEnvironmentVariable::DEV_VIEW_FLAG_DEFAULTS)['A']['settings'])->toBe(['is_literal' => true])
|
||||
->and(fn () => parseEnvDevView("# coolify: buildtime=false\nA=1", SharedEnvironmentVariable::DEV_VIEW_FLAG_DEFAULTS))
|
||||
->toThrow(InvalidArgumentException::class, 'Line 1: unknown flag "buildtime". Allowed flags: literal.');
|
||||
});
|
||||
|
||||
it('keeps ignoring directive lines in parseEnvFormatToArray', function () {
|
||||
expect(parseEnvFormatToArray("# coolify: literal\nA=1\n# coolify: bogus"))
|
||||
->toBe(['A' => ['value' => '1', 'comment' => null]]);
|
||||
});
|
||||
Reference in New Issue
Block a user