Skip to content

Commit

Permalink
CLDSRV-497 Fix BackbeatClient.putMetadata with versionID
Browse files Browse the repository at this point in the history
Issue: When Cloudserver BackbeatClient.putMetadata() option fields are sent to Metadata through the query string, they are converted to strings. As a result, Metadata interprets the value undefined in the versionId field as an empty string ('').

Background: Previously, the 'crrExistingObject' script used this bug/behavior as a workaround to generate an internal version ID to replicate null version (= objects created before versioning was enabled). However, this approach has led to inconsistencies, occasionally resulting in the creation of multiple null internal versions.

Resolution: To address this issue, the 'crrExistingObject' workaround will be deprecated. Instead, Backbeat will be enhanced to support the replication of null versions directly, thereby ensuring more reliable and consistent behavior in handling versioning.
  • Loading branch information
nicolas2bert committed Feb 14, 2024
1 parent 7162577 commit 44a1c1c
Show file tree
Hide file tree
Showing 4 changed files with 221 additions and 38 deletions.
9 changes: 8 additions & 1 deletion lib/routes/routeBackbeat.js
Original file line number Diff line number Diff line change
Expand Up @@ -522,7 +522,6 @@ function putMetadata(request, response, bucketInfo, objMd, log, callback) {
}

const options = {
versionId,
isNull,
};

Expand All @@ -535,6 +534,14 @@ function putMetadata(request, response, bucketInfo, objMd, log, callback) {
options.versioning = true;
}

// NOTE: When options fields are sent to Metadata through the query string,
// they are converted to strings. As a result, Metadata interprets the value undefined
// in the versionId field as an empty string ('').
// To prevent this, the versionId field is only included in options when it is defined.
if (versionId !== undefined) {
options.versionId = versionId;
}

log.trace('putting object version', {
objectKey: request.objectKey, omVal, options });
return metadata.putObjectMD(bucketName, objectKey, omVal, options, log,
Expand Down
35 changes: 1 addition & 34 deletions tests/functional/raw-node/test/routes/routeBackbeat.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const { models, versioning } = require('arsenal');
const versionIdUtils = versioning.VersionID;
const { ObjectMD } = models;

const { makeRequest } = require('../../utils/makeRequest');
const { makeRequest, makeBackbeatRequest } = require('../../utils/makeRequest');
const BucketUtility = require('../../../aws-node-sdk/lib/utility/bucket-util');

const ipAddress = process.env.IP ? process.env.IP : '127.0.0.1';
Expand Down Expand Up @@ -86,39 +86,6 @@ function checkVersionData(s3, bucket, objectKey, versionId, dataValue, done) {
});
}

/** makeBackbeatRequest - utility function to generate a request going
* through backbeat route
* @param {object} params - params for making request
* @param {string} params.method - request method
* @param {string} params.bucket - bucket name
* @param {string} params.objectKey - object key
* @param {string} params.subCommand - subcommand to backbeat
* @param {object} [params.headers] - headers and their string values
* @param {object} [params.authCredentials] - authentication credentials
* @param {object} params.authCredentials.accessKey - access key
* @param {object} params.authCredentials.secretKey - secret key
* @param {string} [params.requestBody] - request body contents
* @param {object} [params.queryObj] - query params
* @param {function} callback - with error and response parameters
* @return {undefined} - and call callback
*/
function makeBackbeatRequest(params, callback) {
const { method, headers, bucket, objectKey, resourceType,
authCredentials, requestBody, queryObj } = params;
const options = {
authCredentials,
hostname: ipAddress,
port: 8000,
method,
headers,
path: `/_/backbeat/${resourceType}/${bucket}/${objectKey}`,
requestBody,
jsonResponse: true,
queryObj,
};
makeRequest(options, callback);
}

