Merge branch 'next' into fix-cloning

This commit is contained in:
Andras Bacsai
2025-01-14 08:50:22 +01:00
committed by GitHub
35 changed files with 725 additions and 180 deletions

View File

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

View File

@@ -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);
}

View 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);
}
}

View File

@@ -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();

View 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;
}

View File

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

View File

@@ -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';
}

View File

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

View File

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

View File

@@ -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();
// }
}

View File

@@ -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);
}

View File

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

View File

@@ -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 = [];
}
}

View File

@@ -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);
}

View File

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

View File

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

View File

@@ -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([]);
}

View File

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

View File

@@ -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');

View File

@@ -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);
}

View File

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

View 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;
}
}

View 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');
}
}