mirror of
https://github.com/coollabsio/coolify.git
synced 2026-10-28 09:34:36 +08:00
275 lines
9.5 KiB
PHP
275 lines
9.5 KiB
PHP
<?php
|
|
|
|
namespace App\Traits;
|
|
|
|
use App\Data\RepositoryDetectionResult;
|
|
use App\Models\Application;
|
|
use App\Models\ApplicationSetting;
|
|
use App\Models\EnvironmentVariable;
|
|
use App\Models\GiteaApp;
|
|
use App\Models\GithubApp;
|
|
use App\Models\GitlabApp;
|
|
use App\Models\PrivateKey;
|
|
use App\Rules\ValidGitBranch;
|
|
use App\Services\RepositoryDetector;
|
|
use App\Support\ValidationPatterns;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\Support\Facades\Validator;
|
|
use Livewire\Attributes\Locked;
|
|
|
|
/**
|
|
* @property string $build_pack
|
|
* @property int $port
|
|
* @property string|null $docker_compose_location
|
|
*
|
|
* @method void updatedBuildPack()
|
|
*/
|
|
trait HasRepositoryDetection
|
|
{
|
|
public bool $detectionRan = false;
|
|
|
|
public bool $detectionFailed = false;
|
|
|
|
/**
|
|
* Build pack that the scan selected from a file in the base directory.
|
|
*/
|
|
#[Locked]
|
|
public ?string $suggestedBuildPack = null;
|
|
|
|
#[Locked]
|
|
public array $detectedDockerfiles = [];
|
|
|
|
#[Locked]
|
|
public array $detectedDockerComposeFiles = [];
|
|
|
|
public ?string $selectedDockerfile = null;
|
|
|
|
public ?string $selectedDockerComposeFile = null;
|
|
|
|
public ?int $detectedPort = null;
|
|
|
|
#[Locked]
|
|
public array $dockerfilePorts = [];
|
|
|
|
#[Locked]
|
|
public array $detectedEnvFiles = [];
|
|
|
|
public ?string $selectedEnvFile = null;
|
|
|
|
#[Locked]
|
|
public array $parsedEnvFiles = [];
|
|
|
|
public array $envExampleVars = [];
|
|
|
|
public bool $envImported = false;
|
|
|
|
/**
|
|
* Unsaved application with the repository, branch, and Git source or deploy key to scan.
|
|
*/
|
|
abstract protected function applicationForDetection(): Application;
|
|
|
|
public function detectRepository(): void
|
|
{
|
|
$this->authorize('create', Application::class);
|
|
try {
|
|
$application = $this->applicationForDetection();
|
|
} catch (\Throwable $e) {
|
|
handleError($e, $this);
|
|
|
|
return;
|
|
}
|
|
Validator::make([
|
|
'branch' => $application->git_branch,
|
|
'base_directory' => $this->base_directory ?? '/',
|
|
], [
|
|
'branch' => ['required', new ValidGitBranch],
|
|
'base_directory' => ['required', 'string', 'regex:~^/?(?:[a-zA-Z0-9_.-]+/)*[a-zA-Z0-9_.-]*$~', 'not_regex:~(^|/)\.\.?(/|$)~'],
|
|
])->validate();
|
|
|
|
$destination = find_resource_destination_for_current_team(data_get($this->query, 'destination'));
|
|
if (! $destination) {
|
|
$this->dispatch('error', 'Destination not found.');
|
|
|
|
return;
|
|
}
|
|
$this->authorize('update', $destination->server);
|
|
|
|
$this->detectionRan = false;
|
|
$this->detectionFailed = false;
|
|
$this->applyDetectionResult(RepositoryDetectionResult::none());
|
|
try {
|
|
$detector = new RepositoryDetector($application, $this->base_directory ?? '/', $destination->server);
|
|
$this->applyDetectionResult($detector->detect());
|
|
} catch (\Throwable $e) {
|
|
// The exception can contain the clone command with a Git token, so do not log the message.
|
|
Log::debug('Repository detection failed', ['exception' => $e::class]);
|
|
$this->detectionFailed = true;
|
|
$this->dispatch('error', 'Smart Scan could not read the repository. Check the repository access and the branch.');
|
|
}
|
|
$this->detectionRan = true;
|
|
}
|
|
|
|
/**
|
|
* Builds an application that is not saved, only to generate the clone command for the scan.
|
|
*/
|
|
protected function unsavedApplicationForDetection(string $repository, string $branch, GithubApp|GitlabApp|GiteaApp|null $source = null, ?PrivateKey $privateKey = null): Application
|
|
{
|
|
$application = (new Application)->forceFill([
|
|
'git_repository' => $repository,
|
|
'git_branch' => $branch,
|
|
'git_commit_sha' => 'HEAD',
|
|
'private_key_id' => $privateKey?->id,
|
|
]);
|
|
$application->setRelation('settings', (new ApplicationSetting)->forceFill([
|
|
'is_git_shallow_clone_enabled' => true,
|
|
'is_git_submodules_enabled' => false,
|
|
]));
|
|
$application->setRelation('source', $source);
|
|
$application->setRelation('private_key', $privateKey);
|
|
|
|
return $application;
|
|
}
|
|
|
|
protected function importDetectedEnvironmentVariables(Application $application): void
|
|
{
|
|
if (! $this->envImported || empty($this->envExampleVars)) {
|
|
return;
|
|
}
|
|
Validator::make(['variables' => $this->envExampleVars], [
|
|
'variables' => ['array'],
|
|
'variables.*' => ['nullable', 'string'],
|
|
])->validate();
|
|
$application->getConnection()->transaction(function () use ($application): void {
|
|
foreach ($this->envExampleVars as $key => $value) {
|
|
if (! preg_match('/^[A-Z_][A-Z0-9_]*$/i', (string) $key)) {
|
|
continue;
|
|
}
|
|
EnvironmentVariable::create([
|
|
'key' => $key,
|
|
'value' => $value,
|
|
'resourceable_type' => $application->getMorphClass(),
|
|
'resourceable_id' => $application->id,
|
|
'is_preview' => false,
|
|
]);
|
|
}
|
|
});
|
|
}
|
|
|
|
protected function applyDetectionResult(RepositoryDetectionResult $result): void
|
|
{
|
|
$this->detectedDockerfiles = $result->dockerfiles;
|
|
$this->detectedDockerComposeFiles = $result->dockerComposeFiles;
|
|
$this->dockerfilePorts = $result->dockerfilePorts;
|
|
|
|
$this->selectedDockerfile = null;
|
|
$this->selectedDockerComposeFile = null;
|
|
$this->detectedPort = null;
|
|
$this->detectedEnvFiles = [];
|
|
$this->parsedEnvFiles = [];
|
|
$this->selectedEnvFile = null;
|
|
$this->envExampleVars = [];
|
|
$this->envImported = false;
|
|
|
|
$this->suggestedBuildPack = $result->getSuggestedBuildPack()?->value;
|
|
if ($this->suggestedBuildPack && $this->suggestedBuildPack !== $this->build_pack) {
|
|
$this->build_pack = $this->suggestedBuildPack;
|
|
$this->updatedBuildPack();
|
|
}
|
|
|
|
if ($result->hasDockerfile()) {
|
|
$this->selectedDockerfile = $result->dockerfiles[0];
|
|
$port = $result->dockerfilePorts[$this->selectedDockerfile] ?? null;
|
|
$this->port = $port ?? 3000;
|
|
$this->detectedPort = $port;
|
|
}
|
|
|
|
if ($result->hasDockerCompose()) {
|
|
$this->selectedDockerComposeFile = $result->dockerComposeFiles[0];
|
|
$this->docker_compose_location = '/'.$this->selectedDockerComposeFile;
|
|
}
|
|
|
|
if ($result->hasEnvFiles()) {
|
|
$this->detectedEnvFiles = array_keys($result->envFiles);
|
|
$this->parsedEnvFiles = [];
|
|
foreach ($result->envFiles as $filename => $content) {
|
|
$parsed = $content !== null ? parseEnvFormatToArray($content) : [];
|
|
// parseEnvFormatToArray returns ['KEY' => ['value' => ..., 'comment' => ...]].
|
|
// Flatten to a plain ['KEY' => 'value'] map for the import form.
|
|
$this->parsedEnvFiles[$filename] = collect($parsed)
|
|
->map(fn ($item) => is_array($item) ? (string) ($item['value'] ?? '') : (string) $item)
|
|
->all();
|
|
}
|
|
$this->selectedEnvFile = $this->detectedEnvFiles[0];
|
|
$this->envExampleVars = $this->parsedEnvFiles[$this->selectedEnvFile] ?? [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Repository path without a leading slash, so a typed path matches the detected paths.
|
|
*/
|
|
protected function normalizeRepositoryFilePath(?string $path): ?string
|
|
{
|
|
$path = ltrim(trim((string) $path), '/');
|
|
|
|
return $path === '' ? null : $path;
|
|
}
|
|
|
|
/**
|
|
* Dockerfile location for the new application: a detected file or a path the user typed.
|
|
*/
|
|
protected function selectedDockerfileLocation(): ?string
|
|
{
|
|
$path = $this->normalizeRepositoryFilePath($this->selectedDockerfile);
|
|
if ($path === null) {
|
|
return null;
|
|
}
|
|
$location = '/'.$path;
|
|
Validator::make(
|
|
['selectedDockerfile' => $location],
|
|
['selectedDockerfile' => ValidationPatterns::filePathRules()],
|
|
ValidationPatterns::filePathMessages('selectedDockerfile', 'Dockerfile'),
|
|
)->validate();
|
|
|
|
return $location;
|
|
}
|
|
|
|
public function updatedSelectedDockerfile(): void
|
|
{
|
|
$this->selectedDockerfile = $this->normalizeRepositoryFilePath($this->selectedDockerfile);
|
|
$this->resetErrorBag('selectedDockerfile');
|
|
$this->selectedDockerfileLocation();
|
|
|
|
$port = $this->dockerfilePorts[$this->selectedDockerfile ?? ''] ?? null;
|
|
$this->port = $port ?? 3000;
|
|
$this->detectedPort = $port;
|
|
}
|
|
|
|
public function updatedSelectedDockerComposeFile(): void
|
|
{
|
|
$this->selectedDockerComposeFile = $this->normalizeRepositoryFilePath($this->selectedDockerComposeFile);
|
|
|
|
if ($this->selectedDockerComposeFile) {
|
|
$this->docker_compose_location = '/'.$this->selectedDockerComposeFile;
|
|
}
|
|
}
|
|
|
|
public function updatedSelectedEnvFile(): void
|
|
{
|
|
if ($this->selectedEnvFile && isset($this->parsedEnvFiles[$this->selectedEnvFile])) {
|
|
$this->envExampleVars = $this->parsedEnvFiles[$this->selectedEnvFile];
|
|
$this->envImported = false;
|
|
}
|
|
}
|
|
|
|
public function confirmEnvImport(): void
|
|
{
|
|
$this->envImported = true;
|
|
}
|
|
|
|
public function clearEnvVars(): void
|
|
{
|
|
$this->envImported = false;
|
|
$this->envExampleVars = $this->parsedEnvFiles[$this->selectedEnvFile] ?? [];
|
|
}
|
|
}
|