mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-06 06:15:39 +00:00
496 lines
19 KiB
PHP
496 lines
19 KiB
PHP
<?php
|
||
|
||
namespace App\Livewire\Project\New;
|
||
|
||
use App\Models\Project;
|
||
use App\Models\Server;
|
||
use Carbon\CarbonImmutable;
|
||
use Illuminate\Support\Collection;
|
||
use Livewire\Component;
|
||
|
||
class Select extends Component
|
||
{
|
||
public $current_step = 'type';
|
||
|
||
public ?Server $server = null;
|
||
|
||
public string $type;
|
||
|
||
public string $server_id;
|
||
|
||
public string $destination_uuid;
|
||
|
||
public Collection|null|Server $allServers;
|
||
|
||
public Collection|null|Server $servers;
|
||
|
||
public ?Collection $buildServers = null;
|
||
|
||
public bool $onlyBuildServerAvailable = false;
|
||
|
||
public ?Collection $standaloneDockers;
|
||
|
||
public ?Collection $swarmDockers;
|
||
|
||
public array $parameters;
|
||
|
||
public Collection|array $services = [];
|
||
|
||
public Collection|array $allServices = [];
|
||
|
||
public bool $isDatabase = false;
|
||
|
||
public bool $includeSwarm = true;
|
||
|
||
public bool $loadingServices = true;
|
||
|
||
public bool $loading = false;
|
||
|
||
public $environments = [];
|
||
|
||
public ?string $selectedEnvironment = null;
|
||
|
||
public string $postgresql_type = 'postgres:16-alpine';
|
||
|
||
public ?string $existingPostgresqlUrl = null;
|
||
|
||
protected $queryString = [
|
||
'server_id',
|
||
'type' => ['except' => ''],
|
||
'destination_uuid' => ['except' => '', 'as' => 'destination'],
|
||
];
|
||
|
||
public function mount()
|
||
{
|
||
try {
|
||
$this->parameters = get_route_parameters();
|
||
if (isDev()) {
|
||
$this->existingPostgresqlUrl = 'postgres://coolify:password@coolify-db:5432';
|
||
}
|
||
$projectUuid = data_get($this->parameters, 'project_uuid');
|
||
$project = Project::ownedByCurrentTeam()->whereUuid($projectUuid)->firstOrFail();
|
||
$this->environments = $project->environments;
|
||
$this->selectedEnvironment = $this->environments->where('uuid', data_get($this->parameters, 'environment_uuid'))->firstOrFail()->name;
|
||
|
||
// Check if we have all required params for PostgreSQL type selection
|
||
// This handles navigation from global search
|
||
$queryType = request()->query('type');
|
||
$queryServerId = request()->query('server_id');
|
||
$queryDestination = request()->query('destination');
|
||
|
||
if ($queryType === 'postgresql' && $queryServerId !== null && $queryDestination) {
|
||
$this->type = $queryType;
|
||
$this->server_id = $queryServerId;
|
||
$this->destination_uuid = $queryDestination;
|
||
$this->server = Server::ownedByCurrentTeam()->find($queryServerId);
|
||
$this->current_step = 'select-postgresql-type';
|
||
}
|
||
} catch (\Exception $e) {
|
||
return handleError($e, $this);
|
||
}
|
||
}
|
||
|
||
public function render()
|
||
{
|
||
return view('livewire.project.new.select');
|
||
}
|
||
|
||
public function updatedSelectedEnvironment()
|
||
{
|
||
$environmentUuid = $this->environments->where('name', $this->selectedEnvironment)->first()->uuid;
|
||
|
||
return redirect()->route('project.resource.create', [
|
||
'project_uuid' => $this->parameters['project_uuid'],
|
||
'environment_uuid' => $environmentUuid,
|
||
]);
|
||
}
|
||
|
||
public function loadServices()
|
||
{
|
||
$services = get_service_templates();
|
||
$templateLastUpdatedMap = $this->serviceTemplateLastUpdatedMap($services);
|
||
|
||
$services = collect($services)->map(function ($service, $key) use ($templateLastUpdatedMap) {
|
||
$default_logo = 'svgs/default.webp';
|
||
$logo = data_get($service, 'logo');
|
||
|
||
if (is_string($logo) && str_starts_with($logo, 'svg/')) {
|
||
$normalizedLogo = 'svgs/'.str($logo)->after('svg/');
|
||
if (file_exists(public_path($normalizedLogo))) {
|
||
$logo = $normalizedLogo;
|
||
}
|
||
}
|
||
|
||
$hasLogo = is_string($logo)
|
||
&& basename($logo) !== basename($default_logo)
|
||
&& file_exists(public_path($logo));
|
||
|
||
if (! $hasLogo) {
|
||
$logo = $default_logo;
|
||
}
|
||
|
||
$local_logo_path = public_path($logo);
|
||
$serviceKey = (string) $key;
|
||
|
||
return [
|
||
'id' => $serviceKey,
|
||
'name' => str($serviceKey)->headline(),
|
||
'docsSlug' => str($serviceKey)->lower()->value(),
|
||
'has_logo' => $hasLogo,
|
||
'logo' => asset($logo),
|
||
'logo_github_url' => file_exists($local_logo_path)
|
||
? 'https://raw.githubusercontent.com/coollabsio/coolify/refs/heads/main/public/'.$logo
|
||
: asset($default_logo),
|
||
'templateLastUpdated' => $templateLastUpdatedMap[$serviceKey] ?? null,
|
||
] + (array) $service;
|
||
})->all();
|
||
|
||
// Extract unique categories from services
|
||
$categories = collect($services)
|
||
->pluck('category')
|
||
->filter()
|
||
->unique()
|
||
->map(function ($category) {
|
||
// Handle multiple categories separated by comma
|
||
if (str_contains($category, ',')) {
|
||
return collect(explode(',', $category))->map(fn ($cat) => trim($cat));
|
||
}
|
||
|
||
return [$category];
|
||
})
|
||
->flatten()
|
||
->unique()
|
||
->map(function ($category) {
|
||
// Format common acronyms to uppercase
|
||
$acronyms = ['ai', 'api', 'ci', 'cd', 'cms', 'crm', 'erp', 'iot', 'vpn', 'vps', 'dns', 'ssl', 'tls', 'ssh', 'ftp', 'http', 'https', 'smtp', 'imap', 'pop3', 'sql', 'nosql', 'json', 'xml', 'yaml', 'csv', 'pdf', 'sms', 'mfa', '2fa', 'oauth', 'saml', 'jwt', 'rest', 'soap', 'grpc', 'graphql', 'websocket', 'webrtc', 'p2p', 'b2b', 'b2c', 'seo', 'sem', 'ppc', 'roi', 'kpi', 'ui', 'ux', 'ide', 'sdk', 'api', 'cli', 'gui', 'cdn', 'ddos', 'dos', 'xss', 'csrf', 'sqli', 'rce', 'lfi', 'rfi', 'ssrf', 'xxe', 'idor', 'owasp', 'gdpr', 'hipaa', 'pci', 'dss', 'iso', 'nist', 'cve', 'cwe', 'cvss'];
|
||
$lower = strtolower($category);
|
||
|
||
if (in_array($lower, $acronyms)) {
|
||
return strtoupper($category);
|
||
}
|
||
|
||
return $category;
|
||
})
|
||
->sort(SORT_NATURAL | SORT_FLAG_CASE)
|
||
->values()
|
||
->all();
|
||
$gitBasedApplications = [
|
||
[
|
||
'id' => 'public',
|
||
'name' => 'Public Repository',
|
||
'description' => 'Connect any public Git repository and let Coolify build and deploy it from source.',
|
||
'documentation' => 'https://coolify.io/docs/applications/ci-cd',
|
||
'logo' => asset('svgs/resources/public-repo.svg'),
|
||
],
|
||
[
|
||
'id' => 'private-gh-app',
|
||
'name' => 'Private Repository (with GitHub App)',
|
||
'description' => 'Deploy a private GitHub repository with automatic webhooks and pull request support.',
|
||
'documentation' => 'https://coolify.io/docs/applications/ci-cd/github/setup-app',
|
||
'logo' => asset('svgs/resources/github-app.svg'),
|
||
'logoDark' => asset('svgs/resources/github-app-dark.svg'),
|
||
],
|
||
[
|
||
'id' => 'private-gitlab-app',
|
||
'name' => 'Private Repository (with GitLab App)',
|
||
'description' => 'You can deploy public & private repositories through your GitLab Apps.',
|
||
'logo' => asset('svgs/resources/gitlab-app.svg'),
|
||
],
|
||
[
|
||
'id' => 'private-deploy-key',
|
||
'name' => 'Private Repository (with Deploy Key)',
|
||
'description' => 'Connect a private Git repository over SSH using a repository-scoped deploy key.',
|
||
'documentation' => 'https://coolify.io/docs/applications/ci-cd/github/deploy-key',
|
||
'logo' => asset('svgs/resources/deploy-key.svg'),
|
||
],
|
||
];
|
||
$dockerBasedApplications = [
|
||
[
|
||
'id' => 'dockerfile',
|
||
'name' => 'Dockerfile',
|
||
'description' => 'Build and deploy an application from a Dockerfile without connecting a Git repository.',
|
||
'documentation' => 'https://coolify.io/docs/applications/build-packs/dockerfile',
|
||
'logo' => asset('svgs/resources/dockerfile.svg'),
|
||
],
|
||
[
|
||
'id' => 'docker-compose-empty',
|
||
'name' => 'Docker Compose Empty',
|
||
'description' => 'Create a multi-container application and provide the Docker Compose definition manually.',
|
||
'documentation' => 'https://coolify.io/docs/applications/build-packs/docker-compose',
|
||
'logo' => asset('svgs/resources/docker-compose.svg'),
|
||
],
|
||
[
|
||
'id' => 'docker-image',
|
||
'name' => 'Docker Image',
|
||
'description' => 'Run a prebuilt image from Docker Hub or another compatible container registry.',
|
||
'documentation' => 'https://coolify.io/docs/applications',
|
||
'logo' => asset('svgs/resources/docker-image.svg'),
|
||
],
|
||
];
|
||
$databases = [
|
||
[
|
||
'id' => 'postgresql',
|
||
'name' => 'PostgreSQL',
|
||
'description' => 'PostgreSQL is an object-relational database known for its robustness, advanced features, and strong standards compliance.',
|
||
'logo' => asset('svgs/resources/postgres.svg'),
|
||
],
|
||
[
|
||
'id' => 'mysql',
|
||
'name' => 'MySQL',
|
||
'description' => 'MySQL is an open-source relational database management system. ',
|
||
'logo' => asset('svgs/resources/mysql.svg'),
|
||
|
||
],
|
||
[
|
||
'id' => 'mariadb',
|
||
'name' => 'MariaDB',
|
||
'description' => 'MariaDB is a community-developed, commercially supported fork of the MySQL relational database management system, intended to remain free and open-source.',
|
||
'logo' => asset('svgs/resources/mariadb.svg'),
|
||
],
|
||
[
|
||
'id' => 'redis',
|
||
'name' => 'Redis',
|
||
'description' => 'Redis is a source-available, in-memory storage, used as a distributed, in-memory key–value database, cache and message broker, with optional durability.',
|
||
'logo' => asset('svgs/resources/redis.svg'),
|
||
],
|
||
[
|
||
'id' => 'keydb',
|
||
'name' => 'KeyDB',
|
||
'description' => 'KeyDB is a database that offers high performance, low latency, and scalability for various data structures and workloads.',
|
||
'logo' => asset('svgs/resources/keydb.svg'),
|
||
'logoDark' => asset('svgs/resources/keydb-dark.svg'),
|
||
],
|
||
[
|
||
'id' => 'dragonfly',
|
||
'name' => 'Dragonfly',
|
||
'description' => 'Dragonfly DB is a drop-in Redis replacement that delivers 25x more throughput and 12x faster snapshotting than Redis.',
|
||
'logo' => asset('svgs/resources/dragonfly.svg'),
|
||
'logoDark' => asset('svgs/resources/dragonfly-dark.svg'),
|
||
],
|
||
[
|
||
'id' => 'mongodb',
|
||
'name' => 'MongoDB',
|
||
'description' => 'MongoDB is a source-available, cross-platform, document-oriented database program.',
|
||
'logo' => asset('svgs/resources/mongodb.svg'),
|
||
],
|
||
[
|
||
'id' => 'clickhouse',
|
||
'name' => 'ClickHouse',
|
||
'description' => 'ClickHouse is a column-oriented database that supports real-time analytics, business intelligence, observability, ML and GenAI, and more.',
|
||
'logo' => asset('svgs/resources/clickhouse.svg'),
|
||
],
|
||
|
||
];
|
||
|
||
return [
|
||
'serviceTemplatesLastUpdated' => $this->serviceTemplatesLastUpdated(),
|
||
'services' => $services,
|
||
'categories' => $categories,
|
||
'gitBasedApplications' => $gitBasedApplications,
|
||
'dockerBasedApplications' => $dockerBasedApplications,
|
||
'databases' => $databases,
|
||
];
|
||
}
|
||
|
||
public function instantSave()
|
||
{
|
||
if ($this->includeSwarm) {
|
||
$this->servers = $this->allServers;
|
||
} else {
|
||
if ($this->allServers instanceof Collection) {
|
||
$this->servers = $this->allServers->where('settings.is_swarm_worker', false)->where('settings.is_swarm_manager', false)->where('settings.is_build_server', false);
|
||
} else {
|
||
$this->servers = $this->allServers;
|
||
}
|
||
}
|
||
}
|
||
|
||
private function serviceTemplatesLastUpdated(): ?string
|
||
{
|
||
$fetchedAt = get_service_templates_fetched_at();
|
||
if ($fetchedAt instanceof CarbonImmutable) {
|
||
return $fetchedAt
|
||
->timezone(config('app.timezone'))
|
||
->format('M j, Y H:i');
|
||
}
|
||
|
||
return $this->formatLastModified($this->serviceTemplatesPath());
|
||
}
|
||
|
||
private function serviceTemplateLastUpdatedMap(Collection $services): array
|
||
{
|
||
return $services
|
||
->mapWithKeys(fn ($service, $serviceName) => [
|
||
(string) $serviceName => $this->serviceTemplateLastUpdatedFromPayload($service)
|
||
?? $this->serviceTemplateLastUpdated((string) $serviceName),
|
||
])
|
||
->all();
|
||
}
|
||
|
||
private function serviceTemplateLastUpdatedFromPayload(mixed $service): ?string
|
||
{
|
||
$timestamp = data_get($service, 'template_last_updated_at');
|
||
|
||
if (! is_string($timestamp) || $timestamp === '') {
|
||
return null;
|
||
}
|
||
|
||
try {
|
||
return CarbonImmutable::parse($timestamp)
|
||
->timezone(config('app.timezone'))
|
||
->format('M j, Y H:i');
|
||
} catch (\Throwable) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
private function serviceTemplateLastUpdated(string $serviceName): ?string
|
||
{
|
||
foreach (['yaml', 'yml'] as $extension) {
|
||
$templatePath = base_path("templates/compose/{$serviceName}.{$extension}");
|
||
|
||
if (file_exists($templatePath)) {
|
||
return $this->formatLastModified($templatePath);
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private function serviceTemplatesPath(): string
|
||
{
|
||
return base_path('templates/'.config('constants.services.file_name'));
|
||
}
|
||
|
||
private function formatLastModified(string $path): ?string
|
||
{
|
||
if (! file_exists($path)) {
|
||
return null;
|
||
}
|
||
|
||
return CarbonImmutable::createFromTimestamp(filemtime($path))
|
||
->timezone(config('app.timezone'))
|
||
->format('M j, Y H:i');
|
||
}
|
||
|
||
public function setType(string $type)
|
||
{
|
||
if (! str($type)->startsWith('one-click-service-')) {
|
||
$type = str($type)->lower()->slug()->value();
|
||
}
|
||
|
||
if ($this->loading) {
|
||
return;
|
||
}
|
||
$this->loading = true;
|
||
$this->type = $type;
|
||
switch ($type) {
|
||
case 'postgresql':
|
||
case 'mysql':
|
||
case 'mariadb':
|
||
case 'redis':
|
||
case 'keydb':
|
||
case 'dragonfly':
|
||
case 'clickhouse':
|
||
case 'mongodb':
|
||
$this->isDatabase = true;
|
||
$this->includeSwarm = false;
|
||
if ($this->allServers instanceof Collection) {
|
||
$this->servers = $this->allServers->where('settings.is_swarm_worker', false)->where('settings.is_swarm_manager', false)->where('settings.is_build_server', false);
|
||
} else {
|
||
$this->servers = $this->allServers;
|
||
}
|
||
break;
|
||
}
|
||
if (str($type)->startsWith('one-click-service') || str($type)->startsWith('docker-compose-empty')) {
|
||
$this->isDatabase = true;
|
||
$this->includeSwarm = false;
|
||
if ($this->allServers instanceof Collection) {
|
||
$this->servers = $this->allServers->where('settings.is_swarm_worker', false)->where('settings.is_swarm_manager', false)->where('settings.is_build_server', false);
|
||
} else {
|
||
$this->servers = $this->allServers;
|
||
}
|
||
}
|
||
if ($type === 'existing-postgresql') {
|
||
$this->current_step = $type;
|
||
|
||
return;
|
||
}
|
||
if (count($this->servers) === 1 && $this->buildServers?->isEmpty()) {
|
||
$server = $this->servers->first();
|
||
if ($server instanceof Server) {
|
||
$this->setServer($server);
|
||
}
|
||
}
|
||
if (! is_null($this->server)) {
|
||
$foundServer = $this->servers->where('id', $this->server->id)->first();
|
||
if ($foundServer) {
|
||
return $this->setServer($foundServer);
|
||
}
|
||
}
|
||
$this->current_step = 'servers';
|
||
}
|
||
|
||
public function setServer(Server $server)
|
||
{
|
||
$this->server_id = $server->id;
|
||
$this->server = $server;
|
||
$this->standaloneDockers = $server->standaloneDockers;
|
||
$this->swarmDockers = $server->swarmDockers;
|
||
$count = count($this->standaloneDockers) + count($this->swarmDockers);
|
||
if ($count === 1) {
|
||
$docker = $this->standaloneDockers->first() ?? $this->swarmDockers->first();
|
||
if ($docker) {
|
||
$this->setDestination($docker->uuid);
|
||
|
||
return $this->whatToDoNext();
|
||
}
|
||
}
|
||
$this->current_step = 'destinations';
|
||
}
|
||
|
||
public function setDestination(string $destination_uuid)
|
||
{
|
||
$this->destination_uuid = $destination_uuid;
|
||
|
||
return $this->whatToDoNext();
|
||
}
|
||
|
||
public function setPostgresqlType(string $type)
|
||
{
|
||
$this->postgresql_type = $type;
|
||
|
||
return redirect()->route('project.resource.create', [
|
||
'project_uuid' => $this->parameters['project_uuid'],
|
||
'environment_uuid' => $this->parameters['environment_uuid'],
|
||
'type' => $this->type,
|
||
'destination' => $this->destination_uuid,
|
||
'server_id' => $this->server_id,
|
||
'database_image' => $this->postgresql_type,
|
||
]);
|
||
}
|
||
|
||
public function whatToDoNext()
|
||
{
|
||
if ($this->type === 'postgresql') {
|
||
$this->current_step = 'select-postgresql-type';
|
||
} else {
|
||
return redirect()->route('project.resource.create', [
|
||
'project_uuid' => $this->parameters['project_uuid'],
|
||
'environment_uuid' => $this->parameters['environment_uuid'],
|
||
'type' => $this->type,
|
||
'destination' => $this->destination_uuid,
|
||
'server_id' => $this->server_id,
|
||
]);
|
||
}
|
||
}
|
||
|
||
public function loadServers()
|
||
{
|
||
$this->servers = Server::isUsable()->get()->sortBy('name');
|
||
$this->buildServers = Server::isUsableBuildServer()->get()->sortBy('name');
|
||
$this->allServers = $this->servers->concat($this->buildServers);
|
||
$this->onlyBuildServerAvailable = $this->servers->isEmpty() && $this->buildServers->isNotEmpty();
|
||
}
|
||
}
|