mirror of
https://github.com/tiennm99/coolify.git
synced 2026-08-20 06:23:23 +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 Git Repository',
|
|
'description' => 'Deploy any public Git repository. Coolify builds it from source, no credentials required.',
|
|
'documentation' => 'https://coolify.io/docs/applications/ci-cd',
|
|
'logo' => asset('svgs/resources/public-repo.svg'),
|
|
],
|
|
[
|
|
'id' => 'private-deploy-key',
|
|
'name' => 'Private Git Repository (with Deploy Key)',
|
|
'description' => 'Deploy a private repository over SSH with a repository-scoped deploy key. Set the URL and branch manually, no automatic deploys.',
|
|
'documentation' => 'https://coolify.io/docs/applications/ci-cd/github/deploy-key',
|
|
'logo' => asset('svgs/resources/deploy-key.svg'),
|
|
],
|
|
[
|
|
'id' => 'private-gh-app',
|
|
'name' => 'Git Repository (with GitHub App)',
|
|
'description' => 'Deploy public or private GitHub repositories through a GitHub App, with automatic webhooks and pull request previews.',
|
|
'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' => 'Git Repository (with GitLab App)',
|
|
'description' => 'Deploy public or private GitLab projects through a GitLab App, with automatic webhooks and merge request previews.',
|
|
'logo' => asset('svgs/resources/gitlab-app.svg'),
|
|
],
|
|
];
|
|
$dockerBasedApplications = [
|
|
[
|
|
'id' => 'dockerfile',
|
|
'name' => 'Dockerfile',
|
|
'description' => 'Deploy an application using a Dockerfile, without 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',
|
|
'description' => 'Deploy a multi-container application using a Docker Compose file, without a Git repository.',
|
|
'documentation' => 'https://coolify.io/docs/applications/build-packs/docker-compose',
|
|
'logo' => asset('svgs/resources/docker-compose.svg'),
|
|
],
|
|
[
|
|
'id' => 'docker-image',
|
|
'name' => 'Docker Image',
|
|
'description' => 'Deploy an application using a prebuilt image from any Docker registry, without a Git repository.',
|
|
'documentation' => 'https://coolify.io/docs/applications',
|
|
'logo' => asset('svgs/resources/docker-image.svg'),
|
|
],
|
|
];
|
|
$databases = [
|
|
[
|
|
'id' => 'postgresql',
|
|
'name' => 'PostgreSQL',
|
|
'description' => 'A relational database with strong SQL standards support and extensibility.',
|
|
'logo' => asset('svgs/resources/postgres.svg'),
|
|
],
|
|
[
|
|
'id' => 'mysql',
|
|
'name' => 'MySQL',
|
|
'description' => 'A relational database for web and general-purpose applications.',
|
|
'logo' => asset('svgs/resources/mysql.svg'),
|
|
|
|
],
|
|
[
|
|
'id' => 'mariadb',
|
|
'name' => 'MariaDB',
|
|
'description' => 'A relational database and drop-in replacement for MySQL.',
|
|
'logo' => asset('svgs/resources/mariadb.svg'),
|
|
],
|
|
[
|
|
'id' => 'redis',
|
|
'name' => 'Redis',
|
|
'description' => 'An in-memory key-value store used as a database, cache, and message broker.',
|
|
'logo' => asset('svgs/resources/redis.svg'),
|
|
],
|
|
[
|
|
'id' => 'keydb',
|
|
'name' => 'KeyDB',
|
|
'description' => 'A multithreaded, Redis-compatible in-memory store.',
|
|
'logo' => asset('svgs/resources/keydb.svg'),
|
|
'logoDark' => asset('svgs/resources/keydb-dark.svg'),
|
|
],
|
|
[
|
|
'id' => 'dragonfly',
|
|
'name' => 'Dragonfly',
|
|
'description' => 'An in-memory datastore compatible with Redis and Memcached.',
|
|
'logo' => asset('svgs/resources/dragonfly.svg'),
|
|
'logoDark' => asset('svgs/resources/dragonfly-dark.svg'),
|
|
],
|
|
[
|
|
'id' => 'mongodb',
|
|
'name' => 'MongoDB',
|
|
'description' => 'A document-oriented NoSQL database that stores JSON-like documents.',
|
|
'logo' => asset('svgs/resources/mongodb.svg'),
|
|
],
|
|
[
|
|
'id' => 'clickhouse',
|
|
'name' => 'ClickHouse',
|
|
'description' => 'A column-oriented database for real-time analytics over large datasets.',
|
|
'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();
|
|
}
|
|
}
|