Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion solution/deps.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ mongodb-connector:
pensieve-api:
sourceRegistry: ghcr.io/scality
image: pensieve-api
tag: 1.9.0
tag: 1.9.1
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

depending on timing, we may want to bump 1.9.2 and update VERSION (when rebasing before the merge) so we can make the release (c.f. https://scality.atlassian.net/browse/ZENKO-5210)
→ ping me if in doubt how to "optimize" rebuilds

envsubst: PENSIEVE_API_TAG
rclone:
sourceRegistry: rclone
Expand Down
6 changes: 4 additions & 2 deletions tests/ctst/common/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,10 @@ async function getTopicsOffsets(topics: string[], kafkaAdmin: Admin) {
return offsets;
}

Given('an account', async function (this: Zenko) {
await this.createAccount();
Given('{int} additional accounts', async function (this: Zenko, count: number) {
for (let i = 0; i < count; i++) {
await this.createAccount();
}
});

async function createBucket(world: Zenko, versioning: string, bucketName: string) {
Expand Down
11 changes: 11 additions & 0 deletions tests/ctst/common/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Zenko from '../world/Zenko';
import { CacheHelper, Identity } from 'cli-testing';
import { prepareQuotaScenarios, teardownQuotaScenarios } from 'steps/quotas/quotas';
import { prepareUtilizationScenarios } from 'steps/utilization/utilizationAPI';
import { prepareMetricsScenarios } from './utils';
import { cleanS3Bucket } from './common';
import { cleanAzureContainer, cleanZenkoLocation } from 'steps/azureArchive';
import { displayDebuggingInformation, preparePRA } from 'steps/pra';
Expand Down Expand Up @@ -60,6 +61,16 @@ Before({ tags: '@UtilizationAPI', timeout: 1200000 }, async function (scenarioOp
await prepareUtilizationScenarios(this as Zenko, scenarioOptions);
});

Before({ tags: '@PrepareStorageUsageReportingScenarios', timeout: 1200000 }, async function (scenarioOptions) {
await prepareMetricsScenarios(this as Zenko, scenarioOptions, {
versioning: '',
jobNamespace: 'storage-usage-reporting-setup',
jobName: 'end2end-ops-count-items',
objectSize: 200,
objectCount: 3,
});
});

After(async function (this: Zenko, results) {
// Reset any configuration set on the endpoint (ssl, port)
CacheHelper.parameters.ssl = this.parameters.ssl;
Expand Down
12 changes: 9 additions & 3 deletions tests/ctst/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,10 +304,12 @@ export async function cleanupAccount(world: Zenko, accountName: string) {
}
}

interface PrepareScenarioOptions {
export interface PrepareScenarioOptions {
versioning?: string;
jobNamespace?: string;
jobName?: string;
objectSize?: number;
objectCount?: number;
}

/**
Expand All @@ -330,7 +332,9 @@ export async function prepareMetricsScenarios(
const {
versioning = '',
jobName = 'end2end-ops-count-items',
jobNamespace = `${featureName}-setup`
jobNamespace = `${featureName}-setup`,
objectSize = 0,
objectCount = 1,
} = options;

if (!fs.existsSync(filePath)) {
Expand Down Expand Up @@ -365,7 +369,9 @@ export async function prepareMetricsScenarios(
for (const scenarioId of scenarioIds) {
await world.createAccount(scenarioId, true);
await createBucketWithConfiguration(world, scenarioId, versioning);
await putObject(world);
for (let i = 0; i < objectCount; i++) {
await putObject(world, undefined, undefined, objectSize);
}
output[scenarioId] = Identity.getCurrentCredentials()!;
}

Expand Down
49 changes: 49 additions & 0 deletions tests/ctst/features/reporting/StorageUsageReporting.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
Feature: Storage Usage Reporting API
The storage usage reporting API allows authorized Keycloak users to retrieve
aggregated storage usage metrics across all accounts and locations.

# Permission tests
@2.14.0
@PreMerge
@StorageUsageReporting
Scenario Outline: Storage usage report access control per role
When the user retrieves the storage usage report as "<role>"
Then the storage usage report http response code is <expectedStatus>

Examples:
| role | expectedStatus |
| storage_manager | 200 |
| data_consumer | 403 |

# Content tests
@2.14.0
@PreMerge
@StorageUsageReporting
Scenario: Storage usage report has a valid structure
When the user retrieves the storage usage report as "storage_manager"
Then the storage usage report http response code is 200
And the storage usage report response has a valid structure

@2.14.0
@PreMerge
@StorageUsageReporting
Scenario: Storage usage report contains multiple accounts
Given 2 additional accounts
When the user retrieves the storage usage report as "storage_manager"
Then the storage usage report http response code is 200
And the storage usage report contains the additional accounts

@2.14.0
@PreMerge
@StorageUsageReporting
@PrepareStorageUsageReportingScenarios
Scenario Outline: Storage usage report returns accurate metrics
Given the environment is set up with bucket created, test data uploaded, and count-items ran
When the user retrieves the storage usage report as "storage_manager"
Then the storage usage report http response code is 200
And the report contains the test account with location "<locationName>"
And the report shows 3 objects and 600 bytes

Examples:
| locationName |
| us-east-1 |
12 changes: 6 additions & 6 deletions tests/ctst/steps/bucket-policies/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,14 +152,14 @@ Given('an {string} IAM Policy that {string} with {string} effect for the current
|| identityType === EntityType.DATA_CONSUMER) {
const result = await IAM.attachRolePolicy({
policyArn,
roleName: this.getSaved<string>('identityNameForScenario'),
roleName: this.getSavedIdentity().identityName,
});
assert.ifError(result.stderr || result.err);
}
if (identityType === EntityType.IAM_USER) {
const result = await IAM.attachUserPolicy({
policyArn,
userName: this.getSaved<string>('identityNameForScenario'),
userName: this.getSavedIdentity().identityName,
});
assert.ifError(result.stderr || result.err);
}
Expand Down Expand Up @@ -361,13 +361,13 @@ Given('an environment setup for the API', async function (this: Zenko) {
|| identityType === EntityType.DATA_CONSUMER) {
const result = await IAM.attachRolePolicy({
policyArn,
roleName: this.getSaved<string>('identityNameForScenario'),
roleName: this.getSavedIdentity().identityName,
});
assert.ifError(result.stderr || result.err);
} else if (identityType === EntityType.IAM_USER) { // accounts do not have any policy
const result = await IAM.attachUserPolicy({
policyArn,
userName: this.getSaved<string>('identityNameForScenario'),
userName: this.getSavedIdentity().identityName,
});
assert.ifError(result.stderr || result.err);
}
Expand Down Expand Up @@ -434,13 +434,13 @@ Given('an environment setup for the API', async function (this: Zenko) {
|| identityType === EntityType.DATA_CONSUMER) {
const result = await IAM.detachRolePolicy({
policyArn,
roleName: this.getSaved<string>('identityNameForScenario'),
roleName: this.getSavedIdentity().identityName,
});
assert.ifError(result.stderr || result.err);
} else if (identityType === EntityType.IAM_USER) { // accounts do not have any policy
const detachResult = await IAM.detachUserPolicy({
policyArn,
userName: this.getSaved<string>('identityNameForScenario'),
userName: this.getSavedIdentity().identityName,
});
assert.ifError(detachResult.stderr || detachResult.err);
}
Expand Down
11 changes: 6 additions & 5 deletions tests/ctst/steps/iam-policies/common.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import { When, Then } from '@cucumber/cucumber';
import { strict as assert } from 'assert';
import Zenko from '../../world/Zenko';
import { CacheHelper, ClientOptions, Command, Identity, IdentityEnum, VaultAuth } from 'cli-testing';
import { CacheHelper, ClientOptions, Command, Identity, VaultAuth } from 'cli-testing';
import { runActionAgainstBucket } from 'steps/utils/utils';

When('the user tries to perform {string} on the bucket', async function (this: Zenko, action: string) {
await runActionAgainstBucket(this, action);
});

When('the user tries to perform vault auth {string}', async function (this: Zenko, action: string) {
const lastIdentity = this.getSavedIdentity();
const userCredentials = Identity.getCredentialsForIdentity(
this.getSaved<IdentityEnum>('identityTypeForScenario'),
this.getSaved<string>('identityNameForScenario'),
this.getSaved<string>('accountNameForScenario'),
lastIdentity.identityType,
lastIdentity.identityName,
lastIdentity.accountName,
);

if (!userCredentials) {
Expand All @@ -35,7 +36,7 @@ When('the user tries to perform vault auth {string}', async function (this: Zenk
switch (action) {
case 'GetAccountInfo':
this.setResult(await VaultAuth.getAccounts([
this.getSaved<string>('accountNameForScenario') || this.parameters.AccountName,
lastIdentity.accountName || this.parameters.AccountName,
], null, null, {
// @ts-expect-error accountNames is not generated by CTST yet
accountNames: true,
Expand Down
79 changes: 79 additions & 0 deletions tests/ctst/steps/reporting/storageUsageReporting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { When, Then } from '@cucumber/cucumber';
import { strict as assert } from 'assert';
import Zenko from '../../world/Zenko';
import { IdentityEnum } from 'cli-testing';

interface LocationUsage {
bytesTotal: number;
objectsTotal: number;
}

interface ReportingUsageResponse {
isTruncated: boolean;
marker: string | null;
accounts: Record<string, Record<string, LocationUsage>>;
}

When('the user retrieves the storage usage report as {string}', async function (this: Zenko, role: string) {
const result = await this.managementAPIRequest(
'GET',
`/instance/${this.parameters.InstanceID}/reporting/usage`,
{},
{},
role,
);
this.addToSaved('reportingResponse', result);
});

Then('the storage usage report http response code is {int}', function (this: Zenko, expectedStatus: number) {
const response = this.getSaved<{ statusCode: number }>('reportingResponse');
assert.strictEqual(response.statusCode, expectedStatus,
`Expected status ${expectedStatus} but got ${response.statusCode}`);
});

Then('the storage usage report response has a valid structure', function (this: Zenko) {
const response = this.getSaved<{ statusCode: number; data: ReportingUsageResponse }>(
'reportingResponse');
const data = response.data;
assert.strictEqual(typeof data.isTruncated, 'boolean',
'isTruncated should be a boolean');
assert.ok(typeof data.marker === 'string' || data.marker === null,
'marker should be a string or null');
assert.strictEqual(typeof data.accounts, 'object',
'accounts should be an object');
});

Then('the storage usage report contains the additional accounts', async function (this: Zenko) {
const response = this.getSaved<{ statusCode: number; data: ReportingUsageResponse }>(
'reportingResponse');
const accountNames = this.getSavedIdentities()
.filter(id => id.identityType === IdentityEnum.ACCOUNT)
.map(id => id.accountName);
for (const accountName of accountNames) {
assert.ok(accountName in response.data.accounts,
`Account ${accountName} should be present in the report`);
}
});

Then('the report contains the test account with location {string}', async function (this: Zenko, locationName: string) {
const response = this.getSaved<{ statusCode: number; data: ReportingUsageResponse }>(
'reportingResponse');
const accountName = this.getSaved<string>('accountName');

assert.ok(accountName in response.data.accounts,
`Account ${accountName} should be present in the report`);

const accountData = response.data.accounts[accountName];
assert.ok(locationName in accountData,
`Location ${locationName} should be present for account ${accountName}`);

this.addToSaved('reportedLocationUsage', accountData[locationName]);
});

Then('the report shows {int} objects and {int} bytes', function (this: Zenko, objects: number, bytes: number) {
const usage = this.getSaved<LocationUsage>('reportedLocationUsage');
assert.strictEqual(usage.objectsTotal, objects,
`Expected ${objects} objects but got ${usage.objectsTotal}`);
assert.strictEqual(usage.bytesTotal, bytes,
`Expected ${bytes} bytes but got ${usage.bytesTotal}`);
});
8 changes: 4 additions & 4 deletions tests/ctst/steps/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,11 @@ export async function deleteFile(path: string) {
return fsp.unlink(path);
}

export async function uploadSetup(world: Zenko, action: string, body?: string) {
export async function uploadSetup(world: Zenko, action: string, body?: string, size?: number) {
if (action !== 'PutObject' && action !== 'UploadPart') {
return;
}
const objectSize = world.getSaved<number>('objectSize') || 0;
const objectSize = size ?? world.getSaved<number>('objectSize') || 0;
if (body || objectSize > 0) {
const tempFileName = `${Utils.randomString()}_${world.getSaved<string>('objectName')}`;
world.addToSaved('tempFileName', `/tmp/${tempFileName}`);
Expand Down Expand Up @@ -289,15 +289,15 @@ async function copyObject(world: Zenko, srcObjectName?: string, dstObjectName?:
return result;
}

async function putObject(world: Zenko, objectName?: string, content?: string) {
async function putObject(world: Zenko, objectName?: string, content?: string, objectSize?: number) {
world.resetCommand();
let finalObjectName = objectName;
if (!finalObjectName) {
finalObjectName = `${Utils.randomString()}`;
}
world.addToSaved('objectName', finalObjectName);
world.logger.debug('Adding object', { objectName: finalObjectName });
await uploadSetup(world, 'PutObject', content);
await uploadSetup(world, 'PutObject', content, objectSize);
world.addCommandParameter({ key: finalObjectName });
world.addCommandParameter({ bucket: world.getSaved<string>('bucketName') });
const userMetadata = world.getSaved<string>('userMetadata');
Expand Down
Loading
Loading