Merge branch 'next' into fix-cloning
This commit is contained in:
@@ -58,7 +58,7 @@ Special thanks to our biggest sponsors!
|
||||
* [PFGlabs](https://pfglabs.com/?ref=coolify.io) - Build real project with Golang.
|
||||
* [Treive](https://trieve.ai/?ref=coolify.io) - An AI-powered search and discovery platform for enhancing information retrieval in large datasets.
|
||||
* [Blacksmith](https://blacksmith.sh/?ref=coolify.io) - A cloud-native platform for automating infrastructure provisioning and management across multiple cloud providers.
|
||||
* [Brand Dev](https://brand.dev/?ref=coolify.io) - A web development agency specializing in creating custom digital experiences and brand identities.
|
||||
* [Brand Dev](https://brand.dev/?ref=coolify.io) - The #1 Brand API for B2B software startups - instantly pull logos, fonts, descriptions, social links, slogans, and so much more from any domain via a single api call.
|
||||
* [Jobscollider](https://jobscollider.com/remote-jobs?ref=coolify.io) - A job search platform connecting professionals with remote work opportunities across various industries.
|
||||
* [Hostinger](https://www.hostinger.com/vps/coolify-hosting?ref=coolify.io) - A web hosting provider offering affordable hosting solutions, domain registration, and website building tools.
|
||||
* [Glueops](https://www.glueops.dev/?ref=coolify.io) - A DevOps consulting company providing infrastructure automation and cloud optimization services.
|
||||
@@ -75,6 +75,7 @@ Special thanks to our biggest sponsors!
|
||||
<a href="https://www.runpod.io/?ref=coolify.io">
|
||||
<svg style="width:60px;height:60px;background:#fff;" xmlns="http://www.w3.org/2000/svg" version="1.0" viewBox="0 0 200 200"><g><path d="M74.5 51.1c-25.4 14.9-27 16-29.6 20.2-1.8 3-1.9 5.3-1.9 32.3 0 21.7.3 29.4 1.3 30.6 1.9 2.5 46.7 27.9 48.5 27.6 1.5-.3 1.7-3.1 2-27.7.2-21.9 0-27.8-1.1-29.5-.8-1.2-9.9-6.8-20.2-12.6-10.3-5.8-19.4-11.5-20.2-12.7-1.8-2.6-.9-5.9 1.8-7.4 1.6-.8 6.3 0 21.8 4C87.8 78.7 98 81 99.6 81c4.4 0 49.9-25.9 49.9-28.4 0-1.6-3.4-2.8-24-8.2-13.2-3.5-25.1-6.3-26.5-6.3-1.4.1-12.4 5.9-24.5 13z"></path><path d="m137.2 68.1-3.3 2.1 6.3 3.7c3.5 2 6.3 4.3 6.3 5.1 0 .9-8 6.1-19.4 12.6-10.6 6-20 11.9-20.7 12.9-1.2 1.6-1.4 7.2-1.2 29.4.3 24.8.5 27.6 2 27.9 1.8.3 46.6-25.1 48.6-27.6.9-1.2 1.2-8.8 1.2-30.2s-.3-29-1.2-30.2c-1.6-1.9-12.1-7.8-13.9-7.8-.8 0-2.9 1-4.7 2.1z"></path></g></svg></a>
|
||||
<a href="https://lightspeed.run/?ref=coolify.io"><img src="https://github.com/lightspeedrun.png" width="60px" alt="Lightspeed.run"/></a>
|
||||
<a href="https://dartnode.com/?ref=coolify.io"><img src="https://github.com/DartNode-com.png" width="60px" alt="DartNode"/></a>
|
||||
<a href="https://www.flint.sh/en/home?ref=coolify.io"> <img src="https://github.com/Flint-company.png" width="60px" alt="FlintCompany"/></a>
|
||||
<a href="https://americancloud.com/?ref=coolify.io"><img src="https://github.com/American-Cloud.png" width="60px" alt="American Cloud"/></a>
|
||||
<a href="https://cryptojobslist.com/?ref=coolify.io"><img src="https://github.com/cryptojobslist.png" width="60px" alt="CryptoJobsList" /></a>
|
||||
|
@@ -91,16 +91,9 @@ class RunRemoteProcess
|
||||
} else {
|
||||
if ($processResult->exitCode() == 0) {
|
||||
$status = ProcessStatus::FINISHED;
|
||||
}
|
||||
if ($processResult->exitCode() != 0 && ! $this->ignore_errors) {
|
||||
} else {
|
||||
$status = ProcessStatus::ERROR;
|
||||
}
|
||||
// if (($processResult->exitCode() == 0 && $this->is_finished) || $this->activity->properties->get('status') === ProcessStatus::FINISHED->value) {
|
||||
// $status = ProcessStatus::FINISHED;
|
||||
// }
|
||||
// if ($processResult->exitCode() != 0 && !$this->ignore_errors) {
|
||||
// $status = ProcessStatus::ERROR;
|
||||
// }
|
||||
}
|
||||
|
||||
$this->activity->properties = $this->activity->properties->merge([
|
||||
@@ -110,9 +103,6 @@ class RunRemoteProcess
|
||||
'status' => $status->value,
|
||||
]);
|
||||
$this->activity->save();
|
||||
if ($processResult->exitCode() != 0 && ! $this->ignore_errors) {
|
||||
throw new \RuntimeException($processResult->errorOutput(), $processResult->exitCode());
|
||||
}
|
||||
if ($this->call_event_on_finish) {
|
||||
try {
|
||||
if ($this->call_event_data) {
|
||||
@@ -128,6 +118,9 @@ class RunRemoteProcess
|
||||
Log::error('Error calling event: '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
if ($processResult->exitCode() != 0 && ! $this->ignore_errors) {
|
||||
throw new \RuntimeException($processResult->errorOutput(), $processResult->exitCode());
|
||||
}
|
||||
|
||||
return $processResult;
|
||||
}
|
||||
|
@@ -39,6 +39,11 @@ class CleanupStuckedResources extends Command
|
||||
$servers = Server::all()->filter(function ($server) {
|
||||
return $server->isFunctional();
|
||||
});
|
||||
if (isCloud()) {
|
||||
$servers = $servers->filter(function ($server) {
|
||||
return data_get($server->team->subscription, 'stripe_invoice_paid', false) === true;
|
||||
});
|
||||
}
|
||||
foreach ($servers as $server) {
|
||||
CleanupHelperContainersJob::dispatch($server);
|
||||
}
|
||||
|
178
app/Console/Commands/HorizonManage.php
Normal file
178
app/Console/Commands/HorizonManage.php
Normal file
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Enums\ApplicationDeploymentStatus;
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
use App\Repositories\CustomJobRepository;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Laravel\Horizon\Contracts\JobRepository;
|
||||
use Laravel\Horizon\Contracts\MetricsRepository;
|
||||
use Laravel\Horizon\Repositories\RedisJobRepository;
|
||||
|
||||
use function Laravel\Prompts\multiselect;
|
||||
use function Laravel\Prompts\select;
|
||||
use function Laravel\Prompts\table;
|
||||
use function Laravel\Prompts\text;
|
||||
|
||||
class HorizonManage extends Command
|
||||
{
|
||||
protected $signature = 'horizon:manage {--can-i-restart-this-worker} {--job-status=}';
|
||||
|
||||
protected $description = 'Manage horizon';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
if ($this->option('can-i-restart-this-worker')) {
|
||||
return $this->isThereAJobInProgress();
|
||||
}
|
||||
|
||||
if ($this->option('job-status')) {
|
||||
return $this->getJobStatus($this->option('job-status'));
|
||||
}
|
||||
|
||||
$action = select(
|
||||
label: 'What to do?',
|
||||
options: [
|
||||
'pending' => 'Pending Jobs',
|
||||
'running' => 'Running Jobs',
|
||||
'can-i-restart-this-worker' => 'Can I restart this worker?',
|
||||
'job-status' => 'Job Status',
|
||||
'workers' => 'Workers',
|
||||
'failed' => 'Failed Jobs',
|
||||
'failed-delete' => 'Failed Jobs - Delete',
|
||||
'purge-queues' => 'Purge Queues',
|
||||
]
|
||||
);
|
||||
|
||||
if ($action === 'can-i-restart-this-worker') {
|
||||
$this->isThereAJobInProgress();
|
||||
}
|
||||
|
||||
if ($action === 'job-status') {
|
||||
$jobId = text('Which job to check?');
|
||||
$jobStatus = $this->getJobStatus($jobId);
|
||||
$this->info('Job Status: '.$jobStatus);
|
||||
}
|
||||
|
||||
if ($action === 'pending') {
|
||||
$pendingJobs = app(JobRepository::class)->getPending();
|
||||
$pendingJobsTable = [];
|
||||
if (count($pendingJobs) === 0) {
|
||||
$this->info('No pending jobs found.');
|
||||
|
||||
return;
|
||||
}
|
||||
foreach ($pendingJobs as $pendingJob) {
|
||||
$pendingJobsTable[] = [
|
||||
'id' => $pendingJob->id,
|
||||
'name' => $pendingJob->name,
|
||||
'status' => $pendingJob->status,
|
||||
'reserved_at' => $pendingJob->reserved_at ? now()->parse($pendingJob->reserved_at)->format('Y-m-d H:i:s') : null,
|
||||
];
|
||||
}
|
||||
table($pendingJobsTable);
|
||||
}
|
||||
|
||||
if ($action === 'failed') {
|
||||
$failedJobs = app(JobRepository::class)->getFailed();
|
||||
$failedJobsTable = [];
|
||||
if (count($failedJobs) === 0) {
|
||||
$this->info('No failed jobs found.');
|
||||
|
||||
return;
|
||||
}
|
||||
foreach ($failedJobs as $failedJob) {
|
||||
$failedJobsTable[] = [
|
||||
'id' => $failedJob->id,
|
||||
'name' => $failedJob->name,
|
||||
'failed_at' => $failedJob->failed_at ? now()->parse($failedJob->failed_at)->format('Y-m-d H:i:s') : null,
|
||||
];
|
||||
}
|
||||
table($failedJobsTable);
|
||||
}
|
||||
|
||||
if ($action === 'failed-delete') {
|
||||
$failedJobs = app(JobRepository::class)->getFailed();
|
||||
$failedJobsTable = [];
|
||||
foreach ($failedJobs as $failedJob) {
|
||||
$failedJobsTable[] = [
|
||||
'id' => $failedJob->id,
|
||||
'name' => $failedJob->name,
|
||||
'failed_at' => $failedJob->failed_at ? now()->parse($failedJob->failed_at)->format('Y-m-d H:i:s') : null,
|
||||
];
|
||||
}
|
||||
app(MetricsRepository::class)->clear();
|
||||
if (count($failedJobsTable) === 0) {
|
||||
$this->info('No failed jobs found.');
|
||||
|
||||
return;
|
||||
}
|
||||
$jobIds = multiselect(
|
||||
label: 'Which job to delete?',
|
||||
options: collect($failedJobsTable)->mapWithKeys(fn ($job) => [$job['id'] => $job['id'].' - '.$job['name']])->toArray(),
|
||||
);
|
||||
foreach ($jobIds as $jobId) {
|
||||
Artisan::queue('horizon:forget', ['id' => $jobId]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($action === 'running') {
|
||||
$redisJobRepository = app(CustomJobRepository::class);
|
||||
$runningJobs = $redisJobRepository->getReservedJobs();
|
||||
$runningJobsTable = [];
|
||||
if (count($runningJobs) === 0) {
|
||||
$this->info('No running jobs found.');
|
||||
|
||||
return;
|
||||
}
|
||||
foreach ($runningJobs as $runningJob) {
|
||||
$runningJobsTable[] = [
|
||||
'id' => $runningJob->id,
|
||||
'name' => $runningJob->name,
|
||||
'reserved_at' => $runningJob->reserved_at ? now()->parse($runningJob->reserved_at)->format('Y-m-d H:i:s') : null,
|
||||
];
|
||||
}
|
||||
table($runningJobsTable);
|
||||
}
|
||||
|
||||
if ($action === 'workers') {
|
||||
$redisJobRepository = app(CustomJobRepository::class);
|
||||
$workers = $redisJobRepository->getHorizonWorkers();
|
||||
$workersTable = [];
|
||||
foreach ($workers as $worker) {
|
||||
$workersTable[] = [
|
||||
'name' => $worker->name,
|
||||
];
|
||||
}
|
||||
table($workersTable);
|
||||
}
|
||||
|
||||
if ($action === 'purge-queues') {
|
||||
$getQueues = app(CustomJobRepository::class)->getQueues();
|
||||
$queueName = select(
|
||||
label: 'Which queue to purge?',
|
||||
options: $getQueues,
|
||||
);
|
||||
$redisJobRepository = app(RedisJobRepository::class);
|
||||
$redisJobRepository->purge($queueName);
|
||||
}
|
||||
}
|
||||
|
||||
public function isThereAJobInProgress()
|
||||
{
|
||||
$runningJobs = ApplicationDeploymentQueue::where('horizon_job_worker', gethostname())->where('status', ApplicationDeploymentStatus::IN_PROGRESS->value)->get();
|
||||
$count = $runningJobs->count();
|
||||
if ($count === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getJobStatus(string $jobId)
|
||||
{
|
||||
return getJobStatus($jobId);
|
||||
}
|
||||
}
|
@@ -91,7 +91,13 @@ class Kernel extends ConsoleKernel
|
||||
|
||||
private function pullImages(): void
|
||||
{
|
||||
$servers = $this->allServers->whereRelation('settings', 'is_usable', true)->whereRelation('settings', 'is_reachable', true)->get();
|
||||
if (isCloud()) {
|
||||
$servers = $this->allServers->whereRelation('team.subscription', 'stripe_invoice_paid', true)->whereRelation('settings', 'is_usable', true)->whereRelation('settings', 'is_reachable', true)->get();
|
||||
$own = Team::find(0)->servers;
|
||||
$servers = $servers->merge($own);
|
||||
} else {
|
||||
$servers = $this->allServers->whereRelation('settings', 'is_usable', true)->whereRelation('settings', 'is_reachable', true)->get();
|
||||
}
|
||||
foreach ($servers as $server) {
|
||||
if ($server->isSentinelEnabled()) {
|
||||
$this->scheduleInstance->job(function () use ($server) {
|
||||
@@ -124,7 +130,7 @@ class Kernel extends ConsoleKernel
|
||||
private function checkResources(): void
|
||||
{
|
||||
if (isCloud()) {
|
||||
$servers = $this->allServers->whereHas('team.subscription')->get();
|
||||
$servers = $this->allServers->whereRelation('team.subscription', 'stripe_invoice_paid', true)->get();
|
||||
$own = Team::find(0)->servers;
|
||||
$servers = $servers->merge($own);
|
||||
} else {
|
||||
@@ -171,18 +177,40 @@ class Kernel extends ConsoleKernel
|
||||
if ($scheduled_backups->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
$finalScheduledBackups = collect();
|
||||
foreach ($scheduled_backups as $scheduled_backup) {
|
||||
if (is_null(data_get($scheduled_backup, 'database'))) {
|
||||
if (blank(data_get($scheduled_backup, 'database'))) {
|
||||
$scheduled_backup->delete();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$server = $scheduled_backup->server();
|
||||
if (blank($server)) {
|
||||
$scheduled_backup->delete();
|
||||
|
||||
if (is_null($server)) {
|
||||
continue;
|
||||
}
|
||||
if ($server->isFunctional() === false) {
|
||||
continue;
|
||||
}
|
||||
if (isCloud() && data_get($server->team->subscription, 'stripe_invoice_paid', false) === false && $server->team->id !== 0) {
|
||||
continue;
|
||||
}
|
||||
$finalScheduledBackups->push($scheduled_backup);
|
||||
}
|
||||
|
||||
foreach ($finalScheduledBackups as $scheduled_backup) {
|
||||
if (isset(VALID_CRON_STRINGS[$scheduled_backup->frequency])) {
|
||||
$scheduled_backup->frequency = VALID_CRON_STRINGS[$scheduled_backup->frequency];
|
||||
}
|
||||
|
||||
$server = $scheduled_backup->server();
|
||||
$serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone);
|
||||
|
||||
if (validate_timezone($serverTimezone) === false) {
|
||||
$serverTimezone = config('app.timezone');
|
||||
}
|
||||
|
||||
if (isset(VALID_CRON_STRINGS[$scheduled_backup->frequency])) {
|
||||
$scheduled_backup->frequency = VALID_CRON_STRINGS[$scheduled_backup->frequency];
|
||||
}
|
||||
@@ -199,35 +227,52 @@ class Kernel extends ConsoleKernel
|
||||
if ($scheduled_tasks->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
$finalScheduledTasks = collect();
|
||||
foreach ($scheduled_tasks as $scheduled_task) {
|
||||
$service = $scheduled_task->service;
|
||||
$application = $scheduled_task->application;
|
||||
|
||||
if (! $application && ! $service) {
|
||||
$server = $scheduled_task->server();
|
||||
if (blank($server)) {
|
||||
$scheduled_task->delete();
|
||||
|
||||
continue;
|
||||
}
|
||||
if ($application) {
|
||||
if (str($application->status)->contains('running') === false) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ($service) {
|
||||
if (str($service->status)->contains('running') === false) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$server = $scheduled_task->server();
|
||||
if (! $server) {
|
||||
if ($server->isFunctional() === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isCloud() && data_get($server->team->subscription, 'stripe_invoice_paid', false) === false && $server->team->id !== 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! $service && ! $application) {
|
||||
$scheduled_task->delete();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($application && str($application->status)->contains('running') === false) {
|
||||
continue;
|
||||
}
|
||||
if ($service && str($service->status)->contains('running') === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$finalScheduledTasks->push($scheduled_task);
|
||||
}
|
||||
|
||||
foreach ($finalScheduledTasks as $scheduled_task) {
|
||||
$server = $scheduled_task->server();
|
||||
if (isset(VALID_CRON_STRINGS[$scheduled_task->frequency])) {
|
||||
$scheduled_task->frequency = VALID_CRON_STRINGS[$scheduled_task->frequency];
|
||||
}
|
||||
$serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone);
|
||||
|
||||
if (validate_timezone($serverTimezone) === false) {
|
||||
$serverTimezone = config('app.timezone');
|
||||
}
|
||||
$this->scheduleInstance->job(new ScheduledTaskJob(
|
||||
task: $scheduled_task
|
||||
))->cron($scheduled_task->frequency)->timezone($serverTimezone)->onOneServer();
|
||||
|
19
app/Contracts/CustomJobRepositoryInterface.php
Normal file
19
app/Contracts/CustomJobRepositoryInterface.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Contracts;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use Laravel\Horizon\Contracts\JobRepository;
|
||||
|
||||
interface CustomJobRepositoryInterface extends JobRepository
|
||||
{
|
||||
/**
|
||||
* Get all jobs with a specific status.
|
||||
*/
|
||||
public function getJobsByStatus(string $status): Collection;
|
||||
|
||||
/**
|
||||
* Get the count of jobs with a specific status.
|
||||
*/
|
||||
public function countJobsByStatus(string $status): int;
|
||||
}
|
@@ -45,8 +45,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
public static int $batch_counter = 0;
|
||||
|
||||
private int $application_deployment_queue_id;
|
||||
|
||||
private bool $newVersionIsHealthy = false;
|
||||
|
||||
private ApplicationDeploymentQueue $application_deployment_queue;
|
||||
@@ -168,18 +166,23 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
private bool $preserveRepository = false;
|
||||
|
||||
public function __construct(int $application_deployment_queue_id)
|
||||
public function tags()
|
||||
{
|
||||
// Do not remove this one, it needs to properly identify which worker is running the job
|
||||
return ['App\Models\ApplicationDeploymentQueue:'.$this->application_deployment_queue_id];
|
||||
}
|
||||
|
||||
public function __construct(public int $application_deployment_queue_id)
|
||||
{
|
||||
$this->onQueue('high');
|
||||
|
||||
$this->application_deployment_queue = ApplicationDeploymentQueue::find($this->application_deployment_queue_id);
|
||||
$this->nixpacks_plan_json = collect([]);
|
||||
|
||||
$this->application_deployment_queue = ApplicationDeploymentQueue::find($application_deployment_queue_id);
|
||||
$this->application = Application::find($this->application_deployment_queue->application_id);
|
||||
$this->build_pack = data_get($this->application, 'build_pack');
|
||||
$this->build_args = collect([]);
|
||||
|
||||
$this->application_deployment_queue_id = $application_deployment_queue_id;
|
||||
$this->deployment_uuid = $this->application_deployment_queue->deployment_uuid;
|
||||
$this->pull_request_id = $this->application_deployment_queue->pull_request_id;
|
||||
$this->commit = $this->application_deployment_queue->commit;
|
||||
@@ -237,15 +240,11 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
}
|
||||
}
|
||||
|
||||
public function tags(): array
|
||||
{
|
||||
return ['server:'.gethostname()];
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$this->application_deployment_queue->update([
|
||||
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
|
||||
'horizon_job_worker' => gethostname(),
|
||||
]);
|
||||
if ($this->server->isFunctional() === false) {
|
||||
$this->application_deployment_queue->addLogEntry('Server is not functional.');
|
||||
@@ -2391,7 +2390,8 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
queue_next_deployment($this->application);
|
||||
// If the deployment is cancelled by the user, don't update the status
|
||||
if (
|
||||
$this->application_deployment_queue->status !== ApplicationDeploymentStatus::CANCELLED_BY_USER->value && $this->application_deployment_queue->status !== ApplicationDeploymentStatus::FAILED->value
|
||||
$this->application_deployment_queue->status !== ApplicationDeploymentStatus::CANCELLED_BY_USER->value &&
|
||||
$this->application_deployment_queue->status !== ApplicationDeploymentStatus::FAILED->value
|
||||
) {
|
||||
$this->application_deployment_queue->update([
|
||||
'status' => $status,
|
||||
|
@@ -24,7 +24,7 @@ class CheckAndStartSentinelJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$latestVersion = get_latest_sentinel_version();
|
||||
|
||||
// Check if sentinel is running
|
||||
$sentinelFound = instant_remote_process(['docker inspect coolify-sentinel'], $this->server, false);
|
||||
$sentinelFound = instant_remote_process_with_timeout(['docker inspect coolify-sentinel'], $this->server, false, 10);
|
||||
$sentinelFoundJson = json_decode($sentinelFound, true);
|
||||
$sentinelStatus = data_get($sentinelFoundJson, '0.State.Status', 'exited');
|
||||
if ($sentinelStatus !== 'running') {
|
||||
@@ -33,7 +33,7 @@ class CheckAndStartSentinelJob implements ShouldBeEncrypted, ShouldQueue
|
||||
return;
|
||||
}
|
||||
// If sentinel is running, check if it needs an update
|
||||
$runningVersion = instant_remote_process(['docker exec coolify-sentinel sh -c "curl http://127.0.0.1:8888/api/version"'], $this->server, false);
|
||||
$runningVersion = instant_remote_process_with_timeout(['docker exec coolify-sentinel sh -c "curl http://127.0.0.1:8888/api/version"'], $this->server, false);
|
||||
if (empty($runningVersion)) {
|
||||
$runningVersion = '0.0.0';
|
||||
}
|
||||
|
@@ -20,11 +20,11 @@ class CleanupHelperContainersJob implements ShouldBeEncrypted, ShouldBeUnique, S
|
||||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
$containers = instant_remote_process(['docker container ps --format \'{{json .}}\' | jq -s \'map(select(.Image | contains("ghcr.io/coollabsio/coolify-helper")))\''], $this->server, false);
|
||||
$containers = instant_remote_process_with_timeout(['docker container ps --format \'{{json .}}\' | jq -s \'map(select(.Image | contains("ghcr.io/coollabsio/coolify-helper")))\''], $this->server, false);
|
||||
$containerIds = collect(json_decode($containers))->pluck('ID');
|
||||
if ($containerIds->count() > 0) {
|
||||
foreach ($containerIds as $containerId) {
|
||||
instant_remote_process(['docker container rm -f '.$containerId], $this->server, false);
|
||||
instant_remote_process_with_timeout(['docker container rm -f '.$containerId], $this->server, false);
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
|
@@ -19,6 +19,7 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
@@ -68,6 +69,11 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
public bool $foundLogDrainContainer = false;
|
||||
|
||||
public function middleware(): array
|
||||
{
|
||||
return [(new WithoutOverlapping($this->server->uuid))->dontRelease()];
|
||||
}
|
||||
|
||||
public function backoff(): int
|
||||
{
|
||||
return isDev() ? 1 : 3;
|
||||
|
@@ -42,14 +42,8 @@ class ActivityMonitor extends Component
|
||||
public function polling()
|
||||
{
|
||||
$this->hydrateActivity();
|
||||
// $this->setStatus(ProcessStatus::IN_PROGRESS);
|
||||
$exit_code = data_get($this->activity, 'properties.exitCode');
|
||||
if ($exit_code !== null) {
|
||||
// if ($exit_code === 0) {
|
||||
// // $this->setStatus(ProcessStatus::FINISHED);
|
||||
// } else {
|
||||
// // $this->setStatus(ProcessStatus::ERROR);
|
||||
// }
|
||||
$this->isPollingActive = false;
|
||||
if ($exit_code === 0) {
|
||||
if ($this->eventToDispatch !== null) {
|
||||
@@ -70,12 +64,4 @@ class ActivityMonitor extends Component
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// protected function setStatus($status)
|
||||
// {
|
||||
// $this->activity->properties = $this->activity->properties->merge([
|
||||
// 'status' => $status,
|
||||
// ]);
|
||||
// $this->activity->save();
|
||||
// }
|
||||
}
|
||||
|
@@ -83,9 +83,7 @@ class Docker extends Component
|
||||
]);
|
||||
}
|
||||
}
|
||||
$connectProxyToDockerNetworks = connectProxyToNetworks($this->selectedServer);
|
||||
instant_remote_process($connectProxyToDockerNetworks, $this->selectedServer, false);
|
||||
$this->dispatch('reloadWindow');
|
||||
$this->redirect(route('destination.show', $docker->uuid));
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
@@ -14,6 +14,8 @@ class Show extends Component
|
||||
|
||||
public string $deployment_uuid;
|
||||
|
||||
public string $horizon_job_status;
|
||||
|
||||
public $isKeepAliveOn = true;
|
||||
|
||||
protected $listeners = ['refreshQueue'];
|
||||
@@ -44,7 +46,9 @@ class Show extends Component
|
||||
}
|
||||
$this->application = $application;
|
||||
$this->application_deployment_queue = $application_deployment_queue;
|
||||
$this->horizon_job_status = $this->application_deployment_queue->getHorizonJobStatus();
|
||||
$this->deployment_uuid = $deploymentUuid;
|
||||
$this->isKeepAliveOn();
|
||||
}
|
||||
|
||||
public function refreshQueue()
|
||||
@@ -52,13 +56,21 @@ class Show extends Component
|
||||
$this->application_deployment_queue->refresh();
|
||||
}
|
||||
|
||||
private function isKeepAliveOn()
|
||||
{
|
||||
if (data_get($this->application_deployment_queue, 'status') === 'finished' || data_get($this->application_deployment_queue, 'status') === 'failed') {
|
||||
$this->isKeepAliveOn = false;
|
||||
} else {
|
||||
$this->isKeepAliveOn = true;
|
||||
}
|
||||
}
|
||||
|
||||
public function polling()
|
||||
{
|
||||
$this->dispatch('deploymentFinished');
|
||||
$this->application_deployment_queue->refresh();
|
||||
if (data_get($this->application_deployment_queue, 'status') === 'finished' || data_get($this->application_deployment_queue, 'status') === 'failed') {
|
||||
$this->isKeepAliveOn = false;
|
||||
}
|
||||
$this->horizon_job_status = $this->application_deployment_queue->getHorizonJobStatus();
|
||||
$this->isKeepAliveOn();
|
||||
}
|
||||
|
||||
public function getLogLinesProperty()
|
||||
|
@@ -83,8 +83,10 @@ class BackupExecutions extends Component
|
||||
|
||||
public function refreshBackupExecutions(): void
|
||||
{
|
||||
if ($this->backup) {
|
||||
$this->executions = $this->backup->executions()->get();
|
||||
if ($this->backup && $this->backup->exists) {
|
||||
$this->executions = $this->backup->executions()->get()->toArray();
|
||||
} else {
|
||||
$this->executions = [];
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -99,7 +99,7 @@ class Configuration extends Component
|
||||
$this->service->databases->each(function ($database) {
|
||||
$database->refresh();
|
||||
});
|
||||
$this->dispatch('$refresh');
|
||||
$this->dispatch('refresh');
|
||||
} catch (\Exception $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
@@ -5,6 +5,7 @@ namespace App\Livewire\Project\Service;
|
||||
use App\Actions\Service\StartService;
|
||||
use App\Actions\Service\StopService;
|
||||
use App\Actions\Shared\PullImage;
|
||||
use App\Enums\ProcessStatus;
|
||||
use App\Events\ServiceStatusChanged;
|
||||
use App\Models\Service;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
@@ -68,11 +69,9 @@ class Navbar extends Component
|
||||
public function checkDeployments()
|
||||
{
|
||||
try {
|
||||
// TODO: This is a temporary solution. We need to refactor this.
|
||||
// We need to delete null bytes somehow.
|
||||
$activity = Activity::where('properties->type_uuid', $this->service->uuid)->latest()->first();
|
||||
$status = data_get($activity, 'properties.status');
|
||||
if ($status === 'queued' || $status === 'in_progress') {
|
||||
if ($status === ProcessStatus::QUEUED->value || $status === ProcessStatus::IN_PROGRESS->value) {
|
||||
$this->isDeploymentProgress = true;
|
||||
} else {
|
||||
$this->isDeploymentProgress = false;
|
||||
@@ -80,25 +79,46 @@ class Navbar extends Component
|
||||
} catch (\Throwable) {
|
||||
$this->isDeploymentProgress = false;
|
||||
}
|
||||
|
||||
return $this->isDeploymentProgress;
|
||||
}
|
||||
|
||||
public function start()
|
||||
{
|
||||
$this->checkDeployments();
|
||||
if ($this->isDeploymentProgress) {
|
||||
$this->dispatch('error', 'There is a deployment in progress.');
|
||||
|
||||
return;
|
||||
}
|
||||
$this->service->parse();
|
||||
$activity = StartService::run($this->service);
|
||||
$this->dispatch('activityMonitor', $activity->id);
|
||||
}
|
||||
|
||||
public function stop()
|
||||
public function forceDeploy()
|
||||
{
|
||||
StopService::run($this->service, false, $this->docker_cleanup);
|
||||
ServiceStatusChanged::dispatch();
|
||||
try {
|
||||
$activities = Activity::where('properties->type_uuid', $this->service->uuid)->where('properties->status', ProcessStatus::IN_PROGRESS->value)->orWhere('properties->status', ProcessStatus::QUEUED->value)->get();
|
||||
foreach ($activities as $activity) {
|
||||
$activity->properties->status = ProcessStatus::ERROR->value;
|
||||
$activity->save();
|
||||
}
|
||||
$this->service->parse();
|
||||
$activity = StartService::run($this->service);
|
||||
$this->dispatch('activityMonitor', $activity->id);
|
||||
} catch (\Exception $e) {
|
||||
$this->dispatch('error', $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function stop($cleanupContainers = false)
|
||||
{
|
||||
try {
|
||||
StopService::run($this->service, false, $this->docker_cleanup);
|
||||
ServiceStatusChanged::dispatch();
|
||||
if ($cleanupContainers) {
|
||||
$this->dispatch('success', 'Containers cleaned up.');
|
||||
} else {
|
||||
$this->dispatch('success', 'Service stopped.');
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->dispatch('error', $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function restart()
|
||||
|
@@ -70,6 +70,11 @@ class ApplicationDeploymentQueue extends Model
|
||||
return collect(json_decode($this->logs))->where('name', $name)->first()?->output ?? null;
|
||||
}
|
||||
|
||||
public function getHorizonJobStatus()
|
||||
{
|
||||
return getJobStatus($this->horizon_job_id);
|
||||
}
|
||||
|
||||
public function commitMessage()
|
||||
{
|
||||
if (empty($this->commit_message) || is_null($this->commit_message)) {
|
||||
|
@@ -54,6 +54,8 @@ class Server extends BaseModel
|
||||
|
||||
public static $batch_counter = 0;
|
||||
|
||||
protected $appends = ['is_coolify_host'];
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
static::saving(function ($server) {
|
||||
@@ -156,6 +158,15 @@ class Server extends BaseModel
|
||||
return 'server';
|
||||
}
|
||||
|
||||
protected function isCoolifyHost(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: function () {
|
||||
return $this->id === 0;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public static function isReachable()
|
||||
{
|
||||
return Server::ownedByCurrentTeam()->whereRelation('settings', 'is_reachable', true);
|
||||
@@ -656,9 +667,9 @@ $schema://$host {
|
||||
$containers = collect([]);
|
||||
$containerReplicates = collect([]);
|
||||
if ($this->isSwarm()) {
|
||||
$containers = instant_remote_process(["docker service inspect $(docker service ls -q) --format '{{json .}}'"], $this, false);
|
||||
$containers = instant_remote_process_with_timeout(["docker service inspect $(docker service ls -q) --format '{{json .}}'"], $this, false);
|
||||
$containers = format_docker_command_output_to_json($containers);
|
||||
$containerReplicates = instant_remote_process(["docker service ls --format '{{json .}}'"], $this, false);
|
||||
$containerReplicates = instant_remote_process_with_timeout(["docker service ls --format '{{json .}}'"], $this, false);
|
||||
if ($containerReplicates) {
|
||||
$containerReplicates = format_docker_command_output_to_json($containerReplicates);
|
||||
foreach ($containerReplicates as $containerReplica) {
|
||||
@@ -682,7 +693,7 @@ $schema://$host {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$containers = instant_remote_process(["docker container inspect $(docker container ls -aq) --format '{{json .}}'"], $this, false);
|
||||
$containers = instant_remote_process_with_timeout(["docker container inspect $(docker container ls -aq) --format '{{json .}}'"], $this, false);
|
||||
$containers = format_docker_command_output_to_json($containers);
|
||||
$containerReplicates = collect([]);
|
||||
}
|
||||
|
@@ -1050,10 +1050,11 @@ class Service extends BaseModel
|
||||
$fields->put('MySQL', $data->toArray());
|
||||
break;
|
||||
case $image->contains('mariadb'):
|
||||
$userVariables = ['SERVICE_USER_MARIADB', 'SERVICE_USER_WORDPRESS', '_APP_DB_USER', 'SERVICE_USER_MYSQL', 'MYSQL_USER'];
|
||||
$userVariables = ['SERVICE_USER_MARIADB', 'SERVICE_USER_WORDPRESS', 'SERVICE_USER_MYSQL', 'MYSQL_USER'];
|
||||
$passwordVariables = ['SERVICE_PASSWORD_MARIADB', 'SERVICE_PASSWORD_WORDPRESS', '_APP_DB_PASS', 'MYSQL_PASSWORD'];
|
||||
$rootPasswordVariables = ['SERVICE_PASSWORD_MARIADBROOT', 'SERVICE_PASSWORD_ROOT', '_APP_DB_ROOT_PASS', 'MYSQL_ROOT_PASSWORD'];
|
||||
$dbNameVariables = ['SERVICE_DATABASE_MARIADB', 'SERVICE_DATABASE_WORDPRESS', '_APP_DB_SCHEMA', 'MYSQL_DATABASE'];
|
||||
|
||||
$mariadb_user = $this->environment_variables()->whereIn('key', $userVariables)->first();
|
||||
$mariadb_password = $this->environment_variables()->whereIn('key', $passwordVariables)->first();
|
||||
$mariadb_root_password = $this->environment_variables()->whereIn('key', $rootPasswordVariables)->first();
|
||||
@@ -1102,6 +1103,23 @@ class Service extends BaseModel
|
||||
break;
|
||||
}
|
||||
}
|
||||
$fields = collect($fields)->map(function ($extraFields) {
|
||||
if (is_array($extraFields)) {
|
||||
$extraFields = collect($extraFields)->map(function ($field) {
|
||||
if (filled($field['value']) && str($field['value'])->startsWith('$SERVICE_')) {
|
||||
$searchValue = str($field['value'])->after('$')->value;
|
||||
$newValue = $this->environment_variables()->where('key', $searchValue)->first();
|
||||
if ($newValue) {
|
||||
$field['value'] = $newValue->value;
|
||||
}
|
||||
}
|
||||
|
||||
return $field;
|
||||
});
|
||||
}
|
||||
|
||||
return $extraFields;
|
||||
});
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
@@ -6,6 +6,19 @@ class StandaloneDocker extends BaseModel
|
||||
{
|
||||
protected $guarded = [];
|
||||
|
||||
protected static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
static::created(function ($newStandaloneDocker) {
|
||||
$server = $newStandaloneDocker->server;
|
||||
instant_remote_process([
|
||||
"docker network inspect $newStandaloneDocker->network >/dev/null 2>&1 || docker network create --driver overlay --attachable $newStandaloneDocker->network >/dev/null",
|
||||
], $server, false);
|
||||
$connectProxyToDockerNetworks = connectProxyToNetworks($server);
|
||||
instant_remote_process($connectProxyToDockerNetworks, $server, false);
|
||||
});
|
||||
}
|
||||
|
||||
public function applications()
|
||||
{
|
||||
return $this->morphMany(Application::class, 'destination');
|
||||
|
@@ -247,8 +247,17 @@ class Team extends Model implements SendsDiscord, SendsEmail, SendsPushover, Sen
|
||||
public function sources()
|
||||
{
|
||||
$sources = collect([]);
|
||||
$github_apps = $this->hasMany(GithubApp::class)->whereisPublic(false)->get();
|
||||
$gitlab_apps = $this->hasMany(GitlabApp::class)->whereisPublic(false)->get();
|
||||
$github_apps = GithubApp::where(function ($query) {
|
||||
$query->where('team_id', $this->id)
|
||||
->Where('is_public', false)
|
||||
->orWhere('is_system_wide', true);
|
||||
})->get();
|
||||
|
||||
$gitlab_apps = GitlabApp::where(function ($query) {
|
||||
$query->where('team_id', $this->id)
|
||||
->Where('is_public', false)
|
||||
->orWhere('is_system_wide', true);
|
||||
})->get();
|
||||
|
||||
return $sources->merge($github_apps)->merge($gitlab_apps);
|
||||
}
|
||||
|
@@ -2,15 +2,52 @@
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Contracts\CustomJobRepositoryInterface;
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
use App\Models\User;
|
||||
use App\Repositories\CustomJobRepository;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Horizon\Contracts\JobRepository;
|
||||
use Laravel\Horizon\Events\JobReserved;
|
||||
use Laravel\Horizon\HorizonApplicationServiceProvider;
|
||||
|
||||
class HorizonServiceProvider extends HorizonApplicationServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->singleton(JobRepository::class, CustomJobRepository::class);
|
||||
$this->app->singleton(CustomJobRepositoryInterface::class, CustomJobRepository::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
parent::boot();
|
||||
Event::listen(function (JobReserved $event) {
|
||||
$payload = $event->payload->decoded;
|
||||
$jobName = $payload['displayName'];
|
||||
if ($jobName === 'App\Jobs\ApplicationDeploymentJob') {
|
||||
$tags = $payload['tags'];
|
||||
$id = $payload['id'];
|
||||
$deploymentQueueId = collect($tags)->first(function ($tag) {
|
||||
return str_contains($tag, 'App\Models\ApplicationDeploymentQueue');
|
||||
});
|
||||
if (blank($deploymentQueueId)) {
|
||||
return;
|
||||
}
|
||||
$deploymentQueueId = explode(':', $deploymentQueueId)[1];
|
||||
$deploymentQueue = ApplicationDeploymentQueue::find($deploymentQueueId);
|
||||
$deploymentQueue->update([
|
||||
'horizon_job_id' => $id,
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected function gate(): void
|
||||
|
51
app/Repositories/CustomJobRepository.php
Normal file
51
app/Repositories/CustomJobRepository.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Contracts\CustomJobRepositoryInterface;
|
||||
use Illuminate\Support\Collection;
|
||||
use Laravel\Horizon\Repositories\RedisJobRepository;
|
||||
use Laravel\Horizon\Repositories\RedisMasterSupervisorRepository;
|
||||
|
||||
class CustomJobRepository extends RedisJobRepository implements CustomJobRepositoryInterface
|
||||
{
|
||||
public function getHorizonWorkers()
|
||||
{
|
||||
$redisMasterSupervisorRepository = app(RedisMasterSupervisorRepository::class);
|
||||
|
||||
return $redisMasterSupervisorRepository->all();
|
||||
}
|
||||
|
||||
public function getReservedJobs(): Collection
|
||||
{
|
||||
return $this->getJobsByStatus('reserved');
|
||||
}
|
||||
|
||||
public function getJobsByStatus(string $status): Collection
|
||||
{
|
||||
$jobs = new Collection;
|
||||
|
||||
$this->getRecent()->each(function ($job) use ($jobs, $status) {
|
||||
if ($job->status === $status) {
|
||||
$jobs->push($job);
|
||||
}
|
||||
});
|
||||
|
||||
return $jobs;
|
||||
}
|
||||
|
||||
public function countJobsByStatus(string $status): int
|
||||
{
|
||||
return $this->getJobsByStatus($status)->count();
|
||||
}
|
||||
|
||||
public function getQueues(): array
|
||||
{
|
||||
$queues = $this->connection()->keys('queue:*');
|
||||
$queues = array_map(function ($queue) {
|
||||
return explode(':', $queue)[2];
|
||||
}, $queues);
|
||||
|
||||
return $queues;
|
||||
}
|
||||
}
|
26
app/View/Components/services/advanced.php
Normal file
26
app/View/Components/services/advanced.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\View\Components\services;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class advanced extends Component
|
||||
{
|
||||
/**
|
||||
* Create a new component instance.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the view / contents that represent the component.
|
||||
*/
|
||||
public function render(): View|Closure|string
|
||||
{
|
||||
return view('components.services.advanced');
|
||||
}
|
||||
}
|
@@ -71,6 +71,31 @@ function instant_scp(string $source, string $dest, Server $server, $throwError =
|
||||
return $output === 'null' ? null : $output;
|
||||
}
|
||||
|
||||
function instant_remote_process_with_timeout(Collection|array $command, Server $server, bool $throwError = true, bool $no_sudo = false): ?string
|
||||
{
|
||||
$command = $command instanceof Collection ? $command->toArray() : $command;
|
||||
if ($server->isNonRoot() && ! $no_sudo) {
|
||||
$command = parseCommandsByLineForSudo(collect($command), $server);
|
||||
}
|
||||
$command_string = implode("\n", $command);
|
||||
|
||||
// $start_time = microtime(true);
|
||||
$sshCommand = SshMultiplexingHelper::generateSshCommand($server, $command_string);
|
||||
$process = Process::timeout(30)->run($sshCommand);
|
||||
// $end_time = microtime(true);
|
||||
|
||||
// $execution_time = ($end_time - $start_time) * 1000; // Convert to milliseconds
|
||||
// ray('SSH command execution time:', $execution_time.' ms')->orange();
|
||||
|
||||
$output = trim($process->output());
|
||||
$exitCode = $process->exitCode();
|
||||
|
||||
if ($exitCode !== 0) {
|
||||
return $throwError ? excludeCertainErrors($process->errorOutput(), $exitCode) : null;
|
||||
}
|
||||
|
||||
return $output === 'null' ? null : $output;
|
||||
}
|
||||
function instant_remote_process(Collection|array $command, Server $server, bool $throwError = true, bool $no_sudo = false): ?string
|
||||
{
|
||||
$command = $command instanceof Collection ? $command->toArray() : $command;
|
||||
|
@@ -41,6 +41,7 @@ use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Stringable;
|
||||
use Laravel\Horizon\Contracts\JobRepository;
|
||||
use Lcobucci\JWT\Encoding\ChainedFormatter;
|
||||
use Lcobucci\JWT\Encoding\JoseEncoder;
|
||||
use Lcobucci\JWT\Signer\Hmac\Sha256;
|
||||
@@ -1257,14 +1258,22 @@ function get_public_ips()
|
||||
|
||||
function isAnyDeploymentInprogress()
|
||||
{
|
||||
// Only use it in the deployment script
|
||||
$count = ApplicationDeploymentQueue::whereIn('status', [ApplicationDeploymentStatus::IN_PROGRESS, ApplicationDeploymentStatus::QUEUED])->count();
|
||||
if ($count > 0) {
|
||||
echo "There are $count deployments in progress. Exiting...\n";
|
||||
exit(1);
|
||||
$runningJobs = ApplicationDeploymentQueue::where('horizon_job_worker', gethostname())->where('status', ApplicationDeploymentStatus::IN_PROGRESS->value)->get();
|
||||
$horizonJobIds = [];
|
||||
foreach ($runningJobs as $runningJob) {
|
||||
$horizonJobStatus = getJobStatus($runningJob->horizon_job_id);
|
||||
if ($horizonJobStatus === 'unknown') {
|
||||
return true;
|
||||
}
|
||||
$horizonJobIds[] = $runningJob->horizon_job_id;
|
||||
}
|
||||
echo "No deployments in progress.\n";
|
||||
exit(0);
|
||||
if (count($horizonJobIds) === 0) {
|
||||
echo "No deployments in progress.\n";
|
||||
exit(0);
|
||||
}
|
||||
$horizonJobIds = collect($horizonJobIds)->unique()->toArray();
|
||||
echo 'There are '.count($horizonJobIds)." deployments in progress.\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
function isBase64Encoded($strValue)
|
||||
@@ -4124,3 +4133,16 @@ function convertGitUrl(string $gitRepository, string $deploymentType, ?GithubApp
|
||||
'port' => $providerInfo['port'],
|
||||
];
|
||||
}
|
||||
|
||||
function getJobStatus(?string $jobId = null)
|
||||
{
|
||||
if (blank($jobId)) {
|
||||
return 'unknown';
|
||||
}
|
||||
$jobFound = app(JobRepository::class)->getJobs([$jobId]);
|
||||
if ($jobFound->isEmpty()) {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
return $jobFound->first()->status;
|
||||
}
|
||||
|
@@ -30,7 +30,7 @@
|
||||
"league/flysystem-sftp-v3": "^3.0",
|
||||
"livewire/livewire": "^3.5",
|
||||
"log1x/laravel-webfonts": "^1.0",
|
||||
"lorisleiva/laravel-actions": "^2.7",
|
||||
"lorisleiva/laravel-actions": "^2.8",
|
||||
"nubs/random-name-generator": "^2.2",
|
||||
"phpseclib/phpseclib": "^3.0",
|
||||
"pion/laravel-chunk-upload": "^1.5",
|
||||
@@ -124,4 +124,4 @@
|
||||
"@php artisan key:generate --ansi"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
28
composer.lock
generated
28
composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "ccced2490c39e4f6f1bf9b036bfa3ef0",
|
||||
"content-hash": "85a775fb1a4b9ea329d8d893f43621c2",
|
||||
"packages": [
|
||||
{
|
||||
"name": "3sidedcube/laravel-redoc",
|
||||
@@ -928,16 +928,16 @@
|
||||
},
|
||||
{
|
||||
"name": "aws/aws-sdk-php",
|
||||
"version": "3.336.11",
|
||||
"version": "3.336.12",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/aws/aws-sdk-php.git",
|
||||
"reference": "442039c766a82f06ecfecb0ac2c610d6aaba228d"
|
||||
"reference": "a173ab3af8d9186d266e4937d8254597f36a9e15"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/442039c766a82f06ecfecb0ac2c610d6aaba228d",
|
||||
"reference": "442039c766a82f06ecfecb0ac2c610d6aaba228d",
|
||||
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/a173ab3af8d9186d266e4937d8254597f36a9e15",
|
||||
"reference": "a173ab3af8d9186d266e4937d8254597f36a9e15",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1020,9 +1020,9 @@
|
||||
"support": {
|
||||
"forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
|
||||
"issues": "https://github.com/aws/aws-sdk-php/issues",
|
||||
"source": "https://github.com/aws/aws-sdk-php/tree/3.336.11"
|
||||
"source": "https://github.com/aws/aws-sdk-php/tree/3.336.12"
|
||||
},
|
||||
"time": "2025-01-08T19:06:59+00:00"
|
||||
"time": "2025-01-09T19:04:34+00:00"
|
||||
},
|
||||
{
|
||||
"name": "bacon/bacon-qr-code",
|
||||
@@ -13198,16 +13198,16 @@
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan",
|
||||
"version": "2.1.1",
|
||||
"version": "1.12.15",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpstan/phpstan.git",
|
||||
"reference": "cd6e973e04b4c2b94c86e8612b5a65f0da0e08e7"
|
||||
"reference": "c91d4e8bc056f46cf653656e6f71004b254574d1"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/cd6e973e04b4c2b94c86e8612b5a65f0da0e08e7",
|
||||
"reference": "cd6e973e04b4c2b94c86e8612b5a65f0da0e08e7",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/c91d4e8bc056f46cf653656e6f71004b254574d1",
|
||||
"reference": "c91d4e8bc056f46cf653656e6f71004b254574d1",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -13252,7 +13252,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2025-01-05T16:43:48+00:00"
|
||||
"time": "2025-01-05T16:40:22+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-code-coverage",
|
||||
@@ -15362,12 +15362,12 @@
|
||||
],
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": {},
|
||||
"stability-flags": [],
|
||||
"prefer-stable": true,
|
||||
"prefer-lowest": false,
|
||||
"platform": {
|
||||
"php": "^8.4"
|
||||
},
|
||||
"platform-dev": {},
|
||||
"platform-dev": [],
|
||||
"plugin-api-version": "2.6.0"
|
||||
}
|
||||
|
@@ -0,0 +1,30 @@
|
||||
<?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('application_deployment_queues', function (Blueprint $table) {
|
||||
$table->string('horizon_job_id')->nullable();
|
||||
$table->string('horizon_job_worker')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('application_deployment_queues', function (Blueprint $table) {
|
||||
$table->dropColumn('horizon_job_id');
|
||||
$table->dropColumn('horizon_job_worker');
|
||||
});
|
||||
}
|
||||
};
|
@@ -11,20 +11,21 @@
|
||||
])
|
||||
|
||||
<div @class([
|
||||
'flex flex-row items-center gap-4 pr-2 py-1 form-control min-w-fit dark:hover:bg-coolgray-100',
|
||||
'flex flex-row items-center gap-4 pr-2 py-1 form-control min-w-fit',
|
||||
'w-full' => $fullWidth,
|
||||
'dark:hover:bg-coolgray-100 cursor-pointer' => !$disabled,
|
||||
])>
|
||||
<label @class([
|
||||
'flex gap-4 items-center px-0 min-w-fit label w-full cursor-pointer',
|
||||
])>
|
||||
<label @class(['flex gap-4 items-center px-0 min-w-fit label w-full'])>
|
||||
<span class="flex flex-grow gap-2">
|
||||
@if ($label)
|
||||
{!! $label !!}
|
||||
@else
|
||||
{{ $id }}
|
||||
@endif
|
||||
@if ($helper)
|
||||
<x-helper :helper="$helper" />
|
||||
@if ($disabled)
|
||||
<span class="opacity-60">{!! $label !!}</span>
|
||||
@else
|
||||
{!! $label !!}
|
||||
@endif
|
||||
@if ($helper)
|
||||
<x-helper :helper="$helper" />
|
||||
@endif
|
||||
@endif
|
||||
</span>
|
||||
@if ($instantSave)
|
||||
|
48
resources/views/components/services/advanced.blade.php
Normal file
48
resources/views/components/services/advanced.blade.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<x-dropdown>
|
||||
<x-slot:title>
|
||||
Advanced
|
||||
</x-slot>
|
||||
@if (str($service->status)->contains('running'))
|
||||
<div class="dropdown-item" @click="$wire.dispatch('pullAndRestartEvent')">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M12.983 8.978c3.955 -.182 7.017 -1.446 7.017 -2.978c0 -1.657 -3.582 -3 -8 -3c-1.661 0 -3.204 .19 -4.483 .515m-2.783 1.228c-.471 .382 -.734 .808 -.734 1.257c0 1.22 1.944 2.271 4.734 2.74" />
|
||||
<path
|
||||
d="M4 6v6c0 1.657 3.582 3 8 3c.986 0 1.93 -.067 2.802 -.19m3.187 -.82c1.251 -.53 2.011 -1.228 2.011 -1.99v-6" />
|
||||
<path d="M4 12v6c0 1.657 3.582 3 8 3c3.217 0 5.991 -.712 7.261 -1.74m.739 -3.26v-4" />
|
||||
<path d="M3 3l18 18" />
|
||||
</svg>
|
||||
Pull Latest Images & Restart
|
||||
</div>
|
||||
@elseif (str($service->status)->contains('degraded'))
|
||||
<div class="dropdown-item" @click="$wire.dispatch('forceDeployEvent')">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-linecap="round" stroke-linejoin="round" data-darkreader-inline-stroke=""
|
||||
style="--darkreader-inline-stroke: currentColor;" class="w-6 h-6" stroke-width="2">
|
||||
<path d="M7 7l5 5l-5 5"></path>
|
||||
<path d="M13 7l5 5l-5 5"></path>
|
||||
</svg>
|
||||
Force Restart
|
||||
</div>
|
||||
@else
|
||||
<div class="dropdown-item" @click="$wire.dispatch('forceDeployEvent')">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-linecap="round" stroke-linejoin="round" data-darkreader-inline-stroke=""
|
||||
style="--darkreader-inline-stroke: currentColor;" class="w-6 h-6" stroke-width="2">
|
||||
<path d="M7 7l5 5l-5 5"></path>
|
||||
<path d="M13 7l5 5l-5 5"></path>
|
||||
</svg>
|
||||
Force Deploy
|
||||
</div>
|
||||
<div class="dropdown-item" wire:click='stop(true)''>
|
||||
<svg class="w-6 h-6" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="currentColor" d="M26 20h-6v-2h6zm4 8h-6v-2h6zm-2-4h-6v-2h6z" />
|
||||
<path fill="currentColor"
|
||||
d="M17.003 20a4.895 4.895 0 0 0-2.404-4.173L22 3l-1.73-1l-7.577 13.126a5.699 5.699 0 0 0-5.243 1.503C3.706 20.24 3.996 28.682 4.01 29.04a1 1 0 0 0 1 .96h14.991a1 1 0 0 0 .6-1.8c-3.54-2.656-3.598-8.146-3.598-8.2Zm-5.073-3.003A3.11 3.11 0 0 1 15.004 20c0 .038.002.208.017.469l-5.9-2.624a3.8 3.8 0 0 1 2.809-.848ZM15.45 28A5.2 5.2 0 0 1 14 25h-2a6.5 6.5 0 0 0 .968 3h-2.223A16.617 16.617 0 0 1 10 24H8a17.342 17.342 0 0 0 .665 4H6c.031-1.836.29-5.892 1.803-8.553l7.533 3.35A13.025 13.025 0 0 0 17.596 28Z" />
|
||||
</svg>
|
||||
Force Cleanup Containers
|
||||
</div>
|
||||
@endif
|
||||
</x-dropdown>
|
@@ -8,7 +8,7 @@
|
||||
</x-slide-over>
|
||||
<h1>{{ $title }}</h1>
|
||||
<x-resources.breadcrumbs :resource="$service" :parameters="$parameters" />
|
||||
<div class="navbar-main" x-data>
|
||||
<div class="navbar-main" x-data">
|
||||
<nav class="flex flex-shrink-0 gap-6 items-center whitespace-nowrap scrollbar min-h-10">
|
||||
<a class="{{ request()->routeIs('project.service.configuration') ? 'dark:text-white' : '' }}"
|
||||
href="{{ route('project.service.configuration', $parameters) }}">
|
||||
@@ -26,26 +26,8 @@
|
||||
</nav>
|
||||
@if ($service->isDeployable)
|
||||
<div class="flex flex-wrap order-first gap-2 items-center sm:order-last">
|
||||
<x-services.advanced :service="$service" />
|
||||
@if (str($service->status)->contains('running'))
|
||||
<x-dropdown>
|
||||
<x-slot:title>
|
||||
Advanced
|
||||
</x-slot>
|
||||
<div class="dropdown-item" @click="$wire.dispatch('pullAndRestartEvent')">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M12.983 8.978c3.955 -.182 7.017 -1.446 7.017 -2.978c0 -1.657 -3.582 -3 -8 -3c-1.661 0 -3.204 .19 -4.483 .515m-2.783 1.228c-.471 .382 -.734 .808 -.734 1.257c0 1.22 1.944 2.271 4.734 2.74" />
|
||||
<path
|
||||
d="M4 6v6c0 1.657 3.582 3 8 3c.986 0 1.93 -.067 2.802 -.19m3.187 -.82c1.251 -.53 2.011 -1.228 2.011 -1.99v-6" />
|
||||
<path d="M4 12v6c0 1.657 3.582 3 8 3c3.217 0 5.991 -.712 7.261 -1.74m.739 -3.26v-4" />
|
||||
<path d="M3 3l18 18" />
|
||||
</svg>
|
||||
Pull Latest Images & Restart
|
||||
</div>
|
||||
</x-dropdown>
|
||||
<x-forms.button title="Restart" @click="$wire.dispatch('restartEvent')">
|
||||
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||
@@ -75,7 +57,7 @@
|
||||
</x-slot:button-title>
|
||||
</x-modal-confirmation>
|
||||
@elseif (str($service->status)->contains('degraded'))
|
||||
<button @click="$wire.dispatch('startEvent')" class="gap-2 button">
|
||||
<x-forms.button title="Restart" @click="$wire.dispatch('restartEvent')">
|
||||
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2">
|
||||
@@ -83,8 +65,8 @@
|
||||
<path d="M20 4v5h-5" />
|
||||
</g>
|
||||
</svg>
|
||||
Restart Degraded Services
|
||||
</button>
|
||||
Restart
|
||||
</x-forms.button>
|
||||
<x-modal-confirmation title="Confirm Service Stopping?" buttonTitle="Stop" submitAction="stop"
|
||||
:checkboxes="$checkboxes" :actions="[__('service.stop'), __('resource.non_persistent')]" :confirmWithText="false" :confirmWithPassword="false"
|
||||
step1ButtonText="Continue" step2ButtonText="Stop Service" :dispatchEvent="true"
|
||||
@@ -104,14 +86,6 @@
|
||||
</x-slot:button-title>
|
||||
</x-modal-confirmation>
|
||||
@elseif (str($service->status)->contains('exited'))
|
||||
<button wire:click='stop(true)' class="gap-2 button">
|
||||
<svg class="w-5 h-5" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="red" d="M26 20h-6v-2h6zm4 8h-6v-2h6zm-2-4h-6v-2h6z" />
|
||||
<path fill="red"
|
||||
d="M17.003 20a4.895 4.895 0 0 0-2.404-4.173L22 3l-1.73-1l-7.577 13.126a5.699 5.699 0 0 0-5.243 1.503C3.706 20.24 3.996 28.682 4.01 29.04a1 1 0 0 0 1 .96h14.991a1 1 0 0 0 .6-1.8c-3.54-2.656-3.598-8.146-3.598-8.2Zm-5.073-3.003A3.11 3.11 0 0 1 15.004 20c0 .038.002.208.017.469l-5.9-2.624a3.8 3.8 0 0 1 2.809-.848ZM15.45 28A5.2 5.2 0 0 1 14 25h-2a6.5 6.5 0 0 0 .968 3h-2.223A16.617 16.617 0 0 1 10 24H8a17.342 17.342 0 0 0 .665 4H6c.031-1.836.29-5.892 1.803-8.553l7.533 3.35A13.025 13.025 0 0 0 17.596 28Z" />
|
||||
</svg>
|
||||
Force Cleanup Containers
|
||||
</button>
|
||||
<button @click="$wire.dispatch('startEvent')" class="gap-2 button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
@@ -167,11 +141,29 @@
|
||||
$wire.$dispatch('info', 'Stopping service.');
|
||||
$wire.$call('stop');
|
||||
});
|
||||
$wire.$on('startEvent', () => {
|
||||
$wire.$on('startEvent', async () => {
|
||||
const isDeploymentProgress = await $wire.$call('checkDeployments');
|
||||
if (isDeploymentProgress) {
|
||||
$wire.$dispatch('error',
|
||||
'There is a deployment in progress.<br><br>You can force deploy in the "Advanced" section.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
window.dispatchEvent(new CustomEvent('startservice'));
|
||||
$wire.$call('start');
|
||||
});
|
||||
$wire.$on('restartEvent', () => {
|
||||
$wire.$on('forceDeployEvent', () => {
|
||||
window.dispatchEvent(new CustomEvent('startservice'));
|
||||
$wire.$call('forceDeploy');
|
||||
});
|
||||
$wire.$on('restartEvent', async () => {
|
||||
const isDeploymentProgress = await $wire.$call('checkDeployments');
|
||||
if (isDeploymentProgress) {
|
||||
$wire.$dispatch('error',
|
||||
'There is a deployment in progress.<br><br>You can force deploy in the "Advanced" section.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
$wire.$dispatch('info', 'Service restart in progress.');
|
||||
$wire.$call('restart');
|
||||
});
|
||||
@@ -185,5 +177,4 @@
|
||||
});
|
||||
</script>
|
||||
@endscript
|
||||
|
||||
</div>
|
||||
|
@@ -223,8 +223,7 @@
|
||||
<x-forms.input id="sentinelMetricsHistoryDays" label="Metrics history (days)"
|
||||
required helper="Number of days to retain metrics data for." />
|
||||
<x-forms.input id="sentinelPushIntervalSeconds" label="Push interval (seconds)"
|
||||
required
|
||||
helper="Interval at which metrics data is sent to the collector." />
|
||||
required helper="Interval at which metrics data is sent to the collector." />
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
@@ -138,13 +138,29 @@ Route::group([
|
||||
return response()->json(['message' => 'Unauthorized'], 401);
|
||||
}
|
||||
$naked_token = str_replace('Bearer ', '', $token);
|
||||
$decrypted = decrypt($naked_token);
|
||||
$decrypted_token = json_decode($decrypted, true);
|
||||
try {
|
||||
$decrypted = decrypt($naked_token);
|
||||
$decrypted_token = json_decode($decrypted, true);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json(['message' => 'Invalid token'], 401);
|
||||
}
|
||||
$server_uuid = data_get($decrypted_token, 'server_uuid');
|
||||
if (! $server_uuid) {
|
||||
return response()->json(['message' => 'Invalid token'], 401);
|
||||
}
|
||||
$server = Server::where('uuid', $server_uuid)->first();
|
||||
if (! $server) {
|
||||
return response()->json(['message' => 'Server not found'], 404);
|
||||
}
|
||||
|
||||
if (isCloud() && data_get($server->team->subscription, 'stripe_invoice_paid', false) === false && $server->team->id !== 0) {
|
||||
return response()->json(['message' => 'Unauthorized'], 401);
|
||||
}
|
||||
|
||||
if ($server->isFunctional() === false) {
|
||||
return response()->json(['message' => 'Server is not functional'], 401);
|
||||
}
|
||||
|
||||
if ($server->settings->sentinel_token !== $naked_token) {
|
||||
return response()->json(['message' => 'Unauthorized'], 401);
|
||||
}
|
||||
@@ -160,22 +176,3 @@ Route::group([
|
||||
Route::any('/{any}', function () {
|
||||
return response()->json(['message' => 'Not found.', 'docs' => 'https://coolify.io/docs'], 404);
|
||||
})->where('any', '.*');
|
||||
|
||||
// Route::middleware(['throttle:5'])->group(function () {
|
||||
// Route::get('/unsubscribe/{token}', function () {
|
||||
// try {
|
||||
// $token = request()->token;
|
||||
// $email = decrypt($token);
|
||||
// if (!User::whereEmail($email)->exists()) {
|
||||
// return redirect(RouteServiceProvider::HOME);
|
||||
// }
|
||||
// if (User::whereEmail($email)->first()->marketing_emails === false) {
|
||||
// return 'You have already unsubscribed from marketing emails.';
|
||||
// }
|
||||
// User::whereEmail($email)->update(['marketing_emails' => false]);
|
||||
// return 'You have been unsubscribed from marketing emails.';
|
||||
// } catch (\Throwable $e) {
|
||||
// return 'Something went wrong. Please try again or contact support.';
|
||||
// }
|
||||
// })->name('unsubscribe.marketing.emails');
|
||||
// });
|
||||
|
@@ -3,7 +3,4 @@ export IMAGE=$1
|
||||
docker system prune -af
|
||||
docker compose pull
|
||||
read -p "Press Enter to update Coolify to $IMAGE..." </dev/tty
|
||||
docker exec coolify sh -c "php artisan tinker --execute='isAnyDeploymentInprogress()'"
|
||||
docker compose up --remove-orphans --force-recreate -d --wait
|
||||
echo $IMAGE > last_version
|
||||
docker compose logs -f
|
||||
while ! (docker exec coolify sh -c "php artisan tinker --execute='isAnyDeploymentInprogress()'" && docker compose up --remove-orphans --force-recreate -d --wait && echo $IMAGE > last_version); do sleep 1; done
|
Reference in New Issue
Block a user