Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/improvement/ZENKO-4941' into w/2…
Browse files Browse the repository at this point in the history
….7/improvement/ZENKO-4941
  • Loading branch information
williamlardier committed Dec 3, 2024
2 parents 8b4f3d4 + 6a8731c commit f3edf94
Show file tree
Hide file tree
Showing 7 changed files with 169 additions and 25 deletions.
3 changes: 3 additions & 0 deletions .github/scripts/end2end/configs/zenko.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,9 @@ spec:
azure:
archiveTier: "hot"
restoreTimeout: "15s"
scuba:
logging:
logLevel: debug
ingress:
workloadPlaneClass: 'nginx'
controlPlaneClass: 'nginx'
Expand Down
2 changes: 1 addition & 1 deletion solution/deps.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ scuba:
sourceRegistry: ghcr.io/scality
dashboard: scuba/scuba-dashboards
image: scuba
tag: 1.0.8
tag: 1.0.9
envsubst: SCUBA_TAG
sorbet:
sourceRegistry: ghcr.io/scality
Expand Down
8 changes: 7 additions & 1 deletion tests/ctst/common/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,13 @@ After(async function (this: Zenko, results) {
);
});

After({ tags: '@Quotas' }, async function () {
After({ tags: '@Quotas' }, async function (this: Zenko, results) {
if (results.result?.status === 'FAILED') {
this.logger.warn('quota was not cleaned for test', {
bucket: this.getSaved<string>('bucketName'),
});
return;
}
await teardownQuotaScenarios(this as Zenko);
});

Expand Down
88 changes: 85 additions & 3 deletions tests/ctst/features/quotas/Quotas.feature
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,9 @@ Feature: Quota Management for APIs
Given an action "DeleteObject"
And a permission to perform the "PutObject" action
And a STORAGE_MANAGER type
And a bucket quota set to 10000 B
And an account quota set to 10000 B
And an upload size of 1000 B for the object "obj-1"
And a bucket quota set to <bucketQuota> B
And an account quota set to <accountQuota> B
And an upload size of 200 B for the object "obj-1"
And a <userType> type
And an environment setup for the API
And an "existing" IAM Policy that "applies" with "ALLOW" effect for the current API
Expand All @@ -96,3 +94,87 @@ Feature: Quota Management for APIs
| 100 | 200 | 0 | IAM_USER |
| 100 | 0 | 200 | IAM_USER |
| 100 | 200 | 200 | IAM_USER |

@2.6.0
@PreMerge
@Quotas
@CronJob
@DataDeletion
@NonVersioned
Scenario Outline: Quotas are affected by deletion operations between count items runs
Given an action "DeleteObject"
And a permission to perform the "PutObject" action
And a STORAGE_MANAGER type
And a bucket quota set to 1000 B
And an account quota set to 1000 B
And an upload size of 1000 B for the object "obj-1"
And a bucket quota set to <bucketQuota> B
And an account quota set to <accountQuota> B
And a <userType> type
And an environment setup for the API
And an "existing" IAM Policy that "applies" with "ALLOW" effect for the current API
When I wait 3 seconds
And I PUT an object with size <uploadSize>
Then the API should "fail" with "QuotaExceeded"
When the "count-items" cronjobs completes without error
# Wait for inflights to be read by SCUBA
When I wait 3 seconds
# At this point if negative inflights are not supported, write should
# not be possible, as the previous inflights are now part of the current
# metrics.
And i delete object "obj-1"
# Wait for inflights to be read by SCUBA
And I wait 3 seconds
And I PUT an object with size <uploadSize>
Then the API should "succeed" with ""

Examples:
| uploadSize | bucketQuota | accountQuota | userType |
| 100 | 200 | 0 | ACCOUNT |
| 100 | 0 | 200 | ACCOUNT |
| 100 | 200 | 200 | ACCOUNT |
| 100 | 200 | 0 | IAM_USER |
| 100 | 0 | 200 | IAM_USER |
| 100 | 200 | 200 | IAM_USER |

@2.6.0
@PreMerge
@Quotas
@CronJob
@DataDeletion
@NonVersioned
Scenario Outline: Negative inflights do not allow to bypass the quota
Given an action "DeleteObject"
And a permission to perform the "PutObject" action
And a STORAGE_MANAGER type
And a bucket quota set to 1000 B
And an account quota set to 1000 B
And an upload size of 1000 B for the object "obj-1"
And a bucket quota set to <bucketQuota> B
And an account quota set to <accountQuota> B
And a <userType> type
And an environment setup for the API
And an "existing" IAM Policy that "applies" with "ALLOW" effect for the current API
When I wait 3 seconds
And I PUT an object with size <uploadSize>
Then the API should "fail" with "QuotaExceeded"
When the "count-items" cronjobs completes without error
# Wait for inflights to be read by SCUBA
When I wait 3 seconds
# At this point if negative inflights are not supported, write should
# not be possible, as the previous inflights are now part of the current
# metrics.
And i delete object "obj-1"
# Wait for inflights to be read by SCUBA
And I wait 3 seconds
And I PUT an object with size 1000
Then the API should "fail" with "QuotaExceeded"

Examples:
| uploadSize | bucketQuota | accountQuota | userType |
| 100 | 200 | 0 | ACCOUNT |
| 100 | 0 | 200 | ACCOUNT |
| 100 | 200 | 200 | ACCOUNT |
| 100 | 200 | 0 | IAM_USER |
| 100 | 0 | 200 | IAM_USER |
| 100 | 200 | 200 | IAM_USER |
14 changes: 14 additions & 0 deletions tests/ctst/steps/quotas/quotas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Scality, Command, Utils, AWSCredentials, Constants, Identity, IdentityE
import { createJobAndWaitForCompletion } from '../utils/kubernetes';
import { createBucketWithConfiguration, putObject } from '../utils/utils';
import { hashStringAndKeepFirst20Characters } from 'common/utils';
import assert from 'assert';

export async function prepareQuotaScenarios(world: Zenko, scenarioConfiguration: ITestCaseHookParameter) {
/**
Expand Down Expand Up @@ -136,6 +137,16 @@ Given('a bucket quota set to {int} B', async function (this: Zenko, quota: numbe
result,
});

// Ensure the quota is set
const resultGet: Command = await Scality.getBucketQuota(
this.parameters,
this.getCommandParameters());
this.logger.debug('GetBucketQuota result', {
resultGet,
});

assert(resultGet.stdout.includes(`${quota}`));

if (result.err) {
throw new Error(result.err);
}
Expand All @@ -158,6 +169,9 @@ Given('an account quota set to {int} B', async function (this: Zenko, quota: num
result,
});

// Ensure the quota is set
assert(JSON.parse(result.stdout).quota === quota);

if (result.err) {
throw new Error(result.err);
}
Expand Down
25 changes: 25 additions & 0 deletions tests/ctst/steps/utils/kubernetes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,34 @@ export function createKubeCustomObjectClient(world: Zenko): CustomObjectsApi {
return KubernetesHelper.customObject;
}

// Do not check job result, only wait till it completes
export async function waitForExistingJobCompletion(world: Zenko, jobName: string) {
const watchClient = createKubeWatchClient(world);
try {
await new Promise<void>(resolve => {
void watchClient.watch(
'/apis/batch/v1/namespaces/default/jobs',
{},
(type: string, apiObj, watchObj) => {
if ((watchObj.object?.metadata?.name as string)?.startsWith?.(jobName)) {
if (watchObj.object?.status?.succeeded || watchObj.object?.status?.failed) {
resolve();
}
}
}, () => resolve());
});
} catch (err: unknown) {
world.logger.error('error waiting for job completion', {
jobName,
err,
});
}
}

export async function createJobAndWaitForCompletion(world: Zenko, jobName: string, customMetadata?: string) {
const batchClient = createKubeBatchClient(world);
const watchClient = createKubeWatchClient(world);
await waitForExistingJobCompletion(world, jobName);
try {
const cronJob = await batchClient.readNamespacedCronJob(jobName, 'default');
const cronJobSpec = cronJob.body.spec?.jobTemplate.spec;
Expand Down
54 changes: 34 additions & 20 deletions tests/ctst/world/Zenko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { AccessKey } from '@aws-sdk/client-iam';
import { Credentials } from '@aws-sdk/client-sts';
import { aws4Interceptor } from 'aws4-axios';
import qs from 'qs';
import fs from 'fs';
import lockFile from 'proper-lockfile';
import Werelogs from 'werelogs';
import {
CacheHelper,
Expand Down Expand Up @@ -633,24 +635,36 @@ export default class Zenko extends World<ZenkoWorldParameters> {

if (!Identity.hasIdentity(IdentityEnum.ACCOUNT, accountName)) {
Identity.useIdentity(IdentityEnum.ADMIN, site.adminIdentityName);

const filePath = `/tmp/account-init-${accountName}.json`;
if (!fs.existsSync(filePath)) {
fs.writeFileSync(filePath, JSON.stringify({
ready: false,
}));
}
let account = null;
CacheHelper.logger.debug('Creating account', {
accountName,
adminIdentityName: site.adminIdentityName,
credentials: Identity.getCurrentCredentials(),
});
// Create the account if already exist will not throw any error
let releaseLock: (() => Promise<void>) | null = null;
try {
await SuperAdmin.createAccount({ accountName });
/* eslint-disable */
} catch (err: any) {
CacheHelper.logger.debug('Error while creating account', {
accountName,
err,
releaseLock = await lockFile.lock(filePath, {
stale: Constants.DEFAULT_TIMEOUT / 2,
retries: {
retries: 5,
factor: 3,
minTimeout: 1000,
maxTimeout: 5000,
}
});
if (!err.EntityAlreadyExists && err.code !== 'EntityAlreadyExists') {
throw err;

try {
await SuperAdmin.createAccount({ accountName });
/* eslint-disable */
} catch (err: any) {
if (!err.EntityAlreadyExists && err.code !== 'EntityAlreadyExists') {
throw err;
}
}
} finally {
if (releaseLock) {
await releaseLock();
}
}
/* eslint-enable */
Expand Down Expand Up @@ -693,7 +707,7 @@ export default class Zenko extends World<ZenkoWorldParameters> {
const accountName = this.sites['source']?.accountName || CacheHelper.parameters.AccountName!;
const accountAccessKeys = Identity.getCredentialsForIdentity(
IdentityEnum.ACCOUNT, this.sites['source']?.accountName
|| CacheHelper.parameters.AccountName!) || {
|| CacheHelper.parameters.AccountName!) || {
accessKeyId: '',
secretAccessKey: '',
};
Expand Down Expand Up @@ -865,7 +879,7 @@ export default class Zenko extends World<ZenkoWorldParameters> {
}

async awsS3Request(method: Method, path: string,
userCredentials: AWSCredentials, headers: object = {}, payload: object = {}) : Promise<Command> {
userCredentials: AWSCredentials, headers: object = {}, payload: object = {}): Promise<Command> {
const interceptor = aws4Interceptor({
options: {
region: 'us-east-1',
Expand All @@ -891,7 +905,7 @@ export default class Zenko extends World<ZenkoWorldParameters> {
statusCode: response.status,
data: response.data as unknown,
};
/* eslint-disable */
/* eslint-disable */
} catch (err: any) {
return {
stdout: '',
Expand Down Expand Up @@ -967,7 +981,7 @@ export default class Zenko extends World<ZenkoWorldParameters> {
}
}

async addWebsiteEndpoint(this: Zenko, endpoint: string) :
async addWebsiteEndpoint(this: Zenko, endpoint: string):
Promise<{ statusCode: number; data: object } | { statusCode: number; err: unknown }> {
return await this.managementAPIRequest('POST',
`/config/${this.parameters.InstanceID}/website/endpoint`,
Expand All @@ -977,7 +991,7 @@ export default class Zenko extends World<ZenkoWorldParameters> {
`"${endpoint}"`);
}

async deleteLocation(this: Zenko, locationName: string) :
async deleteLocation(this: Zenko, locationName: string):
Promise<{ statusCode: number; data: object } | { statusCode: number; err: unknown }> {
return await this.managementAPIRequest('DELETE',
`/config/${this.parameters.InstanceID}/location/${locationName}`);
Expand Down

0 comments on commit f3edf94

Please sign in to comment.