# Conflicts:
#	app/Models/Team.php
This commit is contained in:
Daniel 2024-07-12 13:38:24 +02:00
commit 0c93e35105
153 changed files with 15605 additions and 3104 deletions

View file

@ -2,8 +2,6 @@
)
[![Bounty Issues](https://img.shields.io/static/v1?labelColor=grey&color=6366f1&label=Algora&message=%F0%9F%92%8E+Bounty+issues&style=for-the-badge)](https://console.algora.io/org/coollabsio/bounties/new)
[![Open Bounties](https://img.shields.io/endpoint?url=https%3A%2F%2Fconsole.algora.io%2Fapi%2Fshields%2Fcoollabsio%2Fbounties%3Fstatus%3Dopen&style=for-the-badge)](https://console.algora.io/org/coollabsio/bounties?status=open)
[![Rewarded Bounties](https://img.shields.io/endpoint?url=https%3A%2F%2Fconsole.algora.io%2Fapi%2Fshields%2Fcoollabsio%2Fbounties%3Fstatus%3Dcompleted&style=for-the-badge)](https://console.algora.io/org/coollabsio/bounties?status=completed)
# About the Project
@ -49,6 +47,7 @@ Special thanks to our biggest sponsors!
<a href="https://coolify.ad.vin/?ref=coolify.io" target="_blank"><img src="./other/logos/advin.png" alt="advin logo" width="250"/></a>
<a href="https://trieve.ai/?ref=coolify.io" target="_blank"><img src="./other/logos/trieve_bg.png" alt="trieve logo" width="180"/></a>
<a href="https://blacksmith.sh/?ref=coolify.io" target="_blank"><img src="./other/logos/blacksmith.svg" alt="blacksmith logo" width="200"/></a>
<a href="https://latitude.sh/?ref=coolify.io" target="_blank"><img src="./other/logos/latitude.svg" alt="latitude logo" width="200"/></a>
## Github Sponsors ($40+)
<a href="https://serpapi.com/?ref=coolify.io"><img width="60px" alt="SerpAPI" src="https://github.com/serpapi.png"/></a>

View file

@ -0,0 +1,16 @@
<?php
namespace App\Actions\Application;
use App\Models\Application;
use Lorisleiva\Actions\Concerns\AsAction;
class LoadComposeFile
{
use AsAction;
public function handle(Application $application)
{
$application->loadComposeFile();
}
}

View file

@ -38,6 +38,12 @@ class StopApplication
}
}
}
if ($application->build_pack === 'dockercompose') {
// remove network
$uuid = $application->uuid;
instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false);
instant_remote_process(["docker network rm {$uuid}"], $server, false);
}
}
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace App\Actions\Database;
use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDragonfly;
use App\Models\StandaloneKeydb;
use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Lorisleiva\Actions\Concerns\AsAction;
class RestartDatabase
{
use AsAction;
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $database)
{
$server = $database->destination->server;
if (! $server->isFunctional()) {
return 'Server is not functional';
}
StopDatabase::run($database);
return StartDatabase::run($database);
}
}

View file

@ -0,0 +1,57 @@
<?php
namespace App\Actions\Database;
use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDragonfly;
use App\Models\StandaloneKeydb;
use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Lorisleiva\Actions\Concerns\AsAction;
class StartDatabase
{
use AsAction;
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $database)
{
$server = $database->destination->server;
if (! $server->isFunctional()) {
return 'Server is not functional';
}
switch ($database->getMorphClass()) {
case 'App\Models\StandalonePostgresql':
$activity = StartPostgresql::run($database);
break;
case 'App\Models\StandaloneRedis':
$activity = StartRedis::run($database);
break;
case 'App\Models\StandaloneMongodb':
$activity = StartMongodb::run($database);
break;
case 'App\Models\StandaloneMysql':
$activity = StartMysql::run($database);
break;
case 'App\Models\StandaloneMariadb':
$activity = StartMariadb::run($database);
break;
case 'App\Models\StandaloneKeydb':
$activity = StartKeydb::run($database);
break;
case 'App\Models\StandaloneDragonfly':
$activity = StartDragonfly::run($database);
break;
case 'App\Models\StandaloneClickhouse':
$activity = StartClickhouse::run($database);
break;
}
if ($database->is_public && $database->public_port) {
StartDatabaseProxy::dispatch($database);
}
return $activity;
}
}

View file

@ -29,7 +29,5 @@ class StopDatabase
if ($database->is_public) {
StopDatabaseProxy::run($database);
}
// TODO: make notification for services
// $database->environment->project->team->notify(new StatusChanged($database));
}
}

View file

@ -27,7 +27,6 @@ class StopDatabaseProxy
$server = data_get($database, 'service.server');
}
instant_remote_process(["docker rm -f {$uuid}-proxy"], $server);
$database->is_public = false;
$database->save();
DatabaseStatusChanged::dispatch();
}

View file

