Use find_destination_for_current_team helper across resource creation flows and the destination controller. Pass full destination objects to database creation helpers instead of UUIDs so team relationships are resolved consistently before the resource is created or linked. Add feature tests covering destination, backup storage, and resource proof lookups across teams.
377 lines
14 KiB
PHP
377 lines
14 KiB
PHP
<?php
|
|
|
|
namespace App\Livewire\Project\New;
|
|
|
|
use App\Models\Application;
|
|
use App\Models\GithubApp;
|
|
use App\Models\GitlabApp;
|
|
use App\Models\Project;
|
|
use App\Models\Service;
|
|
use App\Rules\ValidGitBranch;
|
|
use App\Rules\ValidGitRepositoryUrl;
|
|
use App\Support\ValidationPatterns;
|
|
use Carbon\Carbon;
|
|
use Livewire\Component;
|
|
use Spatie\Url\Url;
|
|
|
|
class PublicGitRepository extends Component
|
|
{
|
|
public string $repository_url;
|
|
|
|
public int $port = 3000;
|
|
|
|
public string $type;
|
|
|
|
public $parameters;
|
|
|
|
public $query;
|
|
|
|
public bool $branchFound = false;
|
|
|
|
public string $selectedBranch = 'main';
|
|
|
|
public bool $isStatic = false;
|
|
|
|
public ?string $publish_directory = null;
|
|
|
|
// In case of docker compose
|
|
public string $base_directory = '/';
|
|
|
|
public ?string $docker_compose_location = '/docker-compose.yaml';
|
|
// End of docker compose
|
|
|
|
public string $git_branch = 'main';
|
|
|
|
public int $rate_limit_remaining = 0;
|
|
|
|
public $rate_limit_reset = 0;
|
|
|
|
private object $repository_url_parsed;
|
|
|
|
public GithubApp|GitlabApp|string $git_source = 'other';
|
|
|
|
public string $git_host;
|
|
|
|
public string $git_repository;
|
|
|
|
public $build_pack = 'nixpacks';
|
|
|
|
public bool $show_is_static = true;
|
|
|
|
public bool $new_compose_services = false;
|
|
|
|
protected function rules()
|
|
{
|
|
return [
|
|
'repository_url' => ['required', 'string', new ValidGitRepositoryUrl],
|
|
'port' => 'required|numeric',
|
|
'isStatic' => 'required|boolean',
|
|
'publish_directory' => 'nullable|string',
|
|
'build_pack' => 'required|string',
|
|
'base_directory' => 'nullable|string',
|
|
'docker_compose_location' => ValidationPatterns::filePathRules(),
|
|
'git_branch' => ['required', 'string', new ValidGitBranch],
|
|
];
|
|
}
|
|
|
|
protected $validationAttributes = [
|
|
'repository_url' => 'repository',
|
|
'port' => 'port',
|
|
'isStatic' => 'static',
|
|
'publish_directory' => 'publish directory',
|
|
'build_pack' => 'build pack',
|
|
'base_directory' => 'base directory',
|
|
'docker_compose_location' => 'docker compose location',
|
|
];
|
|
|
|
public function mount()
|
|
{
|
|
if (isDev()) {
|
|
$this->repository_url = 'https://github.com/coollabsio/coolify-examples/tree/v4.x';
|
|
$this->port = 3000;
|
|
}
|
|
$this->parameters = get_route_parameters();
|
|
$this->query = request()->query();
|
|
}
|
|
|
|
public function updatedBuildPack()
|
|
{
|
|
if ($this->build_pack === 'nixpacks') {
|
|
$this->show_is_static = true;
|
|
$this->port = 3000;
|
|
} elseif ($this->build_pack === 'static') {
|
|
$this->show_is_static = false;
|
|
$this->isStatic = false;
|
|
$this->port = 80;
|
|
} else {
|
|
$this->show_is_static = false;
|
|
$this->isStatic = false;
|
|
}
|
|
}
|
|
|
|
public function instantSave()
|
|
{
|
|
if ($this->isStatic) {
|
|
$this->port = 80;
|
|
$this->publish_directory = '/dist';
|
|
} else {
|
|
$this->port = 3000;
|
|
$this->publish_directory = null;
|
|
}
|
|
$this->dispatch('success', 'Application settings updated!');
|
|
}
|
|
|
|
public function loadBranch()
|
|
{
|
|
try {
|
|
// Validate repository URL
|
|
$validator = validator(['repository_url' => $this->repository_url], [
|
|
'repository_url' => ['required', 'string', new ValidGitRepositoryUrl],
|
|
]);
|
|
|
|
if ($validator->fails()) {
|
|
throw new \RuntimeException('Invalid repository URL: '.$validator->errors()->first('repository_url'));
|
|
}
|
|
|
|
if (str($this->repository_url)->startsWith('git@')) {
|
|
$github_instance = str($this->repository_url)->after('git@')->before(':');
|
|
$repository = str($this->repository_url)->after(':')->before('.git');
|
|
$this->repository_url = 'https://'.str($github_instance).'/'.$repository;
|
|
}
|
|
if (
|
|
(str($this->repository_url)->startsWith('https://') ||
|
|
str($this->repository_url)->startsWith('http://')) &&
|
|
! str($this->repository_url)->endsWith('.git') &&
|
|
(! str($this->repository_url)->contains('github.com') ||
|
|
! str($this->repository_url)->contains('git.sr.ht')) &&
|
|
! str($this->repository_url)->contains('tangled')
|
|
) {
|
|
|
|
$this->repository_url = $this->repository_url.'.git';
|
|
}
|
|
if (str($this->repository_url)->contains('github.com') && str($this->repository_url)->endsWith('.git')) {
|
|
$this->repository_url = str($this->repository_url)->beforeLast('.git')->value();
|
|
}
|
|
|
|
} catch (\Throwable $e) {
|
|
return handleError($e, $this);
|
|
}
|
|
try {
|
|
$this->branchFound = false;
|
|
$this->getGitSource();
|
|
$this->getBranch();
|
|
if (str($this->repository_url)->contains('tangled')) {
|
|
$this->git_branch = 'master';
|
|
}
|
|
$this->selectedBranch = $this->git_branch;
|
|
} catch (\Throwable $e) {
|
|
if ($this->rate_limit_remaining == 0) {
|
|
$this->selectedBranch = $this->git_branch;
|
|
$this->branchFound = true;
|
|
|
|
return;
|
|
}
|
|
if (! $this->branchFound && $this->git_branch === 'main') {
|
|
try {
|
|
$this->git_branch = 'master';
|
|
$this->getBranch();
|
|
} catch (\Throwable $e) {
|
|
return handleError($e, $this);
|
|
}
|
|
} else {
|
|
return handleError($e, $this);
|
|
}
|
|
}
|
|
}
|
|
|
|
private function getGitSource()
|
|
{
|
|
$this->git_branch = 'main';
|
|
$this->base_directory = '/';
|
|
|
|
// Validate repository URL before parsing
|
|
$validator = validator(['repository_url' => $this->repository_url], [
|
|
'repository_url' => ['required', 'string', new ValidGitRepositoryUrl],
|
|
]);
|
|
|
|
if ($validator->fails()) {
|
|
throw new \RuntimeException('Invalid repository URL: '.$validator->errors()->first('repository_url'));
|
|
}
|
|
|
|
$this->repository_url_parsed = Url::fromString($this->repository_url);
|
|
$this->git_host = $this->repository_url_parsed->getHost();
|
|
$this->git_repository = $this->repository_url_parsed->getSegment(1).'/'.$this->repository_url_parsed->getSegment(2);
|
|
|
|
if ($this->repository_url_parsed->getSegment(3) === 'tree') {
|
|
$path = str($this->repository_url_parsed->getPath())->trim('/');
|
|
$this->git_branch = str($path)->after('tree/')->value();
|
|
$this->base_directory = '/';
|
|
} else {
|
|
$this->git_branch = 'main';
|
|
}
|
|
if ($this->git_host === 'github.com') {
|
|
$this->git_source = GithubApp::where('name', 'Public GitHub')->first();
|
|
|
|
return;
|
|
}
|
|
$this->git_repository = $this->repository_url;
|
|
$this->git_source = 'other';
|
|
}
|
|
|
|
private function getBranch()
|
|
{
|
|
if ($this->git_source === 'other') {
|
|
$this->branchFound = true;
|
|
|
|
return;
|
|
}
|
|
if ($this->git_source->getMorphClass() === GithubApp::class) {
|
|
$originalBranch = $this->git_branch;
|
|
$branchToTry = $originalBranch;
|
|
|
|
while (true) {
|
|
try {
|
|
$encodedBranch = urlencode($branchToTry);
|
|
['rate_limit_remaining' => $this->rate_limit_remaining, 'rate_limit_reset' => $this->rate_limit_reset] = githubApi(source: $this->git_source, endpoint: "/repos/{$this->git_repository}/branches/{$encodedBranch}");
|
|
$this->rate_limit_reset = Carbon::parse((int) $this->rate_limit_reset)->format('Y-M-d H:i:s');
|
|
$this->git_branch = $branchToTry;
|
|
|
|
$remaining = str($originalBranch)->after($branchToTry)->trim('/')->value();
|
|
$this->base_directory = filled($remaining) ? '/'.$remaining : '/';
|
|
|
|
$this->branchFound = true;
|
|
|
|
return;
|
|
} catch (\Throwable $e) {
|
|
if (str_contains($branchToTry, '/')) {
|
|
$branchToTry = str($branchToTry)->beforeLast('/')->value();
|
|
|
|
continue;
|
|
}
|
|
|
|
throw $e;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public function submit()
|
|
{
|
|
try {
|
|
$this->validate();
|
|
|
|
// Additional validation for git repository and branch
|
|
if ($this->git_source === 'other') {
|
|
// For 'other' sources, git_repository contains the full URL
|
|
$validator = validator(['git_repository' => $this->git_repository], [
|
|
'git_repository' => ['required', 'string', new ValidGitRepositoryUrl],
|
|
]);
|
|
|
|
if ($validator->fails()) {
|
|
throw new \RuntimeException('Invalid repository URL: '.$validator->errors()->first('git_repository'));
|
|
}
|
|
}
|
|
|
|
$branchValidator = validator(['git_branch' => $this->git_branch], [
|
|
'git_branch' => ['required', 'string', new ValidGitBranch],
|
|
]);
|
|
|
|
if ($branchValidator->fails()) {
|
|
throw new \RuntimeException('Invalid branch: '.$branchValidator->errors()->first('git_branch'));
|
|
}
|
|
|
|
$destination_uuid = $this->query['destination'] ?? null;
|
|
$project_uuid = $this->parameters['project_uuid'];
|
|
$environment_uuid = $this->parameters['environment_uuid'];
|
|
|
|
$destination = find_destination_for_current_team($destination_uuid);
|
|
if (! $destination) {
|
|
throw new \Exception('Destination not found.');
|
|
}
|
|
$destination_class = $destination->getMorphClass();
|
|
|
|
$project = Project::ownedByCurrentTeam()->where('uuid', $project_uuid)->firstOrFail();
|
|
$environment = $project->environments()->where('uuid', $environment_uuid)->firstOrFail();
|
|
|
|
if ($this->build_pack === 'dockercompose' && isDev() && $this->new_compose_services) {
|
|
$server = $destination->server;
|
|
$new_service = [
|
|
'name' => 'service'.str()->random(10),
|
|
'docker_compose_raw' => 'coolify',
|
|
'environment_id' => $environment->id,
|
|
'server_id' => $server->id,
|
|
];
|
|
if ($this->git_source === 'other') {
|
|
$new_service['git_repository'] = $this->git_repository;
|
|
$new_service['git_branch'] = $this->git_branch;
|
|
} else {
|
|
$new_service['git_repository'] = $this->git_repository;
|
|
$new_service['git_branch'] = $this->git_branch;
|
|
$new_service['source_id'] = $this->git_source->id;
|
|
$new_service['source_type'] = $this->git_source->getMorphClass();
|
|
}
|
|
$service = Service::create($new_service);
|
|
|
|
return redirect()->route('project.service.configuration', [
|
|
'service_uuid' => $service->uuid,
|
|
'environment_uuid' => $environment->uuid,
|
|
'project_uuid' => $project->uuid,
|
|
]);
|
|
|
|
return;
|
|
}
|
|
if ($this->git_source === 'other') {
|
|
$application_init = [
|
|
'name' => generate_random_name(),
|
|
'git_repository' => $this->git_repository,
|
|
'git_branch' => $this->git_branch,
|
|
'ports_exposes' => $this->port,
|
|
'publish_directory' => $this->publish_directory,
|
|
'environment_id' => $environment->id,
|
|
'destination_id' => $destination->id,
|
|
'destination_type' => $destination_class,
|
|
'build_pack' => $this->build_pack,
|
|
'base_directory' => $this->base_directory,
|
|
];
|
|
} else {
|
|
$application_init = [
|
|
'name' => generate_application_name($this->git_repository, $this->git_branch),
|
|
'git_repository' => $this->git_repository,
|
|
'git_branch' => $this->git_branch,
|
|
'ports_exposes' => $this->port,
|
|
'publish_directory' => $this->publish_directory,
|
|
'environment_id' => $environment->id,
|
|
'destination_id' => $destination->id,
|
|
'destination_type' => $destination_class,
|
|
'source_id' => $this->git_source->id,
|
|
'source_type' => $this->git_source->getMorphClass(),
|
|
'build_pack' => $this->build_pack,
|
|
'base_directory' => $this->base_directory,
|
|
];
|
|
}
|
|
|
|
if ($this->build_pack === 'dockerfile' || $this->build_pack === 'dockerimage') {
|
|
$application_init['health_check_enabled'] = false;
|
|
}
|
|
if ($this->build_pack === 'dockercompose') {
|
|
$application_init['docker_compose_location'] = $this->docker_compose_location;
|
|
$application_init['base_directory'] = $this->base_directory;
|
|
}
|
|
$application = Application::create($application_init);
|
|
|
|
$application->settings->is_static = $this->isStatic;
|
|
$application->settings->save();
|
|
$fqdn = generateUrl(server: $destination->server, random: $application->uuid);
|
|
$application->fqdn = $fqdn;
|
|
$application->save();
|
|
|
|
return redirect()->route('project.application.configuration', [
|
|
'application_uuid' => $application->uuid,
|
|
'environment_uuid' => $environment->uuid,
|
|
'project_uuid' => $project->uuid,
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
return handleError($e, $this);
|
|
}
|
|
}
|
|
}
|