Merge branch 'next' into caddy_stripprefix_fix

This commit is contained in:
konstchri
2024-11-12 19:45:17 +02:00
committed by GitHub
47 changed files with 8946 additions and 439 deletions

View File

@@ -11,7 +11,7 @@ on:
- docker/coolify-helper/Dockerfile - docker/coolify-helper/Dockerfile
- docker/coolify-realtime/Dockerfile - docker/coolify-realtime/Dockerfile
- docker/testing-host/Dockerfile - docker/testing-host/Dockerfile
- templates/* - templates/**
env: env:
GITHUB_REGISTRY: ghcr.io GITHUB_REGISTRY: ghcr.io

View File

@@ -11,7 +11,7 @@ on:
- docker/coolify-helper/Dockerfile - docker/coolify-helper/Dockerfile
- docker/coolify-realtime/Dockerfile - docker/coolify-realtime/Dockerfile
- docker/testing-host/Dockerfile - docker/testing-host/Dockerfile
- templates/* - templates/**
env: env:
GITHUB_REGISTRY: ghcr.io GITHUB_REGISTRY: ghcr.io

View File

@@ -0,0 +1,37 @@
<?php
namespace App\Actions\Application;
use Laravel\Horizon\Contracts\JobRepository;
use Lorisleiva\Actions\Concerns\AsAction;
class IsHorizonQueueEmpty
{
use AsAction;
public function handle()
{
$hostname = gethostname();
$recent = app(JobRepository::class)->getRecent();
if ($recent) {
$running = $recent->filter(function ($job) use ($hostname) {
$payload = json_decode($job->payload);
$tags = data_get($payload, 'tags');
return $job->status != 'completed' &&
$job->status != 'failed' &&
isset($tags) &&
is_array($tags) &&
in_array('server:'.$hostname, $tags);
});
if ($running->count() > 0) {
echo 'false';
return false;
}
}
echo 'true';
return true;
}
}

View File

@@ -12,7 +12,7 @@ class InstallDocker
public function handle(Server $server) public function handle(Server $server)
{ {
$dockerVersion = config('constants.docker_install_version'); $dockerVersion = config('constants.docker.minimum_required_version');
$supported_os_type = $server->validateOS(); $supported_os_type = $server->validateOS();
if (! $supported_os_type) { if (! $supported_os_type) {
throw new \Exception('Server OS type is not supported for automated installation. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://coolify.io/docs/installation#manually">documentation</a>.'); throw new \Exception('Server OS type is not supported for automated installation. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://coolify.io/docs/installation#manually">documentation</a>.');

View File

@@ -0,0 +1,49 @@
<?php
namespace App\Console\Commands;
use App\Models\Team;
use Illuminate\Console\Command;
class CloudCheckSubscription extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'cloud:check-subscription';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Check Cloud subscriptions';
/**
* Execute the console command.
*/
public function handle()
{
$stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key'));
$activeSubscribers = Team::whereRelation('subscription', 'stripe_invoice_paid', true)->get();
foreach ($activeSubscribers as $team) {
$stripeSubscriptionId = $team->subscription->stripe_subscription_id;
$stripeInvoicePaid = $team->subscription->stripe_invoice_paid;
$stripeCustomerId = $team->subscription->stripe_customer_id;
if (! $stripeSubscriptionId) {
echo "Team {$team->id} has no subscription, but invoice status is: {$stripeInvoicePaid}\n";
echo "Link on Stripe: https://dashboard.stripe.com/customers/{$stripeCustomerId}\n";
continue;
}
$subscription = $stripe->subscriptions->retrieve($stripeSubscriptionId);
if ($subscription->status === 'active') {
continue;
}
echo "Subscription {$stripeSubscriptionId} is not active ({$subscription->status})\n";
echo "Link on Stripe: https://dashboard.stripe.com/subscriptions/{$stripeSubscriptionId}\n";
}
}
}

View File

@@ -6,6 +6,7 @@ use App\Models\InstanceSettings;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Process; use Illuminate\Support\Facades\Process;
use Symfony\Component\Yaml\Yaml;
class Dev extends Command class Dev extends Command
{ {
@@ -37,6 +38,11 @@ class Dev extends Command
$error = preg_replace('/^\h*\v+/m', '', $error); $error = preg_replace('/^\h*\v+/m', '', $error);
echo $error; echo $error;
echo $process->output(); echo $process->output();
// Convert YAML to JSON
$yaml = file_get_contents('openapi.yaml');
$json = json_encode(Yaml::parse($yaml), JSON_PRETTY_PRINT);
file_put_contents('openapi.json', $json);
echo "Converted OpenAPI YAML to JSON.\n";
} }
public function init() public function init()

View File

@@ -57,12 +57,19 @@ class Init extends Command
$this->call('cleanup:stucked-resources'); $this->call('cleanup:stucked-resources');
if (isCloud()) { if (isCloud()) {
$response = Http::retry(3, 1000)->get(config('constants.services.official')); try {
if ($response->successful()) { $this->pullTemplatesFromCDN();
$services = $response->json(); } catch (\Throwable $e) {
File::put(base_path('templates/service-templates.json'), json_encode($services)); echo "Could not pull templates from CDN: {$e->getMessage()}\n";
}
}
if (! isCloud()) {
try {
$this->pullTemplatesFromCDN();
} catch (\Throwable $e) {
echo "Could not pull templates from CDN: {$e->getMessage()}\n";
} }
} else {
try { try {
$localhost = $this->servers->where('id', 0)->first(); $localhost = $this->servers->where('id', 0)->first();
$localhost->setupDynamicProxyConfiguration(); $localhost->setupDynamicProxyConfiguration();
@@ -80,6 +87,14 @@ class Init extends Command
} }
} }
private function pullTemplatesFromCDN()
{
$response = Http::retry(3, 1000)->get(config('constants.services.official'));
if ($response->successful()) {
$services = $response->json();
File::put(base_path('templates/service-templates.json'), json_encode($services));
}
}
// private function disable_metrics() // private function disable_metrics()
// { // {
// if (version_compare('4.0.0-beta.312', config('version'), '<=')) { // if (version_compare('4.0.0-beta.312', config('version'), '<=')) {

View File

@@ -116,7 +116,7 @@ class ProjectController extends Controller
responses: [ responses: [
new OA\Response( new OA\Response(
response: 200, response: 200,
description: 'Project details', description: 'Environment details',
content: new OA\JsonContent(ref: '#/components/schemas/Environment')), content: new OA\JsonContent(ref: '#/components/schemas/Environment')),
new OA\Response( new OA\Response(
response: 401, response: 401,

View File

@@ -81,15 +81,8 @@ class SecurityController extends Controller
new OA\Response( new OA\Response(
response: 200, response: 200,
description: 'Get all private keys.', description: 'Get all private keys.',
content: [ content: new OA\JsonContent(ref: '#/components/schemas/PrivateKey')
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'array',
items: new OA\Items(ref: '#/components/schemas/PrivateKey')
)
), ),
]),
new OA\Response( new OA\Response(
response: 401, response: 401,
ref: '#/components/responses/401', ref: '#/components/responses/401',

View File

@@ -426,6 +426,7 @@ class ServersController extends Controller
'private_key_uuid' => ['type' => 'string', 'example' => 'og888os', 'description' => 'The UUID of the private key.'], 'private_key_uuid' => ['type' => 'string', 'example' => 'og888os', 'description' => 'The UUID of the private key.'],
'is_build_server' => ['type' => 'boolean', 'example' => false, 'description' => 'Is build server.'], 'is_build_server' => ['type' => 'boolean', 'example' => false, 'description' => 'Is build server.'],
'instant_validate' => ['type' => 'boolean', 'example' => false, 'description' => 'Instant validate.'], 'instant_validate' => ['type' => 'boolean', 'example' => false, 'description' => 'Instant validate.'],
'proxy_type' => ['type' => 'string', 'enum' => ['traefik', 'caddy', 'none'], 'example' => 'traefik', 'description' => 'The proxy type.'],
], ],
), ),
), ),
@@ -461,7 +462,7 @@ class ServersController extends Controller
)] )]
public function create_server(Request $request) public function create_server(Request $request)
{ {
$allowedFields = ['name', 'description', 'ip', 'port', 'user', 'private_key_uuid', 'is_build_server', 'instant_validate']; $allowedFields = ['name', 'description', 'ip', 'port', 'user', 'private_key_uuid', 'is_build_server', 'instant_validate', 'proxy_type'];
$teamId = getTeamIdFromToken(); $teamId = getTeamIdFromToken();
if (is_null($teamId)) { if (is_null($teamId)) {
@@ -481,6 +482,7 @@ class ServersController extends Controller
'user' => 'string|nullable', 'user' => 'string|nullable',
'is_build_server' => 'boolean|nullable', 'is_build_server' => 'boolean|nullable',
'instant_validate' => 'boolean|nullable', 'instant_validate' => 'boolean|nullable',
'proxy_type' => 'string|nullable',
]); ]);
$extraFields = array_diff(array_keys($request->all()), $allowedFields); $extraFields = array_diff(array_keys($request->all()), $allowedFields);
@@ -512,6 +514,14 @@ class ServersController extends Controller
if (is_null($request->instant_validate)) { if (is_null($request->instant_validate)) {
$request->offsetSet('instant_validate', false); $request->offsetSet('instant_validate', false);
} }
if ($request->proxy_type) {
$validProxyTypes = collect(ProxyTypes::cases())->map(function ($proxyType) {
return str($proxyType->value)->lower();
});
if (! $validProxyTypes->contains(str($request->proxy_type)->lower())) {
return response()->json(['message' => 'Invalid proxy type.'], 422);
}
}
$privateKey = PrivateKey::whereTeamId($teamId)->whereUuid($request->private_key_uuid)->first(); $privateKey = PrivateKey::whereTeamId($teamId)->whereUuid($request->private_key_uuid)->first();
if (! $privateKey) { if (! $privateKey) {
return response()->json(['message' => 'Private key not found.'], 404); return response()->json(['message' => 'Private key not found.'], 404);
@@ -521,6 +531,8 @@ class ServersController extends Controller
return response()->json(['message' => 'Server with this IP already exists.'], 400); return response()->json(['message' => 'Server with this IP already exists.'], 400);
} }
$proxyType = $request->proxy_type ? str($request->proxy_type)->upper() : ProxyTypes::TRAEFIK->value;
$server = ModelsServer::create([ $server = ModelsServer::create([
'name' => $request->name, 'name' => $request->name,
'description' => $request->description, 'description' => $request->description,
@@ -530,7 +542,7 @@ class ServersController extends Controller
'private_key_id' => $privateKey->id, 'private_key_id' => $privateKey->id,
'team_id' => $teamId, 'team_id' => $teamId,
'proxy' => [ 'proxy' => [
'type' => ProxyTypes::TRAEFIK->value, 'type' => $proxyType,
'status' => ProxyStatus::EXITED->value, 'status' => ProxyStatus::EXITED->value,
], ],
]); ]);
@@ -571,6 +583,7 @@ class ServersController extends Controller
'private_key_uuid' => ['type' => 'string', 'description' => 'The UUID of the private key.'], 'private_key_uuid' => ['type' => 'string', 'description' => 'The UUID of the private key.'],
'is_build_server' => ['type' => 'boolean', 'description' => 'Is build server.'], 'is_build_server' => ['type' => 'boolean', 'description' => 'Is build server.'],
'instant_validate' => ['type' => 'boolean', 'description' => 'Instant validate.'], 'instant_validate' => ['type' => 'boolean', 'description' => 'Instant validate.'],
'proxy_type' => ['type' => 'string', 'enum' => ['traefik', 'caddy', 'none'], 'description' => 'The proxy type.'],
], ],
), ),
), ),
@@ -604,7 +617,7 @@ class ServersController extends Controller
)] )]
public function update_server(Request $request) public function update_server(Request $request)
{ {
$allowedFields = ['name', 'description', 'ip', 'port', 'user', 'private_key_uuid', 'is_build_server', 'instant_validate']; $allowedFields = ['name', 'description', 'ip', 'port', 'user', 'private_key_uuid', 'is_build_server', 'instant_validate', 'proxy_type'];
$teamId = getTeamIdFromToken(); $teamId = getTeamIdFromToken();
if (is_null($teamId)) { if (is_null($teamId)) {
@@ -624,6 +637,7 @@ class ServersController extends Controller
'user' => 'string|nullable', 'user' => 'string|nullable',
'is_build_server' => 'boolean|nullable', 'is_build_server' => 'boolean|nullable',
'instant_validate' => 'boolean|nullable', 'instant_validate' => 'boolean|nullable',
'proxy_type' => 'string|nullable',
]); ]);
$extraFields = array_diff(array_keys($request->all()), $allowedFields); $extraFields = array_diff(array_keys($request->all()), $allowedFields);
@@ -644,6 +658,16 @@ class ServersController extends Controller
if (! $server) { if (! $server) {
return response()->json(['message' => 'Server not found.'], 404); return response()->json(['message' => 'Server not found.'], 404);
} }
if ($request->proxy_type) {
$validProxyTypes = collect(ProxyTypes::cases())->map(function ($proxyType) {
return str($proxyType->value)->lower();
});
if ($validProxyTypes->contains(str($request->proxy_type)->lower())) {
$server->changeProxy($request->proxy_type, async: true);
} else {
return response()->json(['message' => 'Invalid proxy type.'], 422);
}
}
$server->update($request->only(['name', 'description', 'ip', 'port', 'user'])); $server->update($request->only(['name', 'description', 'ip', 'port', 'user']));
if ($request->is_build_server) { if ($request->is_build_server) {
$server->settings()->update([ $server->settings()->update([
@@ -654,7 +678,9 @@ class ServersController extends Controller
ValidateServer::dispatch($server)->onQueue('high'); ValidateServer::dispatch($server)->onQueue('high');
} }
return response()->json(serializeApiResponse($server))->setStatusCode(201); return response()->json([
])->setStatusCode(201);
} }
#[OA\Delete( #[OA\Delete(

View File

@@ -33,6 +33,7 @@ class Gitlab extends Controller
return; return;
} }
$return_payloads = collect([]); $return_payloads = collect([]);
$payload = $request->collect(); $payload = $request->collect();
$headers = $request->headers->all(); $headers = $request->headers->all();
@@ -48,6 +49,15 @@ class Gitlab extends Controller
return response($return_payloads); return response($return_payloads);
} }
if (empty($x_gitlab_token)) {
$return_payloads->push([
'status' => 'failed',
'message' => 'Invalid signature.',
]);
return response($return_payloads);
}
if ($x_gitlab_event === 'push') { if ($x_gitlab_event === 'push') {
$branch = data_get($payload, 'ref'); $branch = data_get($payload, 'ref');
$full_name = data_get($payload, 'project.path_with_namespace'); $full_name = data_get($payload, 'project.path_with_namespace');

View File

@@ -5,8 +5,6 @@ namespace App\Http\Controllers\Webhook;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Jobs\ServerLimitCheckJob; use App\Jobs\ServerLimitCheckJob;
use App\Jobs\SubscriptionInvoiceFailedJob; use App\Jobs\SubscriptionInvoiceFailedJob;
use App\Jobs\SubscriptionTrialEndedJob;
use App\Jobs\SubscriptionTrialEndsSoonJob;
use App\Models\Subscription; use App\Models\Subscription;
use App\Models\Team; use App\Models\Team;
use App\Models\Webhook; use App\Models\Webhook;
@@ -260,42 +258,7 @@ class Stripe extends Controller
$customerId = data_get($data, 'customer'); $customerId = data_get($data, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail(); $subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
$team = data_get($subscription, 'team'); $team = data_get($subscription, 'team');
if ($team) { $team?->subscriptionEnded();
$team->trialEnded();
}
$subscription->update([
'stripe_subscription_id' => null,
'stripe_plan_id' => null,
'stripe_cancel_at_period_end' => false,
'stripe_invoice_paid' => false,
'stripe_trial_already_ended' => false,
]);
// send_internal_notification('customer.subscription.deleted for customer: '.$customerId);
break;
case 'customer.subscription.trial_will_end':
// Not used for now
$customerId = data_get($data, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
$team = data_get($subscription, 'team');
if (! $team) {
return response('No team found for subscription: '.$subscription->id, 400);
}
SubscriptionTrialEndsSoonJob::dispatch($team);
break;
case 'customer.subscription.paused':
$customerId = data_get($data, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
$team = data_get($subscription, 'team');
if (! $team) {
return response('No team found for subscription: '.$subscription->id, 400);
}
$team->trialEnded();
$subscription->update([
'stripe_trial_already_ended' => true,
'stripe_invoice_paid' => false,
]);
SubscriptionTrialEndedJob::dispatch($team);
// send_internal_notification('Subscription paused for customer: '.$customerId);
break; break;
default: default:
// Unhandled event type // Unhandled event type

View File

@@ -225,6 +225,11 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
} }
} }
public function tags(): array
{
return ['server:'.gethostname()];
}
public function handle(): void public function handle(): void
{ {
$this->application_deployment_queue->update([ $this->application_deployment_queue->update([

View File

@@ -1,42 +0,0 @@
<?php
namespace App\Jobs;
use App\Models\Team;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class SubscriptionTrialEndedJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(
public Team $team
) {}
public function handle(): void
{
try {
$session = getStripeCustomerPortalSession($this->team);
$mail = new MailMessage;
$mail->subject('Action required: You trial in Coolify Cloud ended.');
$mail->view('emails.trial-ended', [
'stripeCustomerPortal' => $session->url,
]);
$this->team->members()->each(function ($member) use ($mail) {
if ($member->isAdmin()) {
send_user_an_email($mail, $member->email);
send_internal_notification('Trial reminder email sent to '.$member->email);
}
});
} catch (\Throwable $e) {
send_internal_notification('SubscriptionTrialEndsSoonJob failed with: '.$e->getMessage());
throw $e;
}
}
}

View File

@@ -1,42 +0,0 @@
<?php
namespace App\Jobs;
use App\Models\Team;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class SubscriptionTrialEndsSoonJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(
public Team $team
) {}
public function handle(): void
{
try {
$session = getStripeCustomerPortalSession($this->team);
$mail = new MailMessage;
$mail->subject('You trial in Coolify Cloud ends soon.');
$mail->view('emails.trial-ends-soon', [
'stripeCustomerPortal' => $session->url,
]);
$this->team->members()->each(function ($member) use ($mail) {
if ($member->isAdmin()) {
send_user_an_email($mail, $member->email);
send_internal_notification('Trial reminder email sent to '.$member->email);
}
});
} catch (\Throwable $e) {
send_internal_notification('SubscriptionTrialEndsSoonJob failed with: '.$e->getMessage());
throw $e;
}
}
}

View File

@@ -2,8 +2,8 @@
namespace App\Livewire\Admin; namespace App\Livewire\Admin;
use App\Models\Team;
use App\Models\User; use App\Models\User;
use Illuminate\Container\Attributes\Auth as AttributesAuth;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
@@ -43,17 +43,13 @@ class Index extends Component
public function getSubscribers() public function getSubscribers()
{ {
$this->inactiveSubscribers = User::whereDoesntHave('teams', function ($query) { $this->inactiveSubscribers = Team::whereRelation('subscription', 'stripe_invoice_paid', false)->count();
$query->whereRelation('subscription', 'stripe_subscription_id', '!=', null); $this->activeSubscribers = Team::whereRelation('subscription', 'stripe_invoice_paid', true)->count();
})->count();
$this->activeSubscribers = User::whereHas('teams', function ($query) {
$query->whereRelation('subscription', 'stripe_subscription_id', '!=', null);
})->count();
} }
public function switchUser(int $user_id) public function switchUser(int $user_id)
{ {
if (AttributesAuth::id() !== 0) { if (Auth::id() !== 0) {
return redirect()->route('dashboard'); return redirect()->route('dashboard');
} }
$user = User::find($user_id); $user = User::find($user_id);

View File

@@ -66,11 +66,15 @@ class Index extends Component
public bool $serverReachable = true; public bool $serverReachable = true;
public ?string $minDockerVersion = null;
public function mount() public function mount()
{ {
if (auth()->user()?->isMember() && auth()->user()->currentTeam()->show_boarding === true) { if (auth()->user()?->isMember() && auth()->user()->currentTeam()->show_boarding === true) {
return redirect()->route('dashboard'); return redirect()->route('dashboard');
} }
$this->minDockerVersion = str(config('constants.docker.minimum_required_version'))->before('.');
$this->privateKeyName = generate_random_name(); $this->privateKeyName = generate_random_name();
$this->remoteServerName = generate_random_name(); $this->remoteServerName = generate_random_name();
if (isDev()) { if (isDev()) {

View File

@@ -4,7 +4,6 @@ namespace App\Livewire\Server;
use App\Actions\Proxy\CheckConfiguration; use App\Actions\Proxy\CheckConfiguration;
use App\Actions\Proxy\SaveConfiguration; use App\Actions\Proxy\SaveConfiguration;
use App\Actions\Proxy\StartProxy;
use App\Models\Server; use App\Models\Server;
use Livewire\Component; use Livewire\Component;
@@ -44,14 +43,13 @@ class Proxy extends Component
public function selectProxy($proxy_type) public function selectProxy($proxy_type)
{ {
$this->server->proxy->set('status', 'exited'); try {
$this->server->proxy->set('type', $proxy_type); $this->server->changeProxy($proxy_type, async: false);
$this->server->save();
$this->selectedProxy = $this->server->proxy->type; $this->selectedProxy = $this->server->proxy->type;
if ($this->server->proxySet()) {
StartProxy::run($this->server, false);
}
$this->dispatch('proxyStatusUpdated'); $this->dispatch('proxyStatusUpdated');
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
public function instantSave() public function instantSave()

View File

@@ -159,7 +159,8 @@ class ValidateAndInstall extends Component
$this->dispatch('refreshBoardingIndex'); $this->dispatch('refreshBoardingIndex');
$this->dispatch('success', 'Server validated.'); $this->dispatch('success', 'Server validated.');
} else { } else {
$this->error = 'Docker Engine version is not 22+. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://docs.docker.com/engine/install/#server">documentation</a>.'; $requiredDockerVersion = str(config('constants.docker.minimum_required_version'))->before('.');
$this->error = 'Minimum Docker Engine version '.$requiredDockerVersion.' is not instaled. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://docs.docker.com/engine/install/#server">documentation</a>.';
$this->server->update([ $this->server->update([
'validation_logs' => $this->error, 'validation_logs' => $this->error,
]); ]);

View File

@@ -14,13 +14,25 @@ class Index extends Component
public $containers = []; public $containers = [];
public bool $isLoadingContainers = true;
public function mount() public function mount()
{ {
if (! auth()->user()->isAdmin()) { if (! auth()->user()->isAdmin()) {
abort(403); abort(403);
} }
$this->servers = Server::isReachable()->get(); $this->servers = Server::isReachable()->get();
}
public function loadContainers()
{
try {
$this->containers = $this->getAllActiveContainers(); $this->containers = $this->getAllActiveContainers();
} catch (\Exception $e) {
return handleError($e, $this);
} finally {
$this->isLoadingContainers = false;
}
} }
private function getAllActiveContainers() private function getAllActiveContainers()

View File

@@ -906,21 +906,7 @@ class Application extends BaseModel
public function customRepository() public function customRepository()
{ {
preg_match('/(?<=:)\d+(?=\/)/', $this->git_repository, $matches); return convertGitUrl($this->git_repository, $this->deploymentType(), $this->source);
$port = 22;
if (count($matches) === 1) {
$port = $matches[0];
$gitHost = str($this->git_repository)->before(':');
$gitRepo = str($this->git_repository)->after('/');
$repository = "$gitHost:$gitRepo";
} else {
$repository = $this->git_repository;
}
return [
'repository' => $repository,
'port' => $port,
];
} }
public function generateBaseDir(string $uuid) public function generateBaseDir(string $uuid)
@@ -953,6 +939,122 @@ class Application extends BaseModel
return $git_clone_command; return $git_clone_command;
} }
public function getGitRemoteStatus(string $deployment_uuid)
{
try {
['commands' => $lsRemoteCommand] = $this->generateGitLsRemoteCommands(deployment_uuid: $deployment_uuid, exec_in_docker: false);
instant_remote_process([$lsRemoteCommand], $this->destination->server, true);
return [
'is_accessible' => true,
'error' => null,
];
} catch (\RuntimeException $ex) {
return [
'is_accessible' => false,
'error' => $ex->getMessage(),
];
}
}
public function generateGitLsRemoteCommands(string $deployment_uuid, bool $exec_in_docker = true)
{
$branch = $this->git_branch;
['repository' => $customRepository, 'port' => $customPort] = $this->customRepository();
$commands = collect([]);
$base_command = 'git ls-remote';
if ($this->deploymentType() === 'source') {
$source_html_url = data_get($this, 'source.html_url');
$url = parse_url(filter_var($source_html_url, FILTER_SANITIZE_URL));
$source_html_url_host = $url['host'];
$source_html_url_scheme = $url['scheme'];
if ($this->source->getMorphClass() == 'App\Models\GithubApp') {
if ($this->source->is_public) {
$fullRepoUrl = "{$this->source->html_url}/{$customRepository}";
$base_command = "{$base_command} {$this->source->html_url}/{$customRepository}";
} else {
$github_access_token = generate_github_installation_token($this->source);
if ($exec_in_docker) {
$base_command = "{$base_command} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}.git";
$fullRepoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}.git";
} else {
$base_command = "{$base_command} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}";
$fullRepoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}";
}
}
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, $base_command));
} else {
$commands->push($base_command);
}
return [
'commands' => $commands->implode(' && '),
'branch' => $branch,
'fullRepoUrl' => $fullRepoUrl,
];
}
}
if ($this->deploymentType() === 'deploy_key') {
$fullRepoUrl = $customRepository;
$private_key = data_get($this, 'private_key.private_key');
if (is_null($private_key)) {
throw new RuntimeException('Private key not found. Please add a private key to the application and try again.');
}
$private_key = base64_encode($private_key);
$base_comamnd = "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\" {$base_command} {$customRepository}";
if ($exec_in_docker) {
$commands = collect([
executeInDocker($deployment_uuid, 'mkdir -p /root/.ssh'),
executeInDocker($deployment_uuid, "echo '{$private_key}' | base64 -d | tee /root/.ssh/id_rsa > /dev/null"),
executeInDocker($deployment_uuid, 'chmod 600 /root/.ssh/id_rsa'),
]);
} else {
$commands = collect([
'mkdir -p /root/.ssh',
"echo '{$private_key}' | base64 -d | tee /root/.ssh/id_rsa > /dev/null",
'chmod 600 /root/.ssh/id_rsa',
]);
}
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, $base_comamnd));
} else {
$commands->push($base_comamnd);
}
return [
'commands' => $commands->implode(' && '),
'branch' => $branch,
'fullRepoUrl' => $fullRepoUrl,
];
}
if ($this->deploymentType() === 'other') {
$fullRepoUrl = $customRepository;
$base_command = "{$base_command} {$customRepository}";
$base_command = $this->setGitImportSettings($deployment_uuid, $base_command, public: true);
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, $base_command));
} else {
$commands->push($base_command);
}
return [
'commands' => $commands->implode(' && '),
'branch' => $branch,
'fullRepoUrl' => $fullRepoUrl,
];
}
}
public function generateGitImportCommands(string $deployment_uuid, int $pull_request_id = 0, ?string $git_type = null, bool $exec_in_docker = true, bool $only_checkout = false, ?string $custom_base_dir = null, ?string $commit = null) public function generateGitImportCommands(string $deployment_uuid, int $pull_request_id = 0, ?string $git_type = null, bool $exec_in_docker = true, bool $only_checkout = false, ?string $custom_base_dir = null, ?string $commit = null)
{ {
$branch = $this->git_branch; $branch = $this->git_branch;
@@ -1214,6 +1316,11 @@ class Application extends BaseModel
$workdir = rtrim($this->base_directory, '/'); $workdir = rtrim($this->base_directory, '/');
$composeFile = $this->docker_compose_location; $composeFile = $this->docker_compose_location;
$fileList = collect([".$workdir$composeFile"]); $fileList = collect([".$workdir$composeFile"]);
$gitRemoteStatus = $this->getGitRemoteStatus(deployment_uuid: $uuid);
if (! $gitRemoteStatus['is_accessible']) {
throw new \RuntimeException("Failed to read Git source:\n\n{$gitRemoteStatus['error']}");
}
$commands = collect([ $commands = collect([
"rm -rf /tmp/{$uuid}", "rm -rf /tmp/{$uuid}",
"mkdir -p /tmp/{$uuid}", "mkdir -p /tmp/{$uuid}",

View File

@@ -2,6 +2,7 @@
namespace App\Models; namespace App\Models;
use App\Actions\Proxy\StartProxy;
use App\Actions\Server\InstallDocker; use App\Actions\Server\InstallDocker;
use App\Actions\Server\StartSentinel; use App\Actions\Server\StartSentinel;
use App\Enums\ProxyTypes; use App\Enums\ProxyTypes;
@@ -26,22 +27,23 @@ use Symfony\Component\Yaml\Yaml;
description: 'Server model', description: 'Server model',
type: 'object', type: 'object',
properties: [ properties: [
'id' => ['type' => 'integer'], 'id' => ['type' => 'integer', 'description' => 'The server ID.'],
'uuid' => ['type' => 'string'], 'uuid' => ['type' => 'string', 'description' => 'The server UUID.'],
'name' => ['type' => 'string'], 'name' => ['type' => 'string', 'description' => 'The server name.'],
'description' => ['type' => 'string'], 'description' => ['type' => 'string', 'description' => 'The server description.'],
'ip' => ['type' => 'string'], 'ip' => ['type' => 'string', 'description' => 'The IP address.'],
'user' => ['type' => 'string'], 'user' => ['type' => 'string', 'description' => 'The user.'],
'port' => ['type' => 'integer'], 'port' => ['type' => 'integer', 'description' => 'The port number.'],
'proxy' => ['type' => 'object'], 'proxy' => ['type' => 'object', 'description' => 'The proxy configuration.'],
'high_disk_usage_notification_sent' => ['type' => 'boolean'], 'proxy_type' => ['type' => 'string', 'enum' => ['traefik', 'caddy', 'none'], 'description' => 'The proxy type.'],
'unreachable_notification_sent' => ['type' => 'boolean'], 'high_disk_usage_notification_sent' => ['type' => 'boolean', 'description' => 'The flag to indicate if the high disk usage notification has been sent.'],
'unreachable_count' => ['type' => 'integer'], 'unreachable_notification_sent' => ['type' => 'boolean', 'description' => 'The flag to indicate if the unreachable notification has been sent.'],
'validation_logs' => ['type' => 'string'], 'unreachable_count' => ['type' => 'integer', 'description' => 'The unreachable count for your server.'],
'log_drain_notification_sent' => ['type' => 'boolean'], 'validation_logs' => ['type' => 'string', 'description' => 'The validation logs.'],
'swarm_cluster' => ['type' => 'string'], 'log_drain_notification_sent' => ['type' => 'boolean', 'description' => 'The flag to indicate if the log drain notification has been sent.'],
'delete_unused_volumes' => ['type' => 'boolean'], 'swarm_cluster' => ['type' => 'string', 'description' => 'The swarm cluster configuration.'],
'delete_unused_networks' => ['type' => 'boolean'], 'delete_unused_volumes' => ['type' => 'boolean', 'description' => 'The flag to indicate if the unused volumes should be deleted.'],
'delete_unused_networks' => ['type' => 'boolean', 'description' => 'The flag to indicate if the unused networks should be deleted.'],
] ]
)] )]
@@ -1251,4 +1253,25 @@ $schema://$host {
{ {
return instant_remote_process(['docker restart '.$containerName], $this, false); return instant_remote_process(['docker restart '.$containerName], $this, false);
} }
public function changeProxy(string $proxyType, bool $async = true)
{
$validProxyTypes = collect(ProxyTypes::cases())->map(function ($proxyType) {
return str($proxyType->value)->lower();
});
if ($validProxyTypes->contains(str($proxyType)->lower())) {
$this->proxy->set('type', str($proxyType)->upper());
$this->proxy->set('status', 'exited');
$this->save();
if ($this->proxySet()) {
if ($async) {
StartProxy::dispatch($this);
} else {
StartProxy::run($this);
}
}
} else {
throw new \Exception('Invalid proxy type.');
}
}
} }

View File

@@ -257,8 +257,15 @@ class Team extends Model implements SendsDiscord, SendsEmail
return $this->hasMany(S3Storage::class)->where('is_usable', true); return $this->hasMany(S3Storage::class)->where('is_usable', true);
} }
public function trialEnded() public function subscriptionEnded()
{ {
$this->subscription->update([
'stripe_subscription_id' => null,
'stripe_plan_id' => null,
'stripe_cancel_at_period_end' => false,
'stripe_invoice_paid' => false,
'stripe_trial_already_ended' => false,
]);
foreach ($this->servers as $server) { foreach ($this->servers as $server) {
$server->settings()->update([ $server->settings()->update([
'is_usable' => false, 'is_usable' => false,
@@ -267,16 +274,6 @@ class Team extends Model implements SendsDiscord, SendsEmail
} }
} }
public function trialEndedButSubscribed()
{
foreach ($this->servers as $server) {
$server->settings()->update([
'is_usable' => true,
'is_reachable' => true,
]);
}
}
public function isAnyNotificationEnabled() public function isAnyNotificationEnabled()
{ {
if (isCloud()) { if (isCloud()) {

View File

@@ -109,7 +109,8 @@ function format_docker_envs_to_json($rawOutput)
function checkMinimumDockerEngineVersion($dockerVersion) function checkMinimumDockerEngineVersion($dockerVersion)
{ {
$majorDockerVersion = str($dockerVersion)->before('.')->value(); $majorDockerVersion = str($dockerVersion)->before('.')->value();
if ($majorDockerVersion <= 22) { $requiredDockerVersion = str(config('constants.docker.minimum_required_version'))->before('.')->value();
if ($majorDockerVersion < $requiredDockerVersion) {
$dockerVersion = null; $dockerVersion = null;
} }
@@ -225,15 +226,13 @@ function generateServiceSpecificFqdns(ServiceApplication|Application $resource)
case $type?->contains('minio'): case $type?->contains('minio'):
$MINIO_BROWSER_REDIRECT_URL = $variables->where('key', 'MINIO_BROWSER_REDIRECT_URL')->first(); $MINIO_BROWSER_REDIRECT_URL = $variables->where('key', 'MINIO_BROWSER_REDIRECT_URL')->first();
$MINIO_SERVER_URL = $variables->where('key', 'MINIO_SERVER_URL')->first(); $MINIO_SERVER_URL = $variables->where('key', 'MINIO_SERVER_URL')->first();
if (is_null($MINIO_BROWSER_REDIRECT_URL) || is_null($MINIO_SERVER_URL)) {
return $payload; if (str($MINIO_BROWSER_REDIRECT_URL->value)->isEmpty()) {
}
if (is_null($MINIO_BROWSER_REDIRECT_URL?->value)) {
$MINIO_BROWSER_REDIRECT_URL?->update([ $MINIO_BROWSER_REDIRECT_URL?->update([
'value' => generateFqdn($server, 'console-'.$uuid, true), 'value' => generateFqdn($server, 'console-'.$uuid, true),
]); ]);
} }
if (is_null($MINIO_SERVER_URL?->value)) { if (str($MINIO_SERVER_URL->value)->isEmpty()) {
$MINIO_SERVER_URL?->update([ $MINIO_SERVER_URL?->update([
'value' => generateFqdn($server, 'minio-'.$uuid, true), 'value' => generateFqdn($server, 'minio-'.$uuid, true),
]); ]);
@@ -246,15 +245,13 @@ function generateServiceSpecificFqdns(ServiceApplication|Application $resource)
case $type?->contains('logto'): case $type?->contains('logto'):
$LOGTO_ENDPOINT = $variables->where('key', 'LOGTO_ENDPOINT')->first(); $LOGTO_ENDPOINT = $variables->where('key', 'LOGTO_ENDPOINT')->first();
$LOGTO_ADMIN_ENDPOINT = $variables->where('key', 'LOGTO_ADMIN_ENDPOINT')->first(); $LOGTO_ADMIN_ENDPOINT = $variables->where('key', 'LOGTO_ADMIN_ENDPOINT')->first();
if (is_null($LOGTO_ENDPOINT) || is_null($LOGTO_ADMIN_ENDPOINT)) {
return $payload; if (str($LOGTO_ENDPOINT?->value)->isEmpty()) {
}
if (is_null($LOGTO_ENDPOINT?->value)) {
$LOGTO_ENDPOINT?->update([ $LOGTO_ENDPOINT?->update([
'value' => generateFqdn($server, 'logto-'.$uuid), 'value' => generateFqdn($server, 'logto-'.$uuid),
]); ]);
} }
if (is_null($LOGTO_ADMIN_ENDPOINT?->value)) { if (str($LOGTO_ADMIN_ENDPOINT?->value)->isEmpty()) {
$LOGTO_ADMIN_ENDPOINT?->update([ $LOGTO_ADMIN_ENDPOINT?->update([
'value' => generateFqdn($server, 'logto-admin-'.$uuid), 'value' => generateFqdn($server, 'logto-admin-'.$uuid),
]); ]);

View File

@@ -7,6 +7,7 @@ use App\Models\Application;
use App\Models\ApplicationDeploymentQueue; use App\Models\ApplicationDeploymentQueue;
use App\Models\ApplicationPreview; use App\Models\ApplicationPreview;
use App\Models\EnvironmentVariable; use App\Models\EnvironmentVariable;
use App\Models\GithubApp;
use App\Models\InstanceSettings; use App\Models\InstanceSettings;
use App\Models\LocalFileVolume; use App\Models\LocalFileVolume;
use App\Models\LocalPersistentVolume; use App\Models\LocalPersistentVolume;
@@ -4092,3 +4093,53 @@ function defaultNginxConfiguration(): string
} }
}'; }';
} }
function convertGitUrl(string $gitRepository, string $deploymentType, ?GithubApp $source = null): array
{
$repository = $gitRepository;
$providerInfo = [
'host' => null,
'user' => 'git',
'port' => 22,
'repository' => $gitRepository,
];
$sshMatches = [];
$matches = [];
// Let's try and parse the string to detect if it's a valid SSH string or not
preg_match('/((.*?)\:\/\/)?(.*@.*:.*)/', $gitRepository, $sshMatches);
if ($deploymentType === 'deploy_key' && empty($sshMatches) && $source) {
// If this happens, the user may have provided an HTTP URL when they needed an SSH one
// Let's try and fix that for known Git providers
switch ($source->getMorphClass()) {
case \App\Models\GithubApp::class:
$providerInfo['host'] = Url::fromString($source->html_url)->getHost();
$providerInfo['port'] = $source->custom_port;
$providerInfo['user'] = $source->custom_user;
break;
}
if (! empty($providerInfo['host'])) {
// Until we do not support more providers with App (like GithubApp), this will be always true, port will be 22
if ($providerInfo['port'] === 22) {
$repository = "{$providerInfo['user']}@{$providerInfo['host']}:{$providerInfo['repository']}";
} else {
$repository = "ssh://{$providerInfo['user']}@{$providerInfo['host']}:{$providerInfo['port']}/{$providerInfo['repository']}";
}
}
}
preg_match('/(?<=:)\d+(?=\/)/', $gitRepository, $matches);
if (count($matches) === 1) {
$providerInfo['port'] = $matches[0];
$gitHost = str($gitRepository)->before(':');
$gitRepo = str($gitRepository)->after('/');
$repository = "$gitHost:$gitRepo";
}
return [
'repository' => $repository,
'port' => $providerInfo['port'],
];
}

View File

@@ -12,6 +12,7 @@
], ],
"require": { "require": {
"php": "^8.2", "php": "^8.2",
"3sidedcube/laravel-redoc": "^1.0",
"danharrin/livewire-rate-limiting": "^1.1", "danharrin/livewire-rate-limiting": "^1.1",
"doctrine/dbal": "^3.6", "doctrine/dbal": "^3.6",
"guzzlehttp/guzzle": "^7.5.0", "guzzlehttp/guzzle": "^7.5.0",

60
composer.lock generated
View File

@@ -4,8 +4,66 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "3f2342fe6b1ba920c8875f8a8fe41962", "content-hash": "b9f4772191b4680e6f92fa9c7c396b10",
"packages": [ "packages": [
{
"name": "3sidedcube/laravel-redoc",
"version": "v1.0.1",
"source": {
"type": "git",
"url": "https://github.com/3sidedcube/laravel-redoc.git",
"reference": "c33a563885dcdf1e0f623df5a56c106d130261da"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/3sidedcube/laravel-redoc/zipball/c33a563885dcdf1e0f623df5a56c106d130261da",
"reference": "c33a563885dcdf1e0f623df5a56c106d130261da",
"shasum": ""
},
"require": {
"illuminate/routing": "^8.0|^9.0|^10.0|^11.0",
"illuminate/support": "^8.0|^9.0|^10.0|^11.0",
"php": "^7.4|^8.0|^8.1|^8.2|^8.3"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.3",
"orchestra/testbench": "^6.0|^7.0|^8.0|^9.0"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"ThreeSidedCube\\LaravelRedoc\\RedocServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"ThreeSidedCube\\LaravelRedoc\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ben Sherred",
"role": "Developer"
}
],
"description": "A lightweight package for rendering API documentation using OpenAPI and Redoc.",
"homepage": "https://github.com/3sidedcube/laravel-redoc",
"keywords": [
"3sidedcube",
"laravel-redoc"
],
"support": {
"issues": "https://github.com/3sidedcube/laravel-redoc/issues",
"source": "https://github.com/3sidedcube/laravel-redoc/tree/v1.0.1"
},
"time": "2024-05-20T11:37:55+00:00"
},
{ {
"name": "amphp/amp", "name": "amphp/amp",
"version": "v3.0.2", "version": "v3.0.2",

View File

@@ -1,7 +1,6 @@
<?php <?php
return [ return [
'docker_install_version' => '26.0',
'docs' => [ 'docs' => [
'base_url' => 'https://coolify.io/docs', 'base_url' => 'https://coolify.io/docs',
'contact' => 'https://coolify.io/docs/contact', 'contact' => 'https://coolify.io/docs/contact',
@@ -13,6 +12,9 @@ return [
'server_interval' => 20, 'server_interval' => 20,
'command_timeout' => 7200, 'command_timeout' => 7200,
], ],
'docker' => [
'minimum_required_version' => '26.0',
],
'waitlist' => [ 'waitlist' => [
'expiration' => 10, 'expiration' => 10,
], ],

28
config/redoc.php Normal file
View File

@@ -0,0 +1,28 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Directory
|--------------------------------------------------------------------------
|
| The name of the directory where your OpenAPI definitions are stored.
|
*/
'directory' => '',
/*
|--------------------------------------------------------------------------
| Variables
|--------------------------------------------------------------------------
|
| You can automatically replace variables in your OpenAPI definitions by
| adding a key value pair to the array below. This will replace any
| instances of :key with the given value.
|
*/
'variables' => [],
];

View File

@@ -7,7 +7,7 @@ return [
// The release version of your application // The release version of your application
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) // Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
'release' => '4.0.0-beta.367', 'release' => '4.0.0-beta.368',
// When left empty or `null` the Laravel environment will be used // When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'), 'environment' => config('app.env'),

View File

@@ -1,3 +1,3 @@
<?php <?php
return '4.0.0-beta.367'; return '4.0.0-beta.368';

7985
openapi.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -3311,7 +3311,7 @@ paths:
type: string type: string
responses: responses:
'200': '200':
description: 'Project details' description: 'Environment details'
content: content:
application/json: application/json:
schema: schema:
@@ -3467,8 +3467,6 @@ paths:
content: content:
application/json: application/json:
schema: schema:
type: array
items:
$ref: '#/components/schemas/PrivateKey' $ref: '#/components/schemas/PrivateKey'
'401': '401':
$ref: '#/components/responses/401' $ref: '#/components/responses/401'
@@ -3579,6 +3577,11 @@ paths:
type: boolean type: boolean
example: false example: false
description: 'Instant validate.' description: 'Instant validate.'
proxy_type:
type: string
enum: [traefik, caddy, none]
example: traefik
description: 'The proxy type.'
type: object type: object
responses: responses:
'201': '201':
@@ -3699,6 +3702,10 @@ paths:
instant_validate: instant_validate:
type: boolean type: boolean
description: 'Instant validate.' description: 'Instant validate.'
proxy_type:
type: string
enum: [traefik, caddy, none]
description: 'The proxy type.'
type: object type: object
responses: responses:
'201': '201':
@@ -4759,6 +4766,10 @@ components:
compose_parsing_version: compose_parsing_version:
type: string type: string
description: 'How Coolify parse the compose file.' description: 'How Coolify parse the compose file.'
custom_nginx_configuration:
type: string
nullable: true
description: 'Custom Nginx configuration base64 encoded.'
type: object type: object
ApplicationDeploymentQueue: ApplicationDeploymentQueue:
description: 'Project model' description: 'Project model'
@@ -4909,36 +4920,59 @@ components:
properties: properties:
id: id:
type: integer type: integer
description: 'The server ID.'
uuid: uuid:
type: string type: string
description: 'The server UUID.'
name: name:
type: string type: string
description: 'The server name.'
description: description:
type: string type: string
description: 'The server description.'
ip: ip:
type: string type: string
description: 'The IP address.'
user: user:
type: string type: string
description: 'The user.'
port: port:
type: integer type: integer
description: 'The port number.'
proxy: proxy:
type: object type: object
description: 'The proxy configuration.'
proxy_type:
type: string
enum:
- traefik
- caddy
- none
description: 'The proxy type.'
high_disk_usage_notification_sent: high_disk_usage_notification_sent:
type: boolean type: boolean
description: 'The flag to indicate if the high disk usage notification has been sent.'
unreachable_notification_sent: unreachable_notification_sent:
type: boolean type: boolean
description: 'The flag to indicate if the unreachable notification has been sent.'
unreachable_count: unreachable_count:
type: integer type: integer
description: 'The unreachable count for your server.'
validation_logs: validation_logs:
type: string type: string
description: 'The validation logs.'
log_drain_notification_sent: log_drain_notification_sent:
type: boolean type: boolean
description: 'The flag to indicate if the log drain notification has been sent.'
swarm_cluster: swarm_cluster:
type: string type: string
description: 'The swarm cluster configuration.'
delete_unused_volumes: delete_unused_volumes:
type: boolean type: boolean
description: 'The flag to indicate if the unused volumes should be deleted.'
delete_unused_networks: delete_unused_networks:
type: boolean type: boolean
description: 'The flag to indicate if the unused networks should be deleted.'
type: object type: object
ServerSetting: ServerSetting:
description: 'Server Settings model' description: 'Server Settings model'
@@ -5136,6 +5170,9 @@ components:
smtp_notifications_database_backups: smtp_notifications_database_backups:
type: boolean type: boolean
description: 'Whether to send database backup notifications via SMTP.' description: 'Whether to send database backup notifications via SMTP.'
smtp_notifications_server_disk_usage:
type: boolean
description: 'Whether to send server disk usage notifications via SMTP.'
discord_enabled: discord_enabled:
type: boolean type: boolean
description: 'Whether Discord is enabled or not.' description: 'Whether Discord is enabled or not.'
@@ -5157,6 +5194,9 @@ components:
discord_notifications_scheduled_tasks: discord_notifications_scheduled_tasks:
type: boolean type: boolean
description: 'Whether to send scheduled task notifications via Discord.' description: 'Whether to send scheduled task notifications via Discord.'
discord_notifications_server_disk_usage:
type: boolean
description: 'Whether to send server disk usage notifications via Discord.'
show_boarding: show_boarding:
type: boolean type: boolean
description: 'Whether to show the boarding screen or not.' description: 'Whether to show the boarding screen or not.'

View File

@@ -2,7 +2,7 @@
<div class="flex flex-col items-center justify-center h-full"> <div class="flex flex-col items-center justify-center h-full">
<div> <div>
<p class="font-mono font-semibold text-red-500 text-7xl">500</p> <p class="font-mono font-semibold text-red-500 text-7xl">500</p>
<h1 class="mt-4 font-bold tracking-tight dark:text-white">Something is not okay, are you okay?</h1> <h1 class="mt-4 font-bold tracking-tight dark:text-white">Wait, this is not cool...</h1>
<p class="text-base leading-7 text-neutral-300">There has been an error, we are working on it. <p class="text-base leading-7 text-neutral-300">There has been an error, we are working on it.
</p> </p>
@if ($exception->getMessage() !== '') @if ($exception->getMessage() !== '')

View File

@@ -12,7 +12,7 @@
@if ($foundUsers->count() > 0) @if ($foundUsers->count() > 0)
<div class="flex flex-wrap gap-2 pt-4"> <div class="flex flex-wrap gap-2 pt-4">
@foreach ($foundUsers as $user) @foreach ($foundUsers as $user)
<div class="box w-64 group"> <div class="box w-64 group" wire:click="switchUser({{ $user->id }})">
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<div class="box-title">{{ $user->name }}</div> <div class="box-title">{{ $user->name }}</div>
<div class="box-description">{{ $user->email }}</div> <div class="box-description">{{ $user->email }}</div>

View File

@@ -323,7 +323,7 @@
</x-slot:actions> </x-slot:actions>
<x-slot:explanation> <x-slot:explanation>
<p>This will install the latest Docker Engine on your server, configure a few things to be able <p>This will install the latest Docker Engine on your server, configure a few things to be able
to run optimal.<br><br>Minimum Docker Engine version is: 22<br><br>To manually install to run optimal.<br><br>Minimum Docker Engine version is: {{ $minDockerVersion }}<br><br>To manually install
Docker Docker
Engine, check <a target="_blank" class="underline dark:text-warning" Engine, check <a target="_blank" class="underline dark:text-warning"
href="https://docs.docker.com/engine/install/#server">this href="https://docs.docker.com/engine/install/#server">this

View File

@@ -49,12 +49,103 @@
@else @else
<div x-data="searchComponent()"> <div x-data="searchComponent()">
<x-forms.input placeholder="Search for name, fqdn..." x-model="search" id="null" /> <x-forms.input placeholder="Search for name, fqdn..." x-model="search" id="null" />
<div class="grid grid-cols-1 gap-4 pt-4 lg:grid-cols-2 xl:grid-cols-3"> <template
<template x-if="allFilteredItems.length === 0"> x-if="filteredApplications.length === 0 && filteredDatabases.length === 0 && filteredServices.length === 0">
<div>No resource found with the search term "<span x-text="search"></span>".</div> <div>No resource found with the search term "<span x-text="search"></span>".</div>
</template> </template>
<template x-for="item in allFilteredItems" :key="item.uuid"> <template x-if="filteredApplications.length > 0">
<h2 class="pt-4">Applications</h2>
</template>
<div x-show="filteredApplications.length > 0"
class="grid grid-cols-1 gap-4 pt-4 lg:grid-cols-2 xl:grid-cols-3">
<template x-for="item in filteredApplications" :key="item.uuid">
<span>
<a class="h-24 box group" :href="item.hrefLink">
<div class="flex flex-col w-full">
<div class="flex gap-2 px-4">
<div class="pb-2 truncate box-title" x-text="item.name"></div>
<div class="flex-1"></div>
<template x-if="item.status.startsWith('running')">
<div title="running" class="bg-success badge badge-absolute"></div>
</template>
<template x-if="item.status.startsWith('exited')">
<div title="exited" class="bg-error badge badge-absolute"></div>
</template>
<template x-if="item.status.startsWith('restarting')">
<div title="restarting" class="bg-warning badge badge-absolute"></div>
</template>
<template x-if="item.status.startsWith('degraded')">
<div title="degraded" class="bg-warning badge badge-absolute"></div>
</template>
</div>
<div class="max-w-full px-4 truncate box-description" x-text="item.description"></div>
<div class="max-w-full px-4 truncate box-description" x-text="item.fqdn"></div>
<template x-if="item.server_status == false">
<div class="px-4 text-xs font-bold text-error">The underlying server has problems
</div>
</template>
</div>
</a>
<div
class="flex flex-wrap gap-1 pt-1 group-hover:dark:text-white group-hover:text-black group min-h-6">
<template x-for="tag in item.tags">
<div class="tag" @click.prevent="gotoTag(tag.name)" x-text="tag.name"></div>
</template>
<div class="add-tag" @click.prevent="goto(item)">Add tag</div>
</div>
</span>
</template>
</div>
<template x-if="filteredDatabases.length > 0">
<h2 class="pt-4">Databases</h2>
</template>
<div x-show="filteredDatabases.length > 0"
class="grid grid-cols-1 gap-4 pt-4 lg:grid-cols-2 xl:grid-cols-3">
<template x-for="item in filteredDatabases" :key="item.uuid">
<span>
<a class="h-24 box group" :href="item.hrefLink">
<div class="flex flex-col w-full">
<div class="flex gap-2 px-4">
<div class="pb-2 truncate box-title" x-text="item.name"></div>
<div class="flex-1"></div>
<template x-if="item.status.startsWith('running')">
<div title="running" class="bg-success badge badge-absolute"></div>
</template>
<template x-if="item.status.startsWith('exited')">
<div title="exited" class="bg-error badge badge-absolute"></div>
</template>
<template x-if="item.status.startsWith('restarting')">
<div title="restarting" class="bg-warning badge badge-absolute"></div>
</template>
<template x-if="item.status.startsWith('degraded')">
<div title="degraded" class="bg-warning badge badge-absolute"></div>
</template>
</div>
<div class="max-w-full px-4 truncate box-description" x-text="item.description"></div>
<div class="max-w-full px-4 truncate box-description" x-text="item.fqdn"></div>
<template x-if="item.server_status == false">
<div class="px-4 text-xs font-bold text-error">The underlying server has problems
</div>
</template>
</div>
</a>
<div
class="flex flex-wrap gap-1 pt-1 group-hover:dark:text-white group-hover:text-black group min-h-6">
<template x-for="tag in item.tags">
<div class="tag" @click.prevent="gotoTag(tag.name)" x-text="tag.name"></div>
</template>
<div class="add-tag" @click.prevent="goto(item)">Add tag</div>
</div>
</span>
</template>
</div>
<template x-if="filteredServices.length > 0">
<h2 class="pt-4">Services</h2>
</template>
<div x-show="filteredServices.length > 0"
class="grid grid-cols-1 gap-4 pt-4 lg:grid-cols-2 xl:grid-cols-3">
<template x-for="item in filteredServices" :key="item.uuid">
<span> <span>
<a class="h-24 box group" :href="item.hrefLink"> <a class="h-24 box group" :href="item.hrefLink">
<div class="flex flex-col w-full"> <div class="flex flex-col w-full">
@@ -134,9 +225,11 @@
item.tags?.some(tag => tag.name.toLowerCase().includes(searchLower))); item.tags?.some(tag => tag.name.toLowerCase().includes(searchLower)));
}).sort(sortFn); }).sort(sortFn);
}, },
get allFilteredItems() { get filteredApplications() {
return this.filterAndSort(this.applications)
},
get filteredDatabases() {
return [ return [
this.applications,
this.postgresqls, this.postgresqls,
this.redis, this.redis,
this.mongodbs, this.mongodbs,
@@ -145,8 +238,10 @@
this.keydbs, this.keydbs,
this.dragonflies, this.dragonflies,
this.clickhouses, this.clickhouses,
this.services
].flatMap((items) => this.filterAndSort(items)) ].flatMap((items) => this.filterAndSort(items))
},
get filteredServices() {
return this.filterAndSort(this.services)
} }
}; };
} }

View File

@@ -86,6 +86,7 @@
</g> </g>
</svg></div> </svg></div>
@isset($docker_version) @isset($docker_version)
@if($docker_version)
<div class="flex w-64 gap-2">Minimum Docker version: <svg class="w-5 h-5 text-success" <div class="flex w-64 gap-2">Minimum Docker version: <svg class="w-5 h-5 text-success"
viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg"> viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
<g fill="currentColor"> <g fill="currentColor">
@@ -96,6 +97,13 @@
d="m243.28 68.24l-24-23.56a16 16 0 0 0-22.58 0L104 136l-.11-.11l-36.64-35.27a16 16 0 0 0-22.57.06l-24 24a16 16 0 0 0 0 22.61l71.62 72a16 16 0 0 0 22.63 0l128.4-128.38a16 16 0 0 0-.05-22.67M103.62 208L32 136l24-24l.11.11l36.64 35.27a16 16 0 0 0 22.52 0L208.06 56L232 79.6Z" /> d="m243.28 68.24l-24-23.56a16 16 0 0 0-22.58 0L104 136l-.11-.11l-36.64-35.27a16 16 0 0 0-22.57.06l-24 24a16 16 0 0 0 0 22.61l71.62 72a16 16 0 0 0 22.63 0l128.4-128.38a16 16 0 0 0-.05-22.67M103.62 208L32 136l24-24l.11.11l36.64 35.27a16 16 0 0 0 22.52 0L208.06 56L232 79.6Z" />
</g> </g>
</svg></div> </svg></div>
@else
<div class="flex w-64 gap-2">Minimum Docker version: <svg class="w-5 h-5 text-error"
viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor"
d="M208.49 191.51a12 12 0 0 1-17 17L128 145l-63.51 63.49a12 12 0 0 1-17-17L111 128L47.51 64.49a12 12 0 0 1 17-17L128 111l63.51-63.52a12 12 0 0 1 17 17L145 128Z" />
</svg></div>
@endif
@else @else
<div class="w-64"><x-loading text="Minimum Docker version:" /></div> <div class="w-64"><x-loading text="Minimum Docker version:" /></div>
@endisset @endisset

View File

@@ -68,7 +68,7 @@
</div> </div>
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2 md:flex-row flex-col w-full">
<x-forms.input id="public_ipv4" type="password" label="Instance's IPv4" <x-forms.input id="public_ipv4" type="password" label="Instance's IPv4"
helper="Enter the IPv4 address of the instance.<br><br>It is useful if you have several IPv4 addresses and Coolify could not detect the correct one." helper="Enter the IPv4 address of the instance.<br><br>It is useful if you have several IPv4 addresses and Coolify could not detect the correct one."
placeholder="1.2.3.4" /> placeholder="1.2.3.4" />
@@ -92,6 +92,8 @@
</div> </div>
<h4 class="pt-6">API</h4> <h4 class="pt-6">API</h4>
<div class="pb-4">For API documentation, please visit <a class="dark:text-warning underline"
href="/docs/api">/docs/api</a></div>
<div class="md:w-96 pb-2"> <div class="md:w-96 pb-2">
<x-forms.checkbox instantSave id="is_api_enabled" label="Enabled" /> <x-forms.checkbox instantSave id="is_api_enabled" label="Enabled" />
</div> </div>
@@ -131,9 +133,11 @@
<h4 class="py-4">Confirmation Settings</h4> <h4 class="py-4">Confirmation Settings</h4>
<div x-data="{ open: false }" class="mb-32 md:w-[40rem]"> <div x-data="{ open: false }" class="mb-32 md:w-[40rem]">
<button type="button" @click.prevent="open = !open" class="flex items-center justify-between w-full p-4 bg-coolgray-100 hover:bg-coolgray-200 rounded-md"> <button type="button" @click.prevent="open = !open"
class="flex items-center justify-between w-full p-4 bg-coolgray-100 hover:bg-coolgray-200 rounded-md">
<span class="font-medium">Two-Step Confirmation Settings</span> <span class="font-medium">Two-Step Confirmation Settings</span>
<svg class="w-5 h-5 transition-transform" :class="{ 'rotate-180': open }" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-5 h-5 transition-transform" :class="{ 'rotate-180': open }" fill="none"
stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg> </svg>
</button> </button>
@@ -141,24 +145,28 @@
<div x-show="open" x-transition class="mt-4"> <div x-show="open" x-transition class="mt-4">
@if ($disable_two_step_confirmation) @if ($disable_two_step_confirmation)
<div class="md:w-96 pb-4"> <div class="md:w-96 pb-4">
<x-forms.checkbox instantSave id="disable_two_step_confirmation" label="Disable Two Step Confirmation" <x-forms.checkbox instantSave id="disable_two_step_confirmation"
label="Disable Two Step Confirmation"
helper="When disabled, you will not need to confirm actions with a text and user password. This significantly reduces security and may lead to accidental deletions or unwanted changes. Use with extreme caution, especially on production servers." /> helper="When disabled, you will not need to confirm actions with a text and user password. This significantly reduces security and may lead to accidental deletions or unwanted changes. Use with extreme caution, especially on production servers." />
</div> </div>
@else @else
<div class="md:w-96 pb-4"> <div class="md:w-96 pb-4">
<x-modal-confirmation title="Disable Two Step Confirmation?" <x-modal-confirmation title="Disable Two Step Confirmation?"
buttonTitle="Disable Two Step Confirmation" isErrorButton submitAction="toggleTwoStepConfirmation" buttonTitle="Disable Two Step Confirmation" isErrorButton
:actions="[ submitAction="toggleTwoStepConfirmation" :actions="[
'Tow Step confimation will be disabled globally.', 'Tow Step confimation will be disabled globally.',
'Disabling two step confirmation reduces security (as anyone can easily delete anything).', 'Disabling two step confirmation reduces security (as anyone can easily delete anything).',
'The risk of accidental actions will increase.', 'The risk of accidental actions will increase.',
]" confirmationText="DISABLE TWO STEP CONFIRMATION" ]"
confirmationText="DISABLE TWO STEP CONFIRMATION"
confirmationLabel="Please type the confirmation text to disable two step confirmation." confirmationLabel="Please type the confirmation text to disable two step confirmation."
shortConfirmationLabel="Confirmation text" step3ButtonText="Disable Two Step Confirmation" /> shortConfirmationLabel="Confirmation text"
step3ButtonText="Disable Two Step Confirmation" />
</div> </div>
<div class="w-full px-4 py-2 mb-4 text-white rounded-sm border-l-4 border-red-500 bg-error"> <div class="w-full px-4 py-2 mb-4 text-white rounded-sm border-l-4 border-red-500 bg-error">
<p class="font-bold">Warning!</p> <p class="font-bold">Warning!</p>
<p>Disabling two step confirmation reduces security (as anyone can easily delete anything) and increases <p>Disabling two step confirmation reduces security (as anyone can easily delete anything) and
increases
the risk of accidental actions. This is not recommended for production servers.</p> the risk of accidental actions. This is not recommended for production servers.</p>
</div> </div>
@endif @endif

View File

@@ -1,4 +1,6 @@
<div class="w-full"> <div class="w-full">
<div class="mb-4">For more details, please visit the <a class="underline dark:text-warning"
href="https://coolify.io/docs/knowledge-base/s3" target="_blank">Coolify Docs</a>.</div>
<form class="flex flex-col gap-2" wire:submit='submit'> <form class="flex flex-col gap-2" wire:submit='submit'>
<div class="flex gap-2"> <div class="flex gap-2">
<x-forms.input required label="Name" id="name" /> <x-forms.input required label="Name" id="name" />
@@ -7,14 +9,15 @@
<x-forms.input required type="url" label="Endpoint" wire:model.blur="endpoint" /> <x-forms.input required type="url" label="Endpoint" wire:model.blur="endpoint" />
<div class="flex gap-2"> <div class="flex gap-2">
<x-forms.input required label="Bucket" id="bucket" /> <x-forms.input required label="Bucket" id="bucket" />
<x-forms.input required label="Region" id="region" /> <x-forms.input required helper="Region only required for AWS. Leave it as-is for other providers."
label="Region" id="region" />
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<x-forms.input required type="password" label="Access Key" id="key" /> <x-forms.input required type="password" label="Access Key" id="key" />
<x-forms.input required type="password" label="Secret Key" id="secret" /> <x-forms.input required type="password" label="Secret Key" id="secret" />
</div> </div>
<x-forms.button type="submit"> <x-forms.button class="mt-4" type="submit">
Validate Connection & Continue Validate Connection & Continue
</x-forms.button> </x-forms.button>
</form> </form>

View File

@@ -8,7 +8,12 @@
<x-helper <x-helper
helper="If you're having trouble connecting to your server, make sure that the port is open.<br><br><a class='underline' href='https://coolify.io/docs/knowledge-base/server/firewall/#terminal' target='_blank'>Documentation</a>"></x-helper> helper="If you're having trouble connecting to your server, make sure that the port is open.<br><br><a class='underline' href='https://coolify.io/docs/knowledge-base/server/firewall/#terminal' target='_blank'>Documentation</a>"></x-helper>
</div> </div>
<div> <div x-init="$wire.loadContainers()">
@if ($isLoadingContainers)
<div class="pt-1">
<x-loading text="Loading servers and containers..." />
</div>
@else
<form class="flex flex-col gap-2 justify-center xl:items-end xl:flex-row" <form class="flex flex-col gap-2 justify-center xl:items-end xl:flex-row"
wire:submit="$dispatchSelf('connectToContainer')"> wire:submit="$dispatchSelf('connectToContainer')">
<x-forms.select id="server" required wire:model.live="selected_uuid"> <x-forms.select id="server" required wire:model.live="selected_uuid">
@@ -29,6 +34,6 @@
<x-forms.button type="submit">Connect</x-forms.button> <x-forms.button type="submit">Connect</x-forms.button>
</form> </form>
<livewire:project.shared.terminal /> <livewire:project.shared.terminal />
@endif
</div> </div>
</div> </div>

View File

@@ -4,6 +4,7 @@ use App\Http\Controllers\Controller;
use App\Http\Controllers\MagicController; use App\Http\Controllers\MagicController;
use App\Http\Controllers\OauthController; use App\Http\Controllers\OauthController;
use App\Http\Controllers\UploadController; use App\Http\Controllers\UploadController;
use App\Http\Middleware\ApiAllowed;
use App\Livewire\Admin\Index as AdminIndex; use App\Livewire\Admin\Index as AdminIndex;
use App\Livewire\Boarding\Index as BoardingIndex; use App\Livewire\Boarding\Index as BoardingIndex;
use App\Livewire\Dashboard; use App\Livewire\Dashboard;
@@ -72,13 +73,18 @@ use App\Livewire\Team\Member\Index as TeamMemberIndex;
use App\Livewire\Terminal\Index as TerminalIndex; use App\Livewire\Terminal\Index as TerminalIndex;
use App\Models\GitlabApp; use App\Models\GitlabApp;
use App\Models\ScheduledDatabaseBackupExecution; use App\Models\ScheduledDatabaseBackupExecution;
use App\Models\Server;
use App\Providers\RouteServiceProvider; use App\Providers\RouteServiceProvider;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Component\HttpFoundation\StreamedResponse;
use ThreeSidedCube\LaravelRedoc\Http\Controllers\DefinitionController;
use ThreeSidedCube\LaravelRedoc\Http\Controllers\DocumentationController;
Route::group(['middleware' => ['auth:sanctum', ApiAllowed::class]], function () {
Route::get('/docs/api', DocumentationController::class)->name('redoc.documentation');
Route::get('/docs/api/definition', DefinitionController::class)->name('redoc.definition');
});
if (isDev()) { if (isDev()) {
Route::get('/dev/compose', Compose::class)->name('dev.compose'); Route::get('/dev/compose', Compose::class)->name('dev.compose');
} }

View File

@@ -24,7 +24,7 @@ function logs {
docker exec -t coolify tail -f storage/logs/laravel.log docker exec -t coolify tail -f storage/logs/laravel.log
} }
function test { function test {
docker exec -t coolify php artisan test --testsuite=Feature docker exec -t coolify php artisan test --testsuite=Feature -p
} }
function sync:bunny { function sync:bunny {

View File

@@ -0,0 +1,62 @@
<?php
use App\Models\GithubApp;
test('convertGitUrlsForDeployKeyAndGithubAppAndHttpUrl', function () {
$githubApp = GithubApp::find(0);
$result = convertGitUrl('andrasbacsai/coolify-examples.git', 'deploy_key', $githubApp);
expect($result)->toBe([
'repository' => 'git@github.com:andrasbacsai/coolify-examples.git',
'port' => 22,
]);
});
test('convertGitUrlsForDeployKeyAndGithubAppAndSshUrl', function () {
$githubApp = GithubApp::find(0);
$result = convertGitUrl('git@github.com:andrasbacsai/coolify-examples.git', 'deploy_key', $githubApp);
expect($result)->toBe([
'repository' => 'git@github.com:andrasbacsai/coolify-examples.git',
'port' => 22,
]);
});
test('convertGitUrlsForDeployKeyAndHttpUrl', function () {
$result = convertGitUrl('andrasbacsai/coolify-examples.git', 'deploy_key', null);
expect($result)->toBe([
'repository' => 'andrasbacsai/coolify-examples.git',
'port' => 22,
]);
});
test('convertGitUrlsForDeployKeyAndSshUrl', function () {
$result = convertGitUrl('git@github.com:andrasbacsai/coolify-examples.git', 'deploy_key', null);
expect($result)->toBe([
'repository' => 'git@github.com:andrasbacsai/coolify-examples.git',
'port' => 22,
]);
});
test('convertGitUrlsForSourceAndSshUrl', function () {
$result = convertGitUrl('git@github.com:andrasbacsai/coolify-examples.git', 'source', null);
expect($result)->toBe([
'repository' => 'git@github.com:andrasbacsai/coolify-examples.git',
'port' => 22,
]);
});
test('convertGitUrlsForSourceAndHttpUrl', function () {
$result = convertGitUrl('andrasbacsai/coolify-examples.git', 'source', null);
expect($result)->toBe([
'repository' => 'andrasbacsai/coolify-examples.git',
'port' => 22,
]);
});
test('convertGitUrlsForSourceAndSshUrlWithCustomPort', function () {
$result = convertGitUrl('git@git.domain.com:766/group/project.git', 'source', null);
expect($result)->toBe([
'repository' => 'git@git.domain.com:group/project.git',
'port' => '766',
]);
});

View File

@@ -9,171 +9,171 @@ use App\Models\StandaloneDocker;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Symfony\Component\Yaml\Yaml; use Symfony\Component\Yaml\Yaml;
beforeEach(function () { // beforeEach(function () {
$this->applicationYaml = ' // $this->applicationYaml = '
version: "3.8" // version: "3.8"
services: // services:
app: // app:
image: nginx // image: nginx
environment: // environment:
SERVICE_FQDN_APP: /app // SERVICE_FQDN_APP: /app
APP_KEY: base64 // APP_KEY: base64
APP_DEBUG: "${APP_DEBUG:-false}" // APP_DEBUG: "${APP_DEBUG:-false}"
APP_URL: $SERVICE_FQDN_APP // APP_URL: $SERVICE_FQDN_APP
DB_URL: postgres://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@db:5432/postgres?schema=public // DB_URL: postgres://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@db:5432/postgres?schema=public
volumes: // volumes:
- "./nginx:/etc/nginx" // - "./nginx:/etc/nginx"
- "data:/var/www/html" // - "data:/var/www/html"
depends_on: // depends_on:
- db // - db
db: // db:
image: postgres // image: postgres
environment: // environment:
POSTGRES_USER: "${SERVICE_USER_POSTGRES}" // POSTGRES_USER: "${SERVICE_USER_POSTGRES}"
POSTGRES_PASSWORD: "${SERVICE_PASSWORD_POSTGRES}" // POSTGRES_PASSWORD: "${SERVICE_PASSWORD_POSTGRES}"
volumes: // volumes:
- "dbdata:/var/lib/postgresql/data" // - "dbdata:/var/lib/postgresql/data"
healthcheck: // healthcheck:
test: // test:
- CMD // - CMD
- pg_isready // - pg_isready
- "-U" // - "-U"
- "postgres" // - "postgres"
interval: 2s // interval: 2s
timeout: 10s // timeout: 10s
retries: 10 // retries: 10
depends_on: // depends_on:
app: // app:
condition: service_healthy // condition: service_healthy
networks: // networks:
default: // default:
name: something // name: something
external: true // external: true
noinet: // noinet:
driver: bridge // driver: bridge
internal: true'; // internal: true';
$this->applicationComposeFileString = Yaml::parse($this->applicationYaml); // $this->applicationComposeFileString = Yaml::parse($this->applicationYaml);
$this->application = Application::create([ // $this->application = Application::create([
'name' => 'Application for tests', // 'name' => 'Application for tests',
'docker_compose_domains' => json_encode([ // 'docker_compose_domains' => json_encode([
'app' => [ // 'app' => [
'domain' => 'http://bcoowoookw0co4cok4sgc4k8.127.0.0.1.sslip.io', // 'domain' => 'http://bcoowoookw0co4cok4sgc4k8.127.0.0.1.sslip.io',
], // ],
]), // ]),
'preview_url_template' => '{{pr_id}}.{{domain}}', // 'preview_url_template' => '{{pr_id}}.{{domain}}',
'uuid' => 'bcoowoookw0co4cok4sgc4k8s', // 'uuid' => 'bcoowoookw0co4cok4sgc4k8s',
'repository_project_id' => 603035348, // 'repository_project_id' => 603035348,
'git_repository' => 'coollabsio/coolify-examples', // 'git_repository' => 'coollabsio/coolify-examples',
'git_branch' => 'main', // 'git_branch' => 'main',
'base_directory' => '/docker-compose-test', // 'base_directory' => '/docker-compose-test',
'docker_compose_location' => 'docker-compose.yml', // 'docker_compose_location' => 'docker-compose.yml',
'docker_compose_raw' => $this->applicationYaml, // 'docker_compose_raw' => $this->applicationYaml,
'build_pack' => 'dockercompose', // 'build_pack' => 'dockercompose',
'ports_exposes' => '3000', // 'ports_exposes' => '3000',
'environment_id' => 1, // 'environment_id' => 1,
'destination_id' => 0, // 'destination_id' => 0,
'destination_type' => StandaloneDocker::class, // 'destination_type' => StandaloneDocker::class,
'source_id' => 1, // 'source_id' => 1,
'source_type' => GithubApp::class, // 'source_type' => GithubApp::class,
]); // ]);
$this->application->environment_variables_preview()->where('key', 'APP_DEBUG')->update(['value' => 'true']); // $this->application->environment_variables_preview()->where('key', 'APP_DEBUG')->update(['value' => 'true']);
$this->applicationPreview = ApplicationPreview::create([ // $this->applicationPreview = ApplicationPreview::create([
'git_type' => 'github', // 'git_type' => 'github',
'application_id' => $this->application->id, // 'application_id' => $this->application->id,
'pull_request_id' => 1, // 'pull_request_id' => 1,
'pull_request_html_url' => 'https://github.com/coollabsio/coolify-examples/pull/1', // 'pull_request_html_url' => 'https://github.com/coollabsio/coolify-examples/pull/1',
]); // ]);
$this->serviceYaml = ' // $this->serviceYaml = '
services: // services:
activepieces: // activepieces:
image: "ghcr.io/activepieces/activepieces:latest" // image: "ghcr.io/activepieces/activepieces:latest"
environment: // environment:
- SERVICE_FQDN_ACTIVEPIECES // - SERVICE_FQDN_ACTIVEPIECES
- AP_API_KEY=$SERVICE_PASSWORD_64_APIKEY // - AP_API_KEY=$SERVICE_PASSWORD_64_APIKEY
- AP_URL=$SERVICE_URL_ACTIVEPIECES // - AP_URL=$SERVICE_URL_ACTIVEPIECES
- AP_ENCRYPTION_KEY=$SERVICE_PASSWORD_ENCRYPTIONKEY // - AP_ENCRYPTION_KEY=$SERVICE_PASSWORD_ENCRYPTIONKEY
- AP_ENGINE_EXECUTABLE_PATH=dist/packages/engine/main.js // - AP_ENGINE_EXECUTABLE_PATH=dist/packages/engine/main.js
- AP_ENVIRONMENT=prod // - AP_ENVIRONMENT=prod
- AP_EXECUTION_MODE=${AP_EXECUTION_MODE} // - AP_EXECUTION_MODE=${AP_EXECUTION_MODE}
- AP_FRONTEND_URL=$SERVICE_FQDN_ACTIVEPIECES // - AP_FRONTEND_URL=$SERVICE_FQDN_ACTIVEPIECES
- AP_JWT_SECRET=$SERVICE_PASSWORD_64_JWT // - AP_JWT_SECRET=$SERVICE_PASSWORD_64_JWT
- AP_POSTGRES_DATABASE=activepieces // - AP_POSTGRES_DATABASE=activepieces
- AP_POSTGRES_HOST=postgres // - AP_POSTGRES_HOST=postgres
- AP_POSTGRES_PASSWORD=$SERVICE_PASSWORD_POSTGRES // - AP_POSTGRES_PASSWORD=$SERVICE_PASSWORD_POSTGRES
- AP_POSTGRES_PORT=5432 // - AP_POSTGRES_PORT=5432
- AP_POSTGRES_USERNAME=$SERVICE_USER_POSTGRES // - AP_POSTGRES_USERNAME=$SERVICE_USER_POSTGRES
- AP_REDIS_HOST=redis // - AP_REDIS_HOST=redis
- AP_REDIS_PORT=6379 // - AP_REDIS_PORT=6379
- AP_SANDBOX_RUN_TIME_SECONDS=600 // - AP_SANDBOX_RUN_TIME_SECONDS=600
- AP_TELEMETRY_ENABLED=true // - AP_TELEMETRY_ENABLED=true
- "AP_TEMPLATES_SOURCE_URL=https://cloud.activepieces.com/api/v1/flow-templates" // - "AP_TEMPLATES_SOURCE_URL=https://cloud.activepieces.com/api/v1/flow-templates"
- AP_TRIGGER_DEFAULT_POLL_INTERVAL=5 // - AP_TRIGGER_DEFAULT_POLL_INTERVAL=5
- AP_WEBHOOK_TIMEOUT_SECONDS=30 // - AP_WEBHOOK_TIMEOUT_SECONDS=30
depends_on: // depends_on:
postgres: // postgres:
condition: service_healthy // condition: service_healthy
redis: // redis:
condition: service_started // condition: service_started
healthcheck: // healthcheck:
test: ["CMD", "curl", "-f", "http://127.0.0.1:80"] // test: ["CMD", "curl", "-f", "http://127.0.0.1:80"]
interval: 5s // interval: 5s
timeout: 20s // timeout: 20s
retries: 10 // retries: 10
postgres: // postgres:
image: "nginx" // image: "nginx"
environment: // environment:
- SERVICE_FQDN_ACTIVEPIECES=/api // - SERVICE_FQDN_ACTIVEPIECES=/api
- POSTGRES_DB=activepieces // - POSTGRES_DB=activepieces
- PASSW=$AP_POSTGRES_PASSWORD // - PASSW=$AP_POSTGRES_PASSWORD
- AP_FRONTEND_URL=$SERVICE_FQDN_ACTIVEPIECES // - AP_FRONTEND_URL=$SERVICE_FQDN_ACTIVEPIECES
- POSTGRES_PASSWORD=$SERVICE_PASSWORD_POSTGRES // - POSTGRES_PASSWORD=$SERVICE_PASSWORD_POSTGRES
- POSTGRES_USER=$SERVICE_USER_POSTGRES // - POSTGRES_USER=$SERVICE_USER_POSTGRES
volumes: // volumes:
- "pg-data:/var/lib/postgresql/data" // - "pg-data:/var/lib/postgresql/data"
healthcheck: // healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] // test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s // interval: 5s
timeout: 20s // timeout: 20s
retries: 10 // retries: 10
redis: // redis:
image: "redis:latest" // image: "redis:latest"
volumes: // volumes:
- "redis_data:/data" // - "redis_data:/data"
healthcheck: // healthcheck:
test: ["CMD", "redis-cli", "ping"] // test: ["CMD", "redis-cli", "ping"]
interval: 5s // interval: 5s
timeout: 20s // timeout: 20s
retries: 10 // retries: 10
'; // ';
$this->serviceComposeFileString = Yaml::parse($this->serviceYaml); // $this->serviceComposeFileString = Yaml::parse($this->serviceYaml);
$this->service = Service::create([ // $this->service = Service::create([
'name' => 'Service for tests', // 'name' => 'Service for tests',
'uuid' => 'tgwcg8w4s844wkog8kskw44g', // 'uuid' => 'tgwcg8w4s844wkog8kskw44g',
'docker_compose_raw' => $this->serviceYaml, // 'docker_compose_raw' => $this->serviceYaml,
'environment_id' => 1, // 'environment_id' => 1,
'server_id' => 0, // 'server_id' => 0,
'destination_id' => 0, // 'destination_id' => 0,
'destination_type' => StandaloneDocker::class, // 'destination_type' => StandaloneDocker::class,
]); // ]);
}); // });
afterEach(function () { // afterEach(function () {
// $this->applicationPreview->forceDelete(); // // $this->applicationPreview->forceDelete();
$this->application->forceDelete(); // $this->application->forceDelete();
DeleteResourceJob::dispatchSync($this->service); // DeleteResourceJob::dispatchSync($this->service);
$this->service->forceDelete(); // $this->service->forceDelete();
}); // });
test('ServiceComposeParseNew', function () { // test('ServiceComposeParseNew', function () {
$output = newParser($this->service); // $output = newParser($this->service);
$this->service->saveComposeConfigs(); // $this->service->saveComposeConfigs();
expect($output)->toBeInstanceOf(Collection::class); // expect($output)->toBeInstanceOf(Collection::class);
}); // });
// test('ApplicationComposeParse', function () { // test('ApplicationComposeParse', function () {
// expect($this->jsonapplicationComposeFile)->toBeJson()->ray(); // expect($this->jsonapplicationComposeFile)->toBeJson()->ray();

View File

@@ -1,10 +1,10 @@
{ {
"coolify": { "coolify": {
"v4": { "v4": {
"version": "4.0.0-beta.367" "version": "4.0.0-beta.368"
}, },
"nightly": { "nightly": {
"version": "4.0.0-beta.368" "version": "4.0.0-beta.369"
}, },
"helper": { "helper": {
"version": "1.0.3" "version": "1.0.3"