function updateStorageClass(data, storageClass) {
let parsedBody;
try {
Expand Down
175 changes: 175 additions & 0 deletions tests/functional/raw-node/test/routes/routeBackbeatForReplication.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
const assert = require('assert');
const async = require('async');
const { models } = require('arsenal');
const { ObjectMD } = models;

const { makeBackbeatRequest } = require('../../utils/makeRequest');
const BucketUtility = require('../../../aws-node-sdk/lib/utility/bucket-util');

const describeSkipIfAWS = process.env.AWS_ON_AIR ? describe.skip : describe;

const backbeatAuthCredentials = {
accessKey: 'accessKey1',
secretKey: 'verySecretKey1',
};

const testData = 'testkey data';

describeSkipIfAWS('backbeat routes for replication', () => {
const bucketUtil = new BucketUtility(
'default', { signatureVersion: 'v4' });
const s3 = bucketUtil.s3;

const bucketSource = 'backbeatbucket-replication-source';
const bucketDestination = 'backbeatbucket-replication-destination';
const keyName = 'key0';
const storageClass = 'foo';

beforeEach(done =>
bucketUtil.emptyIfExists(bucketSource)
.then(() => s3.createBucket({ Bucket: bucketSource }).promise())
.then(() => bucketUtil.emptyIfExists(bucketDestination))
.then(() => s3.createBucket({ Bucket: bucketDestination }).promise())
.then(() => done(), err => done(err))
);

afterEach(done =>
bucketUtil.empty(bucketSource)
.then(() => s3.deleteBucket({ Bucket: bucketSource }).promise())
.then(() => bucketUtil.empty(bucketDestination))
.then(() => s3.deleteBucket({ Bucket: bucketDestination }).promise())
.then(() => done(), err => done(err))
);

it('should successfully replicate a null version', done => {
let objMD;
return async.series([
next => s3.putObject({ Bucket: bucketSource, Key: keyName, Body: new Buffer(testData) }, next),
next => s3.putBucketVersioning({ Bucket: bucketSource, VersioningConfiguration: { Status: 'Enabled' } },
next),
next => s3.putBucketVersioning({ Bucket: bucketDestination, VersioningConfiguration:
{ Status: 'Enabled' } }, next),
next => makeBackbeatRequest({
method: 'GET',
resourceType: 'metadata',
bucket: bucketSource,
objectKey: keyName,
queryObj: {
versionId: 'null',
},
authCredentials: backbeatAuthCredentials,
}, (err, data) => {
if (err) {
return next(err);
}
objMD = JSON.parse(data.body).Body;
return next();
}),
next => makeBackbeatRequest({
method: 'PUT',
resourceType: 'metadata',
bucket: bucketDestination,
objectKey: keyName,
queryObj: {
versionId: 'null',
},
authCredentials: backbeatAuthCredentials,
requestBody: objMD,
}, next),
next => s3.headObject({ Bucket: bucketDestination, Key: keyName, VersionId: 'null' }, next),
next => s3.listObjectVersions({ Bucket: bucketDestination }, next),
], (err, data) => {
if (err) {
return done(err);
}
const headObjectRes = data[5];
assert.strictEqual(headObjectRes.VersionId, 'null');

const listObjectVersionsRes = data[6];
const { Versions } = listObjectVersionsRes;

assert.strictEqual(Versions.length, 1);

const [currentVersion] = Versions;
assert.strictEqual(currentVersion.IsLatest, true);
assert.strictEqual(currentVersion.VersionId, 'null');
return done();
});
});

it('should successfully replicate a null version and update it', done => {
let objMD;
return async.series([
next => s3.putObject({ Bucket: bucketSource, Key: keyName, Body: new Buffer(testData) }, next),
next => s3.putBucketVersioning({ Bucket: bucketSource, VersioningConfiguration: { Status: 'Enabled' } },
next),
next => s3.putBucketVersioning({ Bucket: bucketDestination, VersioningConfiguration:
{ Status: 'Enabled' } }, next),
next => makeBackbeatRequest({
method: 'GET',
resourceType: 'metadata',
bucket: bucketSource,
objectKey: keyName,
queryObj: {
versionId: 'null',
},
authCredentials: backbeatAuthCredentials,
}, (err, data) => {
if (err) {
return next(err);
}
objMD = JSON.parse(data.body).Body;
return next();
}),
next => makeBackbeatRequest({
method: 'PUT',
resourceType: 'metadata',
bucket: bucketDestination,
objectKey: keyName,
queryObj: {
versionId: 'null',
},
authCredentials: backbeatAuthCredentials,
requestBody: objMD,
}, next),
next => {
const { result, error } = ObjectMD.createFromBlob(objMD);
if (error) {
return next(error);
}
result.setAmzStorageClass(storageClass);
return makeBackbeatRequest({
method: 'PUT',
resourceType: 'metadata',
bucket: bucketDestination,
objectKey: keyName,
queryObj: {
versionId: 'null',
},
authCredentials: backbeatAuthCredentials,
requestBody: result.getSerialized(),
}, next);
},
next => s3.headObject({ Bucket: bucketDestination, Key: keyName, VersionId: 'null' }, next),
next => s3.listObjectVersions({ Bucket: bucketDestination }, next),
], (err, data) => {
if (err) {
return done(err);
}
const headObjectRes = data[6];
assert.strictEqual(headObjectRes.VersionId, 'null');
assert.strictEqual(headObjectRes.StorageClass, storageClass);

const listObjectVersionsRes = data[7];
const { Versions } = listObjectVersionsRes;

assert.strictEqual(Versions.length, 1);

const [currentVersion] = Versions;
assert.strictEqual(currentVersion.IsLatest, true);
assert.strictEqual(currentVersion.VersionId, 'null');
assert.strictEqual(currentVersion.StorageClass, storageClass);
return done();
});
});
});
40 changes: 37 additions & 3 deletions tests/functional/raw-node/utils/makeRequest.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ function _decodeURI(uri) {
* @param {object} [params.headers] - headers and their string values
* @param {string} [params.path] - URL-encoded request path
* @param {object} [params.authCredentials] - authentication credentials
* @param {object} params.authCredentials.accessKey - access key
* @param {object} params.authCredentials.secretKey - secret key
* @param {object} params.GCP - flag to setup for GCP request
* @param {string} params.authCredentials.accessKey - access key
* @param {string} params.authCredentials.secretKey - secret key
* @param {boolean} params.GCP - flag to setup for GCP request
* @param {string} [params.requestBody] - request body contents
* @param {boolean} [params.jsonResponse] - if true, response is
* expected to be received in JSON format (including errors)
Expand Down Expand Up @@ -190,8 +190,42 @@ function makeGcpRequest(params, callback) {
makeRequest(options, callback);
}

/** makeBackbeatRequest - utility function to generate a request going
* through backbeat route
* @param {object} params - params for making request
* @param {string} params.method - request method
* @param {string} params.bucket - bucket name
* @param {string} params.objectKey - object key
* @param {string} params.subCommand - subcommand to backbeat
* @param {object} [params.headers] - headers and their string values
* @param {object} [params.authCredentials] - authentication credentials
* @param {object} params.authCredentials.accessKey - access key
* @param {object} params.authCredentials.secretKey - secret key
* @param {string} [params.requestBody] - request body contents
* @param {object} [params.queryObj] - query params
* @param {function} callback - with error and response parameters
* @return {undefined} - and call callback
*/
function makeBackbeatRequest(params, callback) {
const { method, headers, bucket, objectKey, resourceType,
authCredentials, requestBody, queryObj } = params;
const options = {
authCredentials,
hostname: ipAddress,
port: 8000,
method,
headers,
path: `/_/backbeat/${resourceType}/${bucket}/${objectKey}`,
requestBody,
jsonResponse: true,
queryObj,
};
makeRequest(options, callback);
}

module.exports = {
makeRequest,
makeS3Request,
makeGcpRequest,
makeBackbeatRequest,
};

0 comments on commit 44a1c1c

Please sign in to comment.