Skip to content

Commit

Permalink
Revert "[to be reverted]utapi removal"
Browse files Browse the repository at this point in the history
This reverts commit a20bc5d.

# Conflicts:
#	lib/api/multiObjectDelete.js
#	lib/api/objectDelete.js
#	package.json
  • Loading branch information
benzekrimaha committed Dec 20, 2024
1 parent b5e6e01 commit 24d0888
Show file tree
Hide file tree
Showing 79 changed files with 2,277 additions and 20 deletions.
33 changes: 33 additions & 0 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,39 @@ jobs:
source: /tmp/artifacts
if: always()

utapi-v2-tests:
runs-on: ubuntu-latest
needs: build
env:
ENABLE_UTAPI_V2: t
S3BACKEND: mem
BUCKET_DENY_FILTER: utapi-event-filter-deny-bucket
CLOUDSERVER_IMAGE: ghcr.io/${{ github.repository }}:${{ github.sha }}
MONGODB_IMAGE: ghcr.io/${{ github.repository }}/ci-mongodb:${{ github.sha }}
JOB_NAME: ${{ github.job }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup CI environment
uses: ./.github/actions/setup-ci
- name: Setup CI services
run: docker compose up -d
working-directory: .github/docker
- name: Run file utapi v2 tests
run: |-
set -ex -o pipefail;
bash wait_for_local_port.bash 8000 40
yarn run test_utapi_v2 | tee /tmp/artifacts/${{ github.job }}/tests.log
- name: Upload logs to artifacts
uses: scality/action-artifacts@v4
with:
method: upload
url: https://artifacts.scality.net
user: ${{ secrets.ARTIFACTS_USER }}
password: ${{ secrets.ARTIFACTS_PASSWORD }}
source: /tmp/artifacts
if: always()

quota-tests:
runs-on: ubuntu-latest
needs: build
Expand Down
4 changes: 4 additions & 0 deletions bin/list_bucket_metrics.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env node
'use strict'; // eslint-disable-line strict

require('../lib/utapi/utilities.js').listMetrics('buckets');
4 changes: 4 additions & 0 deletions bin/list_metrics.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env node
'use strict'; // eslint-disable-line strict

require('../lib/utapi/utilities.js').listMetrics();
188 changes: 186 additions & 2 deletions lib/Config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,17 @@ const url = require('url');
const mycrypto = require('crypto');

const { v4: uuidv4 } = require('uuid');
const cronParser = require('cron-parser');
const joi = require('@hapi/joi');
const { s3routes, auth: arsenalAuth, s3middleware } = require('arsenal');
const { isValidBucketName } = s3routes.routesUtils;
const validateAuthConfig = arsenalAuth.inMemory.validateAuthConfig;
const { buildAuthDataAccount } = require('./auth/in_memory/builder');
const validExternalBackends = require('../constants').externalBackends;
const { azureAccountNameRegex, base64Regex } = require('../constants');
const { azureAccountNameRegex, base64Regex,
allowedUtapiEventFilterFields, allowedUtapiEventFilterStates,
} = require('../constants');
const { utapiVersion } = require('utapi');
const { scaleMsPerDay } = s3middleware.objectUtils;

const constants = require('../constants');
Expand Down Expand Up @@ -503,6 +507,47 @@ function locationConstraintAssert(locationConstraints) {
'include us-east-1 as a locationConstraint');
}

function parseUtapiReindex(config) {
const {
enabled,
schedule,
redis,
bucketd,
onlyCountLatestWhenObjectLocked,
} = config;
assert(typeof enabled === 'boolean',
'bad config: utapi.reindex.enabled must be a boolean');

const parsedRedis = parseRedisConfig(redis);
assert(Array.isArray(parsedRedis.sentinels),
'bad config: utapi reindex redis config requires a list of sentinels');

assert(typeof bucketd === 'object',
'bad config: utapi.reindex.bucketd must be an object');
assert(typeof bucketd.port === 'number',
'bad config: utapi.reindex.bucketd.port must be a number');
assert(typeof schedule === 'string',
'bad config: utapi.reindex.schedule must be a string');
if (onlyCountLatestWhenObjectLocked !== undefined) {
assert(typeof onlyCountLatestWhenObjectLocked === 'boolean',
'bad config: utapi.reindex.onlyCountLatestWhenObjectLocked must be a boolean');
}
try {
cronParser.parseExpression(schedule);
} catch (e) {
assert(false,
'bad config: utapi.reindex.schedule must be a valid ' +
`cron schedule. ${e.message}.`);
}
return {
enabled,
schedule,
redis: parsedRedis,
bucketd,
onlyCountLatestWhenObjectLocked,
};
}