@ -11,11 +11,11 @@ class StartProxy
{
use AsAction;
public function handle(Server $server, bool $async = true): string|Activity
public function handle(Server $server, bool $async = true, bool $force = false): string|Activity
{
try {
$proxyType = $server->proxyType();
if (is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop || $server->isBuildServer()) {
if ((is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop || $server->isBuildServer()) && $force === false) {
return 'OK';
}
$commands = collect([]);

View file

@ -11,6 +11,8 @@ class CleanupDocker
public function handle(Server $server, bool $force = true)
{
// cleanup docker images, containers, and builder caches
if ($force) {
instant_remote_process(['docker image prune -af'], $server, false);
instant_remote_process(['docker container prune -f --filter "label=coolify.managed=true"'], $server, false);
@ -20,5 +22,15 @@ class CleanupDocker
instant_remote_process(['docker container prune -f --filter "label=coolify.managed=true"'], $server, false);
instant_remote_process(['docker builder prune -f'], $server, false);
}
// cleanup networks
// $networks = collectDockerNetworksByServer($server);
// $proxyNetworks = collectProxyDockerNetworksByServer($server);
// $diff = $proxyNetworks->diff($networks);
// if ($diff->count() > 0) {
// $diff->map(function ($network) use ($server) {
// instant_remote_process(["docker network disconnect $network coolify-proxy"], $server);
// instant_remote_process(["docker network rm $network"], $server);
// });
// }
}
}

View file

@ -0,0 +1,18 @@
<?php
namespace App\Actions\Service;
use App\Models\Service;
use Lorisleiva\Actions\Concerns\AsAction;
class RestartService
{
use AsAction;
public function handle(Service $service)
{
StopService::run($service);
return StartService::run($service);
}
}

View file

@ -19,18 +19,16 @@ class StopService
ray('Stopping service: '.$service->name);
$applications = $service->applications()->get();
foreach ($applications as $application) {
instant_remote_process(["docker rm -f {$application->name}-{$service->uuid}"], $service->server);
instant_remote_process(["docker rm -f {$application->name}-{$service->uuid}"], $service->server, false);
$application->update(['status' => 'exited']);
}
$dbs = $service->databases()->get();
foreach ($dbs as $db) {
instant_remote_process(["docker rm -f {$db->name}-{$service->uuid}"], $service->server);
instant_remote_process(["docker rm -f {$db->name}-{$service->uuid}"], $service->server, false);
$db->update(['status' => 'exited']);
}
instant_remote_process(["docker network disconnect {$service->uuid} coolify-proxy 2>/dev/null"], $service->server, false);
instant_remote_process(["docker network rm {$service->uuid} 2>/dev/null"], $service->server, false);
// TODO: make notification for databases
// $service->environment->project->team->notify(new StatusChanged($service));
instant_remote_process(["docker network disconnect {$service->uuid} coolify-proxy"], $service->server);
instant_remote_process(["docker network rm {$service->uuid}"], $service->server);
} catch (\Exception $e) {
echo $e->getMessage();
ray($e->getMessage());

View file

@ -0,0 +1,101 @@
<?php
namespace App\Console\Commands;
use App\Models\Team;
use Illuminate\Console\Command;
class CloudCleanupSubscriptions extends Command
{
protected $signature = 'cloud:cleanup-subs';
protected $description = 'Cleanup subcriptions teams';
public function handle()
{
try {
if (! isCloud()) {
$this->error('This command can only be run on cloud');
return;
}
ray()->clearAll();
$this->info('Cleaning up subcriptions teams');
$stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key'));
$teams = Team::all()->filter(function ($team) {
return $team->id !== 0;
})->sortBy('id');
foreach ($teams as $team) {
if ($team) {
$this->info("Checking team {$team->id}");
}
if (! data_get($team, 'subscription')) {
$this->disableServers($team);
continue;
}
// If the team has no subscription id and the invoice is paid, we need to reset the invoice paid status
if (! (data_get($team, 'subscription.stripe_subscription_id'))) {
$this->info("Resetting invoice paid status for team {$team->id} {$team->name}");
$team->subscription->update([
'stripe_invoice_paid' => false,
'stripe_trial_already_ended' => false,
'stripe_subscription_id' => null,
]);
$this->disableServers($team);
continue;
} else {
$subscription = $stripe->subscriptions->retrieve(data_get($team, 'subscription.stripe_subscription_id'), []);
$status = data_get($subscription, 'status');
if ($status === 'active' || $status === 'past_due') {
$team->subscription->update([
'stripe_invoice_paid' => true,
'stripe_trial_already_ended' => false,
]);
continue;
}
$this->info('Subscription status: '.$status);
$this->info('Subscription id: '.data_get($team, 'subscription.stripe_subscription_id'));
$confirm = $this->confirm('Do you want to cancel the subscription?', true);
if (! $confirm) {
$this->info("Skipping team {$team->id} {$team->name}");
} else {
$this->info("Cancelling subscription for team {$team->id} {$team->name}");
$team->subscription->update([
'stripe_invoice_paid' => false,
'stripe_trial_already_ended' => false,
'stripe_subscription_id' => null,
]);
$this->disableServers($team);
}
}
}
} catch (\Exception $e) {
$this->error($e->getMessage());
return;
}
}
private function disableServers(Team $team)
{
foreach ($team->servers as $server) {
if ($server->settings->is_usable === true || $server->settings->is_reachable === true || $server->ip !== '1.2.3.4') {
$this->info("Disabling server {$server->id} {$server->name}");
$server->settings()->update([
'is_usable' => false,
'is_reachable' => false,
]);
$server->update([
'ip' => '1.2.3.4',
]);
}
}
}
}

View file

@ -9,13 +9,41 @@ use Illuminate\Support\Facades\Process;
class Dev extends Command
{
protected $signature = 'dev:init';
protected $signature = 'dev {--init} {--generate-openapi}';
protected $description = 'Init the app in dev mode';
protected $description = 'Helper commands for development.';
public function handle()
{
if ($this->option('init')) {
$this->init();
return;
}
if ($this->option('generate-openapi')) {
$this->generateOpenApi();
return;
}
}
public function generateOpenApi()
{
// Generate OpenAPI documentation
echo "Generating OpenAPI documentation.\n";
$process = Process::run(['/var/www/html/vendor/bin/openapi', 'app', '-o', 'openapi.yaml']);
$error = $process->errorOutput();
$error = preg_replace('/^.*an object literal,.*$/m', '', $error);
$error = preg_replace('/^\h*\v+/m', '', $error);
echo $error;
echo $process->output();
}
public function init()
{
// Generate APP_KEY if not exists
if (empty(env('APP_KEY'))) {
echo "Generating APP_KEY.\n";
Artisan::call('key:generate');

View file

@ -0,0 +1,11 @@
<?php
namespace App\Enums;
enum BuildPackTypes: string
{
case NIXPACKS = 'nixpacks';
case STATIC = 'static';
case DOCKERFILE = 'dockerfile';
case DOCKERCOMPOSE = 'dockercompose';
}

View file

@ -0,0 +1,15 @@
<?php
namespace App\Enums;
enum NewDatabaseTypes: string
{
case POSTGRESQL = 'postgresql';
case MYSQL = 'mysql';
case MONGODB = 'mongodb';
case REDIS = 'redis';
case MARIADB = 'mariadb';
case KEYDB = 'keydb';
case DRAGONFLY = 'dragonfly';
case CLICKHOUSE = 'clickhouse';
}

View file

@ -0,0 +1,22 @@
<?php
namespace App\Enums;
enum NewResourceTypes: string
{
case PUBLIC = 'public';
case PRIVATE_GH_APP = 'private-gh-app';
case PRIVATE_DEPLOY_KEY = 'private-deploy-key';
case DOCKERFILE = 'dockerfile';
case DOCKERCOMPOSE = 'dockercompose';
case DOCKER_IMAGE = 'docker-image';
case SERVICE = 'service';
case POSTGRESQL = 'postgresql';
case MYSQL = 'mysql';
case MONGODB = 'mongodb';
case REDIS = 'redis';
case MARIADB = 'mariadb';
case KEYDB = 'keydb';
case DRAGONFLY = 'dragonfly';
case CLICKHOUSE = 'clickhouse';
}

View file

@ -0,0 +1,10 @@
<?php
namespace App\Enums;
enum RedirectTypes: string
{
case BOTH = 'both';
case WWW = 'www';
case NON_WWW = 'non-www';
}

View file

@ -12,7 +12,7 @@ class DatabaseStatusChanged implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $userId;
public ?string $userId = null;
public function __construct($userId = null)
{
@ -20,15 +20,19 @@ class DatabaseStatusChanged implements ShouldBroadcast
$userId = auth()->user()->id ?? null;
}
if (is_null($userId)) {
throw new \Exception('User id is null');
return false;
}
$this->userId = $userId;
}
public function broadcastOn(): array
public function broadcastOn(): ?array
{
return [
new PrivateChannel("user.{$this->userId}"),
];
if ($this->userId) {
return [
new PrivateChannel("user.{$this->userId}"),
];
}
return null;
}
}

View file

@ -12,7 +12,7 @@ class ServiceStatusChanged implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $userId;
public ?string $userId = null;
public function __construct($userId = null)
{
@ -20,15 +20,19 @@ class ServiceStatusChanged implements ShouldBroadcast
$userId = auth()->user()->id ?? null;
}
if (is_null($userId)) {
throw new \Exception('User id is null');
return false;
}
$this->userId = $userId;
}
public function broadcastOn(): array
public function broadcastOn(): ?array
{
return [
new PrivateChannel("user.{$this->userId}"),
];
if (! is_null($this->userId)) {
return [
new PrivateChannel("user.{$this->userId}"),
];
}
return null;
}
}

View file

@ -50,7 +50,7 @@ class Handler extends ExceptionHandler
return response()->json(['message' => $exception->getMessage()], 401);
}
return redirect()->guest($exception->redirectTo() ?? route('login'));
return redirect()->guest($exception->redirectTo($request) ?? route('login'));
}
/**
@ -65,7 +65,7 @@ class Handler extends ExceptionHandler
if ($e instanceof RuntimeException) {
return;
}
$this->settings = InstanceSettings::get();
$this->settings = view()->shared('instanceSettings');
if ($this->settings->do_not_track) {
return;
}

View file

@ -1,183 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Actions\Application\StopApplication;
use App\Http\Controllers\Controller;
use App\Models\Application;
use App\Models\Project;
use Illuminate\Http\Request;
use Visus\Cuid2\Cuid2;
class Applications extends Controller
{
public function applications(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return invalid_token();
}
$projects = Project::where('team_id', $teamId)->get();
$applications = collect();
$applications->push($projects->pluck('applications')->flatten());
$applications = $applications->flatten();
return response()->json($applications);
}
public function application_by_uuid(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return invalid_token();
}
$uuid = $request->route('uuid');
if (! $uuid) {
return response()->json(['error' => 'UUID is required.'], 400);
}
$application = Application::where('uuid', $uuid)->first();
if (! $application) {
return response()->json(['error' => 'Application not found.'], 404);
}
return response()->json($application);
}
public function update_by_uuid(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return invalid_token();
}
if ($request->collect()->count() == 0) {
return response()->json([
'message' => 'No data provided.',
], 400);
}
$application = Application::where('uuid', $request->uuid)->first();
if (! $application) {
return response()->json([
'success' => false,
'message' => 'Application not found',
], 404);
}
ray($request->collect());
// if ($request->has('domains')) {
// $existingDomains = explode(',', $application->fqdn);
// $newDomains = $request->domains;
// $filteredNewDomains = array_filter($newDomains, function ($domain) use ($existingDomains) {
// return ! in_array($domain, $existingDomains);
// });
// $mergedDomains = array_unique(array_merge($existingDomains, $filteredNewDomains));
// $application->fqdn = implode(',', $mergedDomains);
// $application->custom_labels = base64_encode(implode("\n ", generateLabelsApplication($application)));
// $application->save();
// }
return response()->json([
'message' => 'Application updated successfully.',
'application' => serialize_api_response($application),
]);
}
public function action_deploy(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return invalid_token();
}
$force = $request->query->get('force') ?? false;
$instant_deploy = $request->query->get('instant_deploy') ?? false;
$uuid = $request->route('uuid');
if (! $uuid) {
return response()->json(['error' => 'UUID is required.'], 400);
}
$application = Application::where('uuid', $uuid)->first();
if (! $application) {
return response()->json(['error' => 'Application not found.'], 404);
}
$deployment_uuid = new Cuid2(7);
queue_application_deployment(
application: $application,
deployment_uuid: $deployment_uuid,
force_rebuild: $force,
is_api: true,
no_questions_asked: $instant_deploy
);
return response()->json(
[
'message' => 'Deployment request queued.',
'deployment_uuid' => $deployment_uuid->toString(),
'deployment_api_url' => base_url().'/api/v1/deployment/'.$deployment_uuid->toString(),
],
200
);
}
public function action_stop(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return invalid_token();
}
$uuid = $request->route('uuid');
$sync = $request->query->get('sync') ?? false;
if (! $uuid) {
return response()->json(['error' => 'UUID is required.'], 400);
}
$application = Application::where('uuid', $uuid)->first();
if (! $application) {
return response()->json(['error' => 'Application not found.'], 404);
}
if ($sync) {
StopApplication::run($application);
return response()->json(['message' => 'Stopped the application.'], 200);
} else {
StopApplication::dispatch($application);
return response()->json(['message' => 'Stopping request queued.'], 200);
}
}
public function action_restart(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return invalid_token();
}
$uuid = $request->route('uuid');
if (! $uuid) {
return response()->json(['error' => 'UUID is required.'], 400);
}
$application = Application::where('uuid', $uuid)->first();
if (! $application) {
return response()->json(['error' => 'Application not found.'], 404);
}
$deployment_uuid = new Cuid2(7);
queue_application_deployment(
application: $application,
deployment_uuid: $deployment_uuid,
restart_only: true,
is_api: true,
);
return response()->json(
[
'message' => 'Restart request queued.',
'deployment_uuid' => $deployment_uuid->toString(),
'deployment_api_url' => base_url().'/api/v1/deployment/'.$deployment_uuid->toString(),
],
200
);
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,234 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Actions\Database\StartClickhouse;
use App\Actions\Database\StartDragonfly;
use App\Actions\Database\StartKeydb;
use App\Actions\Database\StartMariadb;
use App\Actions\Database\StartMongodb;
use App\Actions\Database\StartMysql;
use App\Actions\Database\StartPostgresql;
use App\Actions\Database\StartRedis;
use App\Actions\Service\StartService;
use App\Http\Controllers\Controller;
use App\Models\ApplicationDeploymentQueue;
use App\Models\Server;
use App\Models\Tag;
use Illuminate\Http\Request;
use Visus\Cuid2\Cuid2;
class Deploy extends Controller
{
public function deployments(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return invalid_token();
}
$servers = Server::whereTeamId($teamId)->get();
$deployments_per_server = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('server_id', $servers->pluck('id'))->get([
'id',
'application_id',
'application_name',
'deployment_url',
'pull_request_id',
'server_name',
'server_id',
'status',
])->sortBy('id')->toArray();
return response()->json(serialize_api_response($deployments_per_server), 200);
}
public function deployment_by_uuid(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return invalid_token();
}
$uuid = $request->route('uuid');
if (! $uuid) {
return response()->json(['error' => 'UUID is required.'], 400);
}
$deployment = ApplicationDeploymentQueue::where('deployment_uuid', $uuid)->first()->makeHidden('logs');
if (! $deployment) {
return response()->json(['error' => 'Deployment not found.'], 404);
}
return response()->json(serialize_api_response($deployment), 200);
}
public function deploy(Request $request)
{
$teamId = get_team_id_from_token();
$uuids = $request->query->get('uuid');
$tags = $request->query->get('tag');
$force = $request->query->get('force') ?? false;
if ($uuids && $tags) {
return response()->json(['error' => 'You can only use uuid or tag, not both.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 400);
}
if (is_null($teamId)) {
return invalid_token();
}
if ($tags) {
return $this->by_tags($tags, $teamId, $force);
} elseif ($uuids) {
return $this->by_uuids($uuids, $teamId, $force);
}
return response()->json(['error' => 'You must provide uuid or tag.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 400);
}
private function by_uuids(string $uuid, int $teamId, bool $force = false)
{
$uuids = explode(',', $uuid);
$uuids = collect(array_filter($uuids));
if (count($uuids) === 0) {
return response()->json(['error' => 'No UUIDs provided.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 400);
}
$deployments = collect();
$payload = collect();
foreach ($uuids as $uuid) {
$resource = getResourceByUuid($uuid, $teamId);
if ($resource) {
['message' => $return_message, 'deployment_uuid' => $deployment_uuid] = $this->deploy_resource($resource, $force);
if ($deployment_uuid) {
$deployments->push(['message' => $return_message, 'resource_uuid' => $uuid, 'deployment_uuid' => $deployment_uuid->toString()]);
} else {
$deployments->push(['message' => $return_message, 'resource_uuid' => $uuid]);
}
}
}
if ($deployments->count() > 0) {
$payload->put('deployments', $deployments->toArray());
return response()->json($payload->toArray(), 200);
}
return response()->json(['error' => 'No resources found.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 404);
}
public function by_tags(string $tags, int $team_id, bool $force = false)
{
$tags = explode(',', $tags);
$tags = collect(array_filter($tags));
if (count($tags) === 0) {
return response()->json(['error' => 'No TAGs provided.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 400);
}
$message = collect([]);
$deployments = collect();
$payload = collect();
foreach ($tags as $tag) {
$found_tag = Tag::where(['name' => $tag, 'team_id' => $team_id])->first();
if (! $found_tag) {
// $message->push("Tag {$tag} not found.");
continue;
}
$applications = $found_tag->applications()->get();
$services = $found_tag->services()->get();
if ($applications->count() === 0 && $services->count() === 0) {
$message->push("No resources found for tag {$tag}.");
continue;
}
foreach ($applications as $resource) {
['message' => $return_message, 'deployment_uuid' => $deployment_uuid] = $this->deploy_resource($resource, $force);
if ($deployment_uuid) {
$deployments->push(['resource_uuid' => $resource->uuid, 'deployment_uuid' => $deployment_uuid->toString()]);
}
$message = $message->merge($return_message);
}
foreach ($services as $resource) {
['message' => $return_message] = $this->deploy_resource($resource, $force);
$message = $message->merge($return_message);
}
}
ray($message);
if ($message->count() > 0) {
$payload->put('message', $message->toArray());
if ($deployments->count() > 0) {
$payload->put('details', $deployments->toArray());
}
return response()->json($payload->toArray(), 200);
}
return response()->json(['error' => 'No resources found with this tag.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 404);
}
public function deploy_resource($resource, bool $force = false): array
{
$message = null;
$deployment_uuid = null;
if (gettype($resource) !== 'object') {
return ['message' => "Resource ($resource) not found.", 'deployment_uuid' => $deployment_uuid];
}
$type = $resource?->getMorphClass();
if ($type === 'App\Models\Application') {
$deployment_uuid = new Cuid2(7);
queue_application_deployment(
application: $resource,
deployment_uuid: $deployment_uuid,
force_rebuild: $force,
);
$message = "Application {$resource->name} deployment queued.";
} elseif ($type === 'App\Models\StandalonePostgresql') {
StartPostgresql::run($resource);
$resource->update([
'started_at' => now(),
]);
$message = "Database {$resource->name} started.";
} elseif ($type === 'App\Models\StandaloneRedis') {
StartRedis::run($resource);
$resource->update([
'started_at' => now(),
]);
$message = "Database {$resource->name} started.";
} elseif ($type === 'App\Models\StandaloneKeydb') {
StartKeydb::run($resource);
$resource->update([
'started_at' => now(),
]);
$message = "Database {$resource->name} started.";
} elseif ($type === 'App\Models\StandaloneDragonfly') {
StartDragonfly::run($resource);
$resource->update([
'started_at' => now(),
]);
$message = "Database {$resource->name} started.";
} elseif ($type === 'App\Models\StandaloneClickhouse') {
StartClickhouse::run($resource);
$resource->update([
'started_at' => now(),
]);
$message = "Database {$resource->name} started.";
} elseif ($type === 'App\Models\StandaloneMongodb') {
StartMongodb::run($resource);
$resource->update([
'started_at' => now(),
]);
$message = "Database {$resource->name} started.";
} elseif ($type === 'App\Models\StandaloneMysql') {
StartMysql::run($resource);
$resource->update([
'started_at' => now(),
]);
$message = "Database {$resource->name} started.";
} elseif ($type === 'App\Models\StandaloneMariadb') {
StartMariadb::run($resource);
$resource->update([
'started_at' => now(),
]);
$message = "Database {$resource->name} started.";
} elseif ($type === 'App\Models\Service') {
StartService::run($resource);
$message = "Service {$resource->name} started. It could take a while, be patient.";
}
return ['message' => $message, 'deployment_uuid' => $deployment_uuid];
}
}

View file

@ -0,0 +1,317 @@
<?php
namespace App\Http\Controllers\Api;
use App\Actions\Database\StartDatabase;
use App\Actions\Service\StartService;
use App\Http\Controllers\Controller;
use App\Models\ApplicationDeploymentQueue;
use App\Models\Server;
use App\Models\Tag;
use Illuminate\Http\Request;
use OpenApi\Attributes as OA;
use Visus\Cuid2\Cuid2;
class DeployController extends Controller
{
private function removeSensitiveData($deployment)
{
$token = auth()->user()->currentAccessToken();
if ($token->can('view:sensitive')) {
return serializeApiResponse($deployment);
}
$deployment->makeHidden([
'logs',
]);
return serializeApiResponse($deployment);
}
#[OA\Get(
summary: 'List',
description: 'List currently running deployments',
path: '/deployments',
security: [
['bearerAuth' => []],
],
tags: ['Deployments'],
responses: [
new OA\Response(
response: 200,
description: 'Get all currently running deployments.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'array',
items: new OA\Items(ref: '#/components/schemas/ApplicationDeploymentQueue'),
)
),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
]
)]
public function deployments(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$servers = Server::whereTeamId($teamId)->get();
$deployments_per_server = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('server_id', $servers->pluck('id'))->get()->sortBy('id');
$deployments_per_server = $deployments_per_server->map(function ($deployment) {
return $this->removeSensitiveData($deployment);
});
return response()->json($deployments_per_server);
}
#[OA\Get(
summary: 'Get',
description: 'Get deployment by UUID.',
path: '/deployments/{uuid}',
security: [
['bearerAuth' => []],
],
tags: ['Deployments'],
parameters: [
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Deployment Uuid', schema: new OA\Schema(type: 'integer')),
],
responses: [
new OA\Response(
response: 200,
description: 'Get deployment by UUID.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
ref: '#/components/schemas/ApplicationDeploymentQueue',
)
),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
new OA\Response(
response: 404,
ref: '#/components/responses/404',
),
]
)]
public function deployment_by_uuid(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$uuid = $request->route('uuid');
if (! $uuid) {
return response()->json(['message' => 'UUID is required.'], 400);
}
$deployment = ApplicationDeploymentQueue::where('deployment_uuid', $uuid)->first();
if (! $deployment) {
return response()->json(['message' => 'Deployment not found.'], 404);
}
return response()->json($this->removeSensitiveData($deployment));
}
#[OA\Get(
summary: 'Deploy',
description: 'Deploy by tag or uuid. `Post` request also accepted.',
path: '/deploy',
security: [
['bearerAuth' => []],
],
tags: ['Deployments'],
parameters: [
new OA\Parameter(name: 'tag', in: 'query', description: 'Tag name(s). Comma separated list is also accepted.', schema: new OA\Schema(type: 'string')),
new OA\Parameter(name: 'uuid', in: 'query', description: 'Resource UUID(s). Comma separated list is also accepted.', schema: new OA\Schema(type: 'string')),
new OA\Parameter(name: 'force', in: 'query', description: 'Force rebuild (without cache)', schema: new OA\Schema(type: 'boolean')),
],
responses: [
new OA\Response(
response: 200,
description: 'Get deployment(s) Uuid\'s',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'deployments' => new OA\Property(
property: 'deployments',
type: 'array',
items: new OA\Items(
type: 'object',
properties: [
'message' => ['type' => 'string'],
'resource_uuid' => ['type' => 'string'],
'deployment_uuid' => ['type' => 'string'],
]
),
),
],
)
),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
]
)]
public function deploy(Request $request)
{
$teamId = getTeamIdFromToken();
$uuids = $request->query->get('uuid');
$tags = $request->query->get('tag');
$force = $request->query->get('force') ?? false;
if ($uuids && $tags) {
return response()->json(['message' => 'You can only use uuid or tag, not both.'], 400);
}
if (is_null($teamId)) {
return invalidTokenResponse();
}
if ($tags) {
return $this->by_tags($tags, $teamId, $force);
} elseif ($uuids) {
return $this->by_uuids($uuids, $teamId, $force);
}
return response()->json(['message' => 'You must provide uuid or tag.'], 400);
}
private function by_uuids(string $uuid, int $teamId, bool $force = false)
{
$uuids = explode(',', $uuid);
$uuids = collect(array_filter($uuids));
if (count($uuids) === 0) {
return response()->json(['message' => 'No UUIDs provided.'], 400);
}
$deployments = collect();
$payload = collect();
foreach ($uuids as $uuid) {
$resource = getResourceByUuid($uuid, $teamId);
if ($resource) {
['message' => $return_message, 'deployment_uuid' => $deployment_uuid] = $this->deploy_resource($resource, $force);
if ($deployment_uuid) {
$deployments->push(['message' => $return_message, 'resource_uuid' => $uuid, 'deployment_uuid' => $deployment_uuid->toString()]);
} else {
$deployments->push(['message' => $return_message, 'resource_uuid' => $uuid]);
}
}
}
if ($deployments->count() > 0) {
$payload->put('deployments', $deployments->toArray());
return response()->json(serializeApiResponse($payload->toArray()));
}
return response()->json(['message' => 'No resources found.'], 404);
}
public function by_tags(string $tags, int $team_id, bool $force = false)
{
$tags = explode(',', $tags);
$tags = collect(array_filter($tags));
if (count($tags) === 0) {
return response()->json(['message' => 'No TAGs provided.'], 400);
}
$message = collect([]);
$deployments = collect();
$payload = collect();
foreach ($tags as $tag) {
$found_tag = Tag::where(['name' => $tag, 'team_id' => $team_id])->first();
if (! $found_tag) {
// $message->push("Tag {$tag} not found.");
continue;
}
$applications = $found_tag->applications()->get();
$services = $found_tag->services()->get();
if ($applications->count() === 0 && $services->count() === 0) {
$message->push("No resources found for tag {$tag}.");
continue;
}
foreach ($applications as $resource) {
['message' => $return_message, 'deployment_uuid' => $deployment_uuid] = $this->deploy_resource($resource, $force);
if ($deployment_uuid) {
$deployments->push(['resource_uuid' => $resource->uuid, 'deployment_uuid' => $deployment_uuid->toString()]);
}
$message = $message->merge($return_message);
}
foreach ($services as $resource) {
['message' => $return_message] = $this->deploy_resource($resource, $force);
$message = $message->merge($return_message);
}
}
if ($message->count() > 0) {
$payload->put('message', $message->toArray());
if ($deployments->count() > 0) {
$payload->put('details', $deployments->toArray());
}
return response()->json(serializeApiResponse($payload->toArray()));
}
return response()->json(['message' => 'No resources found with this tag.'], 404);
}
public function deploy_resource($resource, bool $force = false): array
{
$message = null;
$deployment_uuid = null;
if (gettype($resource) !== 'object') {
return ['message' => "Resource ($resource) not found.", 'deployment_uuid' => $deployment_uuid];
}
switch ($resource?->getMorphClass()) {
case 'App\Models\Application':
$deployment_uuid = new Cuid2(7);
queue_application_deployment(
application: $resource,
deployment_uuid: $deployment_uuid,
force_rebuild: $force,
);
$message = "Application {$resource->name} deployment queued.";
break;
case 'App\Models\Service':
StartService::run($resource);
$message = "Service {$resource->name} started. It could take a while, be patient.";
break;
default:
// Database resource
StartDatabase::dispatch($resource);
$resource->update([
'started_at' => now(),
]);
$message = "Database {$resource->name} started.";
break;
}
return ['message' => $message, 'deployment_uuid' => $deployment_uuid];
}
}

View file

@ -1,54 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Application;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
class Domains extends Controller
{
public function deleteDomains(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return invalid_token();
}
$validator = Validator::make($request->all(), [
'uuid' => 'required|string|exists:applications,uuid',
'domains' => 'required|array',
'domains.*' => 'required|string|distinct',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'message' => 'Validation failed',
'errors' => $validator->errors(),
], 422);
}
$application = Application::where('uuid', $request->uuid)->first();
if (! $application) {
return response()->json([
'success' => false,
'message' => 'Application not found',
], 404);
}
$existingDomains = explode(',', $application->fqdn);
$domainsToDelete = $request->domains;
$updatedDomains = array_diff($existingDomains, $domainsToDelete);
$application->fqdn = implode(',', $updatedDomains);
$application->custom_labels = base64_encode(implode("\n ", generateLabelsApplication($application)));
$application->save();
return response()->json([
'success' => true,
'message' => 'Domains updated successfully',
'application' => $application,
]);
}
}

View file

@ -0,0 +1,35 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\EnvironmentVariable;
use Illuminate\Http\Request;
class EnvironmentVariablesController extends Controller
{
public function delete_env_by_uuid(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$env = EnvironmentVariable::where('uuid', $request->env_uuid)->first();
if (! $env) {
return response()->json([
'message' => 'Environment variable not found.',
], 404);
}
$found_app = $env->resource()->whereRelation('environment.project.team', 'id', $teamId)->first();
if (! $found_app) {
return response()->json([
'message' => 'Environment variable not found.',
], 404);
}
$env->delete();
return response()->json([
'message' => 'Environment variable deleted.',
]);
}
}

View file

@ -0,0 +1,51 @@
<?php
namespace App\Http\Controllers\Api;
use OpenApi\Attributes as OA;
#[OA\Info(title: 'Coolify', version: '0.1')]
#[OA\Server(url: 'https://app.coolify.io/api/v1')]
#[OA\SecurityScheme(
type: 'http',
scheme: 'bearer',
securityScheme: 'bearerAuth',
description: 'Go to `Keys & Tokens` / `API tokens` and create a new token. Use the token as the bearer token.')]
#[OA\Components(
responses: [
new OA\Response(
response: 400,
description: 'Invalid token.',
content: new OA\JsonContent(
type: 'object',
properties: [
new OA\Property(property: 'message', type: 'string', example: 'Invalid token.'),
]
)),
new OA\Response(
response: 401,
description: 'Unauthenticated.',
content: new OA\JsonContent(
type: 'object',
properties: [
new OA\Property(property: 'message', type: 'string', example: 'Unauthenticated.'),
]
)),
new OA\Response(
response: 404,
description: 'Resource not found.',
content: new OA\JsonContent(
type: 'object',
properties: [
new OA\Property(property: 'message', type: 'string', example: 'Resource not found.'),
]
)),
],
)]
class OpenApi
{
// This class is used to generate OpenAPI documentation
// for the Coolify API. It is not a controller and does
// not contain any routes. It is used to define the
// OpenAPI metadata and security scheme for the API.
}

View file

@ -0,0 +1,183 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use OpenApi\Attributes as OA;
class OtherController extends Controller
{
#[OA\Get(
summary: 'Version',
description: 'Get Coolify version.',
path: '/version',
security: [
['bearerAuth' => []],
],
responses: [
new OA\Response(
response: 200,
description: 'Returns the version of the application',
content: new OA\JsonContent(
type: 'string',
example: 'v4.0.0',
)),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
]
)]
public function version(Request $request)
{
return response(config('version'));
}
#[OA\Get(
summary: 'Enable API',
description: 'Enable API (only with root permissions).',
path: '/enable',
security: [
['bearerAuth' => []],
],
responses: [
new OA\Response(
response: 200,
description: 'Enable API.',
content: new OA\JsonContent(
type: 'object',
properties: [
new OA\Property(property: 'message', type: 'string', example: 'API enabled.'),
]
)),
new OA\Response(
response: 403,
description: 'You are not allowed to enable the API.',
content: new OA\JsonContent(
type: 'object',
properties: [
new OA\Property(property: 'message', type: 'string', example: 'You are not allowed to enable the API.'),
]
)),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
]
)]
public function enable_api(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
if ($teamId !== '0') {
return response()->json(['message' => 'You are not allowed to enable the API.'], 403);
}
$settings = view()->shared('instanceSettings');
$settings->update(['is_api_enabled' => true]);
return response()->json(['message' => 'API enabled.'], 200);
}
#[OA\Get(
summary: 'Disable API',
description: 'Disable API (only with root permissions).',
path: '/disable',
security: [
['bearerAuth' => []],
],
responses: [
new OA\Response(
response: 200,
description: 'Disable API.',
content: new OA\JsonContent(
type: 'object',
properties: [
new OA\Property(property: 'message', type: 'string', example: 'API disabled.'),
]
)),
new OA\Response(
response: 403,
description: 'You are not allowed to disable the API.',
content: new OA\JsonContent(
type: 'object',
properties: [
new OA\Property(property: 'message', type: 'string', example: 'You are not allowed to disable the API.'),
]
)),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
]
)]
public function disable_api(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
if ($teamId !== '0') {
return response()->json(['message' => 'You are not allowed to disable the API.'], 403);
}
$settings = view()->shared('instanceSettings');
$settings->update(['is_api_enabled' => false]);
return response()->json(['message' => 'API disabled.'], 200);
}
public function feedback(Request $request)
{
$content = $request->input('content');
$webhook_url = config('coolify.feedback_discord_webhook');
if ($webhook_url) {
Http::post($webhook_url, [
'content' => $content,
]);
}
return response()->json(['message' => 'Feedback sent.'], 200);
}
#[OA\Get(
summary: 'Healthcheck',
description: 'Healthcheck endpoint.',
path: '/healthcheck',
responses: [
new OA\Response(
response: 200,
description: 'Healthcheck endpoint.',
content: new OA\JsonContent(
type: 'string',
example: 'OK',
)),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
]
)]
public function healthcheck(Request $request)
{
return 'OK';
}
}

View file

@ -1,44 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Project as ModelsProject;
use Illuminate\Http\Request;
class Project extends Controller
{
public function projects(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return invalid_token();
}
$projects = ModelsProject::whereTeamId($teamId)->select('id', 'name', 'uuid')->get();
return response()->json($projects);
}
public function project_by_uuid(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return invalid_token();
}
$project = ModelsProject::whereTeamId($teamId)->whereUuid(request()->uuid)->first()->load(['environments']);
return response()->json($project);
}
public function environment_details(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return invalid_token();
}
$project = ModelsProject::whereTeamId($teamId)->whereUuid(request()->uuid)->first();
$environment = $project->environments()->whereName(request()->environment_name)->first()->load(['applications', 'postgresqls', 'redis', 'mongodbs', 'mysqls', 'mariadbs', 'services']);
return response()->json($environment);
}
}

View file

@ -0,0 +1,147 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Project;
use Illuminate\Http\Request;
use OpenApi\Attributes as OA;
class ProjectController extends Controller
{
#[OA\Get(
summary: 'List',
description: 'list projects.',
path: '/projects',
security: [
['bearerAuth' => []],
],
tags: ['Projects'],
responses: [
new OA\Response(
response: 200,
description: 'Get all projects.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'array',
items: new OA\Items(ref: '#/components/schemas/Project')
)
),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
]
)]
public function projects(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$projects = Project::whereTeamId($teamId)->select('id', 'name', 'uuid')->get();
return response()->json(serializeApiResponse($projects),
);
}
#[OA\Get(
summary: 'Get',
description: 'Get project by Uuid.',
path: '/projects/{uuid}',
security: [
['bearerAuth' => []],
],
tags: ['Projects'],
parameters: [
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Project UUID', schema: new OA\Schema(type: 'integer')),
],
responses: [
new OA\Response(
response: 200,
description: 'Project details',
content: new OA\JsonContent(ref: '#/components/schemas/Project')),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
new OA\Response(
response: 404,
description: 'Project not found.',
),
]
)]
public function project_by_uuid(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$project = Project::whereTeamId($teamId)->whereUuid(request()->uuid)->first()->load(['environments']);
if (! $project) {
return response()->json(['message' => 'Project not found.'], 404);
}
return response()->json(
serializeApiResponse($project),
);
}
#[OA\Get(
summary: 'Environment',
description: 'Get environment by name.',
path: '/projects/{uuid}/{environment_name}',
security: [
['bearerAuth' => []],
],
tags: ['Projects'],
parameters: [
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Project UUID', schema: new OA\Schema(type: 'integer')),
new OA\Parameter(name: 'environment_name', in: 'path', required: true, description: 'Environment name', schema: new OA\Schema(type: 'string')),
],
responses: [
new OA\Response(
response: 200,
description: 'Project details',
content: new OA\JsonContent(ref: '#/components/schemas/Environment')),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
new OA\Response(
response: 404,
ref: '#/components/responses/404',
),
]
)]
public function environment_details(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$project = Project::whereTeamId($teamId)->whereUuid(request()->uuid)->first();
$environment = $project->environments()->whereName(request()->environment_name)->first();
if (! $environment) {
return response()->json(['message' => 'Environment not found.'], 404);
}
$environment = $environment->load(['applications', 'postgresqls', 'redis', 'mongodbs', 'mysqls', 'mariadbs', 'services']);
return response()->json(serializeApiResponse($environment));
}
}

View file

@ -5,14 +5,42 @@ namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Project;
use Illuminate\Http\Request;
use OpenApi\Attributes as OA;
class Resources extends Controller
class ResourcesController extends Controller
{
#[OA\Get(
summary: 'List',
description: 'Get all resources.',
path: '/resources',
security: [
['bearerAuth' => []],
],
tags: ['Resources'],
responses: [
new OA\Response(
response: 200,
description: 'Get all resources',
content: new OA\JsonContent(
type: 'string',
example: 'Content is very complex. Will be implemented later.',
),
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
]
)]
public function resources(Request $request)
{
$teamId = get_team_id_from_token();
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalid_token();
return invalidTokenResponse();
}
$projects = Project::where('team_id', $teamId)->get();
$resources = collect();
@ -34,6 +62,6 @@ class Resources extends Controller
return $payload;
});
return response()->json($resources);
return response()->json(serializeApiResponse($resources));
}
}

View file

@ -0,0 +1,372 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\PrivateKey;
use Illuminate\Http\Request;
use OpenApi\Attributes as OA;
class SecurityController extends Controller
{
private function removeSensitiveData($team)
{
$token = auth()->user()->currentAccessToken();
if ($token->can('view:sensitive')) {
return serializeApiResponse($team);
}
$team->makeHidden([
'private_key',
]);
return serializeApiResponse($team);
}
#[OA\Get(
summary: 'List',
description: 'List all private keys.',
path: '/security/keys',
security: [
['bearerAuth' => []],
],
tags: ['Private Keys'],
responses: [
new OA\Response(
response: 200,
description: 'Get all private keys.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'array',
items: new OA\Items(ref: '#/components/schemas/PrivateKey')
)
),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
]
)]
public function keys(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$keys = PrivateKey::where('team_id', $teamId)->get();
return response()->json($this->removeSensitiveData($keys));
}
#[OA\Get(
summary: 'Get',
description: 'Get key by UUID.',
path: '/security/keys/{uuid}',
security: [
['bearerAuth' => []],
],
tags: ['Private Keys'],
parameters: [
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Private Key Uuid', schema: new OA\Schema(type: 'integer')),
],
responses: [
new OA\Response(
response: 200,
description: 'Get all private keys.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'array',
items: new OA\Items(ref: '#/components/schemas/PrivateKey')
)
),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
new OA\Response(
response: 404,
description: 'Private Key not found.',
),
]
)]
public function key_by_uuid(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$key = PrivateKey::where('team_id', $teamId)->where('uuid', $request->uuid)->first();
if (is_null($key)) {
return response()->json([
'message' => 'Private Key not found.',
], 404);
}
return response()->json($this->removeSensitiveData($key));
}
#[OA\Post(
summary: 'Create',
description: 'Create a new private key.',
path: '/security/keys',
security: [
['bearerAuth' => []],
],
tags: ['Private Keys'],
requestBody: new OA\RequestBody(
required: true,
content: [
'application/json' => new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
required: ['private_key'],
properties: [
'name' => ['type' => 'string'],
'description' => ['type' => 'string'],
'private_key' => ['type' => 'string'],
],
additionalProperties: false,
)
),
]
),
responses: [
new OA\Response(
response: 201,
description: 'The created private key\'s UUID.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'uuid' => ['type' => 'string'],
]
)
),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
]
)]
public function create_key(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$return = validateIncomingRequest($request);
if ($return instanceof \Illuminate\Http\JsonResponse) {
return $return;
}
$validator = customApiValidator($request->all(), [
'name' => 'string|max:255',
'description' => 'string|max:255',
'private_key' => 'required|string',
]);
if ($validator->fails()) {
$errors = $validator->errors();
return response()->json([
'message' => 'Validation failed.',
'errors' => $errors,
], 422);
}
if (! $request->name) {
$request->offsetSet('name', generate_random_name());
}
if (! $request->description) {
$request->offsetSet('description', 'Created by Coolify via API');
}
$key = PrivateKey::create([
'team_id' => $teamId,
'name' => $request->name,
'description' => $request->description,
'private_key' => $request->private_key,
]);
return response()->json(serializeApiResponse([
'uuid' => $key->uuid,
]))->setStatusCode(201);
}
#[OA\Patch(
summary: 'Update',
description: 'Update a private key.',
path: '/security/keys',
security: [
['bearerAuth' => []],
],
tags: ['Private Keys'],
requestBody: new OA\RequestBody(
required: true,
content: [
'application/json' => new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
required: ['private_key'],
properties: [
'name' => ['type' => 'string'],
'description' => ['type' => 'string'],
'private_key' => ['type' => 'string'],
],
additionalProperties: false,
)
),
]
),
responses: [
new OA\Response(
response: 201,
description: 'The updated private key\'s UUID.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'uuid' => ['type' => 'string'],
]
)
),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
]
)]
public function update_key(Request $request)
{
$allowedFields = ['name', 'description', 'private_key'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$return = validateIncomingRequest($request);
if ($return instanceof \Illuminate\Http\JsonResponse) {
return $return;
}
$validator = customApiValidator($request->all(), [
'name' => 'string|max:255',
'description' => 'string|max:255',
'private_key' => 'required|string',
]);
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
if ($validator->fails() || ! empty($extraFields)) {
$errors = $validator->errors();
if (! empty($extraFields)) {
foreach ($extraFields as $field) {
$errors->add($field, 'This field is not allowed.');
}
}
return response()->json([
'message' => 'Validation failed.',
'errors' => $errors,
], 422);
}
$foundKey = PrivateKey::where('team_id', $teamId)->where('uuid', $request->uuid)->first();
if (is_null($foundKey)) {
return response()->json([
'message' => 'Private Key not found.',
], 404);
}
$foundKey->update($request->all());
return response()->json(serializeApiResponse([
'uuid' => $foundKey->uuid,
]))->setStatusCode(201);
}
#[OA\Delete(
summary: 'Delete',
description: 'Delete a private key.',
path: '/security/keys/{uuid}',
security: [
['bearerAuth' => []],
],
tags: ['Private Keys'],
parameters: [
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Private Key Uuid', schema: new OA\Schema(type: 'integer')),
],
responses: [
new OA\Response(
response: 200,
description: 'Private Key deleted.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Private Key deleted.'],
]
)
),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
new OA\Response(
response: 404,
description: 'Private Key not found.',
),
]
)]
public function delete_key(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
if (! $request->uuid) {
return response()->json(['message' => 'UUID is required.'], 422);
}
$key = PrivateKey::where('team_id', $teamId)->where('uuid', $request->uuid)->first();
if (is_null($key)) {
return response()->json(['message' => 'Private Key not found.'], 404);
}
$key->forceDelete();
return response()->json([
'message' => 'Private Key deleted.',
]);
}
}

View file

@ -1,167 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Application;
use App\Models\InstanceSettings;
use App\Models\Project;
use App\Models\Server as ModelsServer;
use Illuminate\Http\Request;
class Server extends Controller
{
public function servers(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return invalid_token();
}
$servers = ModelsServer::whereTeamId($teamId)->select('id', 'name', 'uuid', 'ip', 'user', 'port')->get()->load(['settings'])->map(function ($server) {
$server['is_reachable'] = $server->settings->is_reachable;
$server['is_usable'] = $server->settings->is_usable;
return $server;
});
return response()->json($servers);
}
public function server_by_uuid(Request $request)
{
$with_resources = $request->query('resources');
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return invalid_token();
}
$server = ModelsServer::whereTeamId($teamId)->whereUuid(request()->uuid)->first();
if (is_null($server)) {
return response()->json(['error' => 'Server not found.'], 404);
}
if ($with_resources) {
$server['resources'] = $server->definedResources()->map(function ($resource) {
$payload = [
'id' => $resource->id,
'uuid' => $resource->uuid,
'name' => $resource->name,
'type' => $resource->type(),
'created_at' => $resource->created_at,
'updated_at' => $resource->updated_at,
];
if ($resource->type() === 'service') {
$payload['status'] = $resource->status();
} else {
$payload['status'] = $resource->status;
}
return $payload;
});
} else {
$server->load(['settings']);
}
return response()->json($server);
}
public function get_domains_by_server(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return invalid_token();
}
$uuid = $request->query->get('uuid');
if ($uuid) {
$domains = Application::getDomainsByUuid($uuid);
return response()->json([
'uuid' => $uuid,
'domains' => $domains,
]);
}
$projects = Project::where('team_id', $teamId)->get();
$domains = collect();
$applications = $projects->pluck('applications')->flatten();
$settings = InstanceSettings::get();
if ($applications->count() > 0) {
foreach ($applications as $application) {
$ip = $application->destination->server->ip;
$fqdn = str($application->fqdn)->explode(',')->map(function ($fqdn) {
return str($fqdn)->replace('http://', '')->replace('https://', '')->replace('/', '');
});
if ($ip === 'host.docker.internal') {
if ($settings->public_ipv4) {
$domains->push([
'domain' => $fqdn,
'ip' => $settings->public_ipv4,
]);
}
if ($settings->public_ipv6) {
$domains->push([
'domain' => $fqdn,
'ip' => $settings->public_ipv6,
]);
}
if (! $settings->public_ipv4 && ! $settings->public_ipv6) {
$domains->push([
'domain' => $fqdn,
'ip' => $ip,
]);
}
} else {
$domains->push([
'domain' => $fqdn,
'ip' => $ip,
]);
}
}
}
$services = $projects->pluck('services')->flatten();
if ($services->count() > 0) {
foreach ($services as $service) {
$service_applications = $service->applications;
if ($service_applications->count() > 0) {
foreach ($service_applications as $application) {
$fqdn = str($application->fqdn)->explode(',')->map(function ($fqdn) {
return str($fqdn)->replace('http://', '')->replace('https://', '')->replace('/', '');
});
if ($ip === 'host.docker.internal') {
if ($settings->public_ipv4) {
$domains->push([
'domain' => $fqdn,
'ip' => $settings->public_ipv4,
]);
}
if ($settings->public_ipv6) {
$domains->push([
'domain' => $fqdn,
'ip' => $settings->public_ipv6,
]);
}
if (! $settings->public_ipv4 && ! $settings->public_ipv6) {
$domains->push([
'domain' => $fqdn,
'ip' => $ip,
]);
}
} else {
$domains->push([
'domain' => $fqdn,
'ip' => $ip,
]);
}
}
}
}
}
$domains = $domains->groupBy('ip')->map(function ($domain) {
return $domain->pluck('domain')->flatten();
})->map(function ($domain, $ip) {
return [
'ip' => $ip,
'domains' => $domain,
];
})->values();
return response()->json($domains);
}
}

View file

@ -0,0 +1,395 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Application;
use App\Models\Project;
use App\Models\Server as ModelsServer;
use Illuminate\Http\Request;
use OpenApi\Attributes as OA;
use Stringable;
class ServersController extends Controller
{
private function removeSensitiveDataFromSettings($settings)
{
$token = auth()->user()->currentAccessToken();
if ($token->can('view:sensitive')) {
return serializeApiResponse($settings);
}
$settings = $settings->makeHidden([
'metrics_token',
]);
return serializeApiResponse($settings);
}
private function removeSensitiveData($server)
{
$token = auth()->user()->currentAccessToken();
$server->makeHidden([
'id',
]);
if ($token->can('view:sensitive')) {
return serializeApiResponse($server);
}
return serializeApiResponse($server);
}
#[OA\Get(
summary: 'List',
description: 'List all servers.',
path: '/servers',
security: [
['bearerAuth' => []],
],
tags: ['Servers'],
responses: [
new OA\Response(
response: 200,
description: 'Get all servers.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'array',
items: new OA\Items(ref: '#/components/schemas/Server')
)
),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
]
)]
public function servers(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$servers = ModelsServer::whereTeamId($teamId)->select('id', 'name', 'uuid', 'ip', 'user', 'port')->get()->load(['settings'])->map(function ($server) {
$server['is_reachable'] = $server->settings->is_reachable;
$server['is_usable'] = $server->settings->is_usable;
return $server;
});
$servers = $servers->map(function ($server) {
$settings = $this->removeSensitiveDataFromSettings($server->settings);
$server = $this->removeSensitiveData($server);
data_set($server, 'settings', $settings);
return $server;
});
return response()->json($servers);
}
#[OA\Get(
summary: 'Get',
description: 'Get server by UUID.',
path: '/servers/{uuid}',
security: [
['bearerAuth' => []],
],
tags: ['Servers'],
parameters: [
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server\'s Uuid', schema: new OA\Schema(type: 'integer')),
],
responses: [
new OA\Response(
response: 200,
description: 'Get server by UUID',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
ref: '#/components/schemas/Server'
)
),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
new OA\Response(
response: 404,
ref: '#/components/responses/404',
),
]
)]
public function server_by_uuid(Request $request)
{
$with_resources = $request->query('resources');
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$server = ModelsServer::whereTeamId($teamId)->whereUuid(request()->uuid)->first();
if (is_null($server)) {
return response()->json(['message' => 'Server not found.'], 404);
}
if ($with_resources) {
$server['resources'] = $server->definedResources()->map(function ($resource) {
$payload = [
'id' => $resource->id,
'uuid' => $resource->uuid,
'name' => $resource->name,
'type' => $resource->type(),
'created_at' => $resource->created_at,
'updated_at' => $resource->updated_at,
];
if ($resource->type() === 'service') {
$payload['status'] = $resource->status();
} else {
$payload['status'] = $resource->status;
}
return $payload;
});
} else {
$server->load(['settings']);
}
$settings = $this->removeSensitiveDataFromSettings($server->settings);
$server = $this->removeSensitiveData($server);
data_set($server, 'settings', $settings);
return response()->json(serializeApiResponse($server));
}
#[OA\Get(
summary: 'Resources',
description: 'Get resources by server.',
path: '/servers/{uuid}/resources',
security: [
['bearerAuth' => []],
],
tags: ['Servers'],
parameters: [
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server\'s Uuid', schema: new OA\Schema(type: 'integer')),
],
responses: [
new OA\Response(
response: 200,
description: 'Get resources by server',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'array',
items: new OA\Items(
type: 'object',
properties: [
'id' => ['type' => 'integer'],
'uuid' => ['type' => 'string'],
'name' => ['type' => 'string'],
'type' => ['type' => 'string'],
'created_at' => ['type' => 'string'],
'updated_at' => ['type' => 'string'],
'status' => ['type' => 'string'],
]
)
)),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
]
)]
public function resources_by_server(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$server = ModelsServer::whereTeamId($teamId)->whereUuid(request()->uuid)->first();
if (is_null($server)) {
return response()->json(['message' => 'Server not found.'], 404);
}
$server['resources'] = $server->definedResources()->map(function ($resource) {
$payload = [
'id' => $resource->id,
'uuid' => $resource->uuid,
'name' => $resource->name,
'type' => $resource->type(),
'created_at' => $resource->created_at,
'updated_at' => $resource->updated_at,
];
if ($resource->type() === 'service') {
$payload['status'] = $resource->status();
} else {
$payload['status'] = $resource->status;
}
return $payload;
});
$server = $this->removeSensitiveData($server);
ray($server);
return response()->json(serializeApiResponse(data_get($server, 'resources')));
}
#[OA\Get(
summary: 'Domains',
description: 'Get domains by server.',
path: '/servers/{uuid}/domains',
security: [
['bearerAuth' => []],
],
tags: ['Servers'],
parameters: [
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server\'s Uuid', schema: new OA\Schema(type: 'integer')),
],
responses: [
new OA\Response(
response: 200,
description: 'Get domains by server',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'array',
items: new OA\Items(
type: 'object',
properties: [
'ip' => ['type' => 'string'],
'domains' => ['type' => 'array', 'items' => ['type' => 'string']],
]
)
)),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
]
)]
public function domains_by_server(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$uuid = $request->get('uuid');
if ($uuid) {
$domains = Application::getDomainsByUuid($uuid);
return response()->json(serializeApiResponse($domains));
}
$projects = Project::where('team_id', $teamId)->get();
$domains = collect();
$applications = $projects->pluck('applications')->flatten();
$settings = view()->shared('instanceSettings');
if ($applications->count() > 0) {
foreach ($applications as $application) {
$ip = $application->destination->server->ip;
$fqdn = str($application->fqdn)->explode(',')->map(function ($fqdn) {
$f = str($fqdn)->replace('http://', '')->replace('https://', '')->explode('/');
return str(str($f[0])->explode(':')[0]);
})->filter(function (Stringable $fqdn) {
return $fqdn->isNotEmpty();
});
if ($ip === 'host.docker.internal') {
if ($settings->public_ipv4) {
$domains->push([
'domain' => $fqdn,
'ip' => $settings->public_ipv4,
]);
}
if ($settings->public_ipv6) {
$domains->push([
'domain' => $fqdn,
'ip' => $settings->public_ipv6,
]);
}
if (! $settings->public_ipv4 && ! $settings->public_ipv6) {
$domains->push([
'domain' => $fqdn,
'ip' => $ip,
]);
}
} else {
$domains->push([
'domain' => $fqdn,
'ip' => $ip,
]);
}
}
}
$services = $projects->pluck('services')->flatten();
if ($services->count() > 0) {
foreach ($services as $service) {
$service_applications = $service->applications;
if ($service_applications->count() > 0) {
foreach ($service_applications as $application) {
$fqdn = str($application->fqdn)->explode(',')->map(function ($fqdn) {
$f = str($fqdn)->replace('http://', '')->replace('https://', '')->explode('/');
return str(str($f[0])->explode(':')[0]);
})->filter(function (Stringable $fqdn) {
return $fqdn->isNotEmpty();
});
if ($ip === 'host.docker.internal') {
if ($settings->public_ipv4) {
$domains->push([
'domain' => $fqdn,
'ip' => $settings->public_ipv4,
]);
}
if ($settings->public_ipv6) {
$domains->push([
'domain' => $fqdn,
'ip' => $settings->public_ipv6,
]);
}
if (! $settings->public_ipv4 && ! $settings->public_ipv6) {
$domains->push([
'domain' => $fqdn,
'ip' => $ip,
]);
}
} else {
$domains->push([
'domain' => $fqdn,
'ip' => $ip,
]);
}
}
}
}
}
$domains = $domains->groupBy('ip')->map(function ($domain) {
return $domain->pluck('domain')->flatten();
})->map(function ($domain, $ip) {
return [
'ip' => $ip,
'domains' => $domain,
];
})->values();
return response()->json(serializeApiResponse($domains));
}
}

View file

@ -0,0 +1,702 @@
<?php
namespace App\Http\Controllers\Api;
use App\Actions\Service\RestartService;
use App\Actions\Service\StartService;
use App\Actions\Service\StopService;
use App\Http\Controllers\Controller;
use App\Jobs\DeleteResourceJob;
use App\Models\EnvironmentVariable;
use App\Models\Project;
use App\Models\Server;
use App\Models\Service;
use Illuminate\Http\Request;
use OpenApi\Attributes as OA;
class ServicesController extends Controller
{
private function removeSensitiveData($service)
{
$token = auth()->user()->currentAccessToken();
$service->makeHidden([
'id',
]);
if ($token->can('view:sensitive')) {
return serializeApiResponse($service);
}
$service->makeHidden([
'docker_compose_raw',
'docker_compose',
]);
return serializeApiResponse($service);
}
#[OA\Get(
summary: 'List',
description: 'List all services.',
path: '/services',
security: [
['bearerAuth' => []],
],
tags: ['Services'],
responses: [
new OA\Response(
response: 200,
description: 'Get all services',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'array',
items: new OA\Items(ref: '#/components/schemas/Service')
)
),
]
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
]
)]
public function services(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$projects = Project::where('team_id', $teamId)->get();
$services = collect();
foreach ($projects as $project) {
$services->push($project->services()->get());
}
foreach ($services as $service) {
$service = $this->removeSensitiveData($service);
}
return response()->json($services->flatten());
}
#[OA\Post(
summary: 'Create',
description: 'Create a one-click service',
path: '/services',
security: [
['bearerAuth' => []],
],
tags: ['Services'],
requestBody: new OA\RequestBody(
required: true,
content: new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
required: ['server_uuid', 'project_uuid', 'environment_name', 'type'],
properties: [
'type' => [
'description' => 'The one-click service type',
'type' => 'string',
'enum' => [
'activepieces',
'appsmith',
'appwrite',
'authentik',
'babybuddy',
'budge',
'changedetection',
'chatwoot',
'classicpress-with-mariadb',
'classicpress-with-mysql',
'classicpress-without-database',
'cloudflared',
'code-server',
'dashboard',
'directus',
'directus-with-postgresql',
'docker-registry',
'docuseal',
'docuseal-with-postgres',
'dokuwiki',
'duplicati',
'emby',
'embystat',
'fider',
'filebrowser',
'firefly',
'formbricks',
'ghost',
'gitea',
'gitea-with-mariadb',
'gitea-with-mysql',
'gitea-with-postgresql',
'glance',
'glances',
'glitchtip',
'grafana',
'grafana-with-postgresql',
'grocy',
'heimdall',
'homepage',
'jellyfin',
'kuzzle',
'listmonk',
'logto',
'mediawiki',
'meilisearch',
'metabase',
'metube',
'minio',
'moodle',
'n8n',
'n8n-with-postgresql',
'next-image-transformation',
'nextcloud',
'nocodb',
'odoo',
'openblocks',
'pairdrop',
'penpot',
'phpmyadmin',
'pocketbase',
'posthog',
'reactive-resume',
'rocketchat',
'shlink',
'slash',
'snapdrop',
'statusnook',
'stirling-pdf',
'supabase',
'syncthing',
'tolgee',
'trigger',
'trigger-with-external-database',
'twenty',
'umami',
'unleash-with-postgresql',
'unleash-without-database',
'uptime-kuma',
'vaultwarden',
'vikunja',
'weblate',
'whoogle',
'wordpress-with-mariadb',
'wordpress-with-mysql',
'wordpress-without-database',
],
],
'name' => ['type' => 'string', 'maxLength' => 255, 'description' => 'Name of the service.'],
'description' => ['type' => 'string', 'nullable' => true, 'description' => 'Description of the service.'],
'project_uuid' => ['type' => 'string', 'description' => 'Project UUID.'],
'environment_name' => ['type' => 'string', 'description' => 'Environment name.'],
'server_uuid' => ['type' => 'string', 'description' => 'Server UUID.'],
'destination_uuid' => ['type' => 'string', 'description' => 'Destination UUID. Required if server has multiple destinations.'],
'instant_deploy' => ['type' => 'boolean', 'default' => false, 'description' => 'Start the service immediately after creation.'],
],
),
),
),
responses: [
new OA\Response(
response: 201,
description: 'Create a service.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'uuid' => ['type' => 'string', 'description' => 'Service UUID.'],
'domains' => ['type' => 'array', 'items' => ['type' => 'string'], 'description' => 'Service domains.'],
]
)
),
]
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
]
)]
public function create_service(Request $request)
{
$allowedFields = ['type', 'name', 'description', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$return = validateIncomingRequest($request);
if ($return instanceof \Illuminate\Http\JsonResponse) {
return $return;
}
$validator = customApiValidator($request->all(), [
'type' => 'string|required',
'project_uuid' => 'string|required',
'environment_name' => 'string|required',
'server_uuid' => 'string|required',
'destination_uuid' => 'string',
'name' => 'string|max:255',
'description' => 'string|nullable',
'instant_deploy' => 'boolean',
]);
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
if ($validator->fails() || ! empty($extraFields)) {
$errors = $validator->errors();
if (! empty($extraFields)) {
foreach ($extraFields as $field) {
$errors->add($field, 'This field is not allowed.');
}
}
return response()->json([
'message' => 'Validation failed.',
'errors' => $errors,
], 422);
}
$serverUuid = $request->server_uuid;
$instantDeploy = $request->instant_deploy ?? false;
if ($request->is_public && ! $request->public_port) {
$request->offsetSet('is_public', false);
}
$project = Project::whereTeamId($teamId)->whereUuid($request->project_uuid)->first();
if (! $project) {
return response()->json(['message' => 'Project not found.'], 404);
}
$environment = $project->environments()->where('name', $request->environment_name)->first();
if (! $environment) {
return response()->json(['message' => 'Environment not found.'], 404);
}
$server = Server::whereTeamId($teamId)->whereUuid($serverUuid)->first();
if (! $server) {
return response()->json(['message' => 'Server not found.'], 404);
}
$destinations = $server->destinations();
if ($destinations->count() == 0) {
return response()->json(['message' => 'Server has no destinations.'], 400);
}
if ($destinations->count() > 1 && ! $request->has('destination_uuid')) {
return response()->json(['message' => 'Server has multiple destinations and you do not set destination_uuid.'], 400);
}
$destination = $destinations->first();
$services = get_service_templates();
$serviceKeys = $services->keys();
if ($serviceKeys->contains($request->type)) {
$oneClickServiceName = $request->type;
$oneClickService = data_get($services, "$oneClickServiceName.compose");
$oneClickDotEnvs = data_get($services, "$oneClickServiceName.envs", null);
if ($oneClickDotEnvs) {
$oneClickDotEnvs = str(base64_decode($oneClickDotEnvs))->split('/\r\n|\r|\n/')->filter(function ($value) {
return ! empty($value);
});
}
if ($oneClickService) {
$service_payload = [
'name' => "$oneClickServiceName-".str()->random(10),
'docker_compose_raw' => base64_decode($oneClickService),
'environment_id' => $environment->id,
'service_type' => $oneClickServiceName,
'server_id' => $server->id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),
];
if ($oneClickServiceName === 'cloudflared') {
data_set($service_payload, 'connect_to_docker_network', true);
}
$service = Service::create($service_payload);
$service->name = "$oneClickServiceName-".$service->uuid;
$service->save();
if ($oneClickDotEnvs?->count() > 0) {
$oneClickDotEnvs->each(function ($value) use ($service) {
$key = str()->before($value, '=');
$value = str(str()->after($value, '='));
$generatedValue = $value;
if ($value->contains('SERVICE_')) {
$command = $value->after('SERVICE_')->beforeLast('_');
$generatedValue = generateEnvValue($command->value(), $service);
}
EnvironmentVariable::create([
'key' => $key,
'value' => $generatedValue,
'service_id' => $service->id,
'is_build_time' => false,
'is_preview' => false,
]);
});
}
$service->parse(isNew: true);
if ($instantDeploy) {
StartService::dispatch($service);
}
$domains = $service->applications()->get()->pluck('fqdn')->sort();
$domains = $domains->map(function ($domain) {
return str($domain)->beforeLast(':')->value();
});
return response()->json([
'uuid' => $service->uuid,
'domains' => $domains,
]);
}
return response()->json(['message' => 'Service not found.'], 404);
} else {
return response()->json(['message' => 'Invalid service type.', 'valid_service_types' => $serviceKeys], 400);
}
return response()->json(['message' => 'Invalid service type.'], 400);
}
#[OA\Get(
summary: 'Get',
description: 'Get service by UUID.',
path: '/services/{uuid}',
security: [
['bearerAuth' => []],
],
tags: ['Services'],
parameters: [
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Service UUID', schema: new OA\Schema(type: 'string')),
],
responses: [
new OA\Response(
response: 200,
description: 'Get a service by Uuid.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
ref: '#/components/schemas/Service'
)
),
]
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
new OA\Response(
response: 404,
ref: '#/components/responses/404',
),
]
)]
public function service_by_uuid(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
if (! $request->uuid) {
return response()->json(['message' => 'UUID is required.'], 404);
}
$service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first();
if (! $service) {
return response()->json(['message' => 'Service not found.'], 404);
}
return response()->json($this->removeSensitiveData($service));
}
#[OA\Delete(
summary: 'Delete',
description: 'Delete service by UUID.',
path: '/services/{uuid}',
security: [
['bearerAuth' => []],
],
tags: ['Services'],
parameters: [
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Service UUID', schema: new OA\Schema(type: 'string')),
],
responses: [
new OA\Response(
response: 200,
description: 'Delete a service by Uuid',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Service deletion request queued.'],
],
)
),
]
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
new OA\Response(
response: 404,
ref: '#/components/responses/404',
),
]
)]
public function delete_by_uuid(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
if (! $request->uuid) {
return response()->json(['message' => 'UUID is required.'], 404);
}
$service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first();
if (! $service) {
return response()->json(['message' => 'Service not found.'], 404);
}
DeleteResourceJob::dispatch($service);
return response()->json([
'message' => 'Service deletion request queued.',
]);
}
#[OA\Get(
summary: 'Start',
description: 'Start service. `Post` request is also accepted.',
path: '/services/{uuid}/start',
security: [
['bearerAuth' => []],
],
tags: ['Services'],
parameters: [
new OA\Parameter(
name: 'uuid',
in: 'path',
description: 'UUID of the service.',
required: true,
schema: new OA\Schema(
type: 'string',
format: 'uuid',
)
),
],
responses: [
new OA\Response(
response: 200,
description: 'Start service.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Service starting request queued.'],
])
),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
new OA\Response(
response: 404,
ref: '#/components/responses/404',
),
]
)]
public function action_deploy(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$uuid = $request->route('uuid');
if (! $uuid) {
return response()->json(['message' => 'UUID is required.'], 400);
}
$service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first();
if (! $service) {
return response()->json(['message' => 'Service not found.'], 404);
}
if (str($service->status())->contains('running')) {
return response()->json(['message' => 'Service is already running.'], 400);
}
StartService::dispatch($service);
return response()->json(
[
'message' => 'Service starting request queued.',
],
200
);
}
#[OA\Get(
summary: 'Stop',
description: 'Stop service. `Post` request is also accepted.',
path: '/services/{uuid}/stop',
security: [
['bearerAuth' => []],
],
tags: ['Services'],
parameters: [
new OA\Parameter(
name: 'uuid',
in: 'path',
description: 'UUID of the service.',
required: true,
schema: new OA\Schema(
type: 'string',
format: 'uuid',
)
),
],
responses: [
new OA\Response(
response: 200,
description: 'Stop service.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Service stopping request queued.'],
])
),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
new OA\Response(
response: 404,
ref: '#/components/responses/404',
),
]
)]
public function action_stop(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$uuid = $request->route('uuid');
if (! $uuid) {
return response()->json(['message' => 'UUID is required.'], 400);
}
$service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first();
if (! $service) {
return response()->json(['message' => 'Service not found.'], 404);
}
if (str($service->status())->contains('stopped') || str($service->status())->contains('exited')) {
return response()->json(['message' => 'Service is already stopped.'], 400);
}
StopService::dispatch($service);
return response()->json(
[
'message' => 'Service stopping request queued.',
],
200
);
}
#[OA\Get(
summary: 'Restart',
description: 'Restart service. `Post` request is also accepted.',
path: '/services/{uuid}/restart',
security: [
['bearerAuth' => []],
],
tags: ['Services'],
parameters: [
new OA\Parameter(
name: 'uuid',
in: 'path',
description: 'UUID of the service.',
required: true,
schema: new OA\Schema(
type: 'string',
format: 'uuid',
)
),
],
responses: [
new OA\Response(
response: 200,
description: 'Restart service.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Service restaring request queued.'],
])
),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
new OA\Response(
response: 404,
ref: '#/components/responses/404',
),
]
)]
public function action_restart(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$uuid = $request->route('uuid');
if (! $uuid) {
return response()->json(['message' => 'UUID is required.'], 400);
}
$service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first();
if (! $service) {
return response()->json(['message' => 'Service not found.'], 404);
}
RestartService::dispatch($service);
return response()->json(
[
'message' => 'Service restarting request queued.',
],
200
);
}
}

View file

@ -1,74 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class Team extends Controller
{
public function teams(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return invalid_token();
}
$teams = auth()->user()->teams;
return response()->json($teams);
}
public function team_by_id(Request $request)
{
$id = $request->id;
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return invalid_token();
}
$teams = auth()->user()->teams;
$team = $teams->where('id', $id)->first();
if (is_null($team)) {
return response()->json(['error' => 'Team not found.', 'docs' => 'https://coolify.io/docs/api-reference/get-team-by-teamid'], 404);
}
return response()->json($team);
}
public function members_by_id(Request $request)
{
$id = $request->id;
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return invalid_token();
}
$teams = auth()->user()->teams;
$team = $teams->where('id', $id)->first();
if (is_null($team)) {
return response()->json(['error' => 'Team not found.', 'docs' => 'https://coolify.io/docs/api-reference/get-team-by-teamid-members'], 404);
}
return response()->json($team->members);
}
public function current_team(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return invalid_token();
}
$team = auth()->user()->currentTeam();
return response()->json($team);
}
public function current_team_members(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return invalid_token();
}
$team = auth()->user()->currentTeam();
return response()->json($team->members);
}
}

View file

@ -0,0 +1,270 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use OpenApi\Attributes as OA;
class TeamController extends Controller
{
private function removeSensitiveData($team)
{
$token = auth()->user()->currentAccessToken();
$team->makeHidden([
'custom_server_limit',
'pivot',
]);
if ($token->can('view:sensitive')) {
return serializeApiResponse($team);
}
$team->makeHidden([
'smtp_username',
'smtp_password',
'resend_api_key',
'telegram_token',
]);
return serializeApiResponse($team);
}
#[OA\Get(
summary: 'List',
description: 'Get all teams.',
path: '/teams',
security: [
['bearerAuth' => []],
],
tags: ['Teams'],
responses: [
new OA\Response(
response: 200,
description: 'List of teams.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'array',
items: new OA\Items(ref: '#/components/schemas/Team')
)
),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
]
)]
public function teams(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$teams = auth()->user()->teams->sortBy('id');
$teams = $teams->map(function ($team) {
return $this->removeSensitiveData($team);
});
return response()->json(
$teams,
);
}
#[OA\Get(
summary: 'Get',
description: 'Get team by TeamId.',
path: '/teams/{id}',
security: [
['bearerAuth' => []],
],
tags: ['Teams'],
parameters: [
new OA\Parameter(name: 'id', in: 'path', required: true, description: 'Team ID', schema: new OA\Schema(type: 'integer')),
],
responses: [
new OA\Response(
response: 200,
description: 'List of teams.',
content: new OA\JsonContent(ref: '#/components/schemas/Team')
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
new OA\Response(
response: 404,
ref: '#/components/responses/404',
),
]
)]
public function team_by_id(Request $request)
{
$id = $request->id;
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$teams = auth()->user()->teams;
$team = $teams->where('id', $id)->first();
if (is_null($team)) {
return response()->json(['message' => 'Team not found.'], 404);
}
$team = $this->removeSensitiveData($team);
return response()->json(
serializeApiResponse($team),
);
}
#[OA\Get(
summary: 'Members',
description: 'Get members by TeamId.',
path: '/teams/{id}/members',
security: [
['bearerAuth' => []],
],
tags: ['Teams'],
parameters: [
new OA\Parameter(name: 'id', in: 'path', required: true, description: 'Team ID', schema: new OA\Schema(type: 'integer')),
],
responses: [
new OA\Response(
response: 200,
description: 'List of members.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'array',
items: new OA\Items(ref: '#/components/schemas/User')
)
),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
new OA\Response(
response: 404,
ref: '#/components/responses/404',
),
]
)]
public function members_by_id(Request $request)
{
$id = $request->id;
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$teams = auth()->user()->teams;
$team = $teams->where('id', $id)->first();
if (is_null($team)) {
return response()->json(['message' => 'Team not found.'], 404);
}
$members = $team->members;
$members->makeHidden([
'pivot',
]);
return response()->json(
serializeApiResponse($members),
);
}
#[OA\Get(
summary: 'Authenticated Team',
description: 'Get currently authenticated team.',
path: '/teams/current',
security: [
['bearerAuth' => []],
],
tags: ['Teams'],
responses: [
new OA\Response(
response: 200,
description: 'Current Team.',
content: new OA\JsonContent(ref: '#/components/schemas/Team')),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
]
)]
public function current_team(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$team = auth()->user()->currentTeam();
return response()->json(
$this->removeSensitiveData($team),
);
}
#[OA\Get(
summary: 'Authenticated Team Members',
description: 'Get currently authenticated team members.',
path: '/teams/current/members',
security: [
['bearerAuth' => []],
],
tags: ['Teams'],
responses: [
new OA\Response(
response: 200,
description: 'Currently authenticated team members.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'array',
items: new OA\Items(ref: '#/components/schemas/User')
)
),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
]
)]
public function current_team_members(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$team = auth()->user()->currentTeam();
$team->members->makeHidden([
'pivot',
]);
return response()->json(
serializeApiResponse($team->members),
);
}
}

View file

@ -54,6 +54,34 @@ class Stripe extends Controller
$type = data_get($event, 'type');
$data = data_get($event, 'data.object');
switch ($type) {
case 'radar.early_fraud_warning.created':
$stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key'));
$id = data_get($data, 'id');
$charge = data_get($data, 'charge');
if ($charge) {
$stripe->refunds->create(['charge' => $charge]);
}
$pi = data_get($data, 'payment_intent');
$piData = $stripe->paymentIntents->retrieve($pi, []);
$customerId = data_get($piData, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if (! $subscription) {
Sleep::for(5)->seconds();
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
}
if (! $subscription) {
Sleep::for(5)->seconds();
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
}
if ($subscription) {
$subscriptionId = data_get($subscription, 'stripe_subscription_id');
$stripe->subscriptions->cancel($subscriptionId, []);
$subscription->update([
'stripe_invoice_paid' => false,
]);
}
send_internal_notification("Early fraud warning created Refunded, subscription canceled. Charge: {$charge}, id: {$id}, pi: {$pi}");
break;
case 'checkout.session.completed':
$clientReferenceId = data_get($data, 'client_reference_id');
if (is_null($clientReferenceId)) {
@ -231,7 +259,7 @@ class Stripe extends Controller
'stripe_plan_id' => null,
'stripe_cancel_at_period_end' => false,
'stripe_invoice_paid' => false,
'stripe_trial_already_ended' => true,
'stripe_trial_already_ended' => false,
]);
// send_internal_notification('customer.subscription.deleted for customer: '.$customerId);
break;

View file

@ -67,5 +67,7 @@ class Kernel extends HttpKernel
'signed' => \App\Http\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'abilities' => \Laravel\Sanctum\Http\Middleware\CheckAbilities::class,
'ability' => \Laravel\Sanctum\Http\Middleware\CheckForAnyAbility::class,
];
}

View file

@ -0,0 +1,33 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class ApiAllowed
{
public function handle(Request $request, Closure $next): Response
{
ray()->clearAll();
if (isCloud()) {
return $next($request);
}
$settings = view()->shared('instanceSettings');
if ($settings->is_api_enabled === false) {
return response()->json(['success' => true, 'message' => 'API is disabled.'], 403);
}
if (! isDev()) {
if ($settings->allowed_ips) {
$allowedIps = explode(',', $settings->allowed_ips);
if (! in_array($request->ip(), $allowedIps)) {
return response()->json(['success' => true, 'message' => 'You are not allowed to access the API.'], 403);
}
}
}
return $next($request);
}
}

View file

@ -0,0 +1,28 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class IgnoreReadOnlyApiToken
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
$token = auth()->user()->currentAccessToken();
if ($token->can('*')) {
return $next($request);
}
if ($token->can('read-only')) {
return response()->json(['message' => 'You are not allowed to perform this action.'], 403);
}
return $next($request);
}
}

View file

@ -0,0 +1,25 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class OnlyRootApiToken
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
$token = auth()->user()->currentAccessToken();
if ($token->can('*')) {
return $next($request);
}
return response()->json(['message' => 'You are not allowed to perform this action.'], 403);
}
}

View file

@ -127,7 +127,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private string $dockerfile_location = '/Dockerfile';
private string $docker_compose_location = '/docker-compose.yml';
private string $docker_compose_location = '/docker-compose.yaml';
private ?string $docker_compose_custom_start_command = null;
@ -194,6 +194,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->is_debug_enabled = $this->application->settings->is_debug_enabled;
$this->container_name = generateApplicationContainerName($this->application, $this->pull_request_id);
if ($this->application->settings->custom_internal_name && ! $this->application->settings->is_consistent_container_name_enabled) {
$this->container_name = $this->application->settings->custom_internal_name;
}
ray('New container name: ', $this->container_name);
savePrivateKeyToFs($this->server);
@ -459,7 +462,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
if ($this->env_filename) {
$command .= " --env-file {$this->workdir}/{$this->env_filename}";
}
$command .= " --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build";
$command .= " --project-name {$this->application->uuid} --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build";
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, $command), 'hidden' => true],
);
@ -513,7 +516,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
if ($this->env_filename) {
$command .= " --env-file {$this->workdir}/{$this->env_filename}";
}
$command .= " --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up -d";
$command .= " --project-name {$this->application->uuid} --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up -d";
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, $command), 'hidden' => true],
);
@ -608,10 +611,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
}
$readme = generate_readme_file($this->application->name, $this->application_deployment_queue->updated_at);
if ($this->pull_request_id === 0) {
$composeFileName = "$this->configuration_dir/docker-compose.yml";
$composeFileName = "$this->configuration_dir/docker-compose.yaml";
} else {
$composeFileName = "$this->configuration_dir/docker-compose-pr-{$this->pull_request_id}.yml";
$this->docker_compose_location = "/docker-compose-pr-{$this->pull_request_id}.yml";
$composeFileName = "$this->configuration_dir/docker-compose-pr-{$this->pull_request_id}.yaml";
$this->docker_compose_location = "/docker-compose-pr-{$this->pull_request_id}.yaml";
}
$this->execute_remote_command(
[
@ -1570,23 +1573,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
],
],
];
if (isset($this->application->settings->custom_internal_name)) {
$docker_compose['services'][$this->container_name]['networks'][$this->destination->network]['aliases'][] = $this->application->settings->custom_internal_name;
}
// if (str($this->saved_outputs->get('dotenv'))->isNotEmpty()) {
// if (data_get($docker_compose, "services.{$this->container_name}.env_file")) {
// $docker_compose['services'][$this->container_name]['env_file'][] = '.env';
// } else {
// $docker_compose['services'][$this->container_name]['env_file'] = ['.env'];
// }
// }
// if ($this->env_filename) {
// if (data_get($docker_compose, "services.{$this->container_name}.env_file")) {
// $docker_compose['services'][$this->container_name]['env_file'][] = $this->env_filename;
// } else {
// $docker_compose['services'][$this->container_name]['env_file'] = [$this->env_filename];
// }
// }
if (! is_null($this->env_filename)) {
$docker_compose['services'][$this->container_name]['env_file'] = [$this->env_filename];
}
@ -1697,32 +1683,28 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
// if ($this->build_pack === 'dockerfile') {
// $docker_compose['services'][$this->container_name]['build'] = [
// 'context' => $this->workdir,
// 'dockerfile' => $this->workdir . $this->dockerfile_location,
// ];
// }
if ($this->pull_request_id === 0) {
$custom_compose = convert_docker_run_to_compose($this->application->custom_docker_run_options);
if ((bool) $this->application->settings->is_consistent_container_name_enabled) {
$docker_compose['services'][$this->application->uuid] = $docker_compose['services'][$this->container_name];
if (count($custom_compose) > 0) {
$ipv4 = data_get($custom_compose, 'ip.0');
$ipv6 = data_get($custom_compose, 'ip6.0');
data_forget($custom_compose, 'ip');
data_forget($custom_compose, 'ip6');
if ($ipv4 || $ipv6) {
data_forget($docker_compose['services'][$this->application->uuid], 'networks');
if (! $this->application->settings->custom_internal_name) {
$docker_compose['services'][$this->application->uuid] = $docker_compose['services'][$this->container_name];
if (count($custom_compose) > 0) {
$ipv4 = data_get($custom_compose, 'ip.0');
$ipv6 = data_get($custom_compose, 'ip6.0');
data_forget($custom_compose, 'ip');
data_forget($custom_compose, 'ip6');
if ($ipv4 || $ipv6) {
data_forget($docker_compose['services'][$this->application->uuid], 'networks');
}
if ($ipv4) {
$docker_compose['services'][$this->application->uuid]['networks'][$this->destination->network]['ipv4_address'] = $ipv4;
}
if ($ipv6) {
$docker_compose['services'][$this->application->uuid]['networks'][$this->destination->network]['ipv6_address'] = $ipv6;
}
$docker_compose['services'][$this->application->uuid] = array_merge_recursive($docker_compose['services'][$this->application->uuid], $custom_compose);
}
if ($ipv4) {
$docker_compose['services'][$this->application->uuid]['networks'][$this->destination->network]['ipv4_address'] = $ipv4;
}
if ($ipv6) {
$docker_compose['services'][$this->application->uuid]['networks'][$this->destination->network]['ipv6_address'] = $ipv6;
}
$docker_compose['services'][$this->application->uuid] = array_merge_recursive($docker_compose['services'][$this->application->uuid], $custom_compose);
}
} else {
if (count($custom_compose) > 0) {
@ -1746,7 +1728,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->docker_compose = Yaml::dump($docker_compose, 10);
$this->docker_compose_base64 = base64_encode($this->docker_compose);
$this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d | tee {$this->workdir}/docker-compose.yml > /dev/null"), 'hidden' => true]);
$this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d | tee {$this->workdir}/docker-compose.yaml > /dev/null"), 'hidden' => true]);
}
private function generate_local_persistent_volumes()
@ -2052,12 +2034,12 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
if ($this->application->build_pack === 'dockerimage') {
$this->application_deployment_queue->addLogEntry('Pulling latest images from the registry.');
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} pull"), 'hidden' => true],
[executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-directory {$this->workdir} build"), 'hidden' => true],
[executeInDocker($this->deployment_uuid, "docker compose --project-name {$this->application->uuid} --project-directory {$this->workdir} pull"), 'hidden' => true],
[executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-name {$this->application->uuid} --project-directory {$this->workdir} build"), 'hidden' => true],
);
} else {
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build"), 'hidden' => true],
[executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-name {$this->application->uuid} --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build"), 'hidden' => true],
);
}
$this->application_deployment_queue->addLogEntry('New images built.');
@ -2068,17 +2050,17 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
if ($this->application->build_pack === 'dockerimage') {
$this->application_deployment_queue->addLogEntry('Pulling latest images from the registry.');
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} pull"), 'hidden' => true],
[executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-directory {$this->workdir} up --build -d"), 'hidden' => true],
[executeInDocker($this->deployment_uuid, "docker compose --project-name {$this->application->uuid} --project-directory {$this->workdir} pull"), 'hidden' => true],
[executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-name {$this->application->uuid} --project-directory {$this->workdir} up --build -d"), 'hidden' => true],
);
} else {
if ($this->use_build_server) {
$this->execute_remote_command(
["{$this->coolify_variables} docker compose --project-directory {$this->configuration_dir} -f {$this->configuration_dir}{$this->docker_compose_location} up --build -d", 'hidden' => true],
["{$this->coolify_variables} docker compose --project-name {$this->application->uuid} --project-directory {$this->configuration_dir} -f {$this->configuration_dir}{$this->docker_compose_location} up --build -d", 'hidden' => true],
);
} else {
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up --build -d"), 'hidden' => true],
[executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-name {$this->application->uuid} --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up --build -d"), 'hidden' => true],
);
}
}

View file

@ -332,8 +332,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
private function backup_standalone_mongodb(string $databaseWithCollections): void
{
try {
ray($this->database->toArray());
$url = $this->database->get_db_url(useInternal: true);
$url = $this->database->internal_db_url;
if ($databaseWithCollections === 'all') {
$commands[] = 'mkdir -p '.$this->backup_dir;
if (str($this->database->image)->startsWith('mongo:4.0')) {

View file

@ -28,14 +28,18 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource, public bool $deleteConfigurations = false) {}
public function __construct(
public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource,
public bool $deleteConfigurations = false,
public bool $deleteVolumes = false) {}
public function handle()
{
try {
$this->resource->forceDelete();
$persistentStorages = collect();
switch ($this->resource->type()) {
case 'application':
$persistentStorages = $this->resource?->persistentStorages()?->get();
StopApplication::run($this->resource);
break;
case 'standalone-postgresql':
@ -46,6 +50,7 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
case 'standalone-keydb':
case 'standalone-dragonfly':
case 'standalone-clickhouse':
$persistentStorages = $this->resource?->persistentStorages()?->get();
StopDatabase::run($this->resource);
break;
case 'service':
@ -53,6 +58,10 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
DeleteService::run($this->resource);
break;
}
if ($this->deleteVolumes && $this->resource->type() !== 'service') {
$this->resource?->delete_volumes($persistentStorages);
}
if ($this->deleteConfigurations) {
$this->resource?->delete_configurations();
}
@ -61,6 +70,7 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
send_internal_notification('ContainerStoppingJob failed with: '.$e->getMessage());
throw $e;
} finally {
$this->resource->forceDelete();
Artisan::queue('cleanup:stucked-resources');
}
}

View file

@ -35,9 +35,9 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
return;
}
});
if ($isInprogress) {
throw new RuntimeException('DockerCleanupJob: ApplicationDeploymentQueue is not empty, skipping...');
}
// if ($isInprogress) {
// throw new RuntimeException('DockerCleanupJob: ApplicationDeploymentQueue is not empty, skipping...');
// }
if (! $this->server->isFunctional()) {
return;
}

View file

@ -2,7 +2,6 @@
namespace App\Jobs;
use App\Models\InstanceSettings;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
@ -36,7 +35,7 @@ class PullCoolifyImageJob implements ShouldBeEncrypted, ShouldQueue
$latest_version = get_latest_version_of_coolify();
instant_remote_process(["docker pull -q ghcr.io/coollabsio/coolify:{$latest_version}"], $server, false);
$settings = InstanceSettings::get();
$settings = view()->shared('instanceSettings');
$current_version = config('version');
if (! $settings->is_auto_update_enabled) {
return;

View file

@ -51,7 +51,7 @@ class ServerStatusJob implements ShouldBeEncrypted, ShouldQueue
}
}
} catch (\Throwable $e) {
send_internal_notification('ServerStatusJob failed with: '.$e->getMessage());
// send_internal_notification('ServerStatusJob failed with: '.$e->getMessage());
ray($e->getMessage());
return handleError($e);

View file

@ -2,7 +2,6 @@
namespace App\Livewire;
use App\Models\InstanceSettings;
use DanHarrin\LivewireRateLimiting\WithRateLimiting;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\Facades\Http;
@ -48,7 +47,7 @@ class Help extends Component
]
);
$mail->subject("[HELP]: {$this->subject}");
$settings = InstanceSettings::get();
$settings = view()->shared('instanceSettings');
$type = set_transanctional_email_settings($settings);
if (! $type) {
$url = 'https://app.coolify.io/api/feedback';

View file

@ -2,7 +2,6 @@
namespace App\Livewire\Notifications;
use App\Models\InstanceSettings;
use App\Models\Team;
use App\Notifications\Test;
use Livewire\Component;
@ -173,7 +172,7 @@ class Email extends Component
public function copyFromInstanceSettings()
{
$settings = InstanceSettings::get();
$settings = view()->shared('instanceSettings');
if ($settings->smtp_enabled) {
$team = currentTeam();
$team->update([

View file

@ -40,8 +40,6 @@ class General extends Component
public ?string $initialDockerComposeLocation = null;
public ?string $initialDockerComposePrLocation = null;
public ?Collection $parsedServices;
public $parsedServiceDomains = [];
@ -72,11 +70,8 @@ class General extends Component
'application.docker_registry_image_tag' => 'nullable',
'application.dockerfile_location' => 'nullable',
'application.docker_compose_location' => 'nullable',
'application.docker_compose_pr_location' => 'nullable',
'application.docker_compose' => 'nullable',
'application.docker_compose_pr' => 'nullable',
'application.docker_compose_raw' => 'nullable',
'application.docker_compose_pr_raw' => 'nullable',
'application.dockerfile_target_build' => 'nullable',
'application.docker_compose_custom_start_command' => 'nullable',
'application.docker_compose_custom_build_command' => 'nullable',
@ -114,11 +109,8 @@ class General extends Component
'application.docker_registry_image_tag' => 'Docker registry image tag',
'application.dockerfile_location' => 'Dockerfile location',
'application.docker_compose_location' => 'Docker compose location',
'application.docker_compose_pr_location' => 'Docker compose location',
'application.docker_compose' => 'Docker compose',
'application.docker_compose_pr' => 'Docker compose',
'application.docker_compose_raw' => 'Docker compose raw',
'application.docker_compose_pr_raw' => 'Docker compose raw',
'application.custom_labels' => 'Custom labels',
'application.dockerfile_target_build' => 'Dockerfile target build',
'application.custom_docker_run_options' => 'Custom docker run commands',
@ -183,7 +175,7 @@ class General extends Component
if ($isInit && $this->application->docker_compose_raw) {
return;
}
['parsedServices' => $this->parsedServices, 'initialDockerComposeLocation' => $this->initialDockerComposeLocation, 'initialDockerComposePrLocation' => $this->initialDockerComposePrLocation] = $this->application->loadComposeFile($isInit);
['parsedServices' => $this->parsedServices, 'initialDockerComposeLocation' => $this->initialDockerComposeLocation] = $this->application->loadComposeFile($isInit);
if (is_null($this->parsedServices)) {
$this->dispatch('error', 'Failed to parse your docker-compose file. Please check the syntax and try again.');
@ -222,7 +214,6 @@ class General extends Component
$this->dispatch('refreshEnvs');
} catch (\Throwable $e) {
$this->application->docker_compose_location = $this->initialDockerComposeLocation;
$this->application->docker_compose_pr_location = $this->initialDockerComposePrLocation;
$this->application->save();
return handleError($e, $this);
@ -359,7 +350,6 @@ class General extends Component
$this->checkFqdns();
$this->application->save();
if (! $this->customLabels && $this->application->destination->server->proxyType() !== 'NONE') {
$this->customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n");
$this->application->custom_labels = base64_encode($this->customLabels);
@ -373,6 +363,7 @@ class General extends Component
}
}
$this->validate();
if ($this->ports_exposes !== $this->application->ports_exposes || $this->is_container_label_escape_enabled !== $this->application->settings->is_container_label_escape_enabled) {
$this->resetDefaultLabels();
}
@ -399,6 +390,7 @@ class General extends Component
}
if ($this->application->build_pack === 'dockercompose') {
$this->application->docker_compose_domains = json_encode($this->parsedServiceDomains);
foreach ($this->parsedServiceDomains as $serviceName => $service) {
$domain = data_get($service, 'domain');
if ($domain) {
@ -408,6 +400,9 @@ class General extends Component
check_domain_usage(resource: $this->application);
}
}
if ($this->application->isDirty('docker_compose_domains')) {
$this->resetDefaultLabels();
}
}
$this->application->custom_labels = base64_encode($this->customLabels);
$this->application->save();

View file

@ -46,10 +46,8 @@ class General extends Component
public function mount()
{
$this->db_url = $this->database->get_db_url(true);
if ($this->database->is_public) {
$this->db_url_public = $this->database->get_db_url();
}
$this->db_url = $this->database->internal_db_url;
$this->db_url_public = $this->database->external_db_url;
$this->server = data_get($this->database, 'destination.server');
}
@ -87,13 +85,12 @@ class General extends Component
return;
}
StartDatabaseProxy::run($this->database);
$this->db_url_public = $this->database->get_db_url();
$this->dispatch('success', 'Database is now publicly accessible.');
} else {
StopDatabaseProxy::run($this->database);
$this->db_url_public = null;
$this->dispatch('success', 'Database is no longer publicly accessible.');
}
$this->db_url_public = $this->database->external_db_url;
$this->database->save();
} catch (\Throwable $e) {
$this->database->is_public = ! $this->database->is_public;

View file

@ -44,10 +44,8 @@ class General extends Component
public function mount()
{
$this->db_url = $this->database->get_db_url(true);
if ($this->database->is_public) {
$this->db_url_public = $this->database->get_db_url();
}
$this->db_url = $this->database->internal_db_url;
$this->db_url_public = $this->database->external_db_url;
$this->server = data_get($this->database, 'destination.server');
}
@ -102,13 +100,12 @@ class General extends Component
return;
}
StartDatabaseProxy::run($this->database);
$this->db_url_public = $this->database->get_db_url();
$this->dispatch('success', 'Database is now publicly accessible.');
} else {
StopDatabaseProxy::run($this->database);
$this->db_url_public = null;
$this->dispatch('success', 'Database is no longer publicly accessible.');
}
$this->db_url_public = $this->database->external_db_url;
$this->database->save();
} catch (\Throwable $e) {
$this->database->is_public = ! $this->database->is_public;

View file

@ -2,14 +2,8 @@
namespace App\Livewire\Project\Database;
use App\Actions\Database\StartClickhouse;
use App\Actions\Database\StartDragonfly;
use App\Actions\Database\StartKeydb;
use App\Actions\Database\StartMariadb;
use App\Actions\Database\StartMongodb;
use App\Actions\Database\StartMysql;
use App\Actions\Database\StartPostgresql;
use App\Actions\Database\StartRedis;
use App\Actions\Database\RestartDatabase;
use App\Actions\Database\StartDatabase;
use App\Actions\Database\StopDatabase;
use App\Actions\Docker\GetContainersStatus;
use Livewire\Component;
@ -47,7 +41,6 @@ class Heading extends Component
public function check_status($showNotification = false)
{
GetContainersStatus::run($this->database->destination->server);
// dispatch_sync(new ContainerStatusJob($this->database->destination->server));
$this->database->refresh();
if ($showNotification) {
$this->dispatch('success', 'Database status updated.');
@ -67,32 +60,15 @@ class Heading extends Component
$this->check_status();
}
public function restart()
{
$activity = RestartDatabase::run($this->database);
$this->dispatch('activityMonitor', $activity->id);
}
public function start()
{
if ($this->database->type() === 'standalone-postgresql') {
$activity = StartPostgresql::run($this->database);
$this->dispatch('activityMonitor', $activity->id);
} elseif ($this->database->type() === 'standalone-redis') {
$activity = StartRedis::run($this->database);
$this->dispatch('activityMonitor', $activity->id);
} elseif ($this->database->type() === 'standalone-mongodb') {
$activity = StartMongodb::run($this->database);
$this->dispatch('activityMonitor', $activity->id);
} elseif ($this->database->type() === 'standalone-mysql') {
$activity = StartMysql::run($this->database);
$this->dispatch('activityMonitor', $activity->id);
} elseif ($this->database->type() === 'standalone-mariadb') {
$activity = StartMariadb::run($this->database);
$this->dispatch('activityMonitor', $activity->id);
} elseif ($this->database->type() === 'standalone-keydb') {
$activity = StartKeydb::run($this->database);
$this->dispatch('activityMonitor', $activity->id);
} elseif ($this->database->type() === 'standalone-dragonfly') {
$activity = StartDragonfly::run($this->database);
$this->dispatch('activityMonitor', $activity->id);
} elseif ($this->database->type() === 'standalone-clickhouse') {
$activity = StartClickhouse::run($this->database);
$this->dispatch('activityMonitor', $activity->id);
}
$activity = StartDatabase::run($this->database);
$this->dispatch('activityMonitor', $activity->id);
}
}

View file

@ -46,10 +46,8 @@ class General extends Component
public function mount()
{
$this->db_url = $this->database->get_db_url(true);
if ($this->database->is_public) {
$this->db_url_public = $this->database->get_db_url();
}
$this->db_url = $this->database->internal_db_url;
$this->db_url_public = $this->database->external_db_url;
$this->server = data_get($this->database, 'destination.server');
}
@ -108,13 +106,12 @@ class General extends Component
return;
}
StartDatabaseProxy::run($this->database);
$this->db_url_public = $this->database->get_db_url();
$this->dispatch('success', 'Database is now publicly accessible.');
} else {
StopDatabaseProxy::run($this->database);
$this->db_url_public = null;
$this->dispatch('success', 'Database is no longer publicly accessible.');
}
$this->db_url_public = $this->database->external_db_url;
$this->database->save();
} catch (\Throwable $e) {
$this->database->is_public = ! $this->database->is_public;

View file

@ -52,10 +52,8 @@ class General extends Component
public function mount()
{
$this->db_url = $this->database->get_db_url(true);
if ($this->database->is_public) {
$this->db_url_public = $this->database->get_db_url();
}
$this->db_url = $this->database->internal_db_url;
$this->db_url_public = $this->database->external_db_url;
$this->server = data_get($this->database, 'destination.server');
}
@ -114,13 +112,12 @@ class General extends Component
return;
}
StartDatabaseProxy::run($this->database);
$this->db_url_public = $this->database->get_db_url();
$this->dispatch('success', 'Database is now publicly accessible.');
} else {
StopDatabaseProxy::run($this->database);
$this->db_url_public = null;
$this->dispatch('success', 'Database is no longer publicly accessible.');
}
$this->db_url_public = $this->database->external_db_url;
$this->database->save();
} catch (\Throwable $e) {
$this->database->is_public = ! $this->database->is_public;

View file

@ -50,10 +50,8 @@ class General extends Component
public function mount()
{
$this->db_url = $this->database->get_db_url(true);
if ($this->database->is_public) {
$this->db_url_public = $this->database->get_db_url();
}
$this->db_url = $this->database->internal_db_url;
$this->db_url_public = $this->database->external_db_url;
$this->server = data_get($this->database, 'destination.server');
}
@ -115,13 +113,12 @@ class General extends Component
return;
}
StartDatabaseProxy::run($this->database);
$this->db_url_public = $this->database->get_db_url();
$this->dispatch('success', 'Database is now publicly accessible.');
} else {
StopDatabaseProxy::run($this->database);
$this->db_url_public = null;
$this->dispatch('success', 'Database is no longer publicly accessible.');
}
$this->db_url_public = $this->database->external_db_url;
$this->database->save();
} catch (\Throwable $e) {
$this->database->is_public = ! $this->database->is_public;

View file

@ -52,10 +52,8 @@ class General extends Component
public function mount()
{
$this->db_url = $this->database->get_db_url(true);
if ($this->database->is_public) {
$this->db_url_public = $this->database->get_db_url();
}
$this->db_url = $this->database->internal_db_url;
$this->db_url_public = $this->database->external_db_url;
$this->server = data_get($this->database, 'destination.server');
}
@ -113,13 +111,12 @@ class General extends Component
return;
}
StartDatabaseProxy::run($this->database);
$this->db_url_public = $this->database->get_db_url();
$this->dispatch('success', 'Database is now publicly accessible.');
} else {
StopDatabaseProxy::run($this->database);
$this->db_url_public = null;
$this->dispatch('success', 'Database is no longer publicly accessible.');
}
$this->db_url_public = $this->database->external_db_url;
$this->database->save();
} catch (\Throwable $e) {
$this->database->is_public = ! $this->database->is_public;

View file

@ -27,10 +27,7 @@ class General extends Component
public function getListeners()
{
$userId = auth()->user()->id;
return [
"echo-private:user.{$userId},DatabaseStatusChanged" => 'database_stopped',
'refresh',
'save_init_script',
'delete_init_script',
@ -72,18 +69,11 @@ class General extends Component
public function mount()
{
$this->db_url = $this->database->get_db_url(true);
if ($this->database->is_public) {
$this->db_url_public = $this->database->get_db_url();
}
$this->db_url = $this->database->internal_db_url;
$this->db_url_public = $this->database->external_db_url;
$this->server = data_get($this->database, 'destination.server');
}
public function database_stopped()
{
$this->dispatch('success', 'Database proxy stopped. Database is no longer publicly accessible.');
}
public function instantSaveAdvanced()
{
try {
@ -118,13 +108,12 @@ class General extends Component
return;
}
StartDatabaseProxy::run($this->database);
$this->db_url_public = $this->database->get_db_url();
$this->dispatch('success', 'Database is now publicly accessible.');
} else {
StopDatabaseProxy::run($this->database);
$this->db_url_public = null;
$this->dispatch('success', 'Database is no longer publicly accessible.');
}
$this->db_url_public = $this->database->external_db_url;
$this->database->save();
} catch (\Throwable $e) {
$this->database->is_public = ! $this->database->is_public;

View file

@ -46,10 +46,8 @@ class General extends Component
public function mount()
{
$this->db_url = $this->database->get_db_url(true);
if ($this->database->is_public) {
$this->db_url_public = $this->database->get_db_url();
}
$this->db_url = $this->database->internal_db_url;
$this->db_url_public = $this->database->external_db_url;
$this->server = data_get($this->database, 'destination.server');
}
@ -102,13 +100,12 @@ class General extends Component
return;
}
StartDatabaseProxy::run($this->database);
$this->db_url_public = $this->database->get_db_url();
$this->dispatch('success', 'Database is now publicly accessible.');
} else {
StopDatabaseProxy::run($this->database);
$this->db_url_public = null;
$this->dispatch('success', 'Database is no longer publicly accessible.');
}
$this->db_url_public = $this->database->external_db_url;
$this->database->save();
} catch (\Throwable $e) {
$this->database->is_public = ! $this->database->is_public;

View file

@ -16,6 +16,8 @@ class Danger extends Component
public bool $delete_configurations = true;
public bool $delete_volumes = true;
public ?string $modalId = null;
public function mount()
@ -31,7 +33,7 @@ class Danger extends Component
try {
// $this->authorize('delete', $this->resource);
$this->resource->delete();
DeleteResourceJob::dispatch($this->resource, $this->delete_configurations);
DeleteResourceJob::dispatch($this->resource, $this->delete_configurations, $this->delete_volumes);
return redirect()->route('project.resource.index', [
'project_uuid' => $this->projectUuid,

View file

@ -3,32 +3,63 @@
namespace App\Livewire\Project\Shared;
use App\Models\Tag;
use Livewire\Attributes\Validate;
use Livewire\Component;
// Refactored ✅
class Tags extends Component
{
public $resource = null;
public ?string $new_tag = null;
#[Validate('required|string|min:2')]
public string $newTags;
public $tags = [];
protected $listeners = [
'refresh' => '$refresh',
];
protected $rules = [
'resource.tags.*.name' => 'required|string|min:2',
'new_tag' => 'required|string|min:2',
];
protected $validationAttributes = [
'new_tag' => 'tag',
];
public $filteredTags = [];
public function mount()
{
$this->loadTags();
}
public function loadTags()
{
$this->tags = Tag::ownedByCurrentTeam()->get();
$this->filteredTags = $this->tags->filter(function ($tag) {
return ! $this->resource->tags->contains($tag);
});
}
public function submit()
{
try {
$this->validate();
$tags = str($this->newTags)->trim()->explode(' ');
foreach ($tags as $tag) {
if (strlen($tag) < 2) {
$this->dispatch('error', 'Invalid tag.', "Tag <span class='dark:text-warning'>$tag</span> is invalid. Min length is 2.");
continue;
}
if ($this->resource->tags()->where('name', $tag)->exists()) {
$this->dispatch('error', 'Duplicate tags.', "Tag <span class='dark:text-warning'>$tag</span> already added.");
continue;
}
$found = Tag::ownedByCurrentTeam()->where(['name' => $tag])->exists();
if (! $found) {
$found = Tag::create([
'name' => $tag,
'team_id' => currentTeam()->id,
]);
}
$this->resource->tags()->attach($found->id);
}
$this->refresh();
} catch (\Exception $e) {
return handleError($e, $this);
}
}
public function addTag(string $id, string $name)
@ -39,8 +70,9 @@ class Tags extends Component
return;
}
$this->resource->tags()->syncWithoutDetaching($id);
$this->resource->tags()->attach($id);
$this->refresh();
$this->dispatch('success', 'Tag added.');
} catch (\Exception $e) {
return handleError($e, $this);
}
@ -51,11 +83,12 @@ class Tags extends Component
try {
$this->resource->tags()->detach($id);
$found_more_tags = Tag::where(['id' => $id, 'team_id' => currentTeam()->id])->first();
$found_more_tags = Tag::ownedByCurrentTeam()->find($id);
if ($found_more_tags->applications()->count() == 0 && $found_more_tags->services()->count() == 0) {
$found_more_tags->delete();
}
$this->refresh();
$this->dispatch('success', 'Tag deleted.');
} catch (\Exception $e) {
return handleError($e, $this);
}
@ -63,41 +96,7 @@ class Tags extends Component
public function refresh()
{
$this->resource->load(['tags']);
$this->tags = Tag::ownedByCurrentTeam()->get();
$this->new_tag = null;
}
public function submit()
{
try {
$this->validate([
'new_tag' => 'required|string|min:2',
]);
$tags = str($this->new_tag)->trim()->explode(' ');
foreach ($tags as $tag) {
if ($this->resource->tags()->where('name', $tag)->exists()) {
$this->dispatch('error', 'Duplicate tags.', "Tag <span class='dark:text-warning'>$tag</span> already added.");
continue;
}
$found = Tag::where(['name' => $tag, 'team_id' => currentTeam()->id])->first();
if (! $found) {
$found = Tag::create([
'name' => $tag,
'team_id' => currentTeam()->id,
]);
}
$this->resource->tags()->syncWithoutDetaching($found->id);
}
$this->refresh();
} catch (\Exception $e) {
return handleError($e, $this);
}
}
public function render()
{
return view('livewire.project.shared.tags');
$this->loadTags();
$this->reset('newTags');
}
}

View file

@ -4,49 +4,61 @@ namespace App\Livewire\Project\Shared;
use Livewire\Component;
// Refactored ✅
class Webhooks extends Component
{
public $resource;
public ?string $deploywebhook = null;
public string $deploywebhook;
public ?string $githubManualWebhook = null;
public string $githubManualWebhook;
public ?string $gitlabManualWebhook = null;
public string $gitlabManualWebhook;
public ?string $bitbucketManualWebhook = null;
public string $bitbucketManualWebhook;
public ?string $giteaManualWebhook = null;
public string $giteaManualWebhook;
protected $rules = [
'resource.manual_webhook_secret_github' => 'nullable|string',
'resource.manual_webhook_secret_gitlab' => 'nullable|string',
'resource.manual_webhook_secret_bitbucket' => 'nullable|string',
'resource.manual_webhook_secret_gitea' => 'nullable|string',
];
public ?string $githubManualWebhookSecret = null;
public function saveSecret()
public ?string $gitlabManualWebhookSecret = null;
public ?string $bitbucketManualWebhookSecret = null;
public ?string $giteaManualWebhookSecret = null;
public function mount()
{
// ray()->clearAll();
// ray()->showQueries();
$this->deploywebhook = generateDeployWebhook($this->resource);
$this->githubManualWebhookSecret = data_get($this->resource, 'manual_webhook_secret_github');
$this->githubManualWebhook = generateGitManualWebhook($this->resource, 'github');
$this->gitlabManualWebhookSecret = data_get($this->resource, 'manual_webhook_secret_gitlab');
$this->gitlabManualWebhook = generateGitManualWebhook($this->resource, 'gitlab');
$this->bitbucketManualWebhookSecret = data_get($this->resource, 'manual_webhook_secret_bitbucket');
$this->bitbucketManualWebhook = generateGitManualWebhook($this->resource, 'bitbucket');
$this->giteaManualWebhookSecret = data_get($this->resource, 'manual_webhook_secret_gitea');
$this->giteaManualWebhook = generateGitManualWebhook($this->resource, 'gitea');
}
public function submit()
{
try {
$this->validate();
$this->resource->save();
$this->authorize('update', $this->resource);
$this->resource->update([
'manual_webhook_secret_github' => $this->githubManualWebhookSecret,
'manual_webhook_secret_gitlab' => $this->gitlabManualWebhookSecret,
'manual_webhook_secret_bitbucket' => $this->bitbucketManualWebhookSecret,
'manual_webhook_secret_gitea' => $this->giteaManualWebhookSecret,
]);
$this->dispatch('success', 'Secret Saved.');
} catch (\Exception $e) {
return handleError($e, $this);
}
}
public function mount()
{
$this->deploywebhook = generateDeployWebhook($this->resource);
$this->githubManualWebhook = generateGitManualWebhook($this->resource, 'github');
$this->gitlabManualWebhook = generateGitManualWebhook($this->resource, 'gitlab');
$this->bitbucketManualWebhook = generateGitManualWebhook($this->resource, 'bitbucket');
$this->giteaManualWebhook = generateGitManualWebhook($this->resource, 'gitea');
}
public function render()
{
return view('livewire.project.shared.webhooks');
}
}

View file

@ -10,6 +10,12 @@ class ApiTokens extends Component
public $tokens = [];
public bool $viewSensitiveData = false;
public bool $readOnly = true;
public array $permissions = ['read-only'];
public function render()
{
return view('livewire.security.api-tokens');
@ -17,7 +23,33 @@ class ApiTokens extends Component
public function mount()
{
$this->tokens = auth()->user()->tokens;
$this->tokens = auth()->user()->tokens->sortByDesc('created_at');
}
public function updatedViewSensitiveData()
{
if ($this->viewSensitiveData) {
$this->permissions[] = 'view:sensitive';
$this->permissions = array_diff($this->permissions, ['*']);
} else {
$this->permissions = array_diff($this->permissions, ['view:sensitive']);
}
if (count($this->permissions) == 0) {
$this->permissions = ['*'];
}
}
public function updatedReadOnly()
{
if ($this->readOnly) {
$this->permissions[] = 'read-only';
$this->permissions = array_diff($this->permissions, ['*']);
} else {
$this->permissions = array_diff($this->permissions, ['read-only']);
}
if (count($this->permissions) == 0) {
$this->permissions = ['*'];
}
}
public function addNewToken()
@ -26,7 +58,13 @@ class ApiTokens extends Component
$this->validate([
'description' => 'required|min:3|max:255',
]);
$token = auth()->user()->createToken($this->description);
// if ($this->viewSensitiveData) {
// $this->permissions[] = 'view:sensitive';
// }
// if ($this->readOnly) {
// $this->permissions[] = 'read-only';
// }
$token = auth()->user()->createToken($this->description, $this->permissions);
$this->tokens = auth()->user()->tokens;
session()->flash('token', $token->plainTextToken);
} catch (\Exception $e) {

View file

@ -84,7 +84,7 @@ class Deploy extends Component
try {
$this->server->proxy->force_stop = false;
$this->server->save();
$activity = StartProxy::run($this->server);
$activity = StartProxy::run($this->server, force: true);
$this->dispatch('activityMonitor', $activity->id, ProxyStatusChanged::class);
} catch (\Throwable $e) {
return handleError($e, $this);

View file

@ -18,7 +18,8 @@ class Configuration extends Component
public bool $is_dns_validation_enabled;
// public bool $next_channel;
public bool $is_api_enabled;
protected string $dynamic_config_path = '/data/coolify/proxy/dynamic';
protected Server $server;
@ -30,6 +31,7 @@ class Configuration extends Component
'settings.public_port_max' => 'required',
'settings.custom_dns_servers' => 'nullable',
'settings.instance_name' => 'nullable',
'settings.allowed_ips' => 'nullable',
];
protected $validationAttributes = [
@ -38,6 +40,7 @@ class Configuration extends Component
'settings.public_port_min' => 'Public port min',
'settings.public_port_max' => 'Public port max',
'settings.custom_dns_servers' => 'Custom DNS servers',
'settings.allowed_ips' => 'Allowed IPs',
];
public function mount()
@ -45,8 +48,8 @@ class Configuration extends Component
$this->do_not_track = $this->settings->do_not_track;
$this->is_auto_update_enabled = $this->settings->is_auto_update_enabled;
$this->is_registration_enabled = $this->settings->is_registration_enabled;
// $this->next_channel = $this->settings->next_channel;
$this->is_dns_validation_enabled = $this->settings->is_dns_validation_enabled;
$this->is_api_enabled = $this->settings->is_api_enabled;
}
public function instantSave()
@ -55,12 +58,7 @@ class Configuration extends Component
$this->settings->is_auto_update_enabled = $this->is_auto_update_enabled;
$this->settings->is_registration_enabled = $this->is_registration_enabled;
$this->settings->is_dns_validation_enabled = $this->is_dns_validation_enabled;
// if ($this->next_channel) {
// $this->settings->next_channel = false;
// $this->next_channel = false;
// } else {
// $this->settings->next_channel = $this->next_channel;
// }
$this->settings->is_api_enabled = $this->is_api_enabled;
$this->settings->save();
$this->dispatch('success', 'Settings updated!');
}
@ -94,6 +92,13 @@ class Configuration extends Component
$this->settings->custom_dns_servers = $this->settings->custom_dns_servers->unique();
$this->settings->custom_dns_servers = $this->settings->custom_dns_servers->implode(',');
$this->settings->allowed_ips = str($this->settings->allowed_ips)->replaceEnd(',', '')->trim();
$this->settings->allowed_ips = str($this->settings->allowed_ips)->trim()->explode(',')->map(function ($ip) {
return str($ip)->trim();
});
$this->settings->allowed_ips = $this->settings->allowed_ips->unique();
$this->settings->allowed_ips = $this->settings->allowed_ips->implode(',');
$this->settings->save();
$this->server->setupDynamicProxyConfiguration();
if (! $error_show) {

View file

@ -18,7 +18,7 @@ class Index extends Component
public function mount()
{
if (isInstanceAdmin()) {
$settings = InstanceSettings::get();
$settings = view()->shared('instanceSettings');
$database = StandalonePostgresql::whereName('coolify-db')->first();
$s3s = S3Storage::whereTeamId(0)->get() ?? [];
if ($database) {

View file

@ -29,7 +29,7 @@ class License extends Component
abort(404);
}
$this->instance_id = config('app.id');
$this->settings = InstanceSettings::get();
$this->settings = view()->shared('instanceSettings');
}
public function render()

View file

@ -4,7 +4,6 @@ namespace App\Livewire\Source\Github;
use App\Jobs\GithubAppPermissionJob;
use App\Models\GithubApp;
use App\Models\InstanceSettings;
use Illuminate\Support\Facades\Http;
use Livewire\Component;
@ -100,7 +99,7 @@ class Change extends Component
return redirect()->route('source.all');
}
$this->applications = $this->github_app->applications;
$settings = InstanceSettings::get();
$settings = view()->shared('instanceSettings');
$this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret');
$this->name = str($this->github_app->name)->kebab();

View file

@ -23,7 +23,7 @@ class Index extends Component
if (data_get(currentTeam(), 'subscription') && isSubscriptionActive()) {
return redirect()->route('subscription.show');
}
$this->settings = InstanceSettings::get();
$this->settings = view()->shared('instanceSettings');
$this->alreadySubscribed = currentTeam()->subscription()->exists();
}

View file

@ -8,12 +8,96 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use OpenApi\Attributes as OA;
use RuntimeException;
use Spatie\Activitylog\Models\Activity;
use Spatie\Url\Url;
use Symfony\Component\Yaml\Yaml;
use Visus\Cuid2\Cuid2;
#[OA\Schema(
description: 'Application model',
type: 'object',
properties: [
'id' => ['type' => 'integer', 'description' => 'The application identifier in the database.'],
'description' => ['type' => 'string', 'nullable' => true, 'description' => 'The application description.'],
'repository_project_id' => ['type' => 'integer', 'nullable' => true, 'description' => 'The repository project identifier.'],
'uuid' => ['type' => 'string', 'description' => 'The application UUID.'],
'name' => ['type' => 'string', 'description' => 'The application name.'],
'fqdn' => ['type' => 'string', 'nullable' => true, 'description' => 'The application domains.'],
'config_hash' => ['type' => 'string', 'description' => 'Configuration hash.'],
'git_repository' => ['type' => 'string', 'description' => 'Git repository URL.'],
'git_branch' => ['type' => 'string', 'description' => 'Git branch.'],
'git_commit_sha' => ['type' => 'string', 'description' => 'Git commit SHA.'],
'git_full_url' => ['type' => 'string', 'nullable' => true, 'description' => 'Git full URL.'],
'docker_registry_image_name' => ['type' => 'string', 'nullable' => true, 'description' => 'Docker registry image name.'],
'docker_registry_image_tag' => ['type' => 'string', 'nullable' => true, 'description' => 'Docker registry image tag.'],
'build_pack' => ['type' => 'string', 'description' => 'Build pack.', 'enum' => ['nixpacks', 'static', 'dockerfile', 'dockercompose']],
'static_image' => ['type' => 'string', 'description' => 'Static image used when static site is deployed.'],
'install_command' => ['type' => 'string', 'description' => 'Install command.'],
'build_command' => ['type' => 'string', 'description' => 'Build command.'],
'start_command' => ['type' => 'string', 'description' => 'Start command.'],
'ports_exposes' => ['type' => 'string', 'description' => 'Ports exposes.'],
'ports_mappings' => ['type' => 'string', 'nullable' => true, 'description' => 'Ports mappings.'],
'base_directory' => ['type' => 'string', 'description' => 'Base directory for all commands.'],
'publish_directory' => ['type' => 'string', 'description' => 'Publish directory.'],
'health_check_enabled' => ['type' => 'boolean', 'description' => 'Health check enabled.'],
'health_check_path' => ['type' => 'string', 'description' => 'Health check path.'],
'health_check_port' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check port.'],
'health_check_host' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check host.'],
'health_check_method' => ['type' => 'string', 'description' => 'Health check method.'],
'health_check_return_code' => ['type' => 'integer', 'description' => 'Health check return code.'],
'health_check_scheme' => ['type' => 'string', 'description' => 'Health check scheme.'],
'health_check_response_text' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check response text.'],
'health_check_interval' => ['type' => 'integer', 'description' => 'Health check interval in seconds.'],
'health_check_timeout' => ['type' => 'integer', 'description' => 'Health check timeout in seconds.'],
'health_check_retries' => ['type' => 'integer', 'description' => 'Health check retries count.'],
'health_check_start_period' => ['type' => 'integer', 'description' => 'Health check start period in seconds.'],
'limits_memory' => ['type' => 'string', 'description' => 'Memory limit.'],
'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit.'],
'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness.'],
'limits_memory_reservation' => ['type' => 'string', 'description' => 'Memory reservation.'],
'limits_cpus' => ['type' => 'string', 'description' => 'CPU limit.'],
'limits_cpuset' => ['type' => 'string', 'nullable' => true, 'description' => 'CPU set.'],
'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares.'],
'status' => ['type' => 'string', 'description' => 'Application status.'],
'preview_url_template' => ['type' => 'string', 'description' => 'Preview URL template.'],
'destination_type' => ['type' => 'string', 'description' => 'Destination type.'],
'destination_id' => ['type' => 'integer', 'description' => 'Destination identifier.'],
'source_id' => ['type' => 'integer', 'nullable' => true, 'description' => 'Source identifier.'],
'private_key_id' => ['type' => 'integer', 'nullable' => true, 'description' => 'Private key identifier.'],
'environment_id' => ['type' => 'integer', 'description' => 'Environment identifier.'],
'dockerfile' => ['type' => 'string', 'nullable' => true, 'description' => 'Dockerfile content. Used for dockerfile build pack.'],
'dockerfile_location' => ['type' => 'string', 'description' => 'Dockerfile location.'],
'custom_labels' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom labels.'],
'dockerfile_target_build' => ['type' => 'string', 'nullable' => true, 'description' => 'Dockerfile target build.'],
'manual_webhook_secret_github' => ['type' => 'string', 'nullable' => true, 'description' => 'Manual webhook secret for GitHub.'],
'manual_webhook_secret_gitlab' => ['type' => 'string', 'nullable' => true, 'description' => 'Manual webhook secret for GitLab.'],
'manual_webhook_secret_bitbucket' => ['type' => 'string', 'nullable' => true, 'description' => 'Manual webhook secret for Bitbucket.'],
'manual_webhook_secret_gitea' => ['type' => 'string', 'nullable' => true, 'description' => 'Manual webhook secret for Gitea.'],
'docker_compose_location' => ['type' => 'string', 'description' => 'Docker compose location.'],
'docker_compose' => ['type' => 'string', 'nullable' => true, 'description' => 'Docker compose content. Used for docker compose build pack.'],
'docker_compose_raw' => ['type' => 'string', 'nullable' => true, 'description' => 'Docker compose raw content.'],
'docker_compose_domains' => ['type' => 'string', 'nullable' => true, 'description' => 'Docker compose domains.'],
'docker_compose_custom_start_command' => ['type' => 'string', 'nullable' => true, 'description' => 'Docker compose custom start command.'],
'docker_compose_custom_build_command' => ['type' => 'string', 'nullable' => true, 'description' => 'Docker compose custom build command.'],
'swarm_replicas' => ['type' => 'integer', 'nullable' => true, 'description' => 'Swarm replicas. Only used for swarm deployments.'],
'swarm_placement_constraints' => ['type' => 'string', 'nullable' => true, 'description' => 'Swarm placement constraints. Only used for swarm deployments.'],
'custom_docker_run_options' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom docker run options.'],
'post_deployment_command' => ['type' => 'string', 'nullable' => true, 'description' => 'Post deployment command.'],
'post_deployment_command_container' => ['type' => 'string', 'nullable' => true, 'description' => 'Post deployment command container.'],
'pre_deployment_command' => ['type' => 'string', 'nullable' => true, 'description' => 'Pre deployment command.'],
'pre_deployment_command_container' => ['type' => 'string', 'nullable' => true, 'description' => 'Pre deployment command container.'],
'watch_paths' => ['type' => 'string', 'nullable' => true, 'description' => 'Watch paths.'],
'custom_healthcheck_found' => ['type' => 'boolean', 'description' => 'Custom healthcheck found.'],
'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'How to set redirect with Traefik / Caddy. www<->non-www.', 'enum' => ['www', 'non-www', 'both']],
'created_at' => ['type' => 'string', 'format' => 'date-time', 'description' => 'The date and time when the application was created.'],
'updated_at' => ['type' => 'string', 'format' => 'date-time', 'description' => 'The date and time when the application was last updated.'],
'deleted_at' => ['type' => 'string', 'format' => 'date-time', 'nullable' => true, 'description' => 'The date and time when the application was deleted.'],
'compose_parsing_version' => ['type' => 'string', 'description' => 'How Coolify parse the compose file.'],
]
)]
class Application extends BaseModel
{
use SoftDeletes;
@ -39,17 +123,12 @@ class Application extends BaseModel
ApplicationSetting::create([
'application_id' => $application->id,
]);
$application->compose_parsing_version = '2';
$application->save();
});
static::deleting(function ($application) {
static::forceDeleting(function ($application) {
$application->update(['fqdn' => null]);
$application->settings()->delete();
$storages = $application->persistentStorages()->get();
$server = data_get($application, 'destination.server');
if ($server) {
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
$application->persistentStorages()->delete();
$application->environment_variables()->delete();
$application->environment_variables_preview()->delete();
@ -60,6 +139,11 @@ class Application extends BaseModel
});
}
public static function ownedByCurrentTeamAPI(int $teamId)
{
return Application::whereRelation('environment.project.team', 'id', $teamId)->orderBy('name');
}
public function delete_configurations()
{
$server = data_get($this, 'destination.server');
@ -70,6 +154,23 @@ class Application extends BaseModel
}
}
public function delete_volumes(?Collection $persistentStorages)
{
if ($this->build_pack === 'dockercompose') {
$server = data_get($this, 'destination.server');
ray('Deleting volumes');
instant_remote_process(["cd {$this->dirOnServer()} && docker compose down -v"], $server, false);
} else {
if ($persistentStorages->count() === 0) {
return;
}
$server = data_get($this, 'destination.server');
foreach ($persistentStorages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
}
public function additional_servers()
{
return $this->belongsToMany(Server::class, 'additional_destinations')
@ -687,6 +788,11 @@ class Application extends BaseModel
return "/artifacts/{$uuid}";
}
public function dirOnServer()
{
return application_configuration_dir()."/{$this->uuid}";
}
public function setGitImportSettings(string $deployment_uuid, string $git_clone_command, bool $public = false)
{
$baseDir = $this->generateBaseDir($deployment_uuid);
@ -809,7 +915,7 @@ class Application extends BaseModel
$commands->push("echo 'Checking out $branch'");
}
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name);
} elseif ($git_type === 'github') {
} elseif ($git_type === 'github' || $git_type === 'gitea') {
$branch = "pull/{$pull_request_id}/head:$pr_branch_name";
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
@ -853,7 +959,7 @@ class Application extends BaseModel
$commands->push("echo 'Checking out $branch'");
}
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name);
} elseif ($git_type === 'github') {
} elseif ($git_type === 'github' || $git_type === 'gitea') {
$branch = "pull/{$pull_request_id}/head:$pr_branch_name";
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
@ -964,11 +1070,7 @@ class Application extends BaseModel
['commands' => $cloneCommand] = $this->generateGitImportCommands(deployment_uuid: $uuid, only_checkout: true, exec_in_docker: false, custom_base_dir: '.');
$workdir = rtrim($this->base_directory, '/');
$composeFile = $this->docker_compose_location;
// $prComposeFile = $this->docker_compose_pr_location;
$fileList = collect([".$workdir$composeFile"]);
// if ($composeFile !== $prComposeFile) {
// $fileList->push(".$prComposeFile");
// }
$commands = collect([
"rm -rf /tmp/{$uuid}",
"mkdir -p /tmp/{$uuid}",
@ -1017,7 +1119,6 @@ class Application extends BaseModel
return [
'parsedServices' => $parsedServices,
'initialDockerComposeLocation' => $this->docker_compose_location,
'initialDockerComposePrLocation' => $this->docker_compose_pr_location,
];
}

View file

@ -4,7 +4,37 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Carbon;
use OpenApi\Attributes as OA;
#[OA\Schema(
description: 'Project model',
type: 'object',
properties: [
'id' => ['type' => 'integer'],
'application_id' => ['type' => 'string'],
'deployment_uuid' => ['type' => 'string'],
'pull_request_id' => ['type' => 'integer'],
'force_rebuild' => ['type' => 'boolean'],
'commit' => ['type' => 'string'],
'status' => ['type' => 'string'],
'is_webhook' => ['type' => 'boolean'],
'is_api' => ['type' => 'boolean'],
'created_at' => ['type' => 'string'],
'updated_at' => ['type' => 'string'],
'logs' => ['type' => 'string'],
'current_process_id' => ['type' => 'string'],
'restart_only' => ['type' => 'boolean'],
'git_type' => ['type' => 'string'],
'server_id' => ['type' => 'integer'],
'application_name' => ['type' => 'string'],
'server_name' => ['type' => 'string'],
'deployment_url' => ['type' => 'string'],
'destination_id' => ['type' => 'string'],
'only_this_server' => ['type' => 'boolean'],
'rollback' => ['type' => 'boolean'],
'commit_message' => ['type' => 'string'],
],
)]
class ApplicationDeploymentQueue extends Model
{
protected $guarded = [];

View file

@ -4,7 +4,20 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use OpenApi\Attributes as OA;
#[OA\Schema(
description: 'Environment model',
type: 'object',
properties: [
'id' => ['type' => 'integer'],
'name' => ['type' => 'string'],
'project_id' => ['type' => 'integer'],
'created_at' => ['type' => 'string'],
'updated_at' => ['type' => 'string'],
'description' => ['type' => 'string'],
]
)]
class Environment extends Model
{
protected $guarded = [];
@ -27,6 +40,9 @@ class Environment extends Model
$this->redis()->count() == 0 &&
$this->postgresqls()->count() == 0 &&
$this->mysqls()->count() == 0 &&
$this->keydbs()->count() == 0 &&
$this->dragonflies()->count() == 0 &&
$this->clickhouses()->count() == 0 &&
$this->mariadbs()->count() == 0 &&
$this->mongodbs()->count() == 0 &&
$this->services()->count() == 0;

View file

@ -6,8 +6,33 @@ use App\Models\EnvironmentVariable as ModelsEnvironmentVariable;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
use OpenApi\Attributes as OA;
use Symfony\Component\Yaml\Yaml;
use Visus\Cuid2\Cuid2;
#[OA\Schema(
description: 'Environment Variable model',
type: 'object',
properties: [
'id' => ['type' => 'integer'],
'uuid' => ['type' => 'string'],
'application_id' => ['type' => 'integer'],
'service_id' => ['type' => 'integer'],
'database_id' => ['type' => 'integer'],
'is_build_time' => ['type' => 'boolean'],
'is_literal' => ['type' => 'boolean'],
'is_multiline' => ['type' => 'boolean'],
'is_preview' => ['type' => 'boolean'],
'is_shared' => ['type' => 'boolean'],
'is_shown_once' => ['type' => 'boolean'],
'key' => ['type' => 'string'],
'value' => ['type' => 'string'],
'real_value' => ['type' => 'string'],
'version' => ['type' => 'string'],
'created_at' => ['type' => 'string'],
'updated_at' => ['type' => 'string'],
]
)]
class EnvironmentVariable extends Model
{
protected $guarded = [];
@ -25,6 +50,11 @@ class EnvironmentVariable extends Model
protected static function booted()
{
static::creating(function (Model $model) {
if (! $model->uuid) {
$model->uuid = (string) new Cuid2();
}
});
static::created(function (EnvironmentVariable $environment_variable) {
if ($environment_variable->application_id && ! $environment_variable->is_preview) {
$found = ModelsEnvironmentVariable::where('key', $environment_variable->key)->where('application_id', $environment_variable->application_id)->where('is_preview', true)->first();
@ -220,7 +250,7 @@ class EnvironmentVariable extends Model
protected function key(): Attribute
{
return Attribute::make(
set: fn (string $value) => str($value)->trim(),
set: fn (string $value) => str($value)->trim()->replace(' ', '_')->value,
);
}
}

View file

@ -20,6 +20,17 @@ class GithubApp extends BaseModel
'webhook_secret',
];
protected static function booted(): void
{
static::deleting(function (GithubApp $github_app) {
$applications_count = Application::where('source_id', $github_app->id)->count();
if ($applications_count > 0) {
throw new \Exception('You cannot delete this GitHub App because it is in use by '.$applications_count.' application(s). Delete them first.');
}
$github_app->privateKey()->delete();
});
}
public static function public()
{
return GithubApp::whereTeamId(currentTeam()->id)->whereisPublic(true)->whereNotNull('app_id')->get();
@ -30,15 +41,9 @@ class GithubApp extends BaseModel
return GithubApp::whereTeamId(currentTeam()->id)->whereisPublic(false)->whereNotNull('app_id')->get();
}
protected static function booted(): void
public function team()
{
static::deleting(function (GithubApp $github_app) {
$applications_count = Application::where('source_id', $github_app->id)->count();
if ($applications_count > 0) {
throw new \Exception('You cannot delete this GitHub App because it is in use by '.$applications_count.' application(s). Delete them first.');
}
$github_app->privateKey()->delete();
});
return $this->belongsTo(Team::class);
}
public function applications()

View file

@ -17,6 +17,7 @@ class InstanceSettings extends Model implements SendsEmail
protected $casts = [
'resale_license' => 'encrypted',
'smtp_password' => 'encrypted',
'allowed_ip_ranges' => 'array',
];
public function fqdn(): Attribute

View file

@ -2,8 +2,24 @@
namespace App\Models;
use OpenApi\Attributes as OA;
use phpseclib3\Crypt\PublicKeyLoader;
#[OA\Schema(
description: 'Private Key model',
type: 'object',
properties: [
'id' => ['type' => 'integer'],
'uuid' => ['type' => 'string'],
'name' => ['type' => 'string'],
'description' => ['type' => 'string'],
'private_key' => ['type' => 'string', 'format' => 'private-key'],
'is_git_related' => ['type' => 'boolean'],
'team_id' => ['type' => 'integer'],
'created_at' => ['type' => 'string'],
'updated_at' => ['type' => 'string'],
],
)]
class PrivateKey extends BaseModel
{
protected $fillable = [
@ -14,6 +30,17 @@ class PrivateKey extends BaseModel
'team_id',
];
protected static function booted()
{
static::saving(function ($key) {
$privateKey = data_get($key, 'private_key');
if (substr($privateKey, -1) !== "\n") {
$key->private_key = $privateKey."\n";
}
});
}
public static function ownedByCurrentTeam(array $select = ['*'])
{
$selectArray = collect($select)->concat(['id']);

View file

@ -2,6 +2,23 @@
namespace App\Models;
use OpenApi\Attributes as OA;
#[OA\Schema(
description: 'Project model',
type: 'object',
properties: [
'id' => ['type' => 'integer'],
'uuid' => ['type' => 'string'],
'name' => ['type' => 'string'],
'environments' => new OA\Property(
property: 'environments',
type: 'array',
items: new OA\Items(ref: '#/components/schemas/Environment'),
description: 'The environments of the project.'
),
]
)]
class Project extends BaseModel
{
protected $guarded = [];

View file

@ -12,11 +12,95 @@ use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Stringable;
use OpenApi\Attributes as OA;
use Spatie\SchemalessAttributes\Casts\SchemalessAttributes;
use Spatie\SchemalessAttributes\SchemalessAttributesTrait;
use Spatie\Url\Url;
use Symfony\Component\Yaml\Yaml;
#[OA\Schema(
description: 'Application model',
type: 'object',
properties: [
'id' => ['type' => 'integer'],
'repository_project_id' => ['type' => 'integer', 'nullable' => true],
'uuid' => ['type' => 'string'],
'name' => ['type' => 'string'],
'fqdn' => ['type' => 'string'],
'config_hash' => ['type' => 'string'],
'git_repository' => ['type' => 'string'],
'git_branch' => ['type' => 'string'],
'git_commit_sha' => ['type' => 'string'],
'git_full_url' => ['type' => 'string', 'nullable' => true],
'docker_registry_image_name' => ['type' => 'string', 'nullable' => true],
'docker_registry_image_tag' => ['type' => 'string', 'nullable' => true],
'build_pack' => ['type' => 'string'],
'static_image' => ['type' => 'string'],
'install_command' => ['type' => 'string'],
'build_command' => ['type' => 'string'],
'start_command' => ['type' => 'string'],
'ports_exposes' => ['type' => 'string'],
'ports_mappings' => ['type' => 'string', 'nullable' => true],
'base_directory' => ['type' => 'string'],
'publish_directory' => ['type' => 'string'],
'health_check_path' => ['type' => 'string'],
'health_check_port' => ['type' => 'string', 'nullable' => true],
'health_check_host' => ['type' => 'string'],
'health_check_method' => ['type' => 'string'],
'health_check_return_code' => ['type' => 'integer'],
'health_check_scheme' => ['type' => 'string'],
'health_check_response_text' => ['type' => 'string', 'nullable' => true],
'health_check_interval' => ['type' => 'integer'],
'health_check_timeout' => ['type' => 'integer'],
'health_check_retries' => ['type' => 'integer'],
'health_check_start_period' => ['type' => 'integer'],
'limits_memory' => ['type' => 'string'],
'limits_memory_swap' => ['type' => 'string'],
'limits_memory_swappiness' => ['type' => 'integer'],
'limits_memory_reservation' => ['type' => 'string'],
'limits_cpus' => ['type' => 'string'],
'limits_cpuset' => ['type' => 'string', 'nullable' => true],
'limits_cpu_shares' => ['type' => 'integer'],
'status' => ['type' => 'string'],
'preview_url_template' => ['type' => 'string'],
'destination_type' => ['type' => 'string'],
'destination_id' => ['type' => 'integer'],
'source_type' => ['type' => 'string'],
'source_id' => ['type' => 'integer'],
'private_key_id' => ['type' => 'integer', 'nullable' => true],
'environment_id' => ['type' => 'integer'],
'created_at' => ['type' => 'string', 'format' => 'date-time'],
'updated_at' => ['type' => 'string', 'format' => 'date-time'],
'description' => ['type' => 'string', 'nullable' => true],
'dockerfile' => ['type' => 'string', 'nullable' => true],
'health_check_enabled' => ['type' => 'boolean'],
'dockerfile_location' => ['type' => 'string'],
'custom_labels' => ['type' => 'string'],
'dockerfile_target_build' => ['type' => 'string', 'nullable' => true],
'manual_webhook_secret_github' => ['type' => 'string', 'nullable' => true],
'manual_webhook_secret_gitlab' => ['type' => 'string', 'nullable' => true],
'docker_compose_location' => ['type' => 'string'],
'docker_compose' => ['type' => 'string', 'nullable' => true],
'docker_compose_raw' => ['type' => 'string', 'nullable' => true],
'docker_compose_domains' => ['type' => 'string', 'nullable' => true],
'deleted_at' => ['type' => 'string', 'format' => 'date-time', 'nullable' => true],
'docker_compose_custom_start_command' => ['type' => 'string', 'nullable' => true],
'docker_compose_custom_build_command' => ['type' => 'string', 'nullable' => true],
'swarm_replicas' => ['type' => 'integer'],
'swarm_placement_constraints' => ['type' => 'string', 'nullable' => true],
'manual_webhook_secret_bitbucket' => ['type' => 'string', 'nullable' => true],
'custom_docker_run_options' => ['type' => 'string', 'nullable' => true],
'post_deployment_command' => ['type' => 'string', 'nullable' => true],
'post_deployment_command_container' => ['type' => 'string', 'nullable' => true],
'pre_deployment_command' => ['type' => 'string', 'nullable' => true],
'pre_deployment_command_container' => ['type' => 'string', 'nullable' => true],
'watch_paths' => ['type' => 'string', 'nullable' => true],
'custom_healthcheck_found' => ['type' => 'boolean'],
'manual_webhook_secret_gitea' => ['type' => 'string', 'nullable' => true],
'redirect' => ['type' => 'string'],
]
)]
class Server extends BaseModel
{
use SchemalessAttributesTrait;
@ -234,11 +318,11 @@ respond 404
public function setupDynamicProxyConfiguration()
{
$settings = InstanceSettings::get();
$settings = view()->shared('instanceSettings');
$dynamic_config_path = $this->proxyPath().'/dynamic';
if ($this->proxyType() === 'TRAEFIK_V2') {
$file = "$dynamic_config_path/coolify.yaml";
if (empty($settings->fqdn) || (isCloud() && $this->id !== 0)) {
if (empty($settings->fqdn) || (isCloud() && $this->id !== 0) || ! $this->isLocalhost()) {
instant_remote_process([
"rm -f $file",
], $this);
@ -344,7 +428,7 @@ respond 404
}
} elseif ($this->proxyType() === 'CADDY') {
$file = "$dynamic_config_path/coolify.caddy";
if (empty($settings->fqdn) || (isCloud() && $this->id !== 0)) {
if (empty($settings->fqdn) || (isCloud() && $this->id !== 0) || ! $this->isLocalhost()) {
instant_remote_process([
"rm -f $file",
], $this);
@ -496,16 +580,16 @@ $schema://$host {
public function checkSentinel()
{
ray("Checking sentinel on server: {$this->name}");
// ray("Checking sentinel on server: {$this->name}");
if ($this->isSentinelEnabled()) {
$sentinel_found = instant_remote_process(['docker inspect coolify-sentinel'], $this, false);
$sentinel_found = json_decode($sentinel_found, true);
$status = data_get($sentinel_found, '0.State.Status', 'exited');
if ($status !== 'running') {
ray('Sentinel is not running, starting it...');
// ray('Sentinel is not running, starting it...');
PullSentinelImageJob::dispatch($this);
} else {
ray('Sentinel is running');
// ray('Sentinel is running');
}
}
}

View file

@ -3,7 +3,46 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use OpenApi\Attributes as OA;
#[OA\Schema(
description: 'Server Settings model',
type: 'object',
properties: [
'id' => ['type' => 'integer'],
'cleanup_after_percentage' => ['type' => 'integer'],
'concurrent_builds' => ['type' => 'integer'],
'dynamic_timeout' => ['type' => 'integer'],
'force_disabled' => ['type' => 'boolean'],
'is_build_server' => ['type' => 'boolean'],
'is_cloudflare_tunnel' => ['type' => 'boolean'],
'is_jump_server' => ['type' => 'boolean'],
'is_logdrain_axiom_enabled' => ['type' => 'boolean'],
'is_logdrain_custom_enabled' => ['type' => 'boolean'],
'is_logdrain_highlight_enabled' => ['type' => 'boolean'],
'is_logdrain_newrelic_enabled' => ['type' => 'boolean'],
'is_metrics_enabled' => ['type' => 'boolean'],
'is_reachable' => ['type' => 'boolean'],
'is_server_api_enabled' => ['type' => 'boolean'],
'is_swarm_manager' => ['type' => 'boolean'],
'is_swarm_worker' => ['type' => 'boolean'],
'is_usable' => ['type' => 'boolean'],
'logdrain_axiom_api_key' => ['type' => 'string'],
'logdrain_axiom_dataset_name' => ['type' => 'string'],
'logdrain_custom_config' => ['type' => 'string'],
'logdrain_custom_config_parser' => ['type' => 'string'],
'logdrain_highlight_project_id' => ['type' => 'string'],
'logdrain_newrelic_base_uri' => ['type' => 'string'],
'logdrain_newrelic_license_key' => ['type' => 'string'],
'metrics_history_days' => ['type' => 'integer'],
'metrics_refresh_rate_seconds' => ['type' => 'integer'],
'metrics_token' => ['type' => 'string'],
'server_id' => ['type' => 'integer'],
'wildcard_domain' => ['type' => 'string'],
'created_at' => ['type' => 'string'],
'updated_at' => ['type' => 'string'],
]
)]
class ServerSetting extends Model
{
protected $guarded = [];

View file

@ -6,8 +6,31 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use OpenApi\Attributes as OA;
use Symfony\Component\Yaml\Yaml;
#[OA\Schema(
description: 'Service model',
type: 'object',
properties: [
'id' => ['type' => 'integer', 'description' => 'The unique identifier of the service. Only used for database identification.'],
'uuid' => ['type' => 'string', 'description' => 'The unique identifier of the service.'],
'name' => ['type' => 'string', 'description' => 'The name of the service.'],
'environment_id' => ['type' => 'integer', 'description' => 'The unique identifier of the environment where the service is attached to.'],
'server_id' => ['type' => 'integer', 'description' => 'The unique identifier of the server where the service is running.'],
'description' => ['type' => 'string', 'description' => 'The description of the service.'],
'docker_compose_raw' => ['type' => 'string', 'description' => 'The raw docker-compose.yml file of the service.'],
'docker_compose' => ['type' => 'string', 'description' => 'The docker-compose.yml file that is parsed and modified by Coolify.'],
'destination_id' => ['type' => 'integer', 'description' => 'The unique identifier of the destination where the service is running.'],
'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'],
'is_container_label_escape_enabled' => ['type' => 'boolean', 'description' => 'The flag to enable the container label escape.'],
'config_hash' => ['type' => 'string', 'description' => 'The hash of the service configuration.'],
'service_type' => ['type' => 'string', 'description' => 'The type of the service.'],
'created_at' => ['type' => 'string', 'description' => 'The date and time when the service was created.'],
'updated_at' => ['type' => 'string', 'description' => 'The date and time when the service was last updated.'],
'deleted_at' => ['type' => 'string', 'description' => 'The date and time when the service was deleted.'],
],
)]
class Service extends BaseModel
{
use HasFactory, SoftDeletes;

View file

@ -27,6 +27,11 @@ class ServiceApplication extends BaseModel
instant_remote_process(["docker restart {$container_id}"], $this->service->server);
}
public static function ownedByCurrentTeamAPI(int $teamId)
{
return ServiceApplication::whereRelation('service.environment.project.team', 'id', $teamId)->orderBy('name');
}
public function isLogDrainEnabled()
{
return data_get($this, 'is_log_drain_enabled', false);

View file

@ -3,6 +3,7 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
@ -13,6 +14,8 @@ class StandaloneClickhouse extends BaseModel
protected $guarded = [];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type'];
protected $casts = [
'clickhouse_password' => 'encrypted',
];
@ -29,16 +32,9 @@ class StandaloneClickhouse extends BaseModel
'is_readonly' => true,
]);
});
static::deleting(function ($database) {
$storages = $database->persistentStorages()->get();
$server = data_get($database, 'destination.server');
if ($server) {
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
$database->scheduledBackups()->delete();
static::forceDeleting(function ($database) {
$database->persistentStorages()->delete();
$database->scheduledBackups()->delete();
$database->environment_variables()->delete();
$database->tags()->detach();
});
@ -89,6 +85,17 @@ class StandaloneClickhouse extends BaseModel
}
}
public function delete_volumes(Collection $persistentStorages)
{
if ($persistentStorages->count() === 0) {
return;
}
$server = data_get($this, 'destination.server');
foreach ($persistentStorages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
public function realStatus()
{
return $this->getRawOriginal('status');
@ -178,18 +185,36 @@ class StandaloneClickhouse extends BaseModel
return data_get($this, 'environment.project.team');
}
public function databaseType(): Attribute
{
return new Attribute(
get: fn () => $this->type(),
);
}
public function type(): string
{
return 'standalone-clickhouse';
}
public function get_db_url(bool $useInternal = false): string
protected function internalDbUrl(): Attribute
{
if ($this->is_public && ! $useInternal) {
return "clickhouse://{$this->clickhouse_user}:{$this->clickhouse_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->clickhouse_db}";
} else {
return "clickhouse://{$this->clickhouse_user}:{$this->clickhouse_password}@{$this->uuid}:9000/{$this->clickhouse_db}";
}
return new Attribute(
get: fn () => "clickhouse://{$this->clickhouse_admin_user}:{$this->clickhouse_admin_password}@{$this->uuid}:9000/{$this->clickhouse_db}",
);
}
protected function externalDbUrl(): Attribute
{
return new Attribute(
get: function () {
if ($this->is_public && $this->public_port) {
return "clickhouse://{$this->clickhouse_admin_user}:{$this->clickhouse_admin_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->clickhouse_db}";
}
return null;
}
);
}
public function environment()

View file

@ -3,6 +3,7 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
@ -13,6 +14,8 @@ class StandaloneDragonfly extends BaseModel
protected $guarded = [];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type'];
protected $casts = [
'dragonfly_password' => 'encrypted',
];
@ -29,16 +32,9 @@ class StandaloneDragonfly extends BaseModel
'is_readonly' => true,
]);
});
static::deleting(function ($database) {
$database->scheduledBackups()->delete();
$storages = $database->persistentStorages()->get();
$server = data_get($database, 'destination.server');
if ($server) {
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
static::forceDeleting(function ($database) {
$database->persistentStorages()->delete();
$database->scheduledBackups()->delete();
$database->environment_variables()->delete();
$database->tags()->detach();
});
@ -89,6 +85,17 @@ class StandaloneDragonfly extends BaseModel
}
}
public function delete_volumes(Collection $persistentStorages)
{
if ($persistentStorages->count() === 0) {
return;
}
$server = data_get($this, 'destination.server');
foreach ($persistentStorages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
public function realStatus()
{
return $this->getRawOriginal('status');
@ -178,18 +185,36 @@ class StandaloneDragonfly extends BaseModel
);
}
public function databaseType(): Attribute
{
return new Attribute(
get: fn () => $this->type(),
);
}
public function type(): string
{
return 'standalone-dragonfly';
}
public function get_db_url(bool $useInternal = false): string
protected function internalDbUrl(): Attribute
{
if ($this->is_public && ! $useInternal) {
return "redis://:{$this->dragonfly_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
} else {
return "redis://:{$this->dragonfly_password}@{$this->uuid}:6379/0";
}
return new Attribute(
get: fn () => "redis://:{$this->dragonfly_password}@{$this->uuid}:6379/0",
);
}
protected function externalDbUrl(): Attribute
{
return new Attribute(
get: function () {
if ($this->is_public && $this->public_port) {
return "redis://:{$this->dragonfly_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
}
return null;
}
);
}
public function environment()

View file

@ -3,6 +3,7 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
@ -13,6 +14,8 @@ class StandaloneKeydb extends BaseModel
protected $guarded = [];
protected $appends = ['internal_db_url', 'external_db_url'];
protected $casts = [
'keydb_password' => 'encrypted',
];
@ -29,16 +32,9 @@ class StandaloneKeydb extends BaseModel
'is_readonly' => true,
]);
});
static::deleting(function ($database) {
$database->scheduledBackups()->delete();
$storages = $database->persistentStorages()->get();
$server = data_get($database, 'destination.server');
if ($server) {
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
static::forceDeleting(function ($database) {
$database->persistentStorages()->delete();
$database->scheduledBackups()->delete();
$database->environment_variables()->delete();
$database->tags()->detach();
});
@ -89,6 +85,17 @@ class StandaloneKeydb extends BaseModel
}
}
public function delete_volumes(Collection $persistentStorages)
{
if ($persistentStorages->count() === 0) {
return;
}
$server = data_get($this, 'destination.server');
foreach ($persistentStorages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
public function realStatus()
{
return $this->getRawOriginal('status');
@ -178,18 +185,36 @@ class StandaloneKeydb extends BaseModel
);
}
public function databaseType(): Attribute
{
return new Attribute(
get: fn () => $this->type(),
);
}
public function type(): string
{
return 'standalone-keydb';
}
public function get_db_url(bool $useInternal = false): string
protected function internalDbUrl(): Attribute
{
if ($this->is_public && ! $useInternal) {
return "redis://{$this->keydb_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
} else {
return "redis://{$this->keydb_password}@{$this->uuid}:6379/0";
}
return new Attribute(
get: fn () => "redis://{$this->keydb_password}@{$this->uuid}:6379/0",
);
}
protected function externalDbUrl(): Attribute
{
return new Attribute(
get: function () {
if ($this->is_public && $this->public_port) {
return "redis://{$this->keydb_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
}
return null;
}
);
}
public function environment()

View file

@ -3,6 +3,7 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
@ -13,6 +14,8 @@ class StandaloneMariadb extends BaseModel
protected $guarded = [];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type'];
protected $casts = [
'mariadb_password' => 'encrypted',
];
@ -29,16 +32,9 @@ class StandaloneMariadb extends BaseModel
'is_readonly' => true,
]);
});
static::deleting(function ($database) {
$storages = $database->persistentStorages()->get();
$server = data_get($database, 'destination.server');
if ($server) {
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
$database->scheduledBackups()->delete();
static::forceDeleting(function ($database) {
$database->persistentStorages()->delete();
$database->scheduledBackups()->delete();
$database->environment_variables()->delete();
$database->tags()->detach();
});
@ -89,6 +85,17 @@ class StandaloneMariadb extends BaseModel
}
}
public function delete_volumes(Collection $persistentStorages)
{
if ($persistentStorages->count() === 0) {
return;
}
$server = data_get($this, 'destination.server');
foreach ($persistentStorages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
public function realStatus()
{
return $this->getRawOriginal('status');
@ -161,6 +168,13 @@ class StandaloneMariadb extends BaseModel
return data_get($this, 'is_log_drain_enabled', false);
}
public function databaseType(): Attribute
{
return new Attribute(
get: fn () => $this->type(),
);
}
public function type(): string
{
return 'standalone-mariadb';
@ -183,13 +197,24 @@ class StandaloneMariadb extends BaseModel
);
}
public function get_db_url(bool $useInternal = false): string
protected function internalDbUrl(): Attribute
{
if ($this->is_public && ! $useInternal) {
return "mysql://{$this->mariadb_user}:{$this->mariadb_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mariadb_database}";
} else {
return "mysql://{$this->mariadb_user}:{$this->mariadb_password}@{$this->uuid}:3306/{$this->mariadb_database}";
}
return new Attribute(
get: fn () => "mysql://{$this->mariadb_user}:{$this->mariadb_password}@{$this->uuid}:3306/{$this->mariadb_database}",
);
}
protected function externalDbUrl(): Attribute
{
return new Attribute(
get: function () {
if ($this->is_public && $this->public_port) {
return "mysql://{$this->mariadb_user}:{$this->mariadb_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mariadb_database}";
}
return null;
}
);
}
public function environment()

View file

@ -3,6 +3,7 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
@ -13,6 +14,8 @@ class StandaloneMongodb extends BaseModel
protected $guarded = [];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type'];
protected static function booted()
{
static::created(function ($database) {
@ -33,16 +36,9 @@ class StandaloneMongodb extends BaseModel
'is_readonly' => true,
]);
});
static::deleting(function ($database) {
$storages = $database->persistentStorages()->get();
$server = data_get($database, 'destination.server');
if ($server) {
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
$database->scheduledBackups()->delete();
static::forceDeleting(function ($database) {
$database->persistentStorages()->delete();
$database->scheduledBackups()->delete();
$database->environment_variables()->delete();
$database->tags()->detach();
});
@ -93,6 +89,17 @@ class StandaloneMongodb extends BaseModel
}
}
public function delete_volumes(Collection $persistentStorages)
{
if ($persistentStorages->count() === 0) {
return;
}
$server = data_get($this, 'destination.server');
foreach ($persistentStorages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
public function realStatus()
{
return $this->getRawOriginal('status');
@ -198,18 +205,36 @@ class StandaloneMongodb extends BaseModel
);
}
public function databaseType(): Attribute
{
return new Attribute(
get: fn () => $this->type(),
);
}
public function type(): string
{
return 'standalone-mongodb';
}
public function get_db_url(bool $useInternal = false)
protected function internalDbUrl(): Attribute
{
if ($this->is_public && ! $useInternal) {
return "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->destination->server->getIp}:{$this->public_port}/?directConnection=true";
} else {
return "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->uuid}:27017/?directConnection=true";
}
return new Attribute(
get: fn () => "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->uuid}:27017/?directConnection=true",
);
}
protected function externalDbUrl(): Attribute
{
return new Attribute(
get: function () {
if ($this->is_public && $this->public_port) {
return "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->destination->server->getIp}:{$this->public_port}/?directConnection=true";
}
return null;
}
);
}
public function environment()

View file

@ -3,6 +3,7 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
@ -13,6 +14,8 @@ class StandaloneMysql extends BaseModel
protected $guarded = [];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type'];
protected $casts = [
'mysql_password' => 'encrypted',
'mysql_root_password' => 'encrypted',
@ -30,16 +33,9 @@ class StandaloneMysql extends BaseModel
'is_readonly' => true,
]);
});
static::deleting(function ($database) {
$storages = $database->persistentStorages()->get();
$server = data_get($database, 'destination.server');
if ($server) {
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
$database->scheduledBackups()->delete();
static::forceDeleting(function ($database) {
$database->persistentStorages()->delete();
$database->scheduledBackups()->delete();
$database->environment_variables()->delete();
$database->tags()->detach();
});
@ -90,6 +86,17 @@ class StandaloneMysql extends BaseModel
}
}
public function delete_volumes(Collection $persistentStorages)
{
if ($persistentStorages->count() === 0) {
return;
}
$server = data_get($this, 'destination.server');
foreach ($persistentStorages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
public function realStatus()
{
return $this->getRawOriginal('status');
@ -157,6 +164,13 @@ class StandaloneMysql extends BaseModel
return null;
}
public function databaseType(): Attribute
{
return new Attribute(
get: fn () => $this->type(),
);
}
public function type(): string
{
return 'standalone-mysql';
@ -184,13 +198,24 @@ class StandaloneMysql extends BaseModel
);
}
public function get_db_url(bool $useInternal = false): string
protected function internalDbUrl(): Attribute
{
if ($this->is_public && ! $useInternal) {
return "mysql://{$this->mysql_user}:{$this->mysql_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mysql_database}";
} else {
return "mysql://{$this->mysql_user}:{$this->mysql_password}@{$this->uuid}:3306/{$this->mysql_database}";
}
return new Attribute(
get: fn () => "mysql://{$this->mysql_user}:{$this->mysql_password}@{$this->uuid}:3306/{$this->mysql_database}",
);
}
protected function externalDbUrl(): Attribute
{
return new Attribute(
get: function () {
if ($this->is_public && $this->public_port) {
return "mysql://{$this->mysql_user}:{$this->mysql_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mysql_database}";
}
return null;
}
);
}
public function environment()

View file

@ -3,6 +3,7 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
@ -13,6 +14,8 @@ class StandalonePostgresql extends BaseModel
protected $guarded = [];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type'];
protected $casts = [
'init_scripts' => 'array',
'postgres_password' => 'encrypted',
@ -30,16 +33,9 @@ class StandalonePostgresql extends BaseModel
'is_readonly' => true,
]);
});
static::deleting(function ($database) {
$storages = $database->persistentStorages()->get();
$server = data_get($database, 'destination.server');
if ($server) {
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
$database->scheduledBackups()->delete();
static::forceDeleting(function ($database) {
$database->persistentStorages()->delete();
$database->scheduledBackups()->delete();
$database->environment_variables()->delete();
$database->tags()->detach();
});
@ -59,6 +55,18 @@ class StandalonePostgresql extends BaseModel
}
}
public function delete_volumes(Collection $persistentStorages)
{
if ($persistentStorages->count() === 0) {
return;
}
$server = data_get($this, 'destination.server');
foreach ($persistentStorages as $storage) {
ray('Deleting volume: '.$storage->name);
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
public function isConfigurationChanged(bool $save = false)
{
$newConfigHash = $this->image.$this->ports_mappings.$this->postgres_initdb_args.$this->postgres_host_auth_method;
@ -179,18 +187,36 @@ class StandalonePostgresql extends BaseModel
return data_get($this, 'environment.project.team');
}
public function databaseType(): Attribute
{
return new Attribute(
get: fn () => $this->type(),
);
}
public function type(): string
{
return 'standalone-postgresql';
}
public function get_db_url(bool $useInternal = false): string
protected function internalDbUrl(): Attribute
{
if ($this->is_public && ! $useInternal) {
return "postgres://{$this->postgres_user}:{$this->postgres_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->postgres_db}";
} else {
return "postgres://{$this->postgres_user}:{$this->postgres_password}@{$this->uuid}:5432/{$this->postgres_db}";
}
return new Attribute(
get: fn () => "postgres://{$this->postgres_user}:{$this->postgres_password}@{$this->uuid}:5432/{$this->postgres_db}",
);
}
protected function externalDbUrl(): Attribute
{
return new Attribute(
get: function () {
if ($this->is_public && $this->public_port) {
return "postgres://{$this->postgres_user}:{$this->postgres_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->postgres_db}";
}
return null;
}
);
}
public function environment()

View file

@ -3,6 +3,7 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
@ -13,6 +14,8 @@ class StandaloneRedis extends BaseModel
protected $guarded = [];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type'];
protected static function booted()
{
static::created(function ($database) {
@ -25,16 +28,9 @@ class StandaloneRedis extends BaseModel
'is_readonly' => true,
]);
});
static::deleting(function ($database) {
$database->scheduledBackups()->delete();
$storages = $database->persistentStorages()->get();
$server = data_get($database, 'destination.server');
if ($server) {
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
static::forceDeleting(function ($database) {
$database->persistentStorages()->delete();
$database->scheduledBackups()->delete();
$database->environment_variables()->delete();
$database->tags()->detach();
});
@ -85,6 +81,17 @@ class StandaloneRedis extends BaseModel
}
}
public function delete_volumes(Collection $persistentStorages)
{
if ($persistentStorages->count() === 0) {
return;
}
$server = data_get($this, 'destination.server');
foreach ($persistentStorages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
}
}
public function realStatus()
{
return $this->getRawOriginal('status');
@ -179,13 +186,31 @@ class StandaloneRedis extends BaseModel
return 'standalone-redis';
}
public function get_db_url(bool $useInternal = false): string
public function databaseType(): Attribute
{
if ($this->is_public && ! $useInternal) {
return "redis://:{$this->redis_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
} else {
return "redis://:{$this->redis_password}@{$this->uuid}:6379/0";
}
return new Attribute(
get: fn () => $this->type(),
);
}
protected function internalDbUrl(): Attribute
{
return new Attribute(
get: fn () => "redis://:{$this->redis_password}@{$this->uuid}:6379/0",
);
}
protected function externalDbUrl(): Attribute
{
return new Attribute(
get: function () {
if ($this->is_public && $this->public_port) {
return "redis://:{$this->redis_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
}
return null;
}
);
}
public function environment()

View file

@ -8,7 +8,66 @@ use App\Notifications\Channels\SendsEmail;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\Notifiable;
use OpenApi\Attributes as OA;
#[OA\Schema(
description: 'Team model',
type: 'object',
properties: [
'id' => ['type' => 'integer', 'description' => 'The unique identifier of the team.'],
'name' => ['type' => 'string', 'description' => 'The name of the team.'],
'description' => ['type' => 'string', 'description' => 'The description of the team.'],
'personal_team' => ['type' => 'boolean', 'description' => 'Whether the team is personal or not.'],
'created_at' => ['type' => 'string', 'description' => 'The date and time the team was created.'],
'updated_at' => ['type' => 'string', 'description' => 'The date and time the team was last updated.'],
'smtp_enabled' => ['type' => 'boolean', 'description' => 'Whether SMTP is enabled or not.'],
'smtp_from_address' => ['type' => 'string', 'description' => 'The email address to send emails from.'],
'smtp_from_name' => ['type' => 'string', 'description' => 'The name to send emails from.'],
'smtp_recipients' => ['type' => 'string', 'description' => 'The email addresses to send emails to.'],
'smtp_host' => ['type' => 'string', 'description' => 'The SMTP host.'],
'smtp_port' => ['type' => 'string', 'description' => 'The SMTP port.'],
'smtp_encryption' => ['type' => 'string', 'description' => 'The SMTP encryption.'],
'smtp_username' => ['type' => 'string', 'description' => 'The SMTP username.'],
'smtp_password' => ['type' => 'string', 'description' => 'The SMTP password.'],
'smtp_timeout' => ['type' => 'string', 'description' => 'The SMTP timeout.'],
'smtp_notifications_test' => ['type' => 'boolean', 'description' => 'Whether to send test notifications via SMTP.'],
'smtp_notifications_deployments' => ['type' => 'boolean', 'description' => 'Whether to send deployment notifications via SMTP.'],
'smtp_notifications_status_changes' => ['type' => 'boolean', 'description' => 'Whether to send status change notifications via SMTP.'],
'smtp_notifications_scheduled_tasks' => ['type' => 'boolean', 'description' => 'Whether to send scheduled task notifications via SMTP.'],
'smtp_notifications_database_backups' => ['type' => 'boolean', 'description' => 'Whether to send database backup notifications via SMTP.'],
'discord_enabled' => ['type' => 'boolean', 'description' => 'Whether Discord is enabled or not.'],
'discord_webhook_url' => ['type' => 'string', 'description' => 'The Discord webhook URL.'],
'discord_notifications_test' => ['type' => 'boolean', 'description' => 'Whether to send test notifications via Discord.'],
'discord_notifications_deployments' => ['type' => 'boolean', 'description' => 'Whether to send deployment notifications via Discord.'],
'discord_notifications_status_changes' => ['type' => 'boolean', 'description' => 'Whether to send status change notifications via Discord.'],
'discord_notifications_database_backups' => ['type' => 'boolean', 'description' => 'Whether to send database backup notifications via Discord.'],
'discord_notifications_scheduled_tasks' => ['type' => 'boolean', 'description' => 'Whether to send scheduled task notifications via Discord.'],
'show_boarding' => ['type' => 'boolean', 'description' => 'Whether to show the boarding screen or not.'],
'resend_enabled' => ['type' => 'boolean', 'description' => 'Whether to enable resending or not.'],
'resend_api_key' => ['type' => 'string', 'description' => 'The resending API key.'],
'use_instance_email_settings' => ['type' => 'boolean', 'description' => 'Whether to use instance email settings or not.'],
'telegram_enabled' => ['type' => 'boolean', 'description' => 'Whether Telegram is enabled or not.'],
'telegram_token' => ['type' => 'string', 'description' => 'The Telegram token.'],
'telegram_chat_id' => ['type' => 'string', 'description' => 'The Telegram chat ID.'],
'telegram_notifications_test' => ['type' => 'boolean', 'description' => 'Whether to send test notifications via Telegram.'],
'telegram_notifications_deployments' => ['type' => 'boolean', 'description' => 'Whether to send deployment notifications via Telegram.'],
'telegram_notifications_status_changes' => ['type' => 'boolean', 'description' => 'Whether to send status change notifications via Telegram.'],
'telegram_notifications_database_backups' => ['type' => 'boolean', 'description' => 'Whether to send database backup notifications via Telegram.'],
'telegram_notifications_test_message_thread_id' => ['type' => 'string', 'description' => 'The Telegram test message thread ID.'],
'telegram_notifications_deployments_message_thread_id' => ['type' => 'string', 'description' => 'The Telegram deployment message thread ID.'],
'telegram_notifications_status_changes_message_thread_id' => ['type' => 'string', 'description' => 'The Telegram status change message thread ID.'],
'telegram_notifications_database_backups_message_thread_id' => ['type' => 'string', 'description' => 'The Telegram database backup message thread ID.'],
'custom_server_limit' => ['type' => 'string', 'description' => 'The custom server limit.'],
'telegram_notifications_scheduled_tasks' => ['type' => 'boolean', 'description' => 'Whether to send scheduled task notifications via Telegram.'],
'telegram_notifications_scheduled_tasks_thread_id' => ['type' => 'string', 'description' => 'The Telegram scheduled task message thread ID.'],
'members' => new OA\Property(
property: 'members',
type: 'array',
items: new OA\Items(ref: '#/components/schemas/User'),
description: 'The members of the team.'
),
]
)]
class Team extends Model implements SendsDiscord, SendsEmail, SendsNtfy
{
use Notifiable;

View file

@ -17,7 +17,23 @@ use Illuminate\Support\Str;
use Laravel\Fortify\TwoFactorAuthenticatable;
use Laravel\Sanctum\HasApiTokens;
use Laravel\Sanctum\NewAccessToken;
use OpenApi\Attributes as OA;
#[OA\Schema(
description: 'User model',
type: 'object',
properties: [
'id' => ['type' => 'integer', 'description' => 'The user identifier in the database.'],
'name' => ['type' => 'string', 'description' => 'The user name.'],
'email' => ['type' => 'string', 'description' => 'The user email.'],
'email_verified_at' => ['type' => 'string', 'description' => 'The date when the user email was verified.'],
'created_at' => ['type' => 'string', 'description' => 'The date when the user was created.'],
'updated_at' => ['type' => 'string', 'description' => 'The date when the user was updated.'],
'two_factor_confirmed_at' => ['type' => 'string', 'description' => 'The date when the user two factor was confirmed.'],
'force_password_reset' => ['type' => 'boolean', 'description' => 'The flag to force the user to reset the password.'],
'marketing_emails' => ['type' => 'boolean', 'description' => 'The flag to receive marketing emails.'],
],
)]
class User extends Authenticatable implements SendsEmail
{
use HasApiTokens, HasFactory, Notifiable, TwoFactorAuthenticatable;

View file

@ -2,7 +2,6 @@
namespace App\Notifications\Channels;
use App\Models\InstanceSettings;
use App\Models\User;
use Exception;
use Illuminate\Mail\Message;
@ -14,7 +13,7 @@ class TransactionalEmailChannel
{
public function send(User $notifiable, Notification $notification): void
{
$settings = InstanceSettings::get();
$settings = view()->shared('instanceSettings');
if (! data_get($settings, 'smtp_enabled') && ! data_get($settings, 'resend_enabled')) {
Log::info('SMTP/Resend not enabled');

View file

@ -18,7 +18,7 @@ class ResetPassword extends Notification
public function __construct($token)
{
$this->settings = InstanceSettings::get();
$this->settings = view()->shared('instanceSettings');
$this->token = $token;
}

View file

@ -2,8 +2,10 @@
namespace App\Providers;
use App\Models\InstanceSettings;
use App\Models\PersonalAccessToken;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;
use Laravel\Sanctum\Sanctum;
@ -14,6 +16,7 @@ class AppServiceProvider extends ServiceProvider
public function boot(): void
{
Sanctum::usePersonalAccessTokenModel(PersonalAccessToken::class);
Http::macro('github', function (string $api_url, ?string $github_access_token = null) {
if ($github_access_token) {
return Http::withHeaders([
@ -27,5 +30,11 @@ class AppServiceProvider extends ServiceProvider
])->baseUrl($api_url);
}
});
echo config('app.env');
echo env('CI');
if (! env('CI')) {
View::share('instanceSettings', InstanceSettings::get());
}
}
}

View file

@ -6,7 +6,6 @@ use App\Actions\Fortify\CreateNewUser;
use App\Actions\Fortify\ResetUserPassword;
use App\Actions\Fortify\UpdateUserPassword;
use App\Actions\Fortify\UpdateUserProfileInformation;
use App\Models\InstanceSettings;
use App\Models\OauthSetting;
use App\Models\User;
use Illuminate\Cache\RateLimiting\Limit;
@ -45,7 +44,7 @@ class FortifyServiceProvider extends ServiceProvider
{
Fortify::createUsersUsing(CreateNewUser::class);
Fortify::registerView(function () {
$settings = InstanceSettings::get();
$settings = view()->shared('instanceSettings');
if (! $settings->is_registration_enabled) {
return redirect()->route('login');
}
@ -57,7 +56,7 @@ class FortifyServiceProvider extends ServiceProvider
});
Fortify::loginView(function () {
$settings = InstanceSettings::get();
$settings = view()->shared('instanceSettings');
$enabled_oauth_providers = OauthSetting::where('enabled', true)->get();
$users = User::count();
if ($users == 0) {

View file

@ -1,38 +1,178 @@
<?php
use App\Enums\BuildPackTypes;
use App\Enums\RedirectTypes;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
function get_team_id_from_token()
function getTeamIdFromToken()
{
$token = auth()->user()->currentAccessToken();
return data_get($token, 'team_id');
}
function invalid_token()
function invalidTokenResponse()
{
return response()->json(['error' => 'Invalid token.', 'docs' => 'https://coolify.io/docs/api-reference/authorization'], 400);
return response()->json(['message' => 'Invalid token.', 'docs' => 'https://coolify.io/docs/api-reference/authorization'], 400);
}
function serialize_api_response($data)
function serializeApiResponse($data)
{
if (! $data instanceof Collection) {
$data = collect($data);
}
$data = $data->sortKeys();
$created_at = data_get($data, 'created_at');
$updated_at = data_get($data, 'updated_at');
if ($created_at) {
unset($data['created_at']);
$data['created_at'] = $created_at;
if ($data instanceof Collection) {
$data = $data->map(function ($d) {
$d = collect($d)->sortKeys();
$created_at = data_get($d, 'created_at');
$updated_at = data_get($d, 'updated_at');
if ($created_at) {
unset($d['created_at']);
$d['created_at'] = $created_at;
}
if ($updated_at) {
unset($data['updated_at']);
$data['updated_at'] = $updated_at;
}
if (data_get($data, 'id')) {
$data = $data->prepend($data['id'], 'id');
}
}
if ($updated_at) {
unset($d['updated_at']);
$d['updated_at'] = $updated_at;
}
if (data_get($d, 'name')) {
$d = $d->prepend($d['name'], 'name');
}
if (data_get($d, 'description')) {
$d = $d->prepend($d['description'], 'description');
}
if (data_get($d, 'uuid')) {
$d = $d->prepend($d['uuid'], 'uuid');
}
return $data;
if (! is_null(data_get($d, 'id'))) {
$d = $d->prepend($d['id'], 'id');
}
return $d;
});
return $data;
} else {
$d = collect($data)->sortKeys();
$created_at = data_get($d, 'created_at');
$updated_at = data_get($d, 'updated_at');
if ($created_at) {
unset($d['created_at']);
$d['created_at'] = $created_at;
}
if ($updated_at) {
unset($d['updated_at']);
$d['updated_at'] = $updated_at;
}
if (data_get($d, 'name')) {
$d = $d->prepend($d['name'], 'name');
}
if (data_get($d, 'description')) {
$d = $d->prepend($d['description'], 'description');
}
if (data_get($d, 'uuid')) {
$d = $d->prepend($d['uuid'], 'uuid');
}
if (! is_null(data_get($d, 'id'))) {
$d = $d->prepend($d['id'], 'id');
}
return $d;
}
}
function sharedDataApplications()
{
return [
'git_repository' => 'string',
'git_branch' => 'string',
'build_pack' => Rule::enum(BuildPackTypes::class),
'is_static' => 'boolean',
'domains' => 'string',
'redirect' => Rule::enum(RedirectTypes::class),
'git_commit_sha' => 'string',
'docker_registry_image_name' => 'string|nullable',
'docker_registry_image_tag' => 'string|nullable',
'install_command' => 'string|nullable',
'build_command' => 'string|nullable',
'start_command' => 'string|nullable',
'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/',
'ports_mappings' => 'string|regex:/^(\d+:\d+)(,\d+:\d+)*$/|nullable',
'base_directory' => 'string|nullable',
'publish_directory' => 'string|nullable',
'health_check_enabled' => 'boolean',
'health_check_path' => 'string',
'health_check_port' => 'string|nullable',
'health_check_host' => 'string',
'health_check_method' => 'string',
'health_check_return_code' => 'numeric',
'health_check_scheme' => 'string',
'health_check_response_text' => 'string|nullable',
'health_check_interval' => 'numeric',
'health_check_timeout' => 'numeric',
'health_check_retries' => 'numeric',
'health_check_start_period' => 'numeric',
'limits_memory' => 'string',
'limits_memory_swap' => 'string',
'limits_memory_swappiness' => 'numeric',
'limits_memory_reservation' => 'string',
'limits_cpus' => 'string',
'limits_cpuset' => 'string|nullable',
'limits_cpu_shares' => 'numeric',
'custom_labels' => 'string|nullable',
'custom_docker_run_options' => 'string|nullable',
'post_deployment_command' => 'string|nullable',
'post_deployment_command_container' => 'string',
'pre_deployment_command' => 'string|nullable',
'pre_deployment_command_container' => 'string',
'manual_webhook_secret_github' => 'string|nullable',
'manual_webhook_secret_gitlab' => 'string|nullable',
'manual_webhook_secret_bitbucket' => 'string|nullable',
'manual_webhook_secret_gitea' => 'string|nullable',
'docker_compose_location' => 'string',
'docker_compose' => 'string|nullable',
'docker_compose_raw' => 'string|nullable',
'docker_compose_domains' => 'array|nullable',
'docker_compose_custom_start_command' => 'string|nullable',
'docker_compose_custom_build_command' => 'string|nullable',
];
}
function validateIncomingRequest(Request $request)
{
// check if request is json
if (! $request->isJson()) {
return response()->json([
'message' => 'Invalid request.',
'error' => 'Content-Type must be application/json.',
], 400);
}
// check if request is valid json
if (! json_decode($request->getContent())) {
return response()->json([
'message' => 'Invalid request.',
'error' => 'Invalid JSON.',
], 400);
}
// check if valid json is empty
if (empty($request->json()->all())) {
return response()->json([
'message' => 'Invalid request.',
'error' => 'Empty JSON.',
], 400);
}
}
function removeUnnecessaryFieldsFromRequest(Request $request)
{
$request->offsetUnset('project_uuid');
$request->offsetUnset('environment_name');
$request->offsetUnset('destination_uuid');
$request->offsetUnset('server_uuid');
$request->offsetUnset('type');
$request->offsetUnset('domains');
$request->offsetUnset('instant_deploy');
$request->offsetUnset('github_app_uuid');
$request->offsetUnset('private_key_uuid');
}

View file

@ -40,6 +40,7 @@ const SUPPORTED_OS = [
'ubuntu debian raspbian',
'centos fedora rhel ol rocky amzn almalinux',
'sles opensuse-leap opensuse-tumbleweed',
'arch',
];
const SHARED_VARIABLE_TYPES = ['team', 'project', 'environment'];

View file

@ -19,136 +19,165 @@ function generate_database_name(string $type): string
return $type.'-database-'.$cuid;
}
function create_standalone_postgresql($environment_id, $destination_uuid): StandalonePostgresql
function create_standalone_postgresql($environmentId, $destinationUuid, ?array $otherData = null): StandalonePostgresql
{
// TODO: If another type of destination is added, this will need to be updated.
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
$destination = StandaloneDocker::where('uuid', $destinationUuid)->first();
if (! $destination) {
throw new Exception('Destination not found');
}
$database = new StandalonePostgresql();
$database->name = generate_database_name('postgresql');
$database->postgres_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
$database->environment_id = $environmentId;
$database->destination_id = $destination->id;
$database->destination_type = $destination->getMorphClass();
if ($otherData) {
$database->fill($otherData);
}
$database->save();
return StandalonePostgresql::create([
'name' => generate_database_name('postgresql'),
'postgres_password' => \Illuminate\Support\Str::password(length: 64, symbols: false),
'environment_id' => $environment_id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),
]);
return $database;
}
function create_standalone_redis($environment_id, $destination_uuid): StandaloneRedis
function create_standalone_redis($environment_id, $destination_uuid, ?array $otherData = null): StandaloneRedis
{
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
if (! $destination) {
throw new Exception('Destination not found');
}
$database = new StandaloneRedis();
$database->name = generate_database_name('redis');
$database->redis_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
$database->environment_id = $environment_id;
$database->destination_id = $destination->id;
$database->destination_type = $destination->getMorphClass();
if ($otherData) {
$database->fill($otherData);
}
$database->save();
return StandaloneRedis::create([
'name' => generate_database_name('redis'),
'redis_password' => \Illuminate\Support\Str::password(length: 64, symbols: false),
'environment_id' => $environment_id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),
]);
return $database;
}
function create_standalone_mongodb($environment_id, $destination_uuid): StandaloneMongodb
function create_standalone_mongodb($environment_id, $destination_uuid, ?array $otherData = null): StandaloneMongodb
{
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
if (! $destination) {
throw new Exception('Destination not found');
}
$database = new StandaloneMongodb();
$database->name = generate_database_name('mongodb');
$database->mongo_initdb_root_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
$database->environment_id = $environment_id;
$database->destination_id = $destination->id;
$database->destination_type = $destination->getMorphClass();
if ($otherData) {
$database->fill($otherData);
}
$database->save();
return StandaloneMongodb::create([
'name' => generate_database_name('mongodb'),
'mongo_initdb_root_password' => \Illuminate\Support\Str::password(length: 64, symbols: false),
'environment_id' => $environment_id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),
]);
return $database;
}
function create_standalone_mysql($environment_id, $destination_uuid): StandaloneMysql
function create_standalone_mysql($environment_id, $destination_uuid, ?array $otherData = null): StandaloneMysql
{
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
if (! $destination) {
throw new Exception('Destination not found');
}
$database = new StandaloneMysql();
$database->name = generate_database_name('mysql');
$database->mysql_root_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
$database->mysql_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
$database->environment_id = $environment_id;
$database->destination_id = $destination->id;
$database->destination_type = $destination->getMorphClass();
if ($otherData) {
$database->fill($otherData);
}
$database->save();
return StandaloneMysql::create([
'name' => generate_database_name('mysql'),
'mysql_root_password' => \Illuminate\Support\Str::password(length: 64, symbols: false),
'mysql_password' => \Illuminate\Support\Str::password(length: 64, symbols: false),
'environment_id' => $environment_id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),
]);
return $database;
}
function create_standalone_mariadb($environment_id, $destination_uuid): StandaloneMariadb
function create_standalone_mariadb($environment_id, $destination_uuid, ?array $otherData = null): StandaloneMariadb
{
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
if (! $destination) {
throw new Exception('Destination not found');
}
$database = new StandaloneMariadb();
$database->name = generate_database_name('mariadb');
$database->mariadb_root_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
$database->mariadb_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
$database->environment_id = $environment_id;
$database->destination_id = $destination->id;
$database->destination_type = $destination->getMorphClass();
return StandaloneMariadb::create([
'name' => generate_database_name('mariadb'),
'mariadb_root_password' => \Illuminate\Support\Str::password(length: 64, symbols: false),
'mariadb_password' => \Illuminate\Support\Str::password(length: 64, symbols: false),
'environment_id' => $environment_id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),
]);
if ($otherData) {
$database->fill($otherData);
}
$database->save();
return $database;
}
function create_standalone_keydb($environment_id, $destination_uuid): StandaloneKeydb
function create_standalone_keydb($environment_id, $destination_uuid, ?array $otherData = null): StandaloneKeydb
{
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
if (! $destination) {
throw new Exception('Destination not found');
}
$database = new StandaloneKeydb();
$database->name = generate_database_name('keydb');
$database->keydb_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
$database->environment_id = $environment_id;
$database->destination_id = $destination->id;
$database->destination_type = $destination->getMorphClass();
if ($otherData) {
$database->fill($otherData);
}
$database->save();
return StandaloneKeydb::create([
'name' => generate_database_name('keydb'),
'keydb_password' => \Illuminate\Support\Str::password(length: 64, symbols: false),
'environment_id' => $environment_id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),
]);
return $database;
}
function create_standalone_dragonfly($environment_id, $destination_uuid): StandaloneDragonfly
function create_standalone_dragonfly($environment_id, $destination_uuid, ?array $otherData = null): StandaloneDragonfly
{
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
if (! $destination) {
throw new Exception('Destination not found');
}
$database = new StandaloneDragonfly();
$database->name = generate_database_name('dragonfly');
$database->dragonfly_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
$database->environment_id = $environment_id;
$database->destination_id = $destination->id;
$database->destination_type = $destination->getMorphClass();
if ($otherData) {
$database->fill($otherData);
}
$database->save();
return StandaloneDragonfly::create([
'name' => generate_database_name('dragonfly'),
'dragonfly_password' => \Illuminate\Support\Str::password(length: 64, symbols: false),
'environment_id' => $environment_id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),
]);
return $database;
}
function create_standalone_clickhouse($environment_id, $destination_uuid): StandaloneClickhouse
function create_standalone_clickhouse($environment_id, $destination_uuid, ?array $otherData = null): StandaloneClickhouse
{
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
if (! $destination) {
throw new Exception('Destination not found');
}
$database = new StandaloneClickhouse();
$database->name = generate_database_name('clickhouse');
$database->clickhouse_admin_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
$database->environment_id = $environment_id;
$database->destination_id = $destination->id;
$database->destination_type = $destination->getMorphClass();
if ($otherData) {
$database->fill($otherData);
}
$database->save();
return StandaloneClickhouse::create([
'name' => generate_database_name('clickhouse'),
'clickhouse_admin_password' => \Illuminate\Support\Str::password(length: 64, symbols: false),
'environment_id' => $environment_id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),
]);
return $database;
}
/**
* Delete file locally on the filesystem.
*/
function delete_backup_locally(?string $filename, Server $server): void
{
if (empty($filename)) {
@ -156,3 +185,17 @@ function delete_backup_locally(?string $filename, Server $server): void
}
instant_remote_process(["rm -f \"{$filename}\""], $server, throwError: false);
}
function isPublicPortAlreadyUsed(Server $server, int $port, ?string $id = null): bool
{
if ($id) {
$foundDatabase = $server->databases()->where('public_port', $port)->where('is_public', true)->where('id', '!=', $id)->first();
} else {
$foundDatabase = $server->databases()->where('public_port', $port)->where('is_public', true)->first();
}
if ($foundDatabase) {
return true;
}
return false;
}

Some files were not shown because too many files have changed in this diff Show more