Merge branch 'next' into feat-db-ssl

This commit is contained in:
Andras Bacsai
2025-03-17 15:15:24 +01:00
committed by GitHub
117 changed files with 4584 additions and 1786 deletions

View File

@@ -22,74 +22,27 @@ class StartDatabaseProxy
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse|ServiceDatabase $database)
{
$internalPort = null;
$type = $database->getMorphClass();
$databaseType = $database->database_type;
$network = data_get($database, 'destination.network');
$server = data_get($database, 'destination.server');
$containerName = data_get($database, 'uuid');
$proxyContainerName = "{$database->uuid}-proxy";
if ($database->getMorphClass() === \App\Models\ServiceDatabase::class) {
$databaseType = $database->databaseType();
// $connectPredefined = data_get($database, 'service.connect_to_docker_network');
$network = $database->service->uuid;
$server = data_get($database, 'service.destination.server');
$proxyContainerName = "{$database->service->uuid}-proxy";
switch ($databaseType) {
case 'standalone-mariadb':
$type = \App\Models\StandaloneMariadb::class;
$containerName = "mariadb-{$database->service->uuid}";
break;
case 'standalone-mongodb':
$type = \App\Models\StandaloneMongodb::class;
$containerName = "mongodb-{$database->service->uuid}";
break;
case 'standalone-mysql':
$type = \App\Models\StandaloneMysql::class;
$containerName = "mysql-{$database->service->uuid}";
break;
case 'standalone-postgresql':
$type = \App\Models\StandalonePostgresql::class;
$containerName = "postgresql-{$database->service->uuid}";
break;
case 'standalone-redis':
$type = \App\Models\StandaloneRedis::class;
$containerName = "redis-{$database->service->uuid}";
break;
case 'standalone-keydb':
$type = \App\Models\StandaloneKeydb::class;
$containerName = "keydb-{$database->service->uuid}";
break;
case 'standalone-dragonfly':
$type = \App\Models\StandaloneDragonfly::class;
$containerName = "dragonfly-{$database->service->uuid}";
break;
case 'standalone-clickhouse':
$type = \App\Models\StandaloneClickhouse::class;
$containerName = "clickhouse-{$database->service->uuid}";
break;
case 'standalone-supabase/postgres':
$type = \App\Models\StandalonePostgresql::class;
$containerName = "supabase-db-{$database->service->uuid}";
break;
}
}
if ($type === \App\Models\StandaloneRedis::class) {
$internalPort = 6379;
} elseif ($type === \App\Models\StandalonePostgresql::class) {
$internalPort = 5432;
} elseif ($type === \App\Models\StandaloneMongodb::class) {
$internalPort = 27017;
} elseif ($type === \App\Models\StandaloneMysql::class) {
$internalPort = 3306;
} elseif ($type === \App\Models\StandaloneMariadb::class) {
$internalPort = 3306;
} elseif ($type === \App\Models\StandaloneKeydb::class) {
$internalPort = 6379;
} elseif ($type === \App\Models\StandaloneDragonfly::class) {
$internalPort = 6379;
} elseif ($type === \App\Models\StandaloneClickhouse::class) {
$internalPort = 9000;
$containerName = "{$database->name}-{$database->service->uuid}";
}
$internalPort = match ($databaseType) {
'standalone-mariadb', 'standalone-mysql' => 3306,
'standalone-postgresql', 'standalone-supabase/postgres' => 5432,
'standalone-redis', 'standalone-keydb', 'standalone-dragonfly' => 6379,
'standalone-clickhouse' => 9000,
'standalone-mongodb' => 27017,
default => throw new \Exception("Unsupported database type: $databaseType"),
};
$configuration_dir = database_proxy_dir($database->uuid);
$nginxconf = <<<EOF
user nginx;

View File

@@ -208,7 +208,6 @@ class GetContainersStatus
$foundServices[] = "$service->id-$service->name";
$statusFromDb = $service->status;
if ($statusFromDb !== $containerStatus) {
// ray('Updating status: ' . $containerStatus);
$service->update(['status' => $containerStatus]);
} else {
$service->update(['last_online_at' => now()]);

View File

@@ -50,7 +50,7 @@ class CloudCleanupSubscriptions extends Command
} else {
$subscription = $stripe->subscriptions->retrieve(data_get($team, 'subscription.stripe_subscription_id'), []);
$status = data_get($subscription, 'status');
if ($status === 'active' || $status === 'past_due') {
if ($status === 'active') {
$team->subscription->update([
'stripe_invoice_paid' => true,
'stripe_trial_already_ended' => false,

View File

@@ -18,6 +18,7 @@ use App\Models\Service;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
use OpenApi\Attributes as OA;
use Spatie\Url\Url;
use Symfony\Component\Yaml\Yaml;
use Visus\Cuid2\Cuid2;
@@ -811,6 +812,11 @@ class ApplicationsController extends Controller
'docker_compose_raw' => 'string|nullable',
'docker_compose_domains' => 'array|nullable',
];
// ports_exposes is not required for dockercompose
if ($request->build_pack === 'dockercompose') {
$validationRules['ports_exposes'] = 'string';
$request->offsetSet('ports_exposes', '80');
}
$validationRules = array_merge(sharedDataApplications(), $validationRules);
$validator = customApiValidator($request->all(), $validationRules);
if ($validator->fails()) {
@@ -822,10 +828,6 @@ class ApplicationsController extends Controller
if (! $request->has('name')) {
$request->offsetSet('name', generate_application_name($request->git_repository, $request->git_branch));
}
if ($request->build_pack === 'dockercompose') {
$request->offsetSet('ports_exposes', '80');
}
$return = $this->validateDataApplications($request, $server);
if ($return instanceof \Illuminate\Http\JsonResponse) {
return $return;
@@ -848,7 +850,13 @@ class ApplicationsController extends Controller
if ($dockerComposeDomainsJson->count() > 0) {
$application->docker_compose_domains = $dockerComposeDomainsJson;
}
$repository_url_parsed = Url::fromString($request->git_repository);
$git_host = $repository_url_parsed->getHost();
if ($git_host === 'github.com') {
$application->source_type = GithubApp::class;
$application->source_id = GithubApp::find(0)->id;
}
$application->git_repository = $repository_url_parsed->getSegment(1).'/'.$repository_url_parsed->getSegment(2);
$application->fqdn = $fqdn;
$application->destination_id = $destination->id;
$application->destination_type = $destination->getMorphClass();
@@ -1291,11 +1299,6 @@ class ApplicationsController extends Controller
$dockerCompose = base64_decode($request->docker_compose_raw);
$dockerComposeRaw = Yaml::dump(Yaml::parse($dockerCompose), 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK);
// $isValid = validateComposeFile($dockerComposeRaw, $server_id);
// if ($isValid !== 'OK') {
// return $this->dispatch('error', "Invalid docker-compose file.\n$isValid");
// }
$service = new Service;
removeUnnecessaryFieldsFromRequest($request);
$service->fill($request->all());

View File

@@ -54,7 +54,7 @@ class Controller extends BaseController
'email' => Str::lower($arrayOfRequest['email']),
]);
$type = set_transanctional_email_settings();
if (! $type) {
if (blank($type)) {
return response()->json(['message' => 'Transactional emails are not active'], 400);
}
$request->validate([Fortify::email() => 'required|email']);

View File

@@ -49,7 +49,7 @@ class Bitbucket extends Controller
$full_name = data_get($payload, 'repository.full_name');
$commit = data_get($payload, 'push.changes.0.new.target.hash');
if (!$branch) {
if (! $branch) {
return response([
'status' => 'failed',
'message' => 'Nothing to do. No branch found in the request.',

View File

@@ -152,7 +152,7 @@ class Gitea extends Controller
}
}
if ($x_gitea_event === 'pull_request') {
if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened') {
if ($action === 'opened' || $action === 'synchronized' || $action === 'reopened') {
if ($application->isPRDeployable()) {
$deployment_uuid = new Cuid2;
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
@@ -202,7 +202,6 @@ class Gitea extends Controller
if ($found) {
$found->delete();
$container_name = generateApplicationContainerName($application, $pull_request_id);
// ray('Stopping container: ' . $container_name);
instant_remote_process(["docker rm -f $container_name"], $application->destination->server);
$return_payloads->push([
'application' => $application->name,

View File

@@ -208,7 +208,6 @@ class Github extends Controller
if ($found) {
$found->delete();
$container_name = generateApplicationContainerName($application, $pull_request_id);
// ray('Stopping container: ' . $container_name);
instant_remote_process(["docker rm -f $container_name"], $application->destination->server);
$return_payloads->push([
'application' => $application->name,

View File

@@ -227,7 +227,6 @@ class Gitlab extends Controller
if ($found) {
$found->delete();
$container_name = generateApplicationContainerName($application, $pull_request_id);
// ray('Stopping container: ' . $container_name);
instant_remote_process(["docker rm -f $container_name"], $application->destination->server);
$return_payloads->push([
'application' => $application->name,

View File

@@ -19,6 +19,7 @@ use App\Notifications\Application\DeploymentFailed;
use App\Notifications\Application\DeploymentSuccess;
use App\Traits\ExecuteRemoteCommand;
use Carbon\Carbon;
use Exception;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
@@ -1207,7 +1208,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
if ($this->application->custom_healthcheck_found) {
$this->application_deployment_queue->addLogEntry('Custom healthcheck found, skipping default healthcheck.');
}
// ray('New container name: ', $this->container_name);
if ($this->container_name) {
$counter = 1;
$this->application_deployment_queue->addLogEntry('Waiting for healthcheck to pass on the new container.');
@@ -1410,7 +1410,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
continue;
}
// ray('Deploying to additional destination: ', $server->name);
$deployment_uuid = new Cuid2;
queue_application_deployment(
deployment_uuid: $deployment_uuid,
@@ -2023,6 +2022,8 @@ LABEL coolify.deploymentId={$this->deployment_uuid}
COPY . .
RUN rm -f /usr/share/nginx/html/nginx.conf
RUN rm -f /usr/share/nginx/html/Dockerfile
RUN rm -f /usr/share/nginx/html/docker-compose.yaml
RUN rm -f /usr/share/nginx/html/.env
COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
if (str($this->application->custom_nginx_configuration)->isNotEmpty()) {
$nginx_config = base64_encode($this->application->custom_nginx_configuration);

View File

@@ -73,19 +73,21 @@ class StripeProcessJob implements ShouldQueue
}
$subscription = Subscription::where('team_id', $teamId)->first();
if ($subscription) {
send_internal_notification('Old subscription activated for team: '.$teamId);
// send_internal_notification('Old subscription activated for team: '.$teamId);
$subscription->update([
'stripe_subscription_id' => $subscriptionId,
'stripe_customer_id' => $customerId,
'stripe_invoice_paid' => true,
'stripe_past_due' => false,
]);
} else {
send_internal_notification('New subscription for team: '.$teamId);
// send_internal_notification('New subscription for team: '.$teamId);
Subscription::create([
'team_id' => $teamId,
'stripe_subscription_id' => $subscriptionId,
'stripe_customer_id' => $customerId,
'stripe_invoice_paid' => true,
'stripe_past_due' => false,
]);
}
break;
@@ -100,6 +102,7 @@ class StripeProcessJob implements ShouldQueue
if ($subscription) {
$subscription->update([
'stripe_invoice_paid' => true,
'stripe_past_due' => false,
]);
} else {
throw new \RuntimeException("No subscription found for customer: {$customerId}");
@@ -119,9 +122,7 @@ class StripeProcessJob implements ShouldQueue
}
if (! $subscription->stripe_invoice_paid) {
SubscriptionInvoiceFailedJob::dispatch($team);
send_internal_notification('Invoice payment failed: '.$customerId);
} else {
send_internal_notification('Invoice payment failed but already paid: '.$customerId);
// send_internal_notification('Invoice payment failed: '.$customerId);
}
break;
case 'payment_intent.payment_failed':
@@ -136,7 +137,7 @@ class StripeProcessJob implements ShouldQueue
return;
}
send_internal_notification('Subscription payment failed for customer: '.$customerId);
// send_internal_notification('Subscription payment failed for customer: '.$customerId);
break;
case 'customer.subscription.created':
$customerId = data_get($data, 'customer');
@@ -158,7 +159,7 @@ class StripeProcessJob implements ShouldQueue
}
$subscription = Subscription::where('team_id', $teamId)->first();
if ($subscription) {
send_internal_notification("Subscription already exists for team: {$teamId}");
// send_internal_notification("Subscription already exists for team: {$teamId}");
throw new \RuntimeException("Subscription already exists for team: {$teamId}");
} else {
Subscription::create([
@@ -182,7 +183,7 @@ class StripeProcessJob implements ShouldQueue
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if (! $subscription) {
if ($status === 'incomplete_expired') {
send_internal_notification('Subscription incomplete expired');
// send_internal_notification('Subscription incomplete expired');
throw new \RuntimeException('Subscription incomplete expired');
}
if ($teamId) {
@@ -224,9 +225,33 @@ class StripeProcessJob implements ShouldQueue
]);
}
}
if ($status === 'past_due') {
if ($subscription->stripe_subscription_id === $subscriptionId) {
$subscription->update([
'stripe_past_due' => true,
]);
send_internal_notification('Past Due: '.$customerId.'Subscription ID: '.$subscriptionId);
}
}
if ($status === 'unpaid') {
if ($subscription->stripe_subscription_id === $subscriptionId) {
$subscription->update([
'stripe_invoice_paid' => false,
]);
send_internal_notification('Unpaid: '.$customerId.'Subscription ID: '.$subscriptionId);
}
$team = data_get($subscription, 'team');
if ($team) {
$team->subscriptionEnded();
} else {
send_internal_notification('Subscription unpaid but no team found in Coolify for customer: '.$customerId);
throw new \RuntimeException("No team found in Coolify for customer: {$customerId}");
}
}
if ($status === 'active') {
if ($subscription->stripe_subscription_id === $subscriptionId) {
$subscription->update([
'stripe_past_due' => false,
'stripe_invoice_paid' => true,
]);
}

View File

@@ -36,7 +36,7 @@ class Help extends Component
$type = set_transanctional_email_settings($settings);
// Sending feedback through Cloud API
if ($type === false) {
if (blank($type)) {
$url = 'https://app.coolify.io/api/feedback';
Http::post($url, [
'content' => 'User: `'.auth()->user()?->email.'` with subject: `'.$this->subject.'` has the following problem: `'.$this->description.'`',

View File

@@ -2,7 +2,7 @@
namespace App\Livewire;
//use Livewire\Component;
// use Livewire\Component;
use Illuminate\View\Component;
use Visus\Cuid2\Cuid2;

View File

@@ -22,6 +22,7 @@ class Configuration extends Component
public function mount()
{
$this->currentRoute = request()->route()->getName();
$project = currentTeam()
->projects()
->select('id', 'uuid', 'team_id')
@@ -39,6 +40,9 @@ class Configuration extends Component
$this->project = $project;
$this->environment = $environment;
$this->application = $application;
if ($this->application->build_pack === 'dockercompose' && $this->currentRoute === 'project.application.healthcheck') {
return redirect()->route('project.application.configuration', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]);
}
}
public function render()

View File

@@ -43,8 +43,10 @@ class Heading extends Component
public function check_status($showNotification = false)
{
GetContainersStatus::run($this->database->destination->server);
$this->database->refresh();
if ($this->database->destination->server->isFunctional()) {
GetContainersStatus::dispatch($this->database->destination->server);
}
if ($showNotification) {
$this->dispatch('success', 'Database status updated.');
}

View File

@@ -52,12 +52,6 @@ class DockerCompose extends Component
'dockerComposeRaw' => 'required',
]);
$this->dockerComposeRaw = Yaml::dump(Yaml::parse($this->dockerComposeRaw), 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK);
$isValid = validateComposeFile($this->dockerComposeRaw, $server_id);
if ($isValid !== 'OK') {
return $this->dispatch('error', "Invalid docker-compose file.\n$isValid");
}
$project = Project::where('uuid', $this->parameters['project_uuid'])->first();
$environment = $project->load(['environments'])->environments->where('uuid', $this->parameters['environment_uuid'])->first();

View File

@@ -92,7 +92,9 @@ class Configuration extends Component
public function check_status()
{
try {
GetContainersStatus::run($this->service->server);
if ($this->service->server->isFunctional()) {
GetContainersStatus::dispatch($this->service->server);
}
$this->service->applications->each(function ($application) {
$application->refresh();
});

View File

@@ -4,7 +4,10 @@ namespace App\Livewire\Project\Service;
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Database\StopDatabaseProxy;
use App\Models\InstanceSettings;
use App\Models\ServiceDatabase;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Component;
class Database extends Component
@@ -15,6 +18,8 @@ class Database extends Component
public $fileStorages;
public $parameters;
protected $listeners = ['refreshFileStorages'];
protected $rules = [
@@ -34,12 +39,33 @@ class Database extends Component
public function mount()
{
$this->parameters = get_route_parameters();
if ($this->database->is_public) {
$this->db_url_public = $this->database->getServiceDatabaseUrl();
}
$this->refreshFileStorages();
}
public function delete($password)
{
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
}
}
try {
$this->database->delete();
$this->dispatch('success', 'Database deleted.');
return redirect()->route('project.service.configuration', $this->parameters);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function instantSaveExclude()
{
$this->submit();

View File

@@ -31,12 +31,22 @@ class EditCompose extends Component
public function refreshEnvs()
{
$this->service = Service::find($this->serviceId);
$this->service = Service::ownedByCurrentTeam()->find($this->serviceId);
}
public function mount()
{
$this->service = Service::find($this->serviceId);
$this->service = Service::ownedByCurrentTeam()->find($this->serviceId);
}
public function validateCompose()
{
$isValid = validateComposeFile($this->service->docker_compose_raw, $this->service->server_id);
if ($isValid !== 'OK') {
$this->dispatch('error', "Invalid docker-compose file.\n$isValid");
} else {
$this->dispatch('success', 'Docker compose is valid.');
}
}
public function saveEditedCompose()

View File

@@ -43,12 +43,11 @@ class EditDomain extends Component
updateCompose($this->application);
if (str($this->application->fqdn)->contains(',')) {
$this->dispatch('warning', 'Some services do not support multiple domains, which can lead to problems and is NOT RECOMMENDED.<br><br>Only use multiple domains if you know what you are doing.');
} else {
! $warning && $this->dispatch('success', 'Service saved.');
}
$this->application->service->parse();
$this->dispatch('refresh');
$this->dispatch('configurationChanged');
$this->dispatch('refreshStatus');
} catch (\Throwable $e) {
$originalFqdn = $this->application->getOriginal('fqdn');
if ($originalFqdn !== $this->application->fqdn) {

View File

@@ -63,7 +63,7 @@ class StackForm extends Component
public function saveCompose($raw)
{
$this->service->docker_compose_raw = $raw;
$this->submit(notify: false);
$this->submit(notify: true);
}
public function instantSave()
@@ -76,10 +76,6 @@ class StackForm extends Component
{
try {
$this->validate();
$isValid = validateComposeFile($this->service->docker_compose_raw, $this->service->server->id);
if ($isValid !== 'OK') {
throw new \Exception("Invalid docker-compose file.\n$isValid");
}
$this->service->save();
$this->service->saveExtraFields($this->fields);
$this->service->parse();

View File

@@ -2,15 +2,22 @@
namespace App\Livewire\Project\Shared\ScheduledTask;
use App\Models\ScheduledTask;
use Illuminate\Support\Collection;
use Livewire\Attributes\Locked;
use Livewire\Component;
class Add extends Component
{
public $parameters;
#[Locked]
public string $id;
#[Locked]
public string $type;
#[Locked]
public Collection $containerNames;
public string $name;
@@ -21,8 +28,6 @@ class Add extends Component
public ?string $container = '';
protected $listeners = ['clearScheduledTask' => 'clear'];
protected $rules = [
'name' => 'required|string',
'command' => 'required|string',
@@ -60,18 +65,42 @@ class Add extends Component
$this->container = $this->subServiceName;
}
}
$this->dispatch('saveScheduledTask', [
'name' => $this->name,
'command' => $this->command,
'frequency' => $this->frequency,
'container' => $this->container,
]);
$this->saveScheduledTask();
$this->clear();
} catch (\Exception $e) {
return handleError($e, $this);
}
}
public function saveScheduledTask()
{
try {
$task = new ScheduledTask;
$task->name = $this->name;
$task->command = $this->command;
$task->frequency = $this->frequency;
$task->container = $this->container;
$task->team_id = currentTeam()->id;
switch ($this->type) {
case 'application':
$task->application_id = $this->id;
break;
case 'standalone-postgresql':
$task->standalone_postgresql_id = $this->id;
break;
case 'service':
$task->service_id = $this->id;
break;
}
$task->save();
$this->dispatch('refreshTasks');
$this->dispatch('success', 'Scheduled task added.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function clear()
{
$this->name = '';

View File

@@ -2,22 +2,23 @@
namespace App\Livewire\Project\Shared\ScheduledTask;
use App\Models\ScheduledTask;
use Illuminate\Support\Collection;
use Livewire\Attributes\Locked;
use Livewire\Attributes\On;
use Livewire\Component;
class All extends Component
{
#[Locked]
public $resource;
#[Locked]
public array $parameters;
public Collection $containerNames;
public ?string $variables = null;
public array $parameters;
protected $listeners = ['refreshTasks', 'saveScheduledTask' => 'submit'];
public function mount()
{
$this->parameters = get_route_parameters();
@@ -35,37 +36,9 @@ class All extends Component
}
}
#[On('refreshTasks')]
public function refreshTasks()
{
$this->resource->refresh();
}
public function submit($data)
{
try {
$task = new ScheduledTask;
$task->name = $data['name'];
$task->command = $data['command'];
$task->frequency = $data['frequency'];
$task->container = $data['container'];
$task->team_id = currentTeam()->id;
switch ($this->resource->type()) {
case 'application':
$task->application_id = $this->resource->id;
break;
case 'standalone-postgresql':
$task->standalone_postgresql_id = $this->resource->id;
break;
case 'service':
$task->service_id = $this->resource->id;
break;
}
$task->save();
$this->refreshTasks();
$this->dispatch('success', 'Scheduled task added.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
}

View File

@@ -133,9 +133,9 @@ class Show extends Component
$this->task->delete();
if ($this->type === 'application') {
return redirect()->route('project.application.configuration', $this->parameters, $this->task->name);
return redirect()->route('project.application.scheduled-tasks.show', $this->parameters);
} else {
return redirect()->route('project.service.configuration', $this->parameters, $this->task->name);
return redirect()->route('project.service.scheduled-tasks.show', $this->parameters);
}
} catch (\Exception $e) {
return handleError($e);

View File

@@ -29,8 +29,6 @@ class Webhooks extends Component
public function mount()
{
// ray()->clearAll();
// ray()->showQueries();
$this->deploywebhook = generateDeployWebhook($this->resource);
$this->githubManualWebhookSecret = data_get($this->resource, 'manual_webhook_secret_github');

View File

@@ -105,7 +105,6 @@ class Deploy extends Component
$startTime = Carbon::now()->getTimestamp();
while ($process->running()) {
ray('running');
if (Carbon::now()->getTimestamp() - $startTime >= $timeout) {
$this->forceStopContainer($containerName);
break;

View File

@@ -36,7 +36,7 @@ class SettingsEmail extends Component
public ?int $smtpPort = null;
#[Validate(['nullable', 'string', 'in:starttls,tls,none'])]
public ?string $smtpEncryption = null;
public ?string $smtpEncryption = 'starttls';
#[Validate(['nullable', 'string'])]
public ?string $smtpUsername = null;
@@ -114,19 +114,24 @@ class SettingsEmail extends Component
public function instantSave(string $type)
{
try {
$currentSmtpEnabled = $this->settings->smtp_enabled;
$currentResendEnabled = $this->settings->resend_enabled;
$this->resetErrorBag();
if ($type === 'SMTP') {
$this->submitSmtp();
$this->resendEnabled = $this->settings->resend_enabled = false;
} elseif ($type === 'Resend') {
$this->submitResend();
$this->smtpEnabled = $this->settings->smtp_enabled = false;
}
$this->settings->save();
} catch (\Throwable $e) {
if ($type === 'SMTP') {
$this->smtpEnabled = false;
$this->smtpEnabled = $currentSmtpEnabled;
} elseif ($type === 'Resend') {
$this->resendEnabled = false;
$this->resendEnabled = $currentResendEnabled;
}
return handleError($e, $this);
@@ -156,9 +161,6 @@ class SettingsEmail extends Component
'smtpEncryption.required' => 'Encryption type is required.',
]);
$this->resendEnabled = false;
$this->settings->resend_enabled = false;
$this->settings->smtp_enabled = $this->smtpEnabled;
$this->settings->smtp_host = $this->smtpHost;
$this->settings->smtp_port = $this->smtpPort;
@@ -194,9 +196,6 @@ class SettingsEmail extends Component
'smtpFromName.required' => 'From Name is required.',
]);
$this->smtpEnabled = false;
$this->settings->smtp_enabled = false;
$this->settings->resend_enabled = $this->resendEnabled;
$this->settings->resend_api_key = $this->resendApiKey;
$this->settings->smtp_from_address = $this->smtpFromAddress;

View File

@@ -37,6 +37,8 @@ class Change extends Component
public $applications;
public $privateKeys;
protected $rules = [
'github_app.name' => 'required|string',
'github_app.organization' => 'nullable|string',
@@ -54,6 +56,7 @@ class Change extends Component
'github_app.metadata' => 'nullable|string',
'github_app.pull_requests' => 'nullable|string',
'github_app.administration' => 'nullable|string',
'github_app.private_key_id' => 'required|int',
];
public function boot()
@@ -65,9 +68,13 @@ class Change extends Component
public function checkPermissions()
{
GithubAppPermissionJob::dispatchSync($this->github_app);
$this->github_app->refresh()->makeVisible('client_secret')->makeVisible('webhook_secret');
$this->dispatch('success', 'Github App permissions updated.');
try {
GithubAppPermissionJob::dispatchSync($this->github_app);
$this->github_app->refresh()->makeVisible('client_secret')->makeVisible('webhook_secret');
$this->dispatch('success', 'Github App permissions updated.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
// public function check()
@@ -101,7 +108,6 @@ class Change extends Component
// ]);
// }
// ray($runners_by_repository);
// }
public function mount()
@@ -110,6 +116,7 @@ class Change extends Component
$github_app_uuid = request()->github_app_uuid;
$this->github_app = GithubApp::ownedByCurrentTeam()->whereUuid($github_app_uuid)->firstOrFail();
$this->github_app->makeVisible(['client_secret', 'webhook_secret']);
$this->privateKeys = PrivateKey::ownedByCurrentTeam()->get();
$this->applications = $this->github_app->applications;
$settings = instanceSettings();
@@ -244,6 +251,7 @@ class Change extends Component
'github_app.client_secret' => 'required|string',
'github_app.webhook_secret' => 'required|string',
'github_app.is_system_wide' => 'required|bool',
'github_app.private_key_id' => 'required|int',
]);
$this->github_app->save();
$this->dispatch('success', 'Github App updated.');
@@ -252,6 +260,15 @@ class Change extends Component
}
}
public function createGithubAppManually()
{
$this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret');
$this->github_app->app_id = '1234567890';
$this->github_app->installation_id = '1234567890';
$this->github_app->save();
$this->dispatch('success', 'Github App updated.');
}
public function instantSave()
{
try {

View File

@@ -3,6 +3,7 @@
namespace App\Models;
use App\Events\FileStorageChanged;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class LocalFileVolume extends BaseModel
@@ -17,6 +18,8 @@ class LocalFileVolume extends BaseModel
protected $guarded = [];
public $appends = ['is_binary'];
protected static function booted()
{
static::created(function (LocalFileVolume $fileVolume) {
@@ -25,6 +28,15 @@ class LocalFileVolume extends BaseModel
});
}
protected function isBinary(): Attribute
{
return Attribute::make(
get: function () {
return $this->content === '[binary file]';
}
);
}
public function service()
{
return $this->morphTo('resource');
@@ -50,6 +62,10 @@ class LocalFileVolume extends BaseModel
$isFile = instant_remote_process(["test -f $path && echo OK || echo NOK"], $server);
if ($isFile === 'OK') {
$content = instant_remote_process(["cat $path"], $server, false);
// Check if content contains binary data by looking for null bytes or non-printable characters
if (str_contains($content, "\0") || preg_match('/[\x00-\x08\x0B\x0C\x0E-\x1F]/', $content)) {
$content = '[binary file]';
}
$this->content = $content;
$this->is_directory = false;
$this->save();

View File

@@ -17,6 +17,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Stringable;
use OpenApi\Attributes as OA;
@@ -24,6 +25,7 @@ use Spatie\SchemalessAttributes\Casts\SchemalessAttributes;
use Spatie\SchemalessAttributes\SchemalessAttributesTrait;
use Spatie\Url\Url;
use Symfony\Component\Yaml\Yaml;
use Visus\Cuid2\Cuid2;
#[OA\Schema(
description: 'Server model',
@@ -101,11 +103,13 @@ class Server extends BaseModel
'server_id' => $server->id,
]);
} else {
StandaloneDocker::create([
$standaloneDocker = new StandaloneDocker([
'name' => 'coolify',
'uuid' => (string) new Cuid2,
'network' => 'coolify',
'server_id' => $server->id,
]);
$standaloneDocker->saveQuietly();
}
}
if (! isset($server->proxy->redirect_enabled)) {
@@ -437,10 +441,6 @@ class Server extends BaseModel
"mkdir -p $dynamic_config_path",
"echo '$base64' | base64 -d | tee $file > /dev/null",
], $this);
if (config('app.env') === 'local') {
// ray($yaml);
}
}
} elseif ($this->proxyType() === 'CADDY') {
$file = "$dynamic_config_path/coolify.caddy";
@@ -709,22 +709,6 @@ $schema://$host {
];
}
public function getContainersWithSentinel(): Collection
{
$sentinel_found = instant_remote_process(['docker inspect coolify-sentinel'], $this, false);
$sentinel_found = json_decode($sentinel_found, true);
$status = data_get($sentinel_found, '0.State.Status', 'exited');
if ($status === 'running') {
$containers = instant_remote_process(['docker exec coolify-sentinel sh -c "curl http://127.0.0.1:8888/api/containers"'], $this, false);
if (is_null($containers)) {
return collect([]);
}
$containers = data_get(json_decode($containers, true), 'containers', []);
return collect($containers);
}
}
public function loadAllContainers(): Collection
{
if ($this->isFunctional()) {
@@ -970,10 +954,8 @@ $schema://$host {
}
});
if ($supported->count() === 1) {
// ray('supported');
return str($supported->first());
} else {
// ray('not supported');
return false;
}
}
@@ -1042,7 +1024,7 @@ $schema://$host {
$unreachableNotificationSent = (bool) $this->unreachable_notification_sent;
$isReachable = (bool) $this->settings->is_reachable;
\Log::debug('Server reachability check', [
Log::debug('Server reachability check', [
'server_id' => $this->id,
'is_reachable' => $isReachable,
'notification_sent' => $unreachableNotificationSent,
@@ -1054,7 +1036,7 @@ $schema://$host {
$this->save();
if ($unreachableNotificationSent === true) {
\Log::debug('Server is now reachable, sending notification', [
Log::debug('Server is now reachable, sending notification', [
'server_id' => $this->id,
]);
$this->sendReachableNotification();
@@ -1064,7 +1046,7 @@ $schema://$host {
}
$this->increment('unreachable_count');
\Log::debug('Incremented unreachable count', [
Log::debug('Incremented unreachable count', [
'server_id' => $this->id,
'new_count' => $this->unreachable_count,
]);
@@ -1072,7 +1054,7 @@ $schema://$host {
if ($this->unreachable_count === 1) {
$this->settings->is_reachable = true;
$this->settings->save();
\Log::debug('First unreachable attempt, marking as reachable', [
Log::debug('First unreachable attempt, marking as reachable', [
'server_id' => $this->id,
]);
@@ -1083,7 +1065,7 @@ $schema://$host {
$failedChecks = 0;
for ($i = 0; $i < 3; $i++) {
$status = $this->serverStatus();
\Log::debug('Additional reachability check', [
Log::debug('Additional reachability check', [
'server_id' => $this->id,
'attempt' => $i + 1,
'status' => $status,
@@ -1095,7 +1077,7 @@ $schema://$host {
}
if ($failedChecks === 3 && ! $unreachableNotificationSent) {
\Log::debug('Server confirmed unreachable after 3 attempts, sending notification', [
Log::debug('Server confirmed unreachable after 3 attempts, sending notification', [
'server_id' => $this->id,
]);
$this->sendUnreachableNotification();

View File

@@ -78,11 +78,15 @@ class ServiceDatabase extends BaseModel
public function databaseType()
{
$image = str($this->image)->before(':');
if ($image->contains('postgres') || $image->contains('postgis')) {
$image = 'postgresql';
if ($image->contains('supabase/postgres')) {
$finalImage = 'supabase/postgres';
} elseif ($image->contains('postgres') || $image->contains('postgis')) {
$finalImage = 'postgresql';
} else {
$finalImage = $image;
}
return "standalone-$image";
return "standalone-$finalImage";
}
public function getServiceDatabaseUrl()

View File

@@ -38,6 +38,12 @@ class StandaloneRedis extends BaseModel
$database->forceFill(['last_online_at' => now()]);
}
});
static::retrieved(function ($database) {
if (! $database->redis_username) {
$database->redis_username = 'default';
}
});
}
protected function serverStatus(): Attribute
@@ -198,8 +204,8 @@ class StandaloneRedis extends BaseModel
{
return Attribute::make(
get: fn () => is_null($this->ports_mappings)
? []
: explode(',', $this->ports_mappings),
? []
: explode(',', $this->ports_mappings),
);
}
@@ -366,7 +372,12 @@ class StandaloneRedis extends BaseModel
get: function () {
$username = $this->runtime_environment_variables()->where('key', 'REDIS_USERNAME')->first();
if (! $username) {
return null;
$this->runtime_environment_variables()->create([
'key' => 'REDIS_USERNAME',
'value' => 'default',
]);
return 'default';
}
return $username->value;

View File

@@ -93,6 +93,15 @@ class Team extends Model implements SendsDiscord, SendsEmail, SendsPushover, Sen
return $servers >= $serverLimit;
}
public function subscriptionPastOverDue()
{
if (isCloud()) {
return $this->subscription?->stripe_past_due;
}
return false;
}
public function serverOverflow()
{
if ($this->serverLimit() < $this->servers->count()) {
@@ -185,6 +194,7 @@ class Team extends Model implements SendsDiscord, SendsEmail, SendsPushover, Sen
'stripe_cancel_at_period_end' => false,
'stripe_invoice_paid' => false,
'stripe_trial_already_ended' => false,
'stripe_past_due' => false,
]);
foreach ($this->servers as $server) {
$server->settings()->update([

View File

@@ -44,7 +44,7 @@ class DeploymentSuccess extends CustomEmailNotification
if (str($this->fqdn)->explode(',')->count() > 1) {
$this->fqdn = str($this->fqdn)->explode(',')->first();
}
$this->deployment_url = base_url()."/project/{$this->project_uuid}/environments/{$this->environment_uuid}/application/{$this->application->uuid}/deployment/{$this->deployment_uuid}";
$this->deployment_url = base_url()."/project/{$this->project_uuid}/environment/{$this->environment_uuid}/application/{$this->application->uuid}/deployment/{$this->deployment_uuid}";
}
public function via(object $notifiable): array

View File

@@ -34,7 +34,7 @@ class StatusChanged extends CustomEmailNotification
if (str($this->fqdn)->explode(',')->count() > 1) {
$this->fqdn = str($this->fqdn)->explode(',')->first();
}
$this->resource_url = base_url()."/project/{$this->project_uuid}/environments/{$this->environment_uuid}/application/{$this->resource->uuid}";
$this->resource_url = base_url()."/project/{$this->project_uuid}/environment/{$this->environment_uuid}/application/{$this->resource->uuid}";
}
public function via(object $notifiable): array

View File

@@ -50,10 +50,9 @@ class EmailChannel
if ($emailSettings->use_instance_email_settings) {
$type = set_transanctional_email_settings();
if (! $type) {
if (blank($type)) {
throw new Exception('No email settings found.');
}
config()->set('mail.default', $type);
return;
}

View File

@@ -35,7 +35,7 @@ class TransactionalEmailChannel
private function bootConfigs(): void
{
$type = set_transanctional_email_settings();
if (! $type) {
if (blank($type)) {
throw new Exception('No email settings found.');
}
}

View File

@@ -40,7 +40,7 @@ class PushoverMessage
if ($buttonUrl && str_contains($buttonUrl, 'http://localhost')) {
$buttonUrl = str_replace('http://localhost', config('app.url'), $buttonUrl);
}
$payload['message'] .= "&nbsp;<a href='" . $buttonUrl . "'>" . $text . '</a>';
$payload['message'] .= "&nbsp;<a href='".$buttonUrl."'>".$text.'</a>';
}
Log::info('Pushover message', $payload);

View File

@@ -3,6 +3,7 @@
namespace App\Notifications\TransactionalEmails;
use App\Models\InstanceSettings;
use Exception;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
@@ -35,8 +36,8 @@ class ResetPassword extends Notification
public function via($notifiable)
{
$type = set_transanctional_email_settings();
if (! $type) {
throw new \Exception('No email settings found.');
if (blank($type)) {
throw new Exception('No email settings found.');
}
return ['mail'];

View File

@@ -11,6 +11,7 @@ use Illuminate\Foundation\Events\MaintenanceModeEnabled;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use SocialiteProviders\Authentik\AuthentikExtendSocialite;
use SocialiteProviders\Azure\AzureExtendSocialite;
use SocialiteProviders\Google\GoogleExtendSocialite;
use SocialiteProviders\Infomaniak\InfomaniakExtendSocialite;
use SocialiteProviders\Manager\SocialiteWasCalled;
@@ -26,6 +27,7 @@ class EventServiceProvider extends ServiceProvider
SocialiteWasCalled::class => [
AzureExtendSocialite::class.'@handle',
AuthentikExtendSocialite::class.'@handle',
GoogleExtendSocialite::class.'@handle',
InfomaniakExtendSocialite::class.'@handle',
],
ProxyStarted::class => [