function requestsConfigAssert(requestsConfig) {
if (requestsConfig.viaProxy !== undefined) {
assert(typeof requestsConfig.viaProxy === 'boolean',
Expand Down Expand Up @@ -1175,6 +1220,145 @@ class Config extends EventEmitter {
maxStaleness,
enableInflights,
};
if (config.utapi) {
this.utapi = { component: 's3' };
if (config.utapi.host) {
assert(typeof config.utapi.host === 'string',
'bad config: utapi host must be a string');
this.utapi.host = config.utapi.host;
}
if (config.utapi.port) {
assert(Number.isInteger(config.utapi.port)
&& config.utapi.port > 0,
'bad config: utapi port must be a positive integer');
this.utapi.port = config.utapi.port;
}
if (utapiVersion === 1) {
if (config.utapi.workers !== undefined) {
assert(Number.isInteger(config.utapi.workers)
&& config.utapi.workers > 0,
'bad config: utapi workers must be a positive integer');
this.utapi.workers = config.utapi.workers;
}
// Utapi uses the same localCache config defined for S3 to avoid
// config duplication.
assert(config.localCache, 'missing required property of utapi ' +
'configuration: localCache');
this.utapi.localCache = this.localCache;
assert(config.redis, 'missing required property of utapi ' +
'configuration: redis');
if (config.utapi.redis) {
this.utapi.redis = parseRedisConfig(config.utapi.redis);
if (this.utapi.redis.retry === undefined) {
this.utapi.redis.retry = {
connectBackoff: {
min: 10,
max: 1000,
jitter: 0.1,
factor: 1.5,
deadline: 10000,
},
};
}
}
if (config.utapi.metrics) {
this.utapi.metrics = config.utapi.metrics;
}
this.utapi.enabledOperationCounters = [];
if (config.utapi.enabledOperationCounters !== undefined) {
const { enabledOperationCounters } = config.utapi;
assert(Array.isArray(enabledOperationCounters),
'bad config: utapi.enabledOperationCounters must be an ' +
'array');
assert(enabledOperationCounters.length > 0,
'bad config: utapi.enabledOperationCounters cannot be ' +
'empty');
this.utapi.enabledOperationCounters = enabledOperationCounters;
}
this.utapi.disableOperationCounters = false;
if (config.utapi.disableOperationCounters !== undefined) {
const { disableOperationCounters } = config.utapi;
assert(typeof disableOperationCounters === 'boolean',
'bad config: utapi.disableOperationCounters must be a ' +
'boolean');
this.utapi.disableOperationCounters = disableOperationCounters;
}
if (config.utapi.disableOperationCounters !== undefined &&
config.utapi.enabledOperationCounters !== undefined) {
assert(config.utapi.disableOperationCounters === false,
'bad config: conflicting rules: ' +
'utapi.disableOperationCounters and ' +
'utapi.enabledOperationCounters cannot both be ' +
'specified');
}
if (config.utapi.component) {
this.utapi.component = config.utapi.component;
}
// (optional) The value of the replay schedule should be cron-style
// scheduling. For example, every five minutes: '*/5 * * * *'.
if (config.utapi.replaySchedule) {
assert(typeof config.utapi.replaySchedule === 'string', 'bad' +
'config: utapi.replaySchedule must be a string');
this.utapi.replaySchedule = config.utapi.replaySchedule;
}
// (optional) The number of elements processed by each call to the
// Redis local cache during a replay. For example, 50.
if (config.utapi.batchSize) {
assert(typeof config.utapi.batchSize === 'number', 'bad' +
'config: utapi.batchSize must be a number');
assert(config.utapi.batchSize > 0, 'bad config:' +
'utapi.batchSize must be a number greater than 0');
this.utapi.batchSize = config.utapi.batchSize;
}

// (optional) Expire bucket level metrics on delete bucket
// Disabled by default
this.utapi.expireMetrics = false;
if (config.utapi.expireMetrics !== undefined) {
assert(typeof config.utapi.expireMetrics === 'boolean', 'bad' +
'config: utapi.expireMetrics must be a boolean');
this.utapi.expireMetrics = config.utapi.expireMetrics;
}
// (optional) TTL controlling the expiry for bucket level metrics
// keys when expireMetrics is enabled
this.utapi.expireMetricsTTL = 0;
if (config.utapi.expireMetricsTTL !== undefined) {
assert(typeof config.utapi.expireMetricsTTL === 'number',
'bad config: utapi.expireMetricsTTL must be a number');
this.utapi.expireMetricsTTL = config.utapi.expireMetricsTTL;
}

if (config.utapi && config.utapi.reindex) {
this.utapi.reindex = parseUtapiReindex(config.utapi.reindex);
}
}

if (utapiVersion === 2 && config.utapi.filter) {
const { filter: filterConfig } = config.utapi;
const utapiResourceFilters = {};
allowedUtapiEventFilterFields.forEach(
field => allowedUtapiEventFilterStates.forEach(
state => {
const resources = (filterConfig[state] && filterConfig[state][field]) || null;
if (resources) {
assert.strictEqual(utapiResourceFilters[field], undefined,
`bad config: utapi.filter.${state}.${field} can't define an allow and a deny list`);
assert(resources.every(r => typeof r === 'string'),
`bad config: utapi.filter.${state}.${field} must be an array of strings`);
utapiResourceFilters[field] = { [state]: new Set(resources) };
}
}
));
this.utapi.filter = utapiResourceFilters;
}
}
if (Object.keys(this.locationConstraints).some(
loc => this.locationConstraints[loc].sizeLimitGB)) {
assert(this.utapi && this.utapi.metrics &&
this.utapi.metrics.includes('location'),
'bad config: if storage size limit set on a location ' +
'constraint, Utapi must also be configured correctly');
}

this.log = { logLevel: 'debug', dumpLevel: 'error' };
if (config.log !== undefined) {
Expand Down Expand Up @@ -1298,7 +1482,7 @@ class Config extends EventEmitter {
'bad config: KMIP TLS Host must be a string');
this.kmip.transport.tls.host = host;
}

if (key) {
this.kmip.transport.tls.key = this._loadTlsFile(key);
}
Expand Down
10 changes: 9 additions & 1 deletion lib/api/apiUtils/bucket/bucketDeletion.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const async = require('async');
const { errors } = require('arsenal');

const abortMultipartUpload = require('../object/abortMultipartUpload');
const { pushMetric } = require('../../../utapi/utilities');

const { splitter, oldSplitter, mpuBucketPrefix } =
require('../../../../constants');
Expand Down Expand Up @@ -30,7 +31,14 @@ function _deleteOngoingMPUs(authInfo, bucketName, bucketMD, mpus, request, log,
// `overview${splitter}${objectKey}${splitter}${uploadId}
const [, objectKey, uploadId] = mpu.key.split(splitterChar);
abortMultipartUpload(authInfo, bucketName, objectKey, uploadId, log,
err => {
(err, destBucket, partSizeSum) => {
pushMetric('abortMultipartUpload', log, {
authInfo,
canonicalID: bucketMD.getOwner(),
bucket: bucketName,
keys: [objectKey],
byteLength: partSizeSum,
});
next(err);
}, request);
}, cb);
Expand Down
26 changes: 23 additions & 3 deletions lib/api/apiUtils/object/locationStorageCheck.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
const { errors } = require('arsenal');

const { config } = require('../../../Config');
const { getLocationMetric, pushLocationMetric } =
require('../../../utapi/utilities');

function _gbToBytes(gb) {
return gb * 1024 * 1024 * 1024;
}

/**
* locationStorageCheck - will ensure there is enough space left for object on
Expand All @@ -19,10 +27,22 @@ function locationStorageCheck(location, updateSize, log, cb) {
}
// no need to list location metric, since it should be decreased
if (updateSize < 0) {
return cb();
return pushLocationMetric(location, updateSize, log, cb);
}

return cb();
return getLocationMetric(location, log, (err, bytesStored) => {
if (err) {
log.error(`Error listing metrics from Utapi: ${err.message}`);
return cb(err);
}
const newStorageSize = parseInt(bytesStored, 10) + updateSize;
const sizeLimitBytes = _gbToBytes(sizeLimitGB);
if (sizeLimitBytes < newStorageSize) {
return cb(errors.AccessDenied.customizeDescription(
`The assigned storage space limit for location ${location} ` +
'will be exceeded'));
}
return pushLocationMetric(location, updateSize, log, cb);
});
}

module.exports = locationStorageCheck;
5 changes: 5 additions & 0 deletions lib/api/apiUtils/object/objectRestore.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const { errors, s3middleware } = require('arsenal');
const { allowedRestoreObjectRequestTierValues } = require('../../../../constants');
const coldStorage = require('./coldStorage');
const monitoring = require('../../../utilities/monitoringHandler');
const { pushMetric } = require('../../../utapi/utilities');
const { decodeVersionId } = require('./versioning');
const collectCorsHeaders = require('../../../utilities/collectCorsHeaders');
const { parseRestoreRequestXml } = s3middleware.objectRestore;
Expand Down Expand Up @@ -152,6 +153,10 @@ function objectRestore(metadata, mdUtils, userInfo, request, log, callback) {
'POST', bucketName, err.code, 'restoreObject');
return callback(err, err.code, responseHeaders);
}
pushMetric('restoreObject', log, {
userInfo,
bucket: bucketName,
});
if (isObjectRestored) {
monitoring.promMetrics(
'POST', bucketName, '200', 'restoreObject');
Expand Down
2 changes: 2 additions & 0 deletions lib/api/backbeat/listLifecycleCurrents.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const { errors } = require('arsenal');
const constants = require('../../../constants');
const services = require('../../services');
const { standardMetadataValidateBucket } = require('../../metadata/metadataUtils');
const { pushMetric } = require('../../utapi/utilities');
const monitoring = require('../../utilities/monitoringHandler');
const { getLocationConstraintErrorMessage, processCurrents,
validateMaxScannedEntries } = require('../apiUtils/object/lifecycle');
Expand All @@ -13,6 +14,7 @@ function handleResult(listParams, requestMaxKeys, authInfo,
listParams.maxKeys = requestMaxKeys;
const res = processCurrents(bucketName, listParams, isBucketVersioned, list);

pushMetric('listLifecycleCurrents', log, { authInfo, bucket: bucketName });
monitoring.promMetrics('GET', bucketName, '200', 'listLifecycleCurrents');
return callback(null, res);
}
Expand Down
2 changes: 2 additions & 0 deletions lib/api/backbeat/listLifecycleNonCurrents.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const { errors, versioning } = require('arsenal');
const constants = require('../../../constants');
const services = require('../../services');
const { standardMetadataValidateBucket } = require('../../metadata/metadataUtils');
const { pushMetric } = require('../../utapi/utilities');
const versionIdUtils = versioning.VersionID;
const monitoring = require('../../utilities/monitoringHandler');
const { getLocationConstraintErrorMessage, processNonCurrents,
Expand All @@ -14,6 +15,7 @@ function handleResult(listParams, requestMaxKeys, authInfo,
listParams.maxKeys = requestMaxKeys;
const res = processNonCurrents(bucketName, listParams, list);

pushMetric('listLifecycleNonCurrents', log, { authInfo, bucket: bucketName });
monitoring.promMetrics('GET', bucketName, '200', 'listLifecycleNonCurrents');
return callback(null, res);
}
Expand Down
Loading

0 comments on commit 24d0888

Please sign in to comment.