Merge branch 'next' into fix-cloning
This commit is contained in:
@@ -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');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user