155 lines
5.4 KiB
PHP
155 lines
5.4 KiB
PHP
<?php
|
|
|
|
use App\Models\Host;
|
|
use App\Models\Instance;
|
|
use App\Models\Order;
|
|
use App\Models\ProvisioningRun;
|
|
use App\Provisioning\RunRunner;
|
|
use Illuminate\Support\Facades\Queue;
|
|
|
|
/**
|
|
* The estate that was sold a quota and never given one.
|
|
*
|
|
* ApplyStorageQuota runs in the `customer` and `plan-change` pipelines now, so
|
|
* anything built or changed from here on is enforced on the machine. Everything
|
|
* from before is not: `quota_gb` reached the instance row, Nextcloud was never
|
|
* told, and those customers still have the whole disk whatever they paid for.
|
|
* Nothing in the product could reach them, because both pipelines only run on a
|
|
* build or a change and these machines are having neither.
|
|
*/
|
|
|
|
/** A finished, running instance whose Nextcloud has never been told anything. */
|
|
function unquotedInstance(array $attributes = []): Instance
|
|
{
|
|
$host = Host::factory()->active()->create(['datacenter' => 'fsn', 'node' => 'pve']);
|
|
$order = Order::factory()->withSubscription()->create(['datacenter' => 'fsn', 'plan' => 'business']);
|
|
|
|
return Instance::factory()->create(array_merge([
|
|
'order_id' => $order->id,
|
|
'customer_id' => $order->customer_id,
|
|
'host_id' => $host->id,
|
|
'vmid' => 101,
|
|
'status' => 'active',
|
|
'quota_gb' => 500,
|
|
'quota_applied_gb' => null,
|
|
], $attributes));
|
|
}
|
|
|
|
/** Carry out every quota run the command started. */
|
|
function runQuotaRuns(): void
|
|
{
|
|
$runner = app(RunRunner::class);
|
|
|
|
ProvisioningRun::query()->where('pipeline', 'quota')->get()
|
|
->each(fn (ProvisioningRun $run) => $runner->advance($run));
|
|
}
|
|
|
|
it('applies the allowance to an instance that never had one, and records that it did', function () {
|
|
$services = fakeServices();
|
|
Queue::fake();
|
|
|
|
$instance = unquotedInstance();
|
|
|
|
$this->artisan('clupilot:apply-quotas')->assertSuccessful();
|
|
runQuotaRuns();
|
|
|
|
expect($services['pve']->guestRan("config:app:set files default_quota --value='500 GB'"))->toBeTrue()
|
|
->and($instance->fresh()->quota_applied_gb)->toBe(500)
|
|
->and($instance->fresh()->quotaIsEnforced())->toBeTrue();
|
|
});
|
|
|
|
it('is a no-op the second time round', function () {
|
|
$services = fakeServices();
|
|
Queue::fake();
|
|
|
|
unquotedInstance();
|
|
|
|
$this->artisan('clupilot:apply-quotas')->assertSuccessful();
|
|
runQuotaRuns();
|
|
|
|
$callsAfterFirstSweep = count($services['pve']->guestCommands);
|
|
|
|
$this->artisan('clupilot:apply-quotas')
|
|
->expectsOutputToContain('bereits gesetzt')
|
|
->assertSuccessful();
|
|
runQuotaRuns();
|
|
|
|
expect(ProvisioningRun::query()->where('pipeline', 'quota')->count())->toBe(1)
|
|
->and($services['pve']->guestCommands)->toHaveCount($callsAfterFirstSweep);
|
|
});
|
|
|
|
it('changes nothing at all in dry-run mode', function () {
|
|
$services = fakeServices();
|
|
Queue::fake();
|
|
|
|
$instance = unquotedInstance();
|
|
|
|
$this->artisan('clupilot:apply-quotas', ['--dry-run' => true])
|
|
->expectsOutputToContain('Probelauf')
|
|
->assertSuccessful();
|
|
|
|
expect(ProvisioningRun::query()->where('pipeline', 'quota')->count())->toBe(0)
|
|
->and($services['pve']->guestCommands)->toBe([])
|
|
->and($instance->fresh()->quota_applied_gb)->toBeNull();
|
|
});
|
|
|
|
it('skips what it has no business touching, and says which', function () {
|
|
fakeServices();
|
|
Queue::fake();
|
|
|
|
// Still being built: its own run is writing this machine.
|
|
unquotedInstance(['status' => 'provisioning']);
|
|
// Nothing to reach at all.
|
|
unquotedInstance(['status' => 'failed', 'vmid' => null]);
|
|
// No allowance on the row. Writing "0 GB" would lock the customer out of
|
|
// their own files, which is worse than leaving what is there.
|
|
unquotedInstance(['quota_gb' => 0]);
|
|
// A service that has run out.
|
|
unquotedInstance(['status' => 'ended']);
|
|
|
|
$busy = unquotedInstance();
|
|
ProvisioningRun::factory()->create([
|
|
'subject_type' => Order::class,
|
|
'subject_id' => $busy->order_id,
|
|
'pipeline' => 'plan-change',
|
|
'status' => ProvisioningRun::STATUS_RUNNING,
|
|
]);
|
|
|
|
$this->artisan('clupilot:apply-quotas')
|
|
->expectsOutputToContain('keine erreichbare Maschine')
|
|
->expectsOutputToContain('kein Kontingent hinterlegt')
|
|
->expectsOutputToContain('anderer Lauf aktiv')
|
|
->assertSuccessful();
|
|
|
|
expect(ProvisioningRun::query()->where('pipeline', 'quota')->count())->toBe(0);
|
|
});
|
|
|
|
it('re-applies an allowance that has changed since it was last enforced', function () {
|
|
$services = fakeServices();
|
|
Queue::fake();
|
|
|
|
// A downgrade landed on the row but the run that should have followed it
|
|
// never finished — the customer is paying for less and still holds more.
|
|
$instance = unquotedInstance(['quota_gb' => 200, 'quota_applied_gb' => 500]);
|
|
|
|
$this->artisan('clupilot:apply-quotas')->assertSuccessful();
|
|
runQuotaRuns();
|
|
|
|
expect($services['pve']->guestRan("config:app:set files default_quota --value='200 GB'"))->toBeTrue()
|
|
->and($instance->fresh()->quota_applied_gb)->toBe(200);
|
|
});
|
|
|
|
it('repairs one named instance when asked to', function () {
|
|
fakeServices();
|
|
Queue::fake();
|
|
|
|
$wanted = unquotedInstance();
|
|
unquotedInstance();
|
|
|
|
$this->artisan('clupilot:apply-quotas', ['--instance' => $wanted->uuid])->assertSuccessful();
|
|
|
|
expect(ProvisioningRun::query()->where('pipeline', 'quota')->count())->toBe(1)
|
|
->and(ProvisioningRun::query()->where('pipeline', 'quota')->first()->context('instance_id'))
|
|
->toBe($wanted->id);
|
|
});
|