Merge branch 'next' into dep-and-remove-unused-stuff

This commit is contained in:
🏔️ Peak
2024-11-12 13:58:51 +01:00
committed by GitHub
41 changed files with 8764 additions and 401 deletions

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
{ {
@@ -45,6 +46,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

@@ -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

@@ -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

@@ -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();
$this->containers = $this->getAllActiveContainers(); }
public function loadContainers()
{
try {
$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

@@ -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": "^4.2", "doctrine/dbal": "^4.2",
"guzzlehttp/guzzle": "^7.5.0", "guzzlehttp/guzzle": "^7.5.0",

58
composer.lock generated
View File

@@ -6,6 +6,64 @@
], ],
"content-hash": "fffc935d2809f759ec8467ad802797f6", "content-hash": "fffc935d2809f759ec8467ad802797f6",
"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';

7941
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,9 +3467,7 @@ paths:
content: content:
application/json: application/json:
schema: schema:
type: array $ref: '#/components/schemas/PrivateKey'
items:
$ref: '#/components/schemas/PrivateKey'
'401': '401':
$ref: '#/components/responses/401' $ref: '#/components/responses/401'
'400': '400':
@@ -4759,6 +4757,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'

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

@@ -44,17 +44,108 @@
</nav> </nav>
</div> </div>
@if ($environment->isEmpty()) @if ($environment->isEmpty())
<a href="{{ route('project.resource.create', ['project_uuid' => data_get($parameters, 'project_uuid'), 'environment_name' => data_get($parameters, 'environment_name')]) }} " <a href="{{ route('project.resource.create', ['project_uuid' => data_get($parameters, 'project_uuid'), 'environment_name' => data_get($parameters, 'environment_name')]) }} "
class="items-center justify-center box">+ Add New Resource</a> class="items-center justify-center box">+ Add New Resource</a>
@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,16 +86,24 @@
</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">
<g fill="currentColor">
<path
d="m237.66 85.26l-128.4 128.4a8 8 0 0 1-11.32 0l-71.6-72a8 8 0 0 1 0-11.31l24-24a8 8 0 0 1 11.32 0l36.68 35.32a8 8 0 0 0 11.32 0l92.68-91.32a8 8 0 0 1 11.32 0l24 23.6a8 8 0 0 1 0 11.31"
opacity=".2" />
<path
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>
</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"> viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
<g fill="currentColor"> <path fill="currentColor"
<path 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" />
d="m237.66 85.26l-128.4 128.4a8 8 0 0 1-11.32 0l-71.6-72a8 8 0 0 1 0-11.31l24-24a8 8 0 0 1 11.32 0l36.68 35.32a8 8 0 0 0 11.32 0l92.68-91.32a8 8 0 0 1 11.32 0l24 23.6a8 8 0 0 1 0 11.31"
opacity=".2" />
<path
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>
</svg></div> </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,34 +133,40 @@
<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>
<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

@@ -21,7 +21,7 @@
</div> </div>
@if (!$storage->is_usable) @if (!$storage->is_usable)
<div class="text-red-500">Not Usable</div> <div class="text-red-500">Not Usable</div>
@endif @endif
</div> </div>
</div> </div>
@empty @empty

View File

@@ -8,27 +8,32 @@
<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()">
<form class="flex flex-col gap-2 justify-center xl:items-end xl:flex-row" @if ($isLoadingContainers)
wire:submit="$dispatchSelf('connectToContainer')"> <div class="pt-1">
<x-forms.select id="server" required wire:model.live="selected_uuid"> <x-loading text="Loading servers and containers..." />
@foreach ($servers as $server) </div>
@if ($loop->first) @else
<option disabled value="default">Select a server or container</option> <form class="flex flex-col gap-2 justify-center xl:items-end xl:flex-row"
@endif wire:submit="$dispatchSelf('connectToContainer')">
<option value="{{ $server->uuid }}">{{ $server->name }}</option> <x-forms.select id="server" required wire:model.live="selected_uuid">
@foreach ($containers as $container) @foreach ($servers as $server)
@if ($container['server_uuid'] == $server->uuid) @if ($loop->first)
<option value="{{ $container['uuid'] }}"> <option disabled value="default">Select a server or container</option>
{{ $server->name }} -> {{ $container['name'] }}
</option>
@endif @endif
<option value="{{ $server->uuid }}">{{ $server->name }}</option>
@foreach ($containers as $container)
@if ($container['server_uuid'] == $server->uuid)
<option value="{{ $container['uuid'] }}">
{{ $server->name }} -> {{ $container['name'] }}
</option>
@endif
@endforeach
@endforeach @endforeach
@endforeach </x-forms.select>
</x-forms.select> <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"