mirror of
https://github.com/coollabsio/coolify.git
synced 2026-09-26 09:20:54 -04:00
chore: merge main into next (#11784)
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
# Lessons
|
||||
|
||||
## Check prior fixes before changing a repeated symptom
|
||||
- When a reported regression matches a recent fix, inspect that fix and reproduce why it no longer works before adding another workaround.
|
||||
- Do not claim a redirect or lifecycle root cause from an effects assertion alone. Prove the reported HTTP or browser failure first.
|
||||
- Preserve SPA navigation when it is a product requirement. Do not replace it with a full-page redirect to mask a deletion race; fix the ordering or state race instead.
|
||||
|
||||
## Confirm which surface becomes the modal
|
||||
- When a user wants two settings pages replaced by a modal, identify the parent page that owns the trigger and confirm that the complete child settings view moves into that modal.
|
||||
- Do not make one child page a modal inside the other child page when the user wants both child URLs removed.
|
||||
@@ -82,3 +87,15 @@
|
||||
|
||||
## Keep modal actions in the footer
|
||||
- When a modal has a large editable body, put preview, validation, and save controls in a fixed footer. Keep the title bar for the title and close action.
|
||||
|
||||
## Prefer named API values over numeric sentinels
|
||||
- When an API option means an unbounded or special mode, expose a clear named value such as `all`.
|
||||
- Keep an existing numeric sentinel such as `-1` only as a compatibility alias unless the user requests a breaking change.
|
||||
|
||||
## Do not auto-heal existing deployments without a request
|
||||
- When a parser or label fix can apply only after container recreation, keep the change limited to new deployments and later user-initiated redeployments unless the user explicitly asks for live reconciliation.
|
||||
- Do not add status lookup fallbacks that alter existing deployment behavior when the requested scope is new deployments only.
|
||||
|
||||
## Trace image replacements through build stages
|
||||
- When replacing a container image for development, inspect both Compose services and every development Dockerfile `FROM` stage.
|
||||
- A successful Compose pull does not prove the application build is free of the old image; validate the complete build dependency chain.
|
||||
|
||||
@@ -6,7 +6,6 @@ use App\Jobs\ApiTokenExpirationWarningJob;
|
||||
use App\Jobs\CheckForUpdatesJob;
|
||||
use App\Jobs\CheckHelperImageJob;
|
||||
use App\Jobs\CheckMissingDatabaseBackupsJob;
|
||||
use App\Jobs\CheckTraefikVersionJob;
|
||||
use App\Jobs\CleanupInstanceStuffsJob;
|
||||
use App\Jobs\CleanupOrphanedPreviewContainersJob;
|
||||
use App\Jobs\CleanupStaleMultiplexedConnections;
|
||||
@@ -91,8 +90,6 @@ class Kernel extends ConsoleKernel
|
||||
|
||||
$this->scheduleInstance->job(new RegenerateSslCertJob)->twiceDaily()->onOneServer();
|
||||
|
||||
$this->scheduleInstance->job(new CheckTraefikVersionJob)->weekly()->sundays()->at('00:00')->timezone($this->instanceTimezone)->onOneServer();
|
||||
|
||||
$this->scheduleInstance->command('cleanup:database --yes')->daily();
|
||||
$this->scheduleInstance->command('uploads:clear')->everyTwoMinutes();
|
||||
|
||||
|
||||
@@ -2437,13 +2437,12 @@ class ApplicationsController extends Controller
|
||||
new OA\Parameter(
|
||||
name: 'lines',
|
||||
in: 'query',
|
||||
description: 'Number of lines to show from the end of the logs.',
|
||||
description: 'Number of lines to show from the end of the logs. Use `all` to return all logs. `-1` remains available as a compatibility alias.',
|
||||
required: false,
|
||||
schema: new OA\Schema(
|
||||
type: 'integer',
|
||||
format: 'int32',
|
||||
default: 100,
|
||||
)
|
||||
schema: new OA\Schema(oneOf: [
|
||||
new OA\Schema(type: 'integer', format: 'int32', default: 100, minimum: -1, maximum: 10000),
|
||||
new OA\Schema(type: 'string', enum: ['all']),
|
||||
])
|
||||
),
|
||||
new OA\Parameter(
|
||||
name: 'show_timestamps',
|
||||
|
||||
@@ -2512,13 +2512,12 @@ class DatabasesController extends Controller
|
||||
new OA\Parameter(
|
||||
name: 'lines',
|
||||
in: 'query',
|
||||
description: 'Number of lines to show from the end of the logs.',
|
||||
description: 'Number of lines to show from the end of the logs. Use `all` to return all logs. `-1` remains available as a compatibility alias.',
|
||||
required: false,
|
||||
schema: new OA\Schema(
|
||||
type: 'integer',
|
||||
format: 'int32',
|
||||
default: 100,
|
||||
)
|
||||
schema: new OA\Schema(oneOf: [
|
||||
new OA\Schema(type: 'integer', format: 'int32', default: 100, minimum: -1, maximum: 10000),
|
||||
new OA\Schema(type: 'string', enum: ['all']),
|
||||
])
|
||||
),
|
||||
new OA\Parameter(
|
||||
name: 'show_timestamps',
|
||||
|
||||
@@ -402,9 +402,12 @@ class ServiceApplicationsController extends Controller
|
||||
new OA\Parameter(
|
||||
name: 'lines',
|
||||
in: 'query',
|
||||
description: 'Number of lines to show from the end of the logs.',
|
||||
description: 'Number of lines to show from the end of the logs. Use `all` to return all logs. `-1` remains available as a compatibility alias.',
|
||||
required: false,
|
||||
schema: new OA\Schema(type: 'integer', format: 'int32', default: 100)
|
||||
schema: new OA\Schema(oneOf: [
|
||||
new OA\Schema(type: 'integer', format: 'int32', default: 100, minimum: -1, maximum: 10000),
|
||||
new OA\Schema(type: 'string', enum: ['all']),
|
||||
])
|
||||
),
|
||||
],
|
||||
responses: [
|
||||
@@ -451,7 +454,16 @@ class ServiceApplicationsController extends Controller
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'uuid', in: 'path', required: true, schema: new OA\Schema(type: 'string')),
|
||||
new OA\Parameter(name: 'app_uuid', in: 'path', required: true, schema: new OA\Schema(type: 'string')),
|
||||
new OA\Parameter(name: 'lines', in: 'query', required: false, schema: new OA\Schema(type: 'integer', format: 'int32', default: 100)),
|
||||
new OA\Parameter(
|
||||
name: 'lines',
|
||||
in: 'query',
|
||||
description: 'Number of lines to show from the end of the logs. Use `all` to return all logs. `-1` remains available as a compatibility alias.',
|
||||
required: false,
|
||||
schema: new OA\Schema(oneOf: [
|
||||
new OA\Schema(type: 'integer', format: 'int32', default: 100, minimum: -1, maximum: 10000),
|
||||
new OA\Schema(type: 'string', enum: ['all']),
|
||||
]),
|
||||
),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
|
||||
@@ -366,7 +366,16 @@ class ServiceDatabasesController extends Controller
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'uuid', in: 'path', required: true, schema: new OA\Schema(type: 'string')),
|
||||
new OA\Parameter(name: 'database_uuid', in: 'path', required: true, schema: new OA\Schema(type: 'string')),
|
||||
new OA\Parameter(name: 'lines', in: 'query', required: false, schema: new OA\Schema(type: 'integer', format: 'int32', default: 100)),
|
||||
new OA\Parameter(
|
||||
name: 'lines',
|
||||
in: 'query',
|
||||
description: 'Number of lines to show from the end of the logs. Use `all` to return all logs. `-1` remains available as a compatibility alias.',
|
||||
required: false,
|
||||
schema: new OA\Schema(oneOf: [
|
||||
new OA\Schema(type: 'integer', format: 'int32', default: 100, minimum: -1, maximum: 10000),
|
||||
new OA\Schema(type: 'string', enum: ['all']),
|
||||
]),
|
||||
),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(response: 200, description: 'Logs.', content: new OA\JsonContent(type: 'object', properties: [new OA\Property(property: 'logs', type: 'string')])),
|
||||
|
||||
@@ -869,13 +869,12 @@ class ServicesController extends Controller
|
||||
new OA\Parameter(
|
||||
name: 'lines',
|
||||
in: 'query',
|
||||
description: 'Number of lines to show from the end of the logs.',
|
||||
description: 'Number of lines to show from the end of the logs. Use `all` to return all logs. `-1` remains available as a compatibility alias.',
|
||||
required: false,
|
||||
schema: new OA\Schema(
|
||||
type: 'integer',
|
||||
format: 'int32',
|
||||
default: 100,
|
||||
)
|
||||
schema: new OA\Schema(oneOf: [
|
||||
new OA\Schema(type: 'integer', format: 'int32', default: 100, minimum: -1, maximum: 10000),
|
||||
new OA\Schema(type: 'string', enum: ['all']),
|
||||
])
|
||||
),
|
||||
new OA\Parameter(
|
||||
name: 'show_timestamps',
|
||||
|
||||
@@ -73,6 +73,8 @@ class CheckForUpdatesJob implements ShouldBeEncrypted, ShouldQueue
|
||||
// Invalidate cache to ensure fresh data is loaded
|
||||
invalidate_versions_cache();
|
||||
|
||||
CheckTraefikVersionJob::dispatch();
|
||||
|
||||
// Only mark new version available if Coolify version actually increased
|
||||
if (version_compare($latest_version, $current_version, '>')) {
|
||||
// New version available
|
||||
|
||||
@@ -180,7 +180,8 @@ class BackupEdit extends Component
|
||||
|
||||
public function delete($password, $selectedActions = [])
|
||||
{
|
||||
$this->authorize('manageBackups', $this->backup->database);
|
||||
$database = $this->backup->database;
|
||||
$this->authorize('manageBackups', $database);
|
||||
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return 'The provided password is incorrect.';
|
||||
@@ -188,10 +189,10 @@ class BackupEdit extends Component
|
||||
|
||||
try {
|
||||
$server = null;
|
||||
if ($this->backup->database instanceof ServiceDatabase) {
|
||||
$server = $this->backup->database->service->destination->server;
|
||||
} elseif ($this->backup->database->destination && $this->backup->database->destination->server) {
|
||||
$server = $this->backup->database->destination->server;
|
||||
if ($database instanceof ServiceDatabase) {
|
||||
$server = $database->service->destination->server;
|
||||
} elseif ($database->destination && $database->destination->server) {
|
||||
$server = $database->destination->server;
|
||||
}
|
||||
|
||||
$filenames = $this->backup->executions()
|
||||
@@ -212,9 +213,9 @@ class BackupEdit extends Component
|
||||
}
|
||||
}
|
||||
|
||||
$database = $this->backup->database;
|
||||
$backupUuid = $this->backup->uuid;
|
||||
$this->backup->delete();
|
||||
$this->skipRender();
|
||||
auditLog('ui.database.backup_schedule_deleted', [
|
||||
'team_id' => $database->team()?->id,
|
||||
'database_uuid' => $database->uuid,
|
||||
@@ -222,14 +223,12 @@ class BackupEdit extends Component
|
||||
'backup_uuid' => $backupUuid,
|
||||
]);
|
||||
|
||||
if ($database->getMorphClass() === ServiceDatabase::class) {
|
||||
$serviceDatabase = $database;
|
||||
|
||||
if ($database instanceof ServiceDatabase) {
|
||||
return redirectRoute($this, 'project.service.database.backups', [
|
||||
'project_uuid' => $this->parameters['project_uuid'],
|
||||
'environment_uuid' => $this->parameters['environment_uuid'],
|
||||
'service_uuid' => $serviceDatabase->service->uuid,
|
||||
'stack_service_uuid' => $serviceDatabase->uuid,
|
||||
'project_uuid' => $database->service->project()->uuid,
|
||||
'environment_uuid' => $database->service->environment->uuid,
|
||||
'service_uuid' => $database->service->uuid,
|
||||
'stack_service_uuid' => $database->uuid,
|
||||
]);
|
||||
} else {
|
||||
return redirectRoute($this, 'project.database.backup.index', [
|
||||
|
||||
@@ -131,6 +131,12 @@ class GetLogs extends Component
|
||||
$this->streamLogs = ! $this->streamLogs;
|
||||
}
|
||||
|
||||
public function showAllLogs(): void
|
||||
{
|
||||
$this->numberOfLines = -1;
|
||||
$this->getLogs(true);
|
||||
}
|
||||
|
||||
public function getLogs($refresh = false)
|
||||
{
|
||||
if (! Server::ownedByCurrentTeam()->where('id', $this->server->id)->exists()) {
|
||||
@@ -149,22 +155,25 @@ class GetLogs extends Component
|
||||
if (! $refresh && ! $this->expandByDefault && ($this->resource?->getMorphClass() === Service::class || str($this->container)->contains('-pr-'))) {
|
||||
return;
|
||||
}
|
||||
if ($this->numberOfLines <= 0 || is_null($this->numberOfLines)) {
|
||||
$logTail = $this->numberOfLines === -1 ? 'all' : $this->numberOfLines;
|
||||
if ($logTail !== 'all' && ($logTail <= 0 || is_null($logTail))) {
|
||||
$this->numberOfLines = 1000;
|
||||
$logTail = $this->numberOfLines;
|
||||
}
|
||||
if ($this->numberOfLines > self::MAX_LOG_LINES) {
|
||||
if ($logTail !== 'all' && $logTail > self::MAX_LOG_LINES) {
|
||||
$this->numberOfLines = self::MAX_LOG_LINES;
|
||||
$logTail = $this->numberOfLines;
|
||||
}
|
||||
if ($this->container) {
|
||||
if ($this->showTimeStamps) {
|
||||
if ($this->server->isSwarm()) {
|
||||
$command = "docker service logs -n {$this->numberOfLines} -t {$this->container}";
|
||||
$command = "docker service logs -n {$logTail} -t {$this->container}";
|
||||
if ($this->server->isNonRoot()) {
|
||||
$command = parseCommandsByLineForSudo(collect($command), $this->server);
|
||||
$command = $command[0];
|
||||
}
|
||||
} else {
|
||||
$command = "docker logs -n {$this->numberOfLines} -t {$this->container}";
|
||||
$command = "docker logs -n {$logTail} -t {$this->container}";
|
||||
if ($this->server->isNonRoot()) {
|
||||
$command = parseCommandsByLineForSudo(collect($command), $this->server);
|
||||
$command = $command[0];
|
||||
@@ -172,13 +181,13 @@ class GetLogs extends Component
|
||||
}
|
||||
} else {
|
||||
if ($this->server->isSwarm()) {
|
||||
$command = "docker service logs -n {$this->numberOfLines} {$this->container}";
|
||||
$command = "docker service logs -n {$logTail} {$this->container}";
|
||||
if ($this->server->isNonRoot()) {
|
||||
$command = parseCommandsByLineForSudo(collect($command), $this->server);
|
||||
$command = $command[0];
|
||||
}
|
||||
} else {
|
||||
$command = "docker logs -n {$this->numberOfLines} {$this->container}";
|
||||
$command = "docker logs -n {$logTail} {$this->container}";
|
||||
if ($this->server->isNonRoot()) {
|
||||
$command = parseCommandsByLineForSudo(collect($command), $this->server);
|
||||
$command = $command[0];
|
||||
|
||||
@@ -98,6 +98,8 @@ trait ResolvesResource
|
||||
*/
|
||||
protected function normalizeMcpLogLines(mixed $lines): int
|
||||
{
|
||||
return normalizeLogLines($lines, default: 100, max: 500);
|
||||
$lines = normalizeLogLines($lines, default: 100, max: 500);
|
||||
|
||||
return is_int($lines) && $lines > 0 ? $lines : 100;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1571,10 +1571,17 @@ function validateComposeFile(string $compose, int $server_id): string|Throwable
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeLogLines(mixed $lines, int $default = 100, int $max = 10000): int
|
||||
function normalizeLogLines(mixed $lines, int $default = 100, int $max = 10000): int|string
|
||||
{
|
||||
if ($lines === 'all') {
|
||||
return 'all';
|
||||
}
|
||||
|
||||
$lines = filter_var($lines, FILTER_VALIDATE_INT);
|
||||
if ($lines === false || $lines <= 0) {
|
||||
if ($lines === -1) {
|
||||
return 'all';
|
||||
}
|
||||
if ($lines === false || $lines < -1) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
@@ -1586,7 +1593,7 @@ function parseLogTimestampFlag(mixed $showTimestamps): bool
|
||||
return filter_var($showTimestamps, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) ?? false;
|
||||
}
|
||||
|
||||
function buildContainerLogsCommand(Server $server, string $container_id, int $lines = 100, bool $showTimestamps = false): string
|
||||
function buildContainerLogsCommand(Server $server, string $container_id, int|string $lines = 100, bool $showTimestamps = false): string
|
||||
{
|
||||
$command = "docker logs -n {$lines}";
|
||||
if ($server->isSwarm()) {
|
||||
@@ -1600,7 +1607,7 @@ function buildContainerLogsCommand(Server $server, string $container_id, int $li
|
||||
return "{$command} ".escapeshellarg($container_id).' 2>&1';
|
||||
}
|
||||
|
||||
function getContainerLogs(Server $server, string $container_id, int $lines = 100, bool $showTimestamps = false): string
|
||||
function getContainerLogs(Server $server, string $container_id, int|string $lines = 100, bool $showTimestamps = false): string
|
||||
{
|
||||
$output = instant_remote_process([buildContainerLogsCommand($server, $container_id, $lines, $showTimestamps)], $server);
|
||||
$output = removeAnsiColors($output);
|
||||
|
||||
@@ -2546,7 +2546,7 @@ function serviceParser(Service $resource): Collection
|
||||
projectName: $resource->project()->name,
|
||||
resourceName: $resource->name,
|
||||
type: 'service',
|
||||
subType: $isDatabase ? 'database' : 'application',
|
||||
subType: $savedService instanceof ServiceDatabase ? 'database' : 'application',
|
||||
subId: $savedService->id,
|
||||
subName: $savedService->human_name ?? $savedService->name,
|
||||
environment: $resource->environment->name,
|
||||
|
||||
@@ -162,7 +162,7 @@ services:
|
||||
networks:
|
||||
- coolify
|
||||
# maxio-init:
|
||||
# image: minio/mc:latest
|
||||
# image: quay.io/minio/aistor/mc:latest
|
||||
# pull_policy: always
|
||||
# container_name: coolify-maxio-init
|
||||
# restart: no
|
||||
@@ -182,7 +182,7 @@ services:
|
||||
# networks:
|
||||
# - coolify
|
||||
minio-init:
|
||||
image: minio/mc:latest
|
||||
image: quay.io/minio/aistor/mc:latest
|
||||
pull_policy: always
|
||||
container_name: coolify-minio-init
|
||||
restart: no
|
||||
|
||||
@@ -195,7 +195,7 @@ services:
|
||||
|
||||
minio-init:
|
||||
profiles: ["minio"]
|
||||
image: minio/mc:latest
|
||||
image: quay.io/minio/aistor/mc:latest
|
||||
pull_policy: always
|
||||
restart: "no"
|
||||
depends_on:
|
||||
|
||||
@@ -150,7 +150,7 @@ services:
|
||||
networks:
|
||||
- coolify
|
||||
minio-init:
|
||||
image: minio/mc:latest
|
||||
image: quay.io/minio/aistor/mc:latest
|
||||
pull_policy: always
|
||||
container_name: coolify-minio-init
|
||||
restart: no
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
# Versions
|
||||
# https://hub.docker.com/r/serversideup/php/tags?name=8.4-fpm-nginx-alpine
|
||||
ARG SERVERSIDEUP_PHP_VERSION=8.4-fpm-nginx-alpine
|
||||
# https://github.com/minio/mc/releases
|
||||
ARG MINIO_VERSION=RELEASE.2025-08-13T08-35-41Z
|
||||
# https://github.com/cloudflare/cloudflared/releases
|
||||
ARG CLOUDFLARED_VERSION=2025.7.0
|
||||
# https://www.postgresql.org/support/versioning/
|
||||
@@ -14,7 +12,7 @@ ARG NGINX_VERSION=1.31.2-r1
|
||||
# =================================================================
|
||||
# Get MinIO client
|
||||
# =================================================================
|
||||
FROM minio/mc:${MINIO_VERSION} AS minio-client
|
||||
FROM quay.io/minio/aistor/mc:latest AS minio-client
|
||||
|
||||
# =================================================================
|
||||
# Final Stage: Production image
|
||||
|
||||
@@ -501,9 +501,11 @@
|
||||
<div class="logs-viewer-meta">
|
||||
<form wire:submit="getLogs(true)" class="logs-viewer-lines">
|
||||
<span class="logs-viewer-lines-label">Lines</span>
|
||||
<input type="number" wire:model="numberOfLines" placeholder="100" min="1" max="50000"
|
||||
title="Number of Lines (max 50,000)" {{ $streamLogs ? 'readonly' : '' }}
|
||||
<input type="number" wire:model="numberOfLines" placeholder="100" min="-1" max="50000"
|
||||
title="Number of lines (max 50,000; use -1 for all)" {{ $streamLogs ? 'readonly' : '' }}
|
||||
class="input logs-viewer-lines-input" />
|
||||
<button type="button" wire:click="showAllLogs" title="Show all logs"
|
||||
class="runtime-log-icon-button" {{ $streamLogs ? 'disabled' : '' }}>All</button>
|
||||
</form>
|
||||
<span x-show="searchQuery.trim()" x-text="matchCount + ' matches'"
|
||||
class="text-xs text-gray-500 whitespace-nowrap dark:text-gray-400"></span>
|
||||
|
||||
@@ -8,6 +8,8 @@ use App\Models\Project;
|
||||
use App\Models\S3Storage;
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use App\Models\ServiceDatabase;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\Team;
|
||||
@@ -343,6 +345,61 @@ it('disables S3 backup when saved without a selected S3 storage', function () {
|
||||
expect($backup->s3_storage_id)->toBeNull();
|
||||
});
|
||||
|
||||
it('deletes a service database backup schedule without rendering the deleted relationship', function () {
|
||||
InstanceSettings::get()->update(['disable_two_step_confirmation' => true]);
|
||||
|
||||
$server = Server::factory()->create(['team_id' => $this->team->id]);
|
||||
$destination = StandaloneDocker::where('server_id', $server->id)->firstOrFail();
|
||||
$project = Project::factory()->create(['team_id' => $this->team->id]);
|
||||
$environment = Environment::factory()->create(['project_id' => $project->id]);
|
||||
$service = Service::factory()->create([
|
||||
'server_id' => $server->id,
|
||||
'environment_id' => $environment->id,
|
||||
'destination_id' => $destination->id,
|
||||
'destination_type' => $destination->getMorphClass(),
|
||||
]);
|
||||
$database = ServiceDatabase::create([
|
||||
'service_id' => $service->id,
|
||||
'name' => 'postgres',
|
||||
'image' => 'postgres:16-alpine',
|
||||
'custom_type' => 'postgresql',
|
||||
]);
|
||||
$backup = ScheduledDatabaseBackup::create([
|
||||
'frequency' => '0 0 * * *',
|
||||
'enabled' => true,
|
||||
'save_s3' => false,
|
||||
'database_backup_retention_amount_locally' => 0,
|
||||
'database_backup_retention_days_locally' => 0,
|
||||
'database_backup_retention_max_storage_locally' => 0,
|
||||
'database_backup_retention_amount_s3' => 0,
|
||||
'database_backup_retention_days_s3' => 0,
|
||||
'database_backup_retention_max_storage_s3' => 0,
|
||||
'dump_all' => false,
|
||||
'timeout' => 3600,
|
||||
'missing_backup_notification_days' => 0,
|
||||
'database_type' => $database->getMorphClass(),
|
||||
'database_id' => $database->id,
|
||||
'team_id' => $this->team->id,
|
||||
]);
|
||||
$parameters = [
|
||||
'project_uuid' => $project->uuid,
|
||||
'environment_uuid' => $environment->uuid,
|
||||
'service_uuid' => $service->uuid,
|
||||
'stack_service_uuid' => $database->uuid,
|
||||
];
|
||||
|
||||
$component = Livewire::test(BackupEdit::class, [
|
||||
'backup' => $backup,
|
||||
'availableS3Storages' => collect(),
|
||||
'section' => 'danger',
|
||||
]);
|
||||
$component
|
||||
->call('delete', '')
|
||||
->assertRedirectToRoute('project.service.database.backups', $parameters);
|
||||
|
||||
expect(ScheduledDatabaseBackup::find($backup->id))->toBeNull();
|
||||
});
|
||||
|
||||
it('cascades to disabling local backup deletion when S3 is force-disabled', function () {
|
||||
$backup = createBackupForEditValidationTest($this->team, [
|
||||
'disable_local_backup' => true,
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use App\Jobs\CheckForUpdatesJob;
|
||||
use App\Jobs\CheckTraefikVersionJob;
|
||||
use App\Models\InstanceSettings;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('checks installed Traefik versions after refreshing available versions', function () {
|
||||
Bus::fake();
|
||||
Http::fake([
|
||||
'*' => Http::response([
|
||||
'coolify' => ['v4' => ['version' => '4.0.10']],
|
||||
'traefik' => ['v3.7' => '3.7.13'],
|
||||
]),
|
||||
]);
|
||||
File::shouldReceive('exists')->andReturn(false);
|
||||
File::shouldReceive('put')->once();
|
||||
|
||||
InstanceSettings::forceCreate(['id' => 0]);
|
||||
|
||||
config([
|
||||
'app.env' => 'production',
|
||||
'constants.coolify.self_hosted' => true,
|
||||
'constants.coolify.version' => '4.0.10',
|
||||
]);
|
||||
|
||||
(new CheckForUpdatesJob)->handle();
|
||||
|
||||
Bus::assertDispatched(CheckTraefikVersionJob::class);
|
||||
});
|
||||
@@ -74,6 +74,29 @@ describe('GetLogs locked properties', function () {
|
||||
});
|
||||
|
||||
describe('GetLogs Livewire action validation', function () {
|
||||
test('getLogs requests all logs when the line count is minus one', function () {
|
||||
$this->server->settings->fill([
|
||||
'is_reachable' => true,
|
||||
'is_usable' => true,
|
||||
'force_disabled' => false,
|
||||
])->save();
|
||||
$server = Server::with('settings')->findOrFail($this->server->id);
|
||||
|
||||
Process::fake(['*' => Process::result(output: 'all logs')]);
|
||||
|
||||
Livewire::test(GetLogs::class, [
|
||||
'server' => $server,
|
||||
'resource' => $this->application,
|
||||
'container' => 'test-container',
|
||||
])
|
||||
->assertSee('All')
|
||||
->assertSeeHtml('title="Show all logs"')
|
||||
->call('showAllLogs')
|
||||
->assertSet('numberOfLines', -1);
|
||||
|
||||
Process::assertRan(fn ($process) => str_contains($process->command, 'docker logs -n all'));
|
||||
});
|
||||
|
||||
test('getLogs marks ANSI-colored output truncated based on raw bytes', function () {
|
||||
$this->server->settings->fill([
|
||||
'is_reachable' => true,
|
||||
|
||||
@@ -47,3 +47,38 @@ YAML,
|
||||
|
||||
expect($labels->values()->all())->toContain("traefik.docker.network={$service->uuid}");
|
||||
});
|
||||
|
||||
it('labels an existing service application from its stored type after its image changes', function () {
|
||||
Bus::fake();
|
||||
|
||||
$team = Team::factory()->create();
|
||||
$server = Server::factory()->create(['team_id' => $team->id]);
|
||||
$destination = StandaloneDocker::where('server_id', $server->id)->firstOrFail();
|
||||
$project = Project::factory()->create(['team_id' => $team->id]);
|
||||
$environment = Environment::factory()->create(['project_id' => $project->id]);
|
||||
$service = Service::factory()->create([
|
||||
'environment_id' => $environment->id,
|
||||
'server_id' => $server->id,
|
||||
'destination_id' => $destination->id,
|
||||
'destination_type' => $destination->getMorphClass(),
|
||||
'docker_compose_raw' => <<<'YAML'
|
||||
services:
|
||||
app:
|
||||
image: postgres:17
|
||||
YAML,
|
||||
]);
|
||||
|
||||
$serviceApplication = ServiceApplication::create([
|
||||
'name' => 'app',
|
||||
'service_id' => $service->id,
|
||||
'image' => 'nginx:latest',
|
||||
]);
|
||||
|
||||
$parsedCompose = serviceParser($service);
|
||||
$labels = collect(data_get($parsedCompose, 'services.app.labels'));
|
||||
|
||||
expect($serviceApplication->fresh()->image)->toBe('postgres:17')
|
||||
->and($labels->values()->all())
|
||||
->toContain('coolify.service.subType=application')
|
||||
->not->toContain('coolify.service.subType=database');
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<?php
|
||||
|
||||
use App\Mcp\Concerns\ResolvesResource;
|
||||
use App\Models\Server;
|
||||
|
||||
it('normalizes requested log line counts', function () {
|
||||
@@ -12,12 +13,44 @@ it('normalizes requested log line counts', function () {
|
||||
expect(normalizeLogLines(null))->toBe(100)
|
||||
->and(normalizeLogLines(''))->toBe(100)
|
||||
->and(normalizeLogLines('abc'))->toBe(100)
|
||||
->and(normalizeLogLines('0'))->toBe(100)
|
||||
->and(normalizeLogLines('all'))->toBe('all')
|
||||
->and(normalizeLogLines('ALL'))->toBe(100)
|
||||
->and(normalizeLogLines('-1'))->toBe('all')
|
||||
->and(normalizeLogLines('0'))->toBe(0)
|
||||
->and(normalizeLogLines('-5'))->toBe(100)
|
||||
->and(normalizeLogLines('50'))->toBe(50)
|
||||
->and(normalizeLogLines('50000'))->toBe(10000);
|
||||
});
|
||||
|
||||
it('keeps MCP log requests bounded', function () {
|
||||
$normalizer = new class
|
||||
{
|
||||
use ResolvesResource;
|
||||
|
||||
public function normalize(mixed $lines): int
|
||||
{
|
||||
return $this->normalizeMcpLogLines($lines);
|
||||
}
|
||||
};
|
||||
|
||||
expect($normalizer->normalize('all'))->toBe(100)
|
||||
->and($normalizer->normalize('-1'))->toBe(100)
|
||||
->and($normalizer->normalize('0'))->toBe(100)
|
||||
->and($normalizer->normalize('501'))->toBe(500);
|
||||
});
|
||||
|
||||
it('documents the named all logs option on every REST log endpoint', function (string $controller) {
|
||||
$source = file_get_contents(__DIR__."/../../../app/Http/Controllers/Api/{$controller}.php");
|
||||
|
||||
expect($source)->toContain('Use `all` to return all logs. `-1` remains available as a compatibility alias.');
|
||||
})->with([
|
||||
'applications' => 'ApplicationsController',
|
||||
'databases' => 'DatabasesController',
|
||||
'services' => 'ServicesController',
|
||||
'service applications' => 'ServiceApplicationsController',
|
||||
'service databases' => 'ServiceDatabasesController',
|
||||
]);
|
||||
|
||||
it('normalizes service resource log line counts before invoking Docker', function (string $controller, mixed $lines, int $expectedLines) {
|
||||
$source = file_get_contents(__DIR__."/../../../app/Http/Controllers/Api/{$controller}.php");
|
||||
|
||||
@@ -63,6 +96,16 @@ it('builds docker log commands with options before an escaped container id', fun
|
||||
->toBe("docker logs -n 25 --timestamps 'container-1' 2>&1");
|
||||
});
|
||||
|
||||
it('builds docker log commands for all and zero lines', function () {
|
||||
$server = new Server;
|
||||
$server->settings = ['is_swarm_manager' => false];
|
||||
|
||||
expect(buildContainerLogsCommand($server, 'container-1', 'all'))
|
||||
->toBe("docker logs -n all 'container-1' 2>&1")
|
||||
->and(buildContainerLogsCommand($server, 'container-1', 0))
|
||||
->toBe("docker logs -n 0 'container-1' 2>&1");
|
||||
});
|
||||
|
||||
it('builds swarm service log commands with options before an escaped service id', function () {
|
||||
if (! function_exists('buildContainerLogsCommand')) {
|
||||
expect(function_exists('buildContainerLogsCommand'))->toBeTrue();
|
||||
|
||||
Reference in New Issue
Block a user