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"; $foundServices[] = "$service->id-$service->name";
$statusFromDb = $service->status; $statusFromDb = $service->status;
if ($statusFromDb !== $containerStatus) { if ($statusFromDb !== $containerStatus) {
// ray('Updating status: ' . $containerStatus);
$service->update(['status' => $containerStatus]); $service->update(['status' => $containerStatus]);
} else { } else {
$service->update(['last_online_at' => now()]); $service->update(['last_online_at' => now()]);

View File

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

View File

@@ -1291,11 +1291,6 @@ class ApplicationsController extends Controller
$dockerCompose = base64_decode($request->docker_compose_raw); $dockerCompose = base64_decode($request->docker_compose_raw);
$dockerComposeRaw = Yaml::dump(Yaml::parse($dockerCompose), 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK); $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; $service = new Service;
removeUnnecessaryFieldsFromRequest($request); removeUnnecessaryFieldsFromRequest($request);
$service->fill($request->all()); $service->fill($request->all());

View File

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

View File

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

View File

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

View File

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

View File

@@ -19,6 +19,7 @@ use App\Notifications\Application\DeploymentFailed;
use App\Notifications\Application\DeploymentSuccess; use App\Notifications\Application\DeploymentSuccess;
use App\Traits\ExecuteRemoteCommand; use App\Traits\ExecuteRemoteCommand;
use Carbon\Carbon; use Carbon\Carbon;
use Exception;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
@@ -1207,7 +1208,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
if ($this->application->custom_healthcheck_found) { if ($this->application->custom_healthcheck_found) {
$this->application_deployment_queue->addLogEntry('Custom healthcheck found, skipping default healthcheck.'); $this->application_deployment_queue->addLogEntry('Custom healthcheck found, skipping default healthcheck.');
} }
// ray('New container name: ', $this->container_name);
if ($this->container_name) { if ($this->container_name) {
$counter = 1; $counter = 1;
$this->application_deployment_queue->addLogEntry('Waiting for healthcheck to pass on the new container.'); $this->application_deployment_queue->addLogEntry('Waiting for healthcheck to pass on the new container.');
@@ -1410,7 +1410,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
continue; continue;
} }
// ray('Deploying to additional destination: ', $server->name);
$deployment_uuid = new Cuid2; $deployment_uuid = new Cuid2;
queue_application_deployment( queue_application_deployment(
deployment_uuid: $deployment_uuid, deployment_uuid: $deployment_uuid,
@@ -2023,6 +2022,8 @@ LABEL coolify.deploymentId={$this->deployment_uuid}
COPY . . COPY . .
RUN rm -f /usr/share/nginx/html/nginx.conf RUN rm -f /usr/share/nginx/html/nginx.conf
RUN rm -f /usr/share/nginx/html/Dockerfile 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"); COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
if (str($this->application->custom_nginx_configuration)->isNotEmpty()) { if (str($this->application->custom_nginx_configuration)->isNotEmpty()) {
$nginx_config = base64_encode($this->application->custom_nginx_configuration); $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(); $subscription = Subscription::where('team_id', $teamId)->first();
if ($subscription) { if ($subscription) {
send_internal_notification('Old subscription activated for team: '.$teamId); // send_internal_notification('Old subscription activated for team: '.$teamId);
$subscription->update([ $subscription->update([
'stripe_subscription_id' => $subscriptionId, 'stripe_subscription_id' => $subscriptionId,
'stripe_customer_id' => $customerId, 'stripe_customer_id' => $customerId,
'stripe_invoice_paid' => true, 'stripe_invoice_paid' => true,
'stripe_past_due' => false,
]); ]);
} else { } else {
send_internal_notification('New subscription for team: '.$teamId); // send_internal_notification('New subscription for team: '.$teamId);
Subscription::create([ Subscription::create([
'team_id' => $teamId, 'team_id' => $teamId,
'stripe_subscription_id' => $subscriptionId, 'stripe_subscription_id' => $subscriptionId,
'stripe_customer_id' => $customerId, 'stripe_customer_id' => $customerId,
'stripe_invoice_paid' => true, 'stripe_invoice_paid' => true,
'stripe_past_due' => false,
]); ]);
} }
break; break;
@@ -100,6 +102,7 @@ class StripeProcessJob implements ShouldQueue
if ($subscription) { if ($subscription) {
$subscription->update([ $subscription->update([
'stripe_invoice_paid' => true, 'stripe_invoice_paid' => true,
'stripe_past_due' => false,
]); ]);
} else { } else {
throw new \RuntimeException("No subscription found for customer: {$customerId}"); throw new \RuntimeException("No subscription found for customer: {$customerId}");
@@ -119,9 +122,7 @@ class StripeProcessJob implements ShouldQueue
} }
if (! $subscription->stripe_invoice_paid) { if (! $subscription->stripe_invoice_paid) {
SubscriptionInvoiceFailedJob::dispatch($team); SubscriptionInvoiceFailedJob::dispatch($team);
send_internal_notification('Invoice payment failed: '.$customerId); // send_internal_notification('Invoice payment failed: '.$customerId);
} else {
send_internal_notification('Invoice payment failed but already paid: '.$customerId);
} }
break; break;
case 'payment_intent.payment_failed': case 'payment_intent.payment_failed':
@@ -136,7 +137,7 @@ class StripeProcessJob implements ShouldQueue
return; return;
} }
send_internal_notification('Subscription payment failed for customer: '.$customerId); // send_internal_notification('Subscription payment failed for customer: '.$customerId);
break; break;
case 'customer.subscription.created': case 'customer.subscription.created':
$customerId = data_get($data, 'customer'); $customerId = data_get($data, 'customer');
@@ -158,7 +159,7 @@ class StripeProcessJob implements ShouldQueue
} }
$subscription = Subscription::where('team_id', $teamId)->first(); $subscription = Subscription::where('team_id', $teamId)->first();
if ($subscription) { 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}"); throw new \RuntimeException("Subscription already exists for team: {$teamId}");
} else { } else {
Subscription::create([ Subscription::create([
@@ -182,7 +183,7 @@ class StripeProcessJob implements ShouldQueue
$subscription = Subscription::where('stripe_customer_id', $customerId)->first(); $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if (! $subscription) { if (! $subscription) {
if ($status === 'incomplete_expired') { if ($status === 'incomplete_expired') {
send_internal_notification('Subscription incomplete expired'); // send_internal_notification('Subscription incomplete expired');
throw new \RuntimeException('Subscription incomplete expired'); throw new \RuntimeException('Subscription incomplete expired');
} }
if ($teamId) { 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 ($status === 'active') {
if ($subscription->stripe_subscription_id === $subscriptionId) { if ($subscription->stripe_subscription_id === $subscriptionId) {
$subscription->update([ $subscription->update([
'stripe_past_due' => false,
'stripe_invoice_paid' => true, 'stripe_invoice_paid' => true,
]); ]);
} }

View File

@@ -36,7 +36,7 @@ class Help extends Component
$type = set_transanctional_email_settings($settings); $type = set_transanctional_email_settings($settings);
// Sending feedback through Cloud API // Sending feedback through Cloud API
if ($type === false) { if (blank($type)) {
$url = 'https://app.coolify.io/api/feedback'; $url = 'https://app.coolify.io/api/feedback';
Http::post($url, [ Http::post($url, [
'content' => 'User: `'.auth()->user()?->email.'` with subject: `'.$this->subject.'` has the following problem: `'.$this->description.'`', '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', 'dockerComposeRaw' => 'required',
]); ]);
$this->dockerComposeRaw = Yaml::dump(Yaml::parse($this->dockerComposeRaw), 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK); $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(); $project = Project::where('uuid', $this->parameters['project_uuid'])->first();
$environment = $project->load(['environments'])->environments->where('uuid', $this->parameters['environment_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() public function refreshEnvs()
{ {
$this->service = Service::find($this->serviceId); $this->service = Service::ownedByCurrentTeam()->find($this->serviceId);
} }
public function mount() 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() public function saveEditedCompose()

View File

@@ -63,7 +63,7 @@ class StackForm extends Component
public function saveCompose($raw) public function saveCompose($raw)
{ {
$this->service->docker_compose_raw = $raw; $this->service->docker_compose_raw = $raw;
$this->submit(notify: false); $this->submit(notify: true);
} }
public function instantSave() public function instantSave()
@@ -76,10 +76,6 @@ class StackForm extends Component
{ {
try { try {
$this->validate(); $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->save();
$this->service->saveExtraFields($this->fields); $this->service->saveExtraFields($this->fields);
$this->service->parse(); $this->service->parse();

View File

@@ -2,15 +2,22 @@
namespace App\Livewire\Project\Shared\ScheduledTask; namespace App\Livewire\Project\Shared\ScheduledTask;
use App\Models\ScheduledTask;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Livewire\Attributes\Locked;
use Livewire\Component; use Livewire\Component;
class Add extends Component class Add extends Component
{ {
public $parameters; public $parameters;
#[Locked]
public string $id;
#[Locked]
public string $type; public string $type;
#[Locked]
public Collection $containerNames; public Collection $containerNames;
public string $name; public string $name;
@@ -21,8 +28,6 @@ class Add extends Component
public ?string $container = ''; public ?string $container = '';
protected $listeners = ['clearScheduledTask' => 'clear'];
protected $rules = [ protected $rules = [
'name' => 'required|string', 'name' => 'required|string',
'command' => 'required|string', 'command' => 'required|string',
@@ -60,18 +65,42 @@ class Add extends Component
$this->container = $this->subServiceName; $this->container = $this->subServiceName;
} }
} }
$this->dispatch('saveScheduledTask', [ $this->saveScheduledTask();
'name' => $this->name,
'command' => $this->command,
'frequency' => $this->frequency,
'container' => $this->container,
]);
$this->clear(); $this->clear();
} catch (\Exception $e) { } catch (\Exception $e) {
return handleError($e, $this); 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() public function clear()
{ {
$this->name = ''; $this->name = '';

View File

@@ -4,20 +4,22 @@ namespace App\Livewire\Project\Shared\ScheduledTask;
use App\Models\ScheduledTask; use App\Models\ScheduledTask;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Livewire\Attributes\Locked;
use Livewire\Attributes\On;
use Livewire\Component; use Livewire\Component;
class All extends Component class All extends Component
{ {
#[Locked]
public $resource; public $resource;
#[Locked]
public array $parameters;
public Collection $containerNames; public Collection $containerNames;
public ?string $variables = null; public ?string $variables = null;
public array $parameters;
protected $listeners = ['refreshTasks', 'saveScheduledTask' => 'submit'];
public function mount() public function mount()
{ {
$this->parameters = get_route_parameters(); $this->parameters = get_route_parameters();
@@ -35,37 +37,10 @@ class All extends Component
} }
} }
#[On('refreshTasks')]
public function refreshTasks() public function refreshTasks()
{ {
$this->resource->refresh(); $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(); $this->task->delete();
if ($this->type === 'application') { 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 { } 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) { } catch (\Exception $e) {
return handleError($e); return handleError($e);

View File

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

View File

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

View File

@@ -36,7 +36,7 @@ class SettingsEmail extends Component
public ?int $smtpPort = null; public ?int $smtpPort = null;
#[Validate(['nullable', 'string', 'in:starttls,tls,none'])] #[Validate(['nullable', 'string', 'in:starttls,tls,none'])]
public ?string $smtpEncryption = null; public ?string $smtpEncryption = 'starttls';
#[Validate(['nullable', 'string'])] #[Validate(['nullable', 'string'])]
public ?string $smtpUsername = null; public ?string $smtpUsername = null;

View File

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

View File

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

View File

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

View File

@@ -44,7 +44,7 @@ class DeploymentSuccess extends CustomEmailNotification
if (str($this->fqdn)->explode(',')->count() > 1) { if (str($this->fqdn)->explode(',')->count() > 1) {
$this->fqdn = str($this->fqdn)->explode(',')->first(); $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 public function via(object $notifiable): array

View File

@@ -34,7 +34,7 @@ class StatusChanged extends CustomEmailNotification
if (str($this->fqdn)->explode(',')->count() > 1) { if (str($this->fqdn)->explode(',')->count() > 1) {
$this->fqdn = str($this->fqdn)->explode(',')->first(); $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 public function via(object $notifiable): array

View File

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

View File

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

View File

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

View File

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

View File

@@ -91,8 +91,6 @@ function next_queuable(string $server_id, string $application_id): bool
$server = Server::find($server_id); $server = Server::find($server_id);
$concurrent_builds = $server->settings->concurrent_builds; $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) { if ($deployments->count() > $concurrent_builds) {
return false; return false;
} }

View File

@@ -778,7 +778,6 @@ function convertDockerRunToCompose(?string $custom_docker_run_options = null)
} }
} }
} }
ray($payload);
$compose_options->put('deploy', [ $compose_options->put('deploy', [
'resources' => [ 'resources' => [
'reservations' => [ 'reservations' => [
@@ -829,26 +828,29 @@ function generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker
function validateComposeFile(string $compose, int $server_id): string|Throwable function validateComposeFile(string $compose, int $server_id): string|Throwable
{ {
return 'OK'; $uuid = Str::random(18);
$server = Server::ownedByCurrentTeam()->find($server_id);
try { try {
$uuid = Str::random(10); if (! $server) {
$server = Server::findOrFail($server_id); throw new \Exception('Server not found');
}
$base64_compose = base64_encode($compose); $base64_compose = base64_encode($compose);
$output = instant_remote_process([ instant_remote_process([
"echo {$base64_compose} | base64 -d | tee /tmp/{$uuid}.yml > /dev/null", "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); ], $server);
ray($output);
return 'OK'; return 'OK';
} catch (\Throwable $e) { } catch (\Throwable $e) {
ray($e);
return $e->getMessage(); return $e->getMessage();
} finally { } finally {
if (filled($server)) {
instant_remote_process([ instant_remote_process([
"rm /tmp/{$uuid}.yml", "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(); $settings = instanceSettings();
$type = set_transanctional_email_settings($settings); $type = set_transanctional_email_settings($settings);
if (! $type) { if (blank($type)) {
throw new Exception('No email settings found.'); throw new Exception('No email settings found.');
} }
if ($cc) { 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) { if (! $settings) {
$settings = instanceSettings(); $settings = instanceSettings();
} }
config()->set('mail.from.address', data_get($settings, 'smtp_from_address')); if (! data_get($settings, 'smtp_enabled') && ! data_get($settings, 'resend_enabled')) {
config()->set('mail.from.name', data_get($settings, 'smtp_from_name')); return null;
}
if (data_get($settings, 'resend_enabled')) { if (data_get($settings, 'resend_enabled')) {
config()->set('mail.default', 'resend'); 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')); config()->set('resend.api_key', data_get($settings, 'resend_api_key'));
return 'resend'; return 'resend';
@@ -76,6 +80,8 @@ function set_transanctional_email_settings(?InstanceSettings $settings = null):
}; };
if (data_get($settings, 'smtp_enabled')) { 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.default', 'smtp');
config()->set('mail.mailers.smtp', [ config()->set('mail.mailers.smtp', [
'transport' => 'smtp', 'transport' => 'smtp',
@@ -91,6 +97,4 @@ function set_transanctional_email_settings(?InstanceSettings $settings = null):
return 'smtp'; return 'smtp';
} }
return null;
} }

View File

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

View File

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

View File

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

View File

@@ -38,7 +38,7 @@ return [
'pgsql' => [ 'pgsql' => [
'driver' => 'pgsql', 'driver' => 'pgsql',
'url' => env('DATABASE_URL'), 'url' => env('DATABASE_URL'),
'host' => env('DB_HOST', 'postgres'), 'host' => env('DB_HOST', 'coolify-db'),
'port' => env('DB_PORT', '5432'), 'port' => env('DB_PORT', '5432'),
'database' => env('DB_DATABASE', 'coolify'), 'database' => env('DB_DATABASE', 'coolify'),
'username' => env('DB_USERNAME', '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 retries: 10
timeout: 2s timeout: 2s
soketi: soketi:
image: 'ghcr.io/coollabsio/coolify-realtime:1.0.5' image: 'ghcr.io/coollabsio/coolify-realtime:1.0.6'
ports: ports:
- "${SOKETI_PORT:-6001}:6001" - "${SOKETI_PORT:-6001}:6001"
- "6002:6002" - "6002:6002"

View File

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

View File

@@ -79,6 +79,16 @@
</x-slot:button-text> </x-slot:button-text>
</x-popup> </x-popup>
</span> </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()) @if (currentTeam()->serverOverflow())
<x-banner :closable=false> <x-banner :closable=false>
<div><span class="font-bold text-red-500">WARNING:</span> The number of active servers exceeds the limit <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>
<div class="pb-4">Persistent storage to preserve data between deployments.</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 <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) @foreach ($applications as $application)
<livewire:project.service.storage wire:key="application-{{ $application->id }}" <livewire:project.service.storage wire:key="application-{{ $application->id }}"
:resource="$application" /> :resource="$application" />

View File

@@ -20,17 +20,15 @@
</x-forms.textarea> </x-forms.textarea>
</div> </div>
<div class="pt-2 flex gap-2"> <div class="pt-2 flex gap-2">
<div class="flex flex-col gap-2">
<x-forms.checkbox label="Escape special characters in labels?" <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." 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> id="service.is_container_label_escape_enabled" instantSave></x-forms.checkbox>
<div class="flex-1"></div> <x-forms.checkbox label="Show Normal Textarea" x-model="showNormalTextarea"></x-forms.checkbox>
<div x-cloak x-show="raw">
<x-forms.button class="w-32" @click.prevent="showNormalTextarea = !showNormalTextarea">Switch
Textarea</x-forms.button>
</div> </div>
</div> </div>
<div class="flex justify-end w-full gap-2 pt-4"> <div class="flex w-full gap-2 pt-4">
<div class="flex items-end gap-2">
<div x-cloak x-show="raw"> <div x-cloak x-show="raw">
<x-forms.button class="w-64" @click.prevent="raw = !raw">Show Deployable Compose</x-forms.button> <x-forms.button class="w-64" @click.prevent="raw = !raw">Show Deployable Compose</x-forms.button>
</div> </div>
@@ -38,9 +36,13 @@
<x-forms.button class="w-64" @click.prevent="raw = !raw">Show Source <x-forms.button class="w-64" @click.prevent="raw = !raw">Show Source
Compose</x-forms.button> Compose</x-forms.button>
</div> </div>
</div>
<div class="flex-1"></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 Save
</x-forms.button> </x-forms.button>
</div> </div>

View File

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

View File

@@ -3,9 +3,9 @@
<h2>Scheduled Tasks</h2> <h2>Scheduled Tasks</h2>
<x-modal-input buttonTitle="+ Add" title="New Scheduled Task" :closeOutside="false"> <x-modal-input buttonTitle="+ Add" title="New Scheduled Task" :closeOutside="false">
@if ($resource->type() == 'application') @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') @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 @endif
</x-modal-input> </x-modal-input>
</div> </div>

View File

@@ -132,12 +132,12 @@
<h4 class="py-4">Confirmation Settings</h4> <h4 class="py-4">Confirmation Settings</h4>
@if ($disable_two_step_confirmation) @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" <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" wire:key="two-step-confirmation-disabled">
<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 submitAction="toggleTwoStepConfirmation"
:actions="[ :actions="[

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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