Files
coolify/app/Livewire/Project/New/Select.php
T

496 lines
19 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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();
}
}