mirror of
https://github.com/coollabsio/coolify.git
synced 2026-03-11 08:55:47 +00:00
feat: load dotenv example
This commit is contained in:
parent
b7e0f5577d
commit
5faeac4e2e
3 changed files with 274 additions and 0 deletions
|
|
@ -6,6 +6,7 @@ use App\Models\EnvironmentVariable;
|
|||
use App\Traits\EnvironmentVariableProtection;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class All extends Component
|
||||
{
|
||||
|
|
@ -311,4 +312,171 @@ class All extends Component
|
|||
unset($this->environmentVariablesPreview);
|
||||
$this->getDevView();
|
||||
}
|
||||
|
||||
public function loadFromEnvExample()
|
||||
{
|
||||
try {
|
||||
$this->authorize('manageEnvironment', $this->resource);
|
||||
|
||||
if ($this->resourceClass !== 'App\Models\Application') {
|
||||
throw new \RuntimeException('This feature is only available for applications.');
|
||||
}
|
||||
|
||||
if (! $this->canLoadEnvExample) {
|
||||
throw new \RuntimeException('This feature requires a git repository source.');
|
||||
}
|
||||
|
||||
$envExampleContent = $this->fetchEnvExampleFromGit();
|
||||
|
||||
if ($envExampleContent === null) {
|
||||
$this->dispatch('error', 'No .env.example file found in the repository.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$variables = parseEnvFormatToArray($envExampleContent);
|
||||
|
||||
if (empty($variables)) {
|
||||
$this->dispatch('warning', 'The .env.example file is empty or contains no valid variables.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$addedCount = 0;
|
||||
$skippedCount = 0;
|
||||
$maxOrder = $this->resource->environment_variables()->max('order') ?? 0;
|
||||
|
||||
foreach ($variables as $key => $value) {
|
||||
if (str($key)->startsWith('SERVICE_FQDN') ||
|
||||
str($key)->startsWith('SERVICE_URL') ||
|
||||
str($key)->startsWith('SERVICE_NAME')) {
|
||||
$skippedCount++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$exists = $this->resource->environment_variables()->where('key', $key)->exists();
|
||||
if ($exists) {
|
||||
$skippedCount++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$environment = new EnvironmentVariable;
|
||||
$environment->key = $key;
|
||||
$environment->value = $value;
|
||||
$environment->is_multiline = false;
|
||||
$environment->is_preview = false;
|
||||
$environment->is_runtime = true;
|
||||
$environment->is_buildtime = true;
|
||||
$environment->resourceable_id = $this->resource->id;
|
||||
$environment->resourceable_type = $this->resource->getMorphClass();
|
||||
$environment->order = ++$maxOrder;
|
||||
$environment->save();
|
||||
|
||||
$addedCount++;
|
||||
}
|
||||
|
||||
$this->refreshEnvs();
|
||||
|
||||
if ($addedCount > 0) {
|
||||
$message = "Added {$addedCount} environment variable(s)";
|
||||
if ($skippedCount > 0) {
|
||||
$message .= ", skipped {$skippedCount} existing";
|
||||
}
|
||||
$this->dispatch('success', $message.'.');
|
||||
} else {
|
||||
$this->dispatch('warning', 'All variables from .env.example already exist.');
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
private function fetchEnvExampleFromGit(): ?string
|
||||
{
|
||||
$uuid = (string) new Cuid2;
|
||||
|
||||
['commands' => $cloneCommand] = $this->resource->generateGitImportCommands(
|
||||
deployment_uuid: $uuid,
|
||||
only_checkout: true,
|
||||
exec_in_docker: false,
|
||||
custom_base_dir: '.'
|
||||
);
|
||||
|
||||
$workdir = rtrim($this->resource->base_directory ?? '', '/');
|
||||
$envFile = '.env.example';
|
||||
$fileList = collect([".$workdir/$envFile"]);
|
||||
|
||||
$server = $this->resource->destination->server;
|
||||
|
||||
$getGitVersion = instant_remote_process(['git --version'], $server, false);
|
||||
$gitVersion = str($getGitVersion)->explode(' ')->last();
|
||||
|
||||
if (version_compare($gitVersion, '2.35.1', '<')) {
|
||||
$fileList = $fileList->map(function ($file) {
|
||||
$parts = explode('/', trim($file, '.'));
|
||||
$paths = collect();
|
||||
$currentPath = '';
|
||||
foreach ($parts as $part) {
|
||||
$currentPath .= ($currentPath ? '/' : '').$part;
|
||||
if (str($currentPath)->isNotEmpty()) {
|
||||
$paths->push($currentPath);
|
||||
}
|
||||
}
|
||||
|
||||
return $paths;
|
||||
})->flatten()->unique()->values();
|
||||
|
||||
$commands = collect([
|
||||
"rm -rf /tmp/{$uuid}",
|
||||
"mkdir -p /tmp/{$uuid}",
|
||||
"cd /tmp/{$uuid}",
|
||||
$cloneCommand,
|
||||
'git sparse-checkout init',
|
||||
"git sparse-checkout set {$fileList->implode(' ')}",
|
||||
'git read-tree -mu HEAD',
|
||||
"cat .$workdir/$envFile",
|
||||
]);
|
||||
} else {
|
||||
$commands = collect([
|
||||
"rm -rf /tmp/{$uuid}",
|
||||
"mkdir -p /tmp/{$uuid}",
|
||||
"cd /tmp/{$uuid}",
|
||||
$cloneCommand,
|
||||
'git sparse-checkout init --cone',
|
||||
"git sparse-checkout set {$fileList->implode(' ')}",
|
||||
'git read-tree -mu HEAD',
|
||||
"cat .$workdir/$envFile",
|
||||
]);
|
||||
}
|
||||
|
||||
try {
|
||||
$content = instant_remote_process($commands, $server);
|
||||
|
||||
return $content;
|
||||
} catch (\Exception $e) {
|
||||
if (str($e->getMessage())->contains('No such file')) {
|
||||
return null;
|
||||
}
|
||||
throw $e;
|
||||
} finally {
|
||||
instant_remote_process(["rm -rf /tmp/{$uuid}"], $server, false);
|
||||
}
|
||||
}
|
||||
|
||||
public function getCanLoadEnvExampleProperty(): bool
|
||||
{
|
||||
if ($this->resourceClass !== 'App\Models\Application') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filled(data_get($this->resource, 'dockerfile'))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$deploymentType = $this->resource->deploymentType();
|
||||
|
||||
return in_array($deploymentType, ['source', 'deploy_key']);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,12 @@
|
|||
<livewire:project.shared.environment-variable.add />
|
||||
</x-modal-input>
|
||||
</div>
|
||||
@if ($this->canLoadEnvExample)
|
||||
<x-forms.button wire:click='loadFromEnvExample'
|
||||
title="Import variables from .env.example in your repository">
|
||||
Load from .env.example
|
||||
</x-forms.button>
|
||||
@endif
|
||||
<x-forms.button
|
||||
wire:click='switch'>{{ $view === 'normal' ? 'Developer view' : 'Normal view' }}</x-forms.button>
|
||||
@endcan
|
||||
|
|
|
|||
100
tests/Unit/Livewire/EnvironmentVariableLoadFromExampleTest.php
Normal file
100
tests/Unit/Livewire/EnvironmentVariableLoadFromExampleTest.php
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
<?php
|
||||
|
||||
use App\Livewire\Project\Shared\EnvironmentVariable\All;
|
||||
|
||||
it('canLoadEnvExample returns false for non-Application resources', function () {
|
||||
$component = new All;
|
||||
$component->resourceClass = 'App\Models\Service';
|
||||
|
||||
expect($component->getCanLoadEnvExampleProperty())->toBeFalse();
|
||||
});
|
||||
|
||||
it('canLoadEnvExample returns false for StandalonePostgresql resources', function () {
|
||||
$component = new All;
|
||||
$component->resourceClass = 'App\Models\StandalonePostgresql';
|
||||
|
||||
expect($component->getCanLoadEnvExampleProperty())->toBeFalse();
|
||||
});
|
||||
|
||||
it('canLoadEnvExample returns false for dockerfile applications', function () {
|
||||
$mockApplication = Mockery::mock('App\Models\Application')->shouldIgnoreMissing();
|
||||
$mockApplication->shouldReceive('getAttribute')
|
||||
->with('dockerfile')
|
||||
->andReturn('FROM alpine');
|
||||
|
||||
$component = new All;
|
||||
$component->resourceClass = 'App\Models\Application';
|
||||
$component->resource = $mockApplication;
|
||||
|
||||
expect($component->getCanLoadEnvExampleProperty())->toBeFalse();
|
||||
});
|
||||
|
||||
it('canLoadEnvExample returns true for source deployment type', function () {
|
||||
$mockApplication = Mockery::mock('App\Models\Application')->shouldIgnoreMissing();
|
||||
$mockApplication->shouldReceive('getAttribute')
|
||||
->with('dockerfile')
|
||||
->andReturn(null);
|
||||
$mockApplication->shouldReceive('deploymentType')
|
||||
->andReturn('source');
|
||||
|
||||
$component = new All;
|
||||
$component->resourceClass = 'App\Models\Application';
|
||||
$component->resource = $mockApplication;
|
||||
|
||||
expect($component->getCanLoadEnvExampleProperty())->toBeTrue();
|
||||
});
|
||||
|
||||
it('canLoadEnvExample returns true for deploy_key deployment type', function () {
|
||||
$mockApplication = Mockery::mock('App\Models\Application')->shouldIgnoreMissing();
|
||||
$mockApplication->shouldReceive('getAttribute')
|
||||
->with('dockerfile')
|
||||
->andReturn(null);
|
||||
$mockApplication->shouldReceive('deploymentType')
|
||||
->andReturn('deploy_key');
|
||||
|
||||
$component = new All;
|
||||
$component->resourceClass = 'App\Models\Application';
|
||||
$component->resource = $mockApplication;
|
||||
|
||||
expect($component->getCanLoadEnvExampleProperty())->toBeTrue();
|
||||
});
|
||||
|
||||
it('canLoadEnvExample returns false for other deployment types', function () {
|
||||
$mockApplication = Mockery::mock('App\Models\Application')->shouldIgnoreMissing();
|
||||
$mockApplication->shouldReceive('getAttribute')
|
||||
->with('dockerfile')
|
||||
->andReturn(null);
|
||||
$mockApplication->shouldReceive('deploymentType')
|
||||
->andReturn('dockerimage');
|
||||
|
||||
$component = new All;
|
||||
$component->resourceClass = 'App\Models\Application';
|
||||
$component->resource = $mockApplication;
|
||||
|
||||
expect($component->getCanLoadEnvExampleProperty())->toBeFalse();
|
||||
});
|
||||
|
||||
it('loadFromEnvExample method exists on All component', function () {
|
||||
$component = new All;
|
||||
|
||||
expect(method_exists($component, 'loadFromEnvExample'))->toBeTrue();
|
||||
});
|
||||
|
||||
it('parseEnvFormatToArray correctly parses env file format', function () {
|
||||
$content = <<<'ENV'
|
||||
# Comment line
|
||||
DATABASE_URL=postgres://localhost:5432
|
||||
APP_NAME="My App"
|
||||
EMPTY_VALUE=
|
||||
QUOTED='single quoted'
|
||||
ENV;
|
||||
|
||||
$result = parseEnvFormatToArray($content);
|
||||
|
||||
expect($result)->toBeArray()
|
||||
->and($result['DATABASE_URL'])->toBe('postgres://localhost:5432')
|
||||
->and($result['APP_NAME'])->toBe('My App')
|
||||
->and($result['EMPTY_VALUE'])->toBe('')
|
||||
->and($result['QUOTED'])->toBe('single quoted')
|
||||
->and($result)->not->toHaveKey('# Comment line');
|
||||
});
|
||||
Loading…
Reference in a new issue