mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
v4.7 (#12478)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Co-authored-by: Aditya Tripathi <aditya@climactic.co>
This commit is contained in:
co-authored by
Claude Opus 5.5
peaklabs-dev
Aditya Tripathi
parent
b7dc2e435b
commit
033f0c84da
@@ -0,0 +1,336 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\ImageScanGateMode;
|
||||
use App\Enums\ImageScanOnError;
|
||||
use App\Models\Application;
|
||||
use App\Models\Environment;
|
||||
use App\Models\ImageScanPolicy;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use App\Models\ServiceApplication;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\Team;
|
||||
use App\Services\ImageScan\ImageScanPolicyResolver;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Once;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
Once::flush();
|
||||
Server::flushIdentityMap();
|
||||
InstanceSettings::forceCreate(['id' => 0]);
|
||||
|
||||
$this->team = Team::factory()->create();
|
||||
$this->server = Server::factory()->create(['team_id' => $this->team->id]);
|
||||
$this->destination = StandaloneDocker::query()->where('server_id', $this->server->id)->firstOrFail();
|
||||
$this->project = Project::factory()->create(['team_id' => $this->team->id]);
|
||||
$this->environment = Environment::factory()->create(['project_id' => $this->project->id]);
|
||||
$this->application = Application::factory()->create([
|
||||
'environment_id' => $this->environment->id,
|
||||
'destination_id' => $this->destination->id,
|
||||
'destination_type' => $this->destination->getMorphClass(),
|
||||
]);
|
||||
});
|
||||
|
||||
function resolverPolicyFor(Model $scope, array $values): ImageScanPolicy
|
||||
{
|
||||
return ImageScanPolicy::query()->create([
|
||||
'scope_type' => $scope->getMorphClass(),
|
||||
'scope_id' => $scope->id,
|
||||
...$values,
|
||||
]);
|
||||
}
|
||||
|
||||
function resolverEnableScanning(): void
|
||||
{
|
||||
test()->server->settings->forceFill(['is_image_scan_enabled' => true])->saveQuietly();
|
||||
}
|
||||
|
||||
function resolverService(): Service
|
||||
{
|
||||
return Service::factory()->create([
|
||||
'environment_id' => test()->environment->id,
|
||||
'server_id' => test()->server->id,
|
||||
'destination_id' => test()->destination->id,
|
||||
'destination_type' => test()->destination->getMorphClass(),
|
||||
'docker_compose_raw' => "services:\n app:\n image: nginx:alpine\n",
|
||||
]);
|
||||
}
|
||||
|
||||
it('uses the instance defaults when no level sets a value', function () {
|
||||
$policy = ImageScanPolicyResolver::for($this->application);
|
||||
|
||||
expect($policy->gateMode)->toBe('off')
|
||||
->and($policy->onScanError)->toBe(ImageScanOnError::Deploy)
|
||||
->and($policy->countMedium)->toBeFalse()
|
||||
->and($policy->sources)->toBe([
|
||||
'gate_mode' => 'instance',
|
||||
'on_scan_error' => 'instance',
|
||||
'count_medium' => 'instance',
|
||||
])
|
||||
->and($policy->scanServer->is($this->server))->toBeTrue();
|
||||
});
|
||||
|
||||
it('reads the instance row from the database', function () {
|
||||
ImageScanPolicy::query()->whereNull('scope_type')->update([
|
||||
'gate_mode' => 'strict',
|
||||
'on_scan_error' => 'block',
|
||||
]);
|
||||
|
||||
$policy = ImageScanPolicyResolver::for($this->application);
|
||||
|
||||
expect($policy->gateMode)->toBe('strict')
|
||||
->and($policy->onScanError)->toBe(ImageScanOnError::Block);
|
||||
});
|
||||
|
||||
it('falls back to the code defaults when the instance row is missing', function () {
|
||||
ImageScanPolicy::query()->whereNull('scope_type')->delete();
|
||||
|
||||
$policy = ImageScanPolicyResolver::for($this->application);
|
||||
|
||||
expect($policy->gateMode)->toBe('off')
|
||||
->and($policy->onScanError)->toBe(ImageScanOnError::Deploy)
|
||||
->and($policy->sources['gate_mode'])->toBe('instance');
|
||||
});
|
||||
|
||||
it('reads the lowest id when the instance row exists twice', function () {
|
||||
ImageScanPolicy::query()->whereNull('scope_type')->update(['gate_mode' => 'on']);
|
||||
ImageScanPolicy::query()->create(['gate_mode' => 'strict', 'on_scan_error' => 'block']);
|
||||
|
||||
expect(ImageScanPolicyResolver::for($this->application)->gateMode)->toBe('on');
|
||||
});
|
||||
|
||||
it('lets the team override the instance', function () {
|
||||
resolverPolicyFor($this->team, ['gate_mode' => ImageScanGateMode::On]);
|
||||
|
||||
$policy = ImageScanPolicyResolver::for($this->application);
|
||||
|
||||
expect($policy->gateMode)->toBe('on')
|
||||
->and($policy->sources['gate_mode'])->toBe('team')
|
||||
->and($policy->sources['on_scan_error'])->toBe('instance');
|
||||
});
|
||||
|
||||
it('lets the server override the team and the app override the server', function () {
|
||||
resolverPolicyFor($this->team, ['gate_mode' => ImageScanGateMode::Strict, 'on_scan_error' => ImageScanOnError::Block]);
|
||||
resolverPolicyFor($this->server, ['gate_mode' => ImageScanGateMode::Off]);
|
||||
resolverPolicyFor($this->application, ['gate_mode' => ImageScanGateMode::On]);
|
||||
|
||||
$policy = ImageScanPolicyResolver::for($this->application);
|
||||
|
||||
expect($policy->gateMode)->toBe('on')
|
||||
->and($policy->onScanError)->toBe(ImageScanOnError::Block)
|
||||
->and($policy->sources)->toBe([
|
||||
'gate_mode' => 'app',
|
||||
'on_scan_error' => 'team',
|
||||
'count_medium' => 'instance',
|
||||
]);
|
||||
});
|
||||
|
||||
it('turns the gate off at the app level even when the server sets strict', function () {
|
||||
resolverPolicyFor($this->server, ['gate_mode' => ImageScanGateMode::Strict]);
|
||||
resolverPolicyFor($this->application, ['gate_mode' => ImageScanGateMode::Off]);
|
||||
|
||||
$policy = ImageScanPolicyResolver::for($this->application);
|
||||
|
||||
expect($policy->gateMode)->toBe('off')
|
||||
->and($policy->sources['gate_mode'])->toBe('app');
|
||||
});
|
||||
|
||||
it('inherits the server value when the app value is null', function () {
|
||||
resolverPolicyFor($this->server, ['gate_mode' => ImageScanGateMode::Strict]);
|
||||
resolverPolicyFor($this->application, ['gate_mode' => null, 'on_scan_error' => ImageScanOnError::Block]);
|
||||
|
||||
$policy = ImageScanPolicyResolver::for($this->application);
|
||||
|
||||
expect($policy->gateMode)->toBe('strict')
|
||||
->and($policy->sources['gate_mode'])->toBe('server')
|
||||
->and($policy->sources['on_scan_error'])->toBe('app');
|
||||
});
|
||||
|
||||
it('resolves a service without the app level and never turns the gate on', function () {
|
||||
$service = resolverService();
|
||||
$serviceApplication = ServiceApplication::create(['name' => 'app', 'service_id' => $service->id]);
|
||||
resolverPolicyFor($this->server, ['gate_mode' => ImageScanGateMode::Strict, 'on_scan_error' => ImageScanOnError::Block]);
|
||||
|
||||
foreach ([$service, $serviceApplication] as $resource) {
|
||||
$policy = ImageScanPolicyResolver::for($resource);
|
||||
|
||||
expect($policy->gateMode)->toBe('off')
|
||||
->and($policy->onScanError)->toBe(ImageScanOnError::Block)
|
||||
->and($policy->sources['on_scan_error'])->toBe('server')
|
||||
->and($policy->scanServer->is($this->server))->toBeTrue();
|
||||
}
|
||||
});
|
||||
|
||||
it('does not read app rows for a database', function () {
|
||||
$database = StandalonePostgresql::forceCreate([
|
||||
'name' => 'pg',
|
||||
'postgres_password' => 'secret',
|
||||
'image' => 'postgres:16',
|
||||
'environment_id' => $this->environment->id,
|
||||
'destination_id' => $this->destination->id,
|
||||
'destination_type' => $this->destination->getMorphClass(),
|
||||
]);
|
||||
resolverPolicyFor($this->server, ['gate_mode' => ImageScanGateMode::On]);
|
||||
ImageScanPolicy::query()->create([
|
||||
'scope_type' => $database->getMorphClass(),
|
||||
'scope_id' => $database->id,
|
||||
'on_scan_error' => ImageScanOnError::Block,
|
||||
]);
|
||||
|
||||
$policy = ImageScanPolicyResolver::for($database);
|
||||
|
||||
expect($policy->gateMode)->toBe('off')
|
||||
->and($policy->onScanError)->toBe(ImageScanOnError::Deploy)
|
||||
->and($policy->scanServer->is($this->server))->toBeTrue();
|
||||
});
|
||||
|
||||
it('uses the team of the resource and not the session team', function () {
|
||||
$otherTeam = Team::factory()->create();
|
||||
resolverPolicyFor($this->team, ['gate_mode' => ImageScanGateMode::On]);
|
||||
resolverPolicyFor($otherTeam, ['gate_mode' => ImageScanGateMode::Strict]);
|
||||
session(['currentTeam' => $otherTeam]);
|
||||
|
||||
expect(ImageScanPolicyResolver::for($this->application)->gateMode)->toBe('on');
|
||||
});
|
||||
|
||||
it('scans on the given server but resolves the rules through the main destination server', function () {
|
||||
$buildServer = Server::factory()->create(['team_id' => $this->team->id]);
|
||||
resolverPolicyFor($this->server, ['gate_mode' => ImageScanGateMode::Strict]);
|
||||
resolverPolicyFor($buildServer, ['gate_mode' => ImageScanGateMode::Off]);
|
||||
|
||||
$policy = ImageScanPolicyResolver::for($this->application, $buildServer);
|
||||
|
||||
expect($policy->gateMode)->toBe('strict')
|
||||
->and($policy->sources['gate_mode'])->toBe('server')
|
||||
->and($policy->scanServer->is($buildServer))->toBeTrue();
|
||||
});
|
||||
|
||||
it('lists the inherited value and its level for each field', function () {
|
||||
resolverPolicyFor($this->server, ['gate_mode' => ImageScanGateMode::On]);
|
||||
|
||||
expect(ImageScanPolicyResolver::inheritedFor($this->application))->toBe([
|
||||
'gate_mode' => ['value' => ImageScanGateMode::On, 'source' => 'server'],
|
||||
'on_scan_error' => ['value' => ImageScanOnError::Deploy, 'source' => 'instance'],
|
||||
'count_medium' => ['value' => false, 'source' => 'instance'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('inherits count medium problems through instance, team, server and app', function () {
|
||||
expect(ImageScanPolicyResolver::for($this->application)->countMedium)->toBeFalse();
|
||||
|
||||
resolverPolicyFor($this->team, ['count_medium' => true]);
|
||||
$policy = ImageScanPolicyResolver::for($this->application);
|
||||
expect($policy->countMedium)->toBeTrue()
|
||||
->and($policy->sources['count_medium'])->toBe('team');
|
||||
|
||||
$serverPolicy = resolverPolicyFor($this->server, ['count_medium' => false]);
|
||||
expect(ImageScanPolicyResolver::for($this->application)->countMedium)->toBeFalse()
|
||||
->and(ImageScanPolicyResolver::inheritedFor($this->application)['count_medium'])->toBe(['value' => false, 'source' => 'server']);
|
||||
|
||||
resolverPolicyFor($this->application, ['count_medium' => true]);
|
||||
expect(ImageScanPolicyResolver::for($this->application)->sources['count_medium'])->toBe('app')
|
||||
->and(ImageScanPolicyResolver::countMediumFor($this->application))->toBeTrue();
|
||||
|
||||
$serverPolicy->update(['count_medium' => null]);
|
||||
ImageScanPolicy::query()->where('scope_type', $this->application->getMorphClass())->delete();
|
||||
expect(ImageScanPolicyResolver::countMediumFor($this->application))->toBeTrue();
|
||||
});
|
||||
|
||||
it('applies count medium problems to databases and services, not only apps', function () {
|
||||
$database = StandalonePostgresql::forceCreate([
|
||||
'name' => 'pg', 'postgres_password' => 'secret', 'image' => 'postgres:16',
|
||||
'environment_id' => $this->environment->id,
|
||||
'destination_id' => $this->destination->id,
|
||||
'destination_type' => $this->destination->getMorphClass(),
|
||||
]);
|
||||
$service = resolverService();
|
||||
resolverPolicyFor($this->server, ['count_medium' => true]);
|
||||
|
||||
expect(ImageScanPolicyResolver::for($database)->countMedium)->toBeTrue()
|
||||
->and(ImageScanPolicyResolver::countMediumFor($database))->toBeTrue()
|
||||
->and(ImageScanPolicyResolver::countMediumFor($service))->toBeTrue();
|
||||
});
|
||||
|
||||
it('resolves count medium problems for many resources with a fixed number of queries', function () {
|
||||
$otherServer = Server::factory()->create(['team_id' => $this->team->id]);
|
||||
$otherDestination = StandaloneDocker::query()->where('server_id', $otherServer->id)->firstOrFail();
|
||||
$applications = collect(range(1, 3))->map(fn () => Application::factory()->create([
|
||||
'environment_id' => $this->environment->id,
|
||||
'destination_id' => $otherDestination->id,
|
||||
'destination_type' => $otherDestination->getMorphClass(),
|
||||
]))->push($this->application);
|
||||
resolverPolicyFor($this->team, ['count_medium' => true]);
|
||||
resolverPolicyFor($otherServer, ['count_medium' => false]);
|
||||
resolverPolicyFor($applications[0], ['count_medium' => true]);
|
||||
$applications->each->load('destination.server');
|
||||
|
||||
DB::enableQueryLog();
|
||||
$many = ImageScanPolicyResolver::countMediumForMany($this->team->id, $applications);
|
||||
$queries = count(DB::getQueryLog());
|
||||
DB::disableQueryLog();
|
||||
|
||||
expect($queries)->toBeLessThanOrEqual(3)
|
||||
->and($many)->toBe($applications->mapWithKeys(fn (Application $application): array => [
|
||||
$application->getMorphClass().':'.$application->id => ImageScanPolicyResolver::countMediumFor($application),
|
||||
])->all())
|
||||
->and(array_values($many))->toBe([true, false, false, true]);
|
||||
});
|
||||
|
||||
it('keeps scanning off until the server opts in', function () {
|
||||
expect(ImageScanPolicyResolver::for($this->application)->scanEnabled)->toBeFalse();
|
||||
});
|
||||
|
||||
it('enables scanning when the instance, the server, and the app allow it', function () {
|
||||
resolverEnableScanning();
|
||||
|
||||
expect(ImageScanPolicyResolver::for($this->application)->scanEnabled)->toBeTrue();
|
||||
});
|
||||
|
||||
it('disables scanning when the instance does not allow it', function () {
|
||||
resolverEnableScanning();
|
||||
InstanceSettings::findOrFail(0)->forceFill(['is_image_scanning_allowed' => false])->saveQuietly();
|
||||
Once::flush();
|
||||
|
||||
expect(ImageScanPolicyResolver::for($this->application)->scanEnabled)->toBeFalse();
|
||||
});
|
||||
|
||||
it('disables scanning when the app turned its scan off', function () {
|
||||
resolverEnableScanning();
|
||||
$this->application->settings->forceFill(['is_image_scan_enabled' => false])->saveQuietly();
|
||||
|
||||
expect(ImageScanPolicyResolver::for($this->application)->scanEnabled)->toBeFalse();
|
||||
});
|
||||
|
||||
it('checks the server opt-in of the given server', function () {
|
||||
resolverEnableScanning();
|
||||
$buildServer = Server::factory()->create(['team_id' => $this->team->id]);
|
||||
|
||||
expect(ImageScanPolicyResolver::for($this->application, $buildServer)->scanEnabled)->toBeFalse();
|
||||
});
|
||||
|
||||
it('does not apply the app scan switch to services', function () {
|
||||
resolverEnableScanning();
|
||||
|
||||
expect(ImageScanPolicyResolver::for(resolverService())->scanEnabled)->toBeTrue();
|
||||
});
|
||||
|
||||
it('returns the instance row, an existing row, or a new unsaved row to edit', function () {
|
||||
$instance = ImageScanPolicyResolver::forScope(null);
|
||||
$new = ImageScanPolicyResolver::forScope($this->server);
|
||||
$existing = resolverPolicyFor($this->team, ['gate_mode' => ImageScanGateMode::On]);
|
||||
|
||||
expect($instance->exists)->toBeTrue()
|
||||
->and($instance->scope_type)->toBeNull()
|
||||
->and($new->exists)->toBeFalse()
|
||||
->and($new->scope_type)->toBe($this->server->getMorphClass())
|
||||
->and($new->scope_id)->toBe($this->server->id)
|
||||
->and(ImageScanPolicyResolver::forScope($this->team)->is($existing))->toBeTrue()
|
||||
->and(ImageScanPolicy::query()->count())->toBe(2);
|
||||
});
|
||||
Reference in New Issue
Block a user