Merge branch 'next' into plane-app

This commit is contained in:
Manish Gupta
2025-03-04 17:13:05 +05:30
committed by GitHub
57 changed files with 288 additions and 211 deletions

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

@@ -1291,11 +1291,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

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

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

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

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

@@ -4,20 +4,22 @@ 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 +37,10 @@ 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;

View File

@@ -101,7 +101,6 @@ class Change extends Component
// ]);
// }
// ray($runners_by_repository);
// }
public function mount()

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

@@ -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,11 +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

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

@@ -4,9 +4,9 @@ namespace App\Traits;
use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel;
use App\Notifications\Channels\PushoverChannel;
use App\Notifications\Channels\SlackChannel;
use App\Notifications\Channels\TelegramChannel;
use App\Notifications\Channels\PushoverChannel;
use Illuminate\Database\Eloquent\Model;
trait HasNotificationSettings

View File

@@ -91,8 +91,6 @@ function next_queuable(string $server_id, string $application_id): bool
$server = Server::find($server_id);
$concurrent_builds = $server->settings->concurrent_builds;
// ray("serverId:{$server->id}", "concurrentBuilds:{$concurrent_builds}", "deployments:{$deployments->count()}", "sameApplicationDeployments:{$same_application_deployments->count()}")->green();
if ($deployments->count() > $concurrent_builds) {
return false;
}

View File

@@ -778,7 +778,6 @@ function convertDockerRunToCompose(?string $custom_docker_run_options = null)
}
}
}
ray($payload);
$compose_options->put('deploy', [
'resources' => [
'reservations' => [
@@ -829,26 +828,29 @@ function generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker
function validateComposeFile(string $compose, int $server_id): string|Throwable
{
return 'OK';
$uuid = Str::random(18);
$server = Server::ownedByCurrentTeam()->find($server_id);
try {
$uuid = Str::random(10);
$server = Server::findOrFail($server_id);
if (! $server) {
throw new \Exception('Server not found');
}
$base64_compose = base64_encode($compose);
$output = instant_remote_process([
instant_remote_process([
"echo {$base64_compose} | base64 -d | tee /tmp/{$uuid}.yml > /dev/null",
"docker compose -f /tmp/{$uuid}.yml config",
"chmod 600 /tmp/{$uuid}.yml",
"docker compose -f /tmp/{$uuid}.yml config --no-interpolate --no-path-resolution -q",
"rm /tmp/{$uuid}.yml",
], $server);
ray($output);
return 'OK';
} catch (\Throwable $e) {
ray($e);
return $e->getMessage();
} finally {
if (filled($server)) {
instant_remote_process([
"rm /tmp/{$uuid}.yml",
], $server);
], $server, throwError: false);
}
}
}

View File

@@ -28,7 +28,7 @@ function send_user_an_email(MailMessage $mail, string $email, ?string $cc = null
{
$settings = instanceSettings();
$type = set_transanctional_email_settings($settings);
if (! $type) {
if (blank($type)) {
throw new Exception('No email settings found.');
}
if ($cc) {
@@ -54,15 +54,19 @@ function send_user_an_email(MailMessage $mail, string $email, ?string $cc = null
}
}
function set_transanctional_email_settings(?InstanceSettings $settings = null): ?string //
function set_transanctional_email_settings(?InstanceSettings $settings = null): ?string // returns null|resend|smtp and defaults to array based on mail.php config
{
if (! $settings) {
$settings = instanceSettings();
}
config()->set('mail.from.address', data_get($settings, 'smtp_from_address'));
config()->set('mail.from.name', data_get($settings, 'smtp_from_name'));
if (! data_get($settings, 'smtp_enabled') && ! data_get($settings, 'resend_enabled')) {
return null;
}
if (data_get($settings, 'resend_enabled')) {
config()->set('mail.default', 'resend');
config()->set('mail.from.address', data_get($settings, 'smtp_from_address'));
config()->set('mail.from.name', data_get($settings, 'smtp_from_name'));
config()->set('resend.api_key', data_get($settings, 'resend_api_key'));
return 'resend';
@@ -76,6 +80,8 @@ function set_transanctional_email_settings(?InstanceSettings $settings = null):
};
if (data_get($settings, 'smtp_enabled')) {
config()->set('mail.from.address', data_get($settings, 'smtp_from_address'));
config()->set('mail.from.name', data_get($settings, 'smtp_from_name'));
config()->set('mail.default', 'smtp');
config()->set('mail.mailers.smtp', [
'transport' => 'smtp',
@@ -91,6 +97,4 @@ function set_transanctional_email_settings(?InstanceSettings $settings = null):
return 'smtp';
}
return null;
}

View File

@@ -748,6 +748,7 @@ function parseCommandFromMagicEnvVariable(Str|string $key): Stringable
{
$value = str($key);
$count = substr_count($value->value(), '_');
$command = null;
if ($count === 2) {
if ($value->startsWith('SERVICE_FQDN') || $value->startsWith('SERVICE_URL')) {
// SERVICE_FQDN_UMAMI
@@ -800,7 +801,6 @@ function parseEnvVariable(Str|string $value)
} else {
// SERVICE_BASE64_64_UMAMI
$command = $value->after('SERVICE_')->beforeLast('_');
ray($command);
}
}
}
@@ -952,7 +952,6 @@ function validate_dns_entry(string $fqdn, Server $server)
$type = \PurplePixie\PhpDns\DNSTypes::NAME_A;
foreach ($dns_servers as $dns_server) {
try {
ray("Checking $host on $dns_server");
$query = new DNSQuery($dns_server);
$results = $query->query($host, $type);
if ($results === false || $query->hasError()) {
@@ -961,13 +960,10 @@ function validate_dns_entry(string $fqdn, Server $server)
foreach ($results as $result) {
if ($result->getType() == $type) {
if (ip_match($result->getData(), $cloudflare_ips->toArray(), $match)) {
ray("Found match in Cloudflare IPs: $match");
$found_matching_ip = true;
break;
}
if ($result->getData() === $ip) {
ray($host.' has IP address '.$result->getData());
ray($result->getString());
$found_matching_ip = true;
break;
}
@@ -977,7 +973,6 @@ function validate_dns_entry(string $fqdn, Server $server)
} catch (\Exception) {
}
}
ray("Found match: $found_matching_ip");
return $found_matching_ip;
}
@@ -1331,7 +1326,6 @@ function parseServiceVolumes($serviceVolumes, $resource, $topLevelVolumes, $pull
$isDirectory = (bool) data_get($volume, 'isDirectory', null) || (bool) data_get($volume, 'is_directory', null);
if ((is_null($isDirectory) || ! $isDirectory) && is_null($content)) {
// if isDirectory is not set (or false) & content is also not set, we assume it is a directory
ray('setting isDirectory to true');
$isDirectory = true;
}
}
@@ -1499,7 +1493,6 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$serviceLabels->push("$removedLabelName=$removedLabel");
}
}
$containerName = "$serviceName-{$resource->uuid}";
// Decide if the service is a database
@@ -1662,7 +1655,6 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
}
if (is_null($isDirectory) && is_null($content)) {
// if isDirectory is not set & content is also not set, we assume it is a directory
ray('setting isDirectory to true');
$isDirectory = true;
}
}
@@ -2529,9 +2521,6 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
}
}
}
if ($collectedPorts->count() > 0) {
ray($collectedPorts->implode(','));
}
$definedNetworkExists = $topLevelNetworks->contains(function ($value, $_) use ($definedNetwork) {
return $value == $definedNetwork;
});
@@ -2956,7 +2945,6 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
}
$parsedServices = collect([]);
// ray()->clearAll();
$allMagicEnvironments = collect([]);
foreach ($services as $serviceName => $service) {
@@ -3016,7 +3004,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
$environment = $environment->merge($buildArgs);
// convert environment variables to one format
$environment = convertComposeEnvironmentToArray($environment);
$environment = convertToKeyValueCollection($environment);
// Add Coolify defined environments
$allEnvironments = $resource->environment_variables()->get(['key', 'value']);
@@ -3197,7 +3185,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
$buildArgs = collect(data_get($service, 'build.args', []));
$environment = $environment->merge($buildArgs);
$environment = convertComposeEnvironmentToArray($environment);
$environment = convertToKeyValueCollection($environment);
$coolifyEnvironments = collect([]);
$isDatabase = isDatabaseImage(data_get_str($service, 'image'));
@@ -3934,7 +3922,7 @@ function add_coolify_default_environment_variables(StandaloneRedis|StandalonePos
}
}
function convertComposeEnvironmentToArray($environment)
function convertToKeyValueCollection($environment)
{
$convertedServiceVariables = collect([]);
if (isAssociativeArray($environment)) {

View File

@@ -1,4 +1,5 @@
<?php
/**
* @see https://github.com/pionl/laravel-chunk-upload
*/

View File

@@ -2,7 +2,7 @@
return [
'coolify' => [
'version' => '4.0.0-beta.396',
'version' => '4.0.0-beta.398',
'helper_version' => '1.0.7',
'realtime_version' => '1.0.6',
'self_hosted' => env('SELF_HOSTED', true),

View File

@@ -38,7 +38,7 @@ return [
'pgsql' => [
'driver' => 'pgsql',
'url' => env('DATABASE_URL'),
'host' => env('DB_HOST', 'postgres'),
'host' => env('DB_HOST', 'coolify-db'),
'port' => env('DB_PORT', '5432'),
'database' => env('DB_DATABASE', 'coolify'),
'username' => env('DB_USERNAME', 'coolify'),

View File

@@ -13,7 +13,7 @@ return [
|
*/
'default' => env('MAIL_MAILER', null),
'default' => env('MAIL_MAILER', 'array'),
/*
|--------------------------------------------------------------------------

View File

@@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('scheduled_task_executions', function (Blueprint $table) {
$table->index(['scheduled_task_id', 'created_at'], 'scheduled_task_executions_task_id_created_at_index');
});
Schema::table('scheduled_database_backup_executions', function (Blueprint $table) {
$table->index(
['scheduled_database_backup_id', 'created_at'],
'scheduled_db_backup_executions_backup_id_created_at_index'
);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('scheduled_task_executions', function (Blueprint $table) {
$table->dropIndex('scheduled_task_executions_task_id_created_at_index');
});
Schema::table('scheduled_database_backup_executions', function (Blueprint $table) {
$table->dropIndex('scheduled_db_backup_executions_backup_id_created_at_index');
});
}
};

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('subscriptions', function (Blueprint $table) {
$table->boolean('stripe_past_due')->default(false);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('subscriptions', function (Blueprint $table) {
$table->dropColumn('stripe_past_due');
});
}
};

View File

@@ -61,7 +61,7 @@ services:
retries: 10
timeout: 2s
soketi:
image: 'ghcr.io/coollabsio/coolify-realtime:1.0.5'
image: 'ghcr.io/coollabsio/coolify-realtime:1.0.6'
ports:
- "${SOKETI_PORT:-6001}:6001"
- "6002:6002"

View File

@@ -1,10 +1,10 @@
{
"coolify": {
"v4": {
"version": "4.0.0-beta.382"
"version": "4.0.0-beta.397"
},
"nightly": {
"version": "4.0.0-beta.383"
"version": "4.0.0-beta.398"
},
"helper": {
"version": "1.0.7"

View File

@@ -79,6 +79,16 @@
</x-slot:button-text>
</x-popup>
</span>
@if (currentTeam()->subscriptionPastOverDue())
<x-banner :closable=false>
<div><span class="font-bold text-red-500">WARNING:</span> Your subscription is in over-due. If your latest
payment is not paid within a week, all automations <span class="font-bold text-red-500">will
be deactivated</span>. Visit <a href="{{ route('subscription.show') }}"
class="underline dark:text-white">/subscription</a> to check your subscription status or pay your
invoice (or check your email for the invoice).
</div>
</x-banner>
@endif
@if (currentTeam()->serverOverflow())
<x-banner :closable=false>
<div><span class="font-bold text-red-500">WARNING:</span> The number of active servers exceeds the limit

View File

@@ -176,7 +176,9 @@
</div>
<div class="pb-4">Persistent storage to preserve data between deployments.</div>
<div class="pb-4 dark:text-warning text-coollabs">If you would like to add a volume, you must add it to
your compose file (Service Stack tab).</div>
your compose file (<a class="underline"
href="{{ route('project.service.configuration', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'service_uuid' => $service->uuid]) }}"
wire:navigate>General tab</a>).</div>
@foreach ($applications as $application)
<livewire:project.service.storage wire:key="application-{{ $application->id }}"
:resource="$application" />

View File

@@ -20,17 +20,15 @@
</x-forms.textarea>
</div>
<div class="pt-2 flex gap-2">
<div class="flex flex-col gap-2">
<x-forms.checkbox label="Escape special characters in labels?"
helper="By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$.<br><br>If you want to use env variables inside the labels, turn this off."
id="service.is_container_label_escape_enabled" instantSave></x-forms.checkbox>
<div class="flex-1"></div>
<div x-cloak x-show="raw">
<x-forms.button class="w-32" @click.prevent="showNormalTextarea = !showNormalTextarea">Switch
Textarea</x-forms.button>
<x-forms.checkbox label="Show Normal Textarea" x-model="showNormalTextarea"></x-forms.checkbox>
</div>
</div>
<div class="flex justify-end w-full gap-2 pt-4">
<div class="flex items-end gap-2">
<div class="flex w-full gap-2 pt-4">
<div x-cloak x-show="raw">
<x-forms.button class="w-64" @click.prevent="raw = !raw">Show Deployable Compose</x-forms.button>
</div>
@@ -38,9 +36,13 @@
<x-forms.button class="w-64" @click.prevent="raw = !raw">Show Source
Compose</x-forms.button>
</div>
</div>
<div class="flex-1"></div>
<x-forms.button class="w-64" wire:click.prevent='saveEditedCompose'>
@if (blank($service->service_type))
<x-forms.button class="w-28" wire:click.prevent='validateCompose'>
Validate
</x-forms.button>
@endif
<x-forms.button class="w-28" wire:click.prevent='saveEditedCompose'>
Save
</x-forms.button>
</div>

View File

@@ -21,15 +21,6 @@
]" confirmationText="{{ $fs_path }}"
confirmationLabel="Please confirm the execution of the actions by entering the Filepath below"
shortConfirmationLabel="Filepath" :confirmWithPassword="false" step2ButtonText="Convert to file" />
@else
<x-modal-confirmation title="Confirm File Conversion to Directory?" buttonTitle="Convert to directory"
submitAction="convertToDirectory" :actions="[
'The selected file will be permanently deleted and an empty directory will be created in its place.',
]" confirmationText="{{ $fs_path }}"
confirmationLabel="Please confirm the execution of the actions by entering the Filepath below"
shortConfirmationLabel="Filepath" :confirmWithPassword="false" step2ButtonText="Convert to directory" />
@endif
@if ($fileStorage->is_directory)
<x-modal-confirmation title="Confirm Directory Deletion?" buttonTitle="Delete Directory" isErrorButton
submitAction="delete" :checkboxes="$directoryDeletionCheckboxes" :actions="[
'The selected directory and all its contents will be permanently deleted from the container.',
@@ -37,13 +28,18 @@
confirmationLabel="Please confirm the execution of the actions by entering the Filepath below"
shortConfirmationLabel="Filepath" step3ButtonText="Permanently Delete" />
@else
<x-modal-confirmation title="Confirm File Conversion to Directory?" buttonTitle="Convert to directory"
submitAction="convertToDirectory" :actions="[
'The selected file will be permanently deleted and an empty directory will be created in its place.',
]" confirmationText="{{ $fs_path }}"
confirmationLabel="Please confirm the execution of the actions by entering the Filepath below"
shortConfirmationLabel="Filepath" :confirmWithPassword="false" step2ButtonText="Convert to directory" />
<x-modal-confirmation title="Confirm File Deletion?" buttonTitle="Delete File" isErrorButton
submitAction="delete" :checkboxes="$fileDeletionCheckboxes" :actions="['The selected file will be permanently deleted from the container.']" confirmationText="{{ $fs_path }}"
confirmationLabel="Please confirm the execution of the actions by entering the Filepath below"
shortConfirmationLabel="Filepath" step3ButtonText="Permanently Delete" />
@endif
@if (!$fileStorage->is_based_on_git)
{{-- @if (!$fileStorage->is_based_on_git)
<x-modal-confirmation isErrorButton buttonTitle="Delete">
<div class="px-2">This storage will be deleted. It is not reversible. <strong
class="text-error">Please
@@ -58,7 +54,7 @@
label="Permanently delete file from the server?"></x-forms.checkbox>
@endif
</x-modal-confirmation>
@endif
@endif --}}
</div>
@if (!$fileStorage->is_directory)
@if (data_get($resource, 'settings.is_preserve_repository_enabled'))
@@ -75,6 +71,5 @@
<x-forms.button class="w-full" type="submit">Save</x-forms.button>
@endif
@endif
</form>
</div>

View File

@@ -3,9 +3,9 @@
<h2>Scheduled Tasks</h2>
<x-modal-input buttonTitle="+ Add" title="New Scheduled Task" :closeOutside="false">
@if ($resource->type() == 'application')
<livewire:project.shared.scheduled-task.add :type="$resource->type()" :containerNames="$containerNames" />
<livewire:project.shared.scheduled-task.add :type="$resource->type()" :id="$resource->id" :containerNames="$containerNames" />
@elseif ($resource->type() == 'service')
<livewire:project.shared.scheduled-task.add :type="$resource->type()" :containerNames="$containerNames" />
<livewire:project.shared.scheduled-task.add :type="$resource->type()" :id="$resource->id" :containerNames="$containerNames" />
@endif
</x-modal-input>
</div>

View File

@@ -132,12 +132,12 @@
<h4 class="py-4">Confirmation Settings</h4>
@if ($disable_two_step_confirmation)
<div class="md:w-96 pb-4">
<div class="md:w-96 pb-4" wire:key="two-step-confirmation-enabled">
<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." />
</div>
@else
<div class="md:w-96 pb-4">
<div class="md:w-96 pb-4" wire:key="two-step-confirmation-disabled">
<x-modal-confirmation title="Disable Two Step Confirmation?"
buttonTitle="Disable Two Step Confirmation" isErrorButton submitAction="toggleTwoStepConfirmation"
:actions="[

View File

@@ -16,7 +16,7 @@ class LoginTest extends DuskTestCase
*
* @throws Throwable
*/
public function testLogin()
public function test_login()
{
$this->browse(callback: function (Browser $browser) {
$browser->loginWithRootUser()

View File

@@ -16,7 +16,7 @@ class ProjectAddNewTest extends DuskTestCase
*
* @throws Throwable
*/
public function testLogin()
public function test_login()
{
$this->browse(function (Browser $browser) {
$browser->loginWithRootUser()

View File

@@ -16,7 +16,7 @@ class ProjectSearchTest extends DuskTestCase
*
* @throws Throwable
*/
public function testLogin()
public function test_login()
{
$this->browse(function (Browser $browser) {
$browser->loginWithRootUser()

View File

@@ -16,7 +16,7 @@ class ProjectTest extends DuskTestCase
*
* @throws Throwable
*/
public function testLogin()
public function test_login()
{
$this->browse(function (Browser $browser) {
$browser->loginWithRootUser()

View File

@@ -358,7 +358,7 @@ use Symfony\Component\Yaml\Yaml;
// expect($output)->toContain('Docker version');
// });
// test('ConvertComposeEnvironmentToArray', function () {
// test('convertToKeyValueCollection', function () {
// ray()->clearAll();
// $yaml = '
// services:
@@ -374,9 +374,9 @@ use Symfony\Component\Yaml\Yaml;
// - POSTGRES_DB: activepieces
// ';
// $parsedYaml = Yaml::parse($yaml);
// $output = convertComposeEnvironmentToArray($parsedYaml['services']['activepieces']['environment']);
// $output2 = convertComposeEnvironmentToArray($parsedYaml['services']['activepieces2']['environment']);
// $dboutput = convertComposeEnvironmentToArray($parsedYaml['services']['postgres']['environment']);
// $output = convertToKeyValueCollection($parsedYaml['services']['activepieces']['environment']);
// $output2 = convertToKeyValueCollection($parsedYaml['services']['activepieces2']['environment']);
// $dboutput = convertToKeyValueCollection($parsedYaml['services']['postgres']['environment']);
// ray($output);
// ray($output2);
// ray($dboutput);

View File

@@ -1,4 +1,5 @@
<?php
/*
|--------------------------------------------------------------------------
| Test Case

View File

@@ -1,10 +1,10 @@
{
"coolify": {
"v4": {
"version": "4.0.0-beta.396"
"version": "4.0.0-beta.398"
},
"nightly": {
"version": "4.0.0-beta.397"
"version": "4.0.0-beta.399"
},
"helper": {
"version": "1.0.7"