Skip to content

Commit

Permalink
ZENKO-4789: implement quota tests and logic
Browse files Browse the repository at this point in the history
  • Loading branch information
williamlardier committed May 5, 2024
1 parent e70363a commit c118d12
Show file tree
Hide file tree
Showing 7 changed files with 301 additions and 34 deletions.
73 changes: 66 additions & 7 deletions tests/ctst/common/common.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
import { Given, setDefaultTimeout, Then } from '@cucumber/cucumber';
import { Given, setDefaultTimeout, Then, When } from '@cucumber/cucumber';
import { Constants, S3, Utils } from 'cli-testing';
import Zenko from 'world/Zenko';
import { extractPropertyFromResults } from './utils';
import assert from 'assert';
import { Admin, Kafka } from 'kafkajs';
import { createBucketWithConfiguration, putObject } from 'steps/utils/utils';
import { createBucketWithConfiguration, putObject, runActionAgainstBucket } from 'steps/utils/utils';
import { ActionPermissionsType } from 'steps/bucket-policies/utils';

setDefaultTimeout(Constants.DEFAULT_TIMEOUT);

async function getTopicsOffsets(topics:string[], kafkaAdmin:Admin) {
async function getTopicsOffsets(topics: string[], kafkaAdmin: Admin) {
const offsets = [];
for (const topic of topics) {
const partitions: ({ high: string; low: string; })[] =
await kafkaAdmin.fetchTopicOffsets(topic);
await kafkaAdmin.fetchTopicOffsets(topic);
offsets.push({ topic, partitions });
}
return offsets;
}

Given('an account', async function (this: Zenko) {
await this.createAccount();
});

Given('a {string} bucket', async function (this: Zenko, versioning: string) {
this.resetCommand();
const preName = this.parameters.AccountName || Constants.ACCOUNT_NAME;
Expand Down Expand Up @@ -49,7 +53,7 @@ Then('kafka consumed messages should not take too much place on disk',
const kafkaAdmin = new Kafka({ brokers: [this.parameters.KafkaHosts] }).admin();
const topics: string[] = (await kafkaAdmin.listTopics())
.filter(t => (t.includes(this.parameters.InstanceID) &&
!ignoredTopics.some(e => t.includes(e))));
!ignoredTopics.some(e => t.includes(e))));
const previousOffsets = await getTopicsOffsets(topics, kafkaAdmin);

const seconds = parseInt(this.parameters.KafkaCleanerInterval);
Expand All @@ -59,7 +63,7 @@ Then('kafka consumed messages should not take too much place on disk',
// verify that the timestamp is not older than last kafkacleaner run
// Instead of waiting for a fixed amount of time,
// we could also check for metrics to see last kafkacleaner run

// 10 seconds added to be sure kafkacleaner had time to process
await Utils.sleep(seconds * 1000 + 10000);

Expand All @@ -85,3 +89,58 @@ Given('an object {string} that {string}', async function (this: Zenko, objectNam
await putObject(this, objectName);
}
});

When('the user tries to perform the current S3 action on the bucket {string} times with a {string} ms delay',
async function (this: Zenko, numberOfRuns: string, delay: string) {
this.setAuthMode('test_identity');
const action = {
...this.getSaved<ActionPermissionsType>('currentAction'),
};
if (action.action === 'ListObjectVersions') {
action.action = 'ListObjects';
this.addToSaved('currentAction', action);
}
if (action.action.includes('Version') && !action.action.includes('Versioning')) {
action.action = action.action.replace('Version', '');
this.addToSaved('currentAction', action);
}
for (let i = 0; i < Number(numberOfRuns); i++) {
// For repeated WRITE actions, we want to change the object name
if (action.action === 'PutObject') {
this.addToSaved('objectName', `objectrepeat-${Utils.randomString()}`);
} else if (action.action === 'CopyObject' || action.action === 'UploadPartCopy') {
this.addToSaved('copyObject', `objectrepeatcopy-${Utils.randomString()}`);
}
await runActionAgainstBucket(this, this.getSaved<ActionPermissionsType>('currentAction').action);
if (this.getResult().err) {
// stop at any error, the error will be evaluated in a separated step
return;
}
await Utils.sleep(Number(delay));
}
});

Then('the API should {string} with {string}', function (this: Zenko, result: string, expected: string) {
this.cleanupEntity();
const action = this.getSaved<ActionPermissionsType>('currentAction');
switch (result) {
case 'success':
if (action.expectedResultOnAllowTest) {
assert.strictEqual(
this.getResult().err?.includes(action.expectedResultOnAllowTest) ||
this.getResult().stdout?.includes(action.expectedResultOnAllowTest) ||
this.getResult().err === null, true);
} else {
assert.strictEqual(!!this.getResult().err, false);
}
break;
case 'fail':
assert.strictEqual(this.getResult().err?.includes(expected), true);
break;
}
});

Then('the operation finished without error', function (this: Zenko) {
this.cleanupEntity();
assert.strictEqual(!!this.getResult().err, false);
});
12 changes: 10 additions & 2 deletions tests/ctst/steps/bucket-policies/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ Given('an action {string}', function (this: Zenko, apiName: string) {
}
});

Given('an upload size of {string} B', async function (this: Zenko, size: string) {
this.addToSaved('objectSize', parseInt(size, 10));
if (this.getSaved<boolean>('preExistingObject')) {
this.addToSaved('objectName', `objectforbptests-${Utils.randomString()}`);
await putObject(this, this.getSaved<string>('objectName'));
}
});

Given('an existing bucket prepared for the action', async function (this: Zenko) {
await createBucketWithConfiguration(this,
this.getSaved<string>('bucketName'),
Expand Down Expand Up @@ -335,7 +343,7 @@ Given('an environment setup for the API', async function (this: Zenko) {
roleName: this.getSaved<string>('identityName'),
});
assert.ifError(result.stderr || result.err);
} else {
} else if (identityType === EntityType.IAM_USER) { // accounts do not have any policy
const result = await IAM.attachUserPolicy({
policyArn,
userName: this.getSaved<string>('identityName'),
Expand Down Expand Up @@ -397,7 +405,7 @@ Given('an environment setup for the API', async function (this: Zenko) {
roleName: this.getSaved<string>('identityName'),
});
assert.ifError(result.stderr || result.err);
} else {
} else if (identityType === EntityType.IAM_USER) { // accounts do not have any policy
const detachResult = await IAM.detachUserPolicy({
policyArn,
userName: this.getSaved<string>('identityName'),
Expand Down
11 changes: 9 additions & 2 deletions tests/ctst/steps/bucket-policies/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const needObjectLock = [
];

const needObject = [
'CopyObject',
'PutObjectLegalHold',
'PutObjectRetention',
'PutObjectTagging',
Expand All @@ -46,6 +47,7 @@ const needObject = [
'PutObjectVersionTagging',
'PutObjectVersionRetention',
'PutObjectVersionLegalHold',
'RestoreObject',
];

const needVersioning = [
Expand Down Expand Up @@ -325,12 +327,12 @@ const actionPermissions: ActionPermissionsType[] = [
{
action: 'UploadPart',
permissions: ['s3:PutObject'],
expectedResultOnAllowTest: 'NoSuchUpload',
needsSetup: true,
},
{
action: 'UploadPartCopy',
permissions: ['s3:PutObject', 's3:GetObject'],
expectedResultOnAllowTest: 'NoSuchUpload',
needsSetup: true,
},
{
action: 'CopyObject',
Expand Down Expand Up @@ -362,6 +364,11 @@ const actionPermissions: ActionPermissionsType[] = [
permissions: ['s3:ListBucketVersions', 's3:ListBucket'],
excludePermissionOnBucketObjects: true,
},
{
action: 'RestoreObject',
permissions: ['s3:RestoreObject'],
expectedResultOnAllowTest: 'InvalidObjectState',
},
{
action: 'MetadataSearch',
permissions: ['s3:MetadataSearch'],
Expand Down
136 changes: 136 additions & 0 deletions tests/ctst/steps/quotas/quotas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/* eslint-disable no-case-declarations */
import fs from 'fs';
import lockFile from 'proper-lockfile';
import { createHash } from 'crypto';
import { Given, Before } from '@cucumber/cucumber';
import Zenko from '../../world/Zenko';
import { Scality, Command, CacheHelper, Constants, Utils } from 'cli-testing';
import { createJobAndWaitForCompletion } from 'steps/utils/kubernetes';
import { createBucketWithConfiguration, putObject } from 'steps/utils/utils';

function hashStringAndKeepFirst40Characters(input: string) {
return createHash('sha256').update(input).digest('hex').slice(0, 40);
}

/**
* The objective of this hook is to prepare all the buckets and accounts
* we use during quota checks, so that we avoid running the job multiple
* times, which affects the performance of the tests.
* The steps arer: create an account, then create a simple bucket
*/
Before({tags: '@Quotas'}, async function ({ gherkinDocument, pickle }) {
let initiated = false;
let releaseLock: (() => Promise<void>) | false = false;
const output: { [key: string]: { AccessKey: string, SecretKey: string }} = {};
const world = this as Zenko;

await Zenko.init(world.parameters);

const featureName = gherkinDocument.feature?.name?.replace(/ /g, '-').toLowerCase() || 'quotas';
const filePath = `/tmp/${featureName}`;

if (!fs.existsSync(filePath)) {
fs.writeFileSync(filePath, JSON.stringify({
ready: false,
}));
} else {
initiated = true;
}

if (!initiated) {
try {
releaseLock = await lockFile.lock(filePath, { stale: Constants.DEFAULT_TIMEOUT / 2 });
} catch (err) {
releaseLock = false;
}
}

if (releaseLock) {
for (const scenario of gherkinDocument.feature?.children || []) {
for (const example of scenario.scenario?.examples || []) {
for (const values of example.tableBody || []) {
const scenarioWithExampleID = hashStringAndKeepFirst40Characters(`${values.id}`);
await world.createAccount(scenarioWithExampleID);
await createBucketWithConfiguration(world, scenarioWithExampleID, 'with');
await putObject(world);
output[scenarioWithExampleID] = {
AccessKey: CacheHelper.parameters?.AccessKey || Constants.DEFAULT_ACCESS_KEY,
SecretKey: CacheHelper.parameters?.SecretKey || Constants.DEFAULT_SECRET_KEY,
};
}
}
}

await createJobAndWaitForCompletion(world, 'end2end-ops-count-items', 'quotas-setup');
await Utils.sleep(2000);
fs.writeFileSync(filePath, JSON.stringify({
ready: true,
...output,
}));

await releaseLock();
} else {
while (!fs.existsSync(filePath)) {
await Utils.sleep(100);
}

let configuration: { ready: boolean } = JSON.parse(fs.readFileSync(filePath, 'utf8')) as { ready: boolean };
while (!configuration.ready) {
await Utils.sleep(100);
configuration = JSON.parse(fs.readFileSync(filePath, 'utf8')) as { ready: boolean };
}
}

const configuration: typeof output = JSON.parse(fs.readFileSync(`/tmp/${featureName}`, 'utf8')) as typeof output;
const key = hashStringAndKeepFirst40Characters(`${pickle.astNodeIds[1]}`);
world.parameters.logger?.debug('Scenario key', { key, from: `${pickle.astNodeIds[1]}`, configuration });
const config = configuration[key];
world.resetGlobalType();
Zenko.saveAccountAccessKeys(config.AccessKey, config.SecretKey);
world.addToSaved('bucketName', key);
});

Given('a bucket quota set to {string} B', async function (this: Zenko, quota: string) {
if (quota === '0') {
return;
}
this.addCommandParameter({
quota,
});
this.addCommandParameter({
bucket: this.getSaved<string>('bucketName'),
});
const result: Command = await Scality.updateBucketQuota(
this.parameters,
this.getCliMode(),
this.getCommandParameters());

this.parameters.logger?.debug('UpdateBucketQuota result', {
result,
});

if (result.err) {
throw new Error(result.err);
}
});

Given('an account quota set to {string} B', async function (this: Zenko, quota: string) {
if (quota === '0') {
return;
}
this.addCommandParameter({
quotaMax: quota,
});
const result: Command = await Scality.updateAccountQuota(
this.parameters,
this.getCliMode(),
this.getCommandParameters());

this.parameters.logger?.debug('UpdateAccountQuota result', {
result,
});

if (result.err) {
throw new Error(result.err);
}
});
11 changes: 1 addition & 10 deletions tests/ctst/steps/sosapi.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { promises as fsp } from 'fs';
import { join } from 'path';
import Zenko from 'world/Zenko';
import { Then, When } from '@cucumber/cucumber';
import { strict as assert } from 'assert';
import { CacheHelper, S3, Utils } from 'cli-testing';
import { deleteFile, saveAsFile } from './utils/utils';

const validSystemXml = `
<?xml version="1.0" encoding="UTF-8"?>
Expand Down Expand Up @@ -45,14 +44,6 @@ const invalidCapacityXml = `
<Used>0</Used>
</CapacityInfo>`;

async function saveAsFile(name: string, content: string) {
return fsp.writeFile(join('/tmp', name), content);
}

async function deleteFile(path: string) {
return fsp.unlink(path);
}

const veeamPrefix = '.system-d26a9498-cb7c-4a87-a44a-8ae204f5ba6c/';

When('I PUT the {string} {string} XML file',
Expand Down
Loading

0 comments on commit c118d12

Please sign in to comment.