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 App\Traits\EnvironmentVariableProtection;
|
||||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
use Visus\Cuid2\Cuid2;
|
||||||
|
|
||||||
class All extends Component
|
class All extends Component
|
||||||
{
|
{
|
||||||
|
|
@ -311,4 +312,171 @@ class All extends Component
|
||||||
unset($this->environmentVariablesPreview);
|
unset($this->environmentVariablesPreview);
|
||||||
$this->getDevView();
|
$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 />
|
<livewire:project.shared.environment-variable.add />
|
||||||
</x-modal-input>
|
</x-modal-input>
|
||||||
</div>
|
</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
|
<x-forms.button
|
||||||
wire:click='switch'>{{ $view === 'normal' ? 'Developer view' : 'Normal view' }}</x-forms.button>
|
wire:click='switch'>{{ $view === 'normal' ? 'Developer view' : 'Normal view' }}</x-forms.button>
|
||||||
@endcan
|
@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