Compare commits
3 Commits
developmen
...
poc/replic
Author | SHA1 | Date |
---|---|---|
Bennett Buchanan | 8e9cfb514c | |
Bennett Buchanan | 332c0e141a | |
Bennett Buchanan | aedced5ff5 |
|
@ -16,6 +16,8 @@ const bucketPutCors = require('./bucketPutCors');
|
|||
const bucketPutVersioning = require('./bucketPutVersioning');
|
||||
const bucketPutWebsite = require('./bucketPutWebsite');
|
||||
const bucketPutReplication = require('./bucketPutReplication');
|
||||
const bucketGetReplication = require('./bucketGetReplication');
|
||||
const bucketDeleteReplication = require('./bucketDeleteReplication');
|
||||
const corsPreflight = require('./corsPreflight');
|
||||
const completeMultipartUpload = require('./completeMultipartUpload');
|
||||
const initiateMultipartUpload = require('./initiateMultipartUpload');
|
||||
|
@ -160,6 +162,8 @@ const api = {
|
|||
bucketPutVersioning,
|
||||
bucketPutWebsite,
|
||||
bucketPutReplication,
|
||||
bucketGetReplication,
|
||||
bucketDeleteReplication,
|
||||
corsPreflight,
|
||||
completeMultipartUpload,
|
||||
initiateMultipartUpload,
|
||||
|
|
|
@ -13,4 +13,12 @@ function getReplicationConfiguration(xml, log, cb) {
|
|||
});
|
||||
}
|
||||
|
||||
module.exports = getReplicationConfiguration;
|
||||
// Get the XML representation of the bucket replication configuration.
|
||||
function getReplicationConfigurationXML(config) {
|
||||
return ReplicationConfiguration.getConfigXML(config);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getReplicationConfiguration,
|
||||
getReplicationConfigurationXML,
|
||||
};
|
||||
|
|
|
@ -314,6 +314,37 @@ class ReplicationConfiguration {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the XML representation of the configuration object
|
||||
* @param {object} config - The bucket replication configuration
|
||||
* @return {string} - The XML representation of the configuration
|
||||
*/
|
||||
static getConfigXML(config) {
|
||||
const { role, destination, rules } = config;
|
||||
const Role = `<Role>${role}</Role>`;
|
||||
const Bucket = `<Bucket>${destination}</Bucket>`;
|
||||
const rulesXML = rules.map(rule => {
|
||||
const { prefix, enabled, storageClass, id } = rule;
|
||||
const Prefix = prefix === '' ? '<Prefix/>' :
|
||||
`<Prefix>${prefix}</Prefix>`;
|
||||
const Status =
|
||||
`<Status>${enabled ? 'Enabled' : 'Disabled'}</Status>`;
|
||||
const StorageClass = storageClass ?
|
||||
`<StorageClass>${storageClass}</StorageClass>` : '';
|
||||
const Destination =
|
||||
`<Destination>${Bucket}${StorageClass}</Destination>`;
|
||||
// If the ID property was omitted in the configuration object, we
|
||||
// create an ID for the rule. Hence it is always defined.
|
||||
const ID = `<ID>${id}</ID>`;
|
||||
return `<Rule>${ID}${Prefix}${Status}${Destination}</Rule>`;
|
||||
}).join('');
|
||||
return '<?xml version="1.0" encoding="UTF-8"?>' +
|
||||
'<ReplicationConfiguration ' +
|
||||
'xmlns="http://s3.amazonaws.com/doc/2006-03-01/">' +
|
||||
`${rulesXML}${Role}` +
|
||||
'</ReplicationConfiguration>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the bucket metadata replication configuration structure and
|
||||
* value types
|
||||
|
|
|
@ -11,6 +11,7 @@ const locationConstraintCheck = require('./locationConstraintCheck');
|
|||
const { versioningPreprocessing } = require('./versioning');
|
||||
const removeAWSChunked = require('./removeAWSChunked');
|
||||
const { decodeVersionId } = require('./versioning');
|
||||
const { buildReplicationInfoForObject } = require('./replication');
|
||||
const { config } = require('../../../Config');
|
||||
|
||||
function _storeInMDandDeleteData(bucketName, dataGetInfo, cipherBundle,
|
||||
|
@ -108,6 +109,9 @@ function createAndStoreObject(bucketName, bucketMD, objectKey, objMD, authInfo,
|
|||
metadataStoreParams.tagging = request.headers['x-amz-tagging'];
|
||||
}
|
||||
|
||||
buildReplicationInfoForObject(metadataStoreParams, objectKey, bucketMD,
|
||||
isDeleteMarker);
|
||||
|
||||
const decodedVidResult = decodeVersionId(request.query);
|
||||
if (decodedVidResult instanceof Error) {
|
||||
log.trace('invalid versionId query', {
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
/**
|
||||
* Check that a bucket replication rule applies for the given object key. If it
|
||||
* does, assign replication information to the given object.
|
||||
* @param {object} obj - The object to set replicationInfo for
|
||||
* @param {string} objKey - The key of the object
|
||||
* @param {object} bucketMD - The bucket metadata
|
||||
* @param {array} content - The content type that should be replicated
|
||||
* @return {undefined}
|
||||
*/
|
||||
function buildReplicationInfo(obj, objKey, bucketMD, content) {
|
||||
const config = bucketMD.getReplicationConfiguration();
|
||||
// If bucket does not have a replication configuration, do not replicate.
|
||||
if (config) {
|
||||
const rule = config.rules.find(rule => objKey.startsWith(rule.prefix));
|
||||
if (rule) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
obj.replicationInfo = {
|
||||
status: 'PENDING',
|
||||
content,
|
||||
destination: config.destination,
|
||||
storageClass: rule.storageClass || '',
|
||||
};
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the object replicationInfo to replicate data and metadata, or only
|
||||
* metadata if the object is a delete marker
|
||||
* @param {object} obj - The object to set replicationInfo for
|
||||
* @param {string} objKey - The key of the object
|
||||
* @param {object} bucketMD - The bucket metadata
|
||||
* @param {boolean} isDeleteMarker - Whether the object is a deleteMarker
|
||||
* @return {undefined}
|
||||
*/
|
||||
function buildReplicationInfoForObject(obj, objKey, bucketMD, isDeleteMarker) {
|
||||
// Delete markers have no data, so we only replicate metadata.
|
||||
const content = isDeleteMarker ? ['METADATA'] : ['DATA', 'METADATA'];
|
||||
return buildReplicationInfo(obj, objKey, bucketMD, content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the object replicationInfo to replicate only metadata
|
||||
* @param {object} obj - The object to set replicationInfo for
|
||||
* @param {string} objKey - The key of the object
|
||||
* @param {object} bucketMD - The bucket metadata
|
||||
* @return {undefined}
|
||||
*/
|
||||
function buildReplicationInfoForObjectMD(obj, objKey, bucketMD) {
|
||||
return buildReplicationInfo(obj, objKey, bucketMD, ['METADATA']);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
buildReplicationInfoForObject,
|
||||
buildReplicationInfoForObjectMD,
|
||||
};
|
|
@ -0,0 +1,56 @@
|
|||
const metadata = require('../metadata/wrapper');
|
||||
const { metadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const { pushMetric } = require('../utapi/utilities');
|
||||
const collectCorsHeaders = require('../utilities/collectCorsHeaders');
|
||||
|
||||
/**
|
||||
* bucketDeleteReplication - Delete the bucket replication configuration
|
||||
* @param {AuthInfo} authInfo - Instance of AuthInfo class with requester's info
|
||||
* @param {object} request - http request object
|
||||
* @param {object} log - Werelogs logger
|
||||
* @param {function} callback - callback to server
|
||||
* @return {undefined}
|
||||
*/
|
||||
function bucketDeleteReplication(authInfo, request, log, callback) {
|
||||
log.debug('processing request', { method: 'bucketDeleteReplication' });
|
||||
const { bucketName, headers, method } = request;
|
||||
const metadataValParams = {
|
||||
authInfo,
|
||||
bucketName,
|
||||
requestType: 'bucketOwnerAction',
|
||||
};
|
||||
return metadataValidateBucket(metadataValParams, log, (err, bucket) => {
|
||||
const corsHeaders = collectCorsHeaders(headers.origin, method, bucket);
|
||||
if (err) {
|
||||
log.debug('error processing request', {
|
||||
error: err,
|
||||
method: 'bucketDeleteReplication',
|
||||
});
|
||||
return callback(err, corsHeaders);
|
||||
}
|
||||
if (!bucket.getReplicationConfiguration()) {
|
||||
log.trace('no existing replication configuration', {
|
||||
method: 'bucketDeleteReplication',
|
||||
});
|
||||
pushMetric('deleteBucketReplication', log, {
|
||||
authInfo,
|
||||
bucket: bucketName,
|
||||
});
|
||||
return callback(null, corsHeaders);
|
||||
}
|
||||
log.trace('deleting replication configuration in metadata');
|
||||
bucket.setReplicationConfiguration(null);
|
||||
return metadata.updateBucket(bucketName, bucket, log, err => {
|
||||
if (err) {
|
||||
return callback(err, corsHeaders);
|
||||
}
|
||||
pushMetric('deleteBucketReplication', log, {
|
||||
authInfo,
|
||||
bucket: bucketName,
|
||||
});
|
||||
return callback(null, corsHeaders);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = bucketDeleteReplication;
|
|
@ -0,0 +1,52 @@
|
|||
const { errors } = require('arsenal');
|
||||
|
||||
const { metadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const { pushMetric } = require('../utapi/utilities');
|
||||
const { getReplicationConfigurationXML } =
|
||||
require('./apiUtils/bucket/getReplicationConfiguration');
|
||||
const collectCorsHeaders = require('../utilities/collectCorsHeaders');
|
||||
|
||||
/**
|
||||
* bucketGetReplication - Get the bucket replication configuration
|
||||
* @param {AuthInfo} authInfo - Instance of AuthInfo class with requester's info
|
||||
* @param {object} request - http request object
|
||||
* @param {object} log - Werelogs logger
|
||||
* @param {function} callback - callback to server
|
||||
* @return {undefined}
|
||||
*/
|
||||
function bucketGetReplication(authInfo, request, log, callback) {
|
||||
log.debug('processing request', { method: 'bucketGetReplication' });
|
||||
const { bucketName, headers, method } = request;
|
||||
const metadataValParams = {
|
||||
authInfo,
|
||||
bucketName,
|
||||
requestType: 'bucketOwnerAction',
|
||||
};
|
||||
return metadataValidateBucket(metadataValParams, log, (err, bucket) => {
|
||||
const corsHeaders = collectCorsHeaders(headers.origin, method, bucket);
|
||||
if (err) {
|
||||
log.debug('error processing request', {
|
||||
error: err,
|
||||
method: 'bucketGetReplication',
|
||||
});
|
||||
return callback(err, null, corsHeaders);
|
||||
}
|
||||
const replicationConfig = bucket.getReplicationConfiguration();
|
||||
if (!replicationConfig) {
|
||||
log.debug('error processing request', {
|
||||
error: errors.ReplicationConfigurationNotFoundError,
|
||||
method: 'bucketGetReplication',
|
||||
});
|
||||
return callback(errors.ReplicationConfigurationNotFoundError, null,
|
||||
corsHeaders);
|
||||
}
|
||||
const xml = getReplicationConfigurationXML(replicationConfig);
|
||||
pushMetric('getBucketReplication', log, {
|
||||
authInfo,
|
||||
bucket: bucketName,
|
||||
});
|
||||
return callback(null, xml, corsHeaders);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = bucketGetReplication;
|
|
@ -4,7 +4,7 @@ const { errors } = require('arsenal');
|
|||
const metadata = require('../metadata/wrapper');
|
||||
const { metadataValidateBucket } = require('../metadata/metadataUtils');
|
||||
const { pushMetric } = require('../utapi/utilities');
|
||||
const getReplicationConfiguration =
|
||||
const { getReplicationConfiguration } =
|
||||
require('./apiUtils/bucket/getReplicationConfiguration');
|
||||
const collectCorsHeaders = require('../utilities/collectCorsHeaders');
|
||||
|
||||
|
|
|
@ -8,6 +8,8 @@ const { metadataValidateBucketAndObj } = require('../metadata/metadataUtils');
|
|||
const { pushMetric } = require('../utapi/utilities');
|
||||
const collectCorsHeaders = require('../utilities/collectCorsHeaders');
|
||||
const metadata = require('../metadata/wrapper');
|
||||
const { buildReplicationInfoForObjectMD } =
|
||||
require('./apiUtils/object/replication');
|
||||
|
||||
/**
|
||||
* Object Delete Tagging - Delete tag set from an object
|
||||
|
@ -68,6 +70,7 @@ function objectDeleteTagging(authInfo, request, log, callback) {
|
|||
objectMD.tags = {};
|
||||
const params = objectMD.versionId ? { versionId:
|
||||
objectMD.versionId } : {};
|
||||
buildReplicationInfoForObjectMD(objectMD, objectKey, bucket);
|
||||
metadata.putObjectMD(bucket.getName(), objectKey, objectMD, params,
|
||||
log, err =>
|
||||
next(err, bucket, objectMD));
|
||||
|
|
|
@ -6,6 +6,8 @@ const { decodeVersionId, getVersionIdResHeader } =
|
|||
|
||||
const { metadataValidateBucketAndObj } = require('../metadata/metadataUtils');
|
||||
const { pushMetric } = require('../utapi/utilities');
|
||||
const { buildReplicationInfoForObjectMD } =
|
||||
require('./apiUtils/object/replication');
|
||||
const collectCorsHeaders = require('../utilities/collectCorsHeaders');
|
||||
const metadata = require('../metadata/wrapper');
|
||||
const { parseTagXml } = require('./apiUtils/object/tagging');
|
||||
|
@ -74,6 +76,7 @@ function objectPutTagging(authInfo, request, log, callback) {
|
|||
objectMD.tags = tags;
|
||||
const params = objectMD.versionId ? { versionId:
|
||||
objectMD.versionId } : {};
|
||||
buildReplicationInfoForObjectMD(objectMD, objectKey, bucket);
|
||||
metadata.putObjectMD(bucket.getName(), objectKey, objectMD, params,
|
||||
log, err =>
|
||||
next(err, bucket, objectMD));
|
||||
|
|
|
@ -300,6 +300,14 @@ class BucketInfo {
|
|||
this._replicationConfiguration = replicationConfiguration;
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Get replication configuration information
|
||||
* @return {object|null} replication configuration information or `null` if
|
||||
* the bucket does not have a replication configuration
|
||||
*/
|
||||
getReplicationConfiguration() {
|
||||
return this._replicationConfiguration;
|
||||
}
|
||||
/**
|
||||
* Get cors resource
|
||||
* @return {object[]} cors
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
const { errors } = require('arsenal');
|
||||
|
||||
const { buildReplicationInfoForObjectMD } =
|
||||
require('../api/apiUtils/object/replication');
|
||||
const aclUtils = require('../utilities/aclUtils');
|
||||
const constants = require('../../constants');
|
||||
const metadata = require('../metadata/wrapper');
|
||||
|
@ -16,6 +18,7 @@ const acl = {
|
|||
log.trace('updating object acl in metadata');
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
objectMD.acl = addACLParams;
|
||||
buildReplicationInfoForObjectMD(objectMD, objectKey, bucket);
|
||||
metadata.putObjectMD(bucket.getName(), objectKey, objectMD, params, log,
|
||||
cb);
|
||||
},
|
||||
|
|
|
@ -53,6 +53,12 @@ module.exports = class ObjectMD {
|
|||
'isDeleteMarker': '',
|
||||
'versionId': undefined, // If no versionId, it should be undefined
|
||||
'tags': {},
|
||||
'replicationInfo': {
|
||||
status: '',
|
||||
content: [],
|
||||
destination: '',
|
||||
storageClass: '',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -570,6 +576,32 @@ module.exports = class ObjectMD {
|
|||
return this._data.tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set replication information
|
||||
*
|
||||
* @param {object} replicationInfo - replication information object
|
||||
* @return {ObjectMD} itself
|
||||
*/
|
||||
setReplicationInfo(replicationInfo) {
|
||||
const { status, content, destination, storageClass } = replicationInfo;
|
||||
this._data.replicationInfo = {
|
||||
status,
|
||||
content,
|
||||
destination,
|
||||
storageClass: storageClass || '',
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get replication information
|
||||
*
|
||||
* @return {object} replication object
|
||||
*/
|
||||
getReplicationInfo() {
|
||||
return this._data.replicationInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set custom meta headers
|
||||
*
|
||||
|
|
|
@ -34,6 +34,13 @@ function routeDELETE(request, response, log, statsClient) {
|
|||
return routesUtils.responseNoBody(err, corsHeaders,
|
||||
response, 204, log);
|
||||
});
|
||||
} else if (request.query.replication !== undefined) {
|
||||
return api.callApiMethod('bucketDeleteReplication', request,
|
||||
response, log, (err, corsHeaders) => {
|
||||
statsReport500(err, statsClient);
|
||||
return routesUtils.responseNoBody(err, corsHeaders,
|
||||
response, 204, log);
|
||||
});
|
||||
}
|
||||
api.callApiMethod('bucketDelete', request, response, log,
|
||||
(err, corsHeaders) => {
|
||||
|
|
|
@ -24,6 +24,13 @@ function routerGET(request, response, log, statsClient) {
|
|||
return routesUtils.responseXMLBody(err, xml, response, log,
|
||||
corsHeaders);
|
||||
});
|
||||
} else if (request.query.replication !== undefined) {
|
||||
api.callApiMethod('bucketGetReplication', request, response, log,
|
||||
(err, xml, corsHeaders) => {
|
||||
statsReport500(err, statsClient);
|
||||
return routesUtils.responseXMLBody(err, xml, response, log,
|
||||
corsHeaders);
|
||||
});
|
||||
} else if (request.query.cors !== undefined) {
|
||||
api.callApiMethod('bucketGetCors', request, response, log,
|
||||
(err, xml, corsHeaders) => {
|
||||
|
|
|
@ -92,8 +92,8 @@ const services = {
|
|||
const { objectKey, authInfo, size, contentMD5, metaHeaders,
|
||||
contentType, cacheControl, contentDisposition, contentEncoding,
|
||||
expires, multipart, headers, overrideMetadata, log,
|
||||
lastModifiedDate, versioning, versionId, tagging, taggingCopy }
|
||||
= params;
|
||||
lastModifiedDate, versioning, versionId, tagging, taggingCopy,
|
||||
replicationInfo } = params;
|
||||
log.trace('storing object in metadata');
|
||||
assert.strictEqual(typeof bucketName, 'string');
|
||||
const md = new ObjectMD();
|
||||
|
@ -125,6 +125,9 @@ const services = {
|
|||
if (headers && headers['x-amz-website-redirect-location']) {
|
||||
md.setRedirectLocation(headers['x-amz-website-redirect-location']);
|
||||
}
|
||||
if (replicationInfo) {
|
||||
md.setReplicationInfo(replicationInfo);
|
||||
}
|
||||
// options to send to metadata to create or overwrite versions
|
||||
// when putting the object MD
|
||||
const options = {};
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
const assert = require('assert');
|
||||
const { S3 } = require('aws-sdk');
|
||||
const { series } = require('async');
|
||||
const { errors } = require('arsenal');
|
||||
|
||||
const getConfig = require('../support/config');
|
||||
const BucketUtility = require('../../lib/utility/bucket-util');
|
||||
|
||||
const bucket = 'source-bucket';
|
||||
const replicationConfig = {
|
||||
Role: 'arn:partition:service::account-id:resourcetype/resource',
|
||||
Rules: [
|
||||
{
|
||||
Destination: { Bucket: 'arn:aws:s3:::destination-bucket' },
|
||||
Prefix: 'test-prefix',
|
||||
Status: 'Enabled',
|
||||
ID: 'test-id',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe.only('aws-node-sdk test deleteBucketReplication', () => {
|
||||
let s3;
|
||||
let otherAccountS3;
|
||||
const config = getConfig('default', { signatureVersion: 'v4' });
|
||||
|
||||
function putVersioningOnBucket(bucket, cb) {
|
||||
return s3.putBucketVersioning({
|
||||
Bucket: bucket,
|
||||
VersioningConfiguration: { Status: 'Enabled' },
|
||||
}, cb);
|
||||
}
|
||||
|
||||
function putReplicationOnBucket(bucket, cb) {
|
||||
return s3.putBucketReplication({
|
||||
Bucket: bucket,
|
||||
ReplicationConfiguration: replicationConfig,
|
||||
}, cb);
|
||||
}
|
||||
|
||||
function deleteReplicationAndCheckResponse(bucket, cb) {
|
||||
return s3.deleteBucketReplication({ Bucket: bucket }, (err, data) => {
|
||||
assert.strictEqual(err, null);
|
||||
assert.deepStrictEqual(data, {});
|
||||
return cb();
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(done => {
|
||||
s3 = new S3(config);
|
||||
otherAccountS3 = new BucketUtility('lisa', {}).s3;
|
||||
return s3.createBucket({ Bucket: bucket }, done);
|
||||
});
|
||||
|
||||
afterEach(done => s3.deleteBucket({ Bucket: bucket }, done));
|
||||
|
||||
it('should return empty object if bucket is not versioned enabled', done =>
|
||||
deleteReplicationAndCheckResponse(bucket, done));
|
||||
|
||||
it('should return empty object if bucket is version enabled but has no ' +
|
||||
'replication config', done => series([
|
||||
next => putVersioningOnBucket(bucket, next),
|
||||
next => deleteReplicationAndCheckResponse(bucket, next),
|
||||
], done));
|
||||
|
||||
it('should delete a bucket replication config when it has one', done =>
|
||||
series([
|
||||
next => putVersioningOnBucket(bucket, next),
|
||||
next => putReplicationOnBucket(bucket, next),
|
||||
next => deleteReplicationAndCheckResponse(bucket, next),
|
||||
], done));
|
||||
|
||||
// TODO: Include test when getBucketReplication API is merged.
|
||||
it.skip('should return ReplicationConfigurationNotFoundError if getting ' +
|
||||
'replication config after it has been deleted', done =>
|
||||
series([
|
||||
next => putVersioningOnBucket(bucket, next),
|
||||
next => putReplicationOnBucket(bucket, next),
|
||||
next => s3.getBucketReplication({ Bucket: bucket }, (err, data) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
assert.deepStrictEqual(data, {
|
||||
ReplicationConfiguration: replicationConfig,
|
||||
});
|
||||
return next();
|
||||
}),
|
||||
next => deleteReplicationAndCheckResponse(bucket, next),
|
||||
next => s3.getBucketReplication({ Bucket: bucket }, err => {
|
||||
assert(errors.ReplicationConfigurationNotFoundError[err.code]);
|
||||
return next();
|
||||
}),
|
||||
], done));
|
||||
|
||||
it('should return AccessDenied if user is not bucket owner', done =>
|
||||
otherAccountS3.deleteBucketReplication({ Bucket: bucket }, err => {
|
||||
assert(err);
|
||||
assert.strictEqual(err.code, 'AccessDenied');
|
||||
assert.strictEqual(err.statusCode, 403);
|
||||
return done();
|
||||
}));
|
||||
});
|
|
@ -0,0 +1,78 @@
|
|||
const assert = require('assert');
|
||||
const { S3 } = require('aws-sdk');
|
||||
const { series } = require('async');
|
||||
const { errors } = require('arsenal');
|
||||
|
||||
const getConfig = require('../support/config');
|
||||
const BucketUtility = require('../../lib/utility/bucket-util');
|
||||
|
||||
const bucket = 'source-bucket';
|
||||
|
||||
const replicationConfig = {
|
||||
Role: 'arn:partition:service::account-id:resourcetype/resource',
|
||||
Rules: [
|
||||
{
|
||||
Destination: { Bucket: 'arn:aws:s3:::destination-bucket' },
|
||||
Prefix: 'test-prefix',
|
||||
Status: 'Enabled',
|
||||
ID: 'test-id',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('aws-node-sdk test getBucketReplication', () => {
|
||||
let s3;
|
||||
let otherAccountS3;
|
||||
|
||||
beforeEach(done => {
|
||||
const config = getConfig('default', { signatureVersion: 'v4' });
|
||||
s3 = new S3(config);
|
||||
otherAccountS3 = new BucketUtility('lisa', {}).s3;
|
||||
return series([
|
||||
next => s3.createBucket({ Bucket: bucket }, next),
|
||||
next => s3.putBucketVersioning({
|
||||
Bucket: bucket,
|
||||
VersioningConfiguration: {
|
||||
Status: 'Enabled',
|
||||
},
|
||||
}, next),
|
||||
], done);
|
||||
});
|
||||
|
||||
afterEach(done => s3.deleteBucket({ Bucket: bucket }, done));
|
||||
|
||||
it("should return 'ReplicationConfigurationNotFoundError' if bucket does " +
|
||||
'not have a replication configuration', done =>
|
||||
s3.getBucketReplication({ Bucket: bucket }, err => {
|
||||
assert(errors.ReplicationConfigurationNotFoundError[err.code]);
|
||||
return done();
|
||||
}));
|
||||
|
||||
it('should get the replication configuration that was put on a bucket',
|
||||
done => s3.putBucketReplication({
|
||||
Bucket: bucket,
|
||||
ReplicationConfiguration: replicationConfig,
|
||||
}, err => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
return s3.getBucketReplication({ Bucket: bucket }, (err, data) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
const expectedObj = {
|
||||
ReplicationConfiguration: replicationConfig,
|
||||
};
|
||||
assert.deepStrictEqual(data, expectedObj);
|
||||
return done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should return AccessDenied if user is not bucket owner', done =>
|
||||
otherAccountS3.getBucketReplication({ Bucket: bucket }, err => {
|
||||
assert(err);
|
||||
assert.strictEqual(err.code, 'AccessDenied');
|
||||
assert.strictEqual(err.statusCode, 403);
|
||||
return done();
|
||||
}));
|
||||
});
|
|
@ -0,0 +1,92 @@
|
|||
const assert = require('assert');
|
||||
const { parseString } = require('xml2js');
|
||||
|
||||
const { DummyRequestLogger } = require('../helpers');
|
||||
const { getReplicationConfigurationXML } =
|
||||
require('../../../lib/api/apiUtils/bucket/getReplicationConfiguration');
|
||||
|
||||
// Compare the values from the parsedXML with the original configuration values.
|
||||
function checkXML(parsedXML, config) {
|
||||
const { Rule, Role } = parsedXML.ReplicationConfiguration;
|
||||
const { role, destination } = config;
|
||||
assert.strictEqual(Role[0], role);
|
||||
assert.strictEqual(Array.isArray(Rule), true);
|
||||
for (let i = 0; i < Rule.length; i++) {
|
||||
const { ID, Prefix, Status, Destination } = Rule[i];
|
||||
const { Bucket, StorageClass } = Destination[0];
|
||||
const { id, prefix, enabled, storageClass } = config.rules[i];
|
||||
assert.strictEqual(ID[0], id);
|
||||
assert.strictEqual(Prefix[0], prefix);
|
||||
assert.strictEqual(Status[0], enabled ? 'Enabled' : 'Disabled');
|
||||
if (storageClass !== undefined) {
|
||||
assert.strictEqual(StorageClass[0], storageClass);
|
||||
}
|
||||
assert.strictEqual(Bucket[0], destination);
|
||||
}
|
||||
}
|
||||
|
||||
// Get the replication XML, parse it, and check that values are as expected.
|
||||
function getAndCheckXML(bucketReplicationConfig, cb) {
|
||||
const log = new DummyRequestLogger();
|
||||
const xml = getReplicationConfigurationXML(bucketReplicationConfig, log);
|
||||
return parseString(xml, (err, res) => {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
checkXML(res, bucketReplicationConfig);
|
||||
return cb(null, res);
|
||||
});
|
||||
}
|
||||
|
||||
// Get an example bucket replication configuration.
|
||||
function getReplicationConfig() {
|
||||
return {
|
||||
role: 'arn:partition:service::account-id:resourcetype/resource',
|
||||
destination: 'destination-bucket',
|
||||
rules: [
|
||||
{
|
||||
id: 'test-id-1',
|
||||
prefix: 'test-prefix-1',
|
||||
enabled: true,
|
||||
storageClass: 'STANDARD',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
describe("'getReplicationConfigurationXML' function", () => {
|
||||
it('should return XML from the bucket replication configuration', done =>
|
||||
getAndCheckXML(getReplicationConfig(), done));
|
||||
|
||||
it('should not return XML with StorageClass tag if `storageClass` ' +
|
||||
'property is omitted', done => {
|
||||
const config = getReplicationConfig();
|
||||
delete config.rules[0].storageClass;
|
||||
return getAndCheckXML(config, done);
|
||||
});
|
||||
|
||||
it("should return XML with StorageClass tag set to 'Disabled' if " +
|
||||
'`enabled` property is false', done => {
|
||||
const config = getReplicationConfig();
|
||||
config.rules[0].enabled = false;
|
||||
return getAndCheckXML(config, done);
|
||||
});
|
||||
|
||||
it('should return XML with a self-closing Prefix tag if `prefix` ' +
|
||||
"property is ''", done => {
|
||||
const config = getReplicationConfig();
|
||||
config.rules[0].prefix = '';
|
||||
return getAndCheckXML(config, done);
|
||||
});
|
||||
|
||||
it('should return XML from the bucket replication configuration with ' +
|
||||
'multiple rules', done => {
|
||||
const config = getReplicationConfig();
|
||||
config.rules.push({
|
||||
id: 'test-id-2',
|
||||
prefix: 'test-prefix-2',
|
||||
enabled: true,
|
||||
});
|
||||
return getAndCheckXML(config, done);
|
||||
});
|
||||
});
|
|
@ -1,7 +1,7 @@
|
|||
const assert = require('assert');
|
||||
|
||||
const { DummyRequestLogger } = require('../helpers');
|
||||
const getReplicationConfiguration =
|
||||
const { getReplicationConfiguration } =
|
||||
require('../../../lib/api/apiUtils/bucket/getReplicationConfiguration');
|
||||
const replicationUtils =
|
||||
require('../../functional/aws-node-sdk/lib/utility/replication');
|
||||
|
|
|
@ -0,0 +1,276 @@
|
|||
const assert = require('assert');
|
||||
const async = require('async');
|
||||
|
||||
const BucketInfo = require('../../../lib/metadata/BucketInfo');
|
||||
|
||||
const { cleanup, DummyRequestLogger, makeAuthInfo, TaggingConfigTester } =
|
||||
require('../helpers');
|
||||
const { metadata } = require('../../../lib/metadata/in_memory/metadata');
|
||||
const DummyRequest = require('../DummyRequest');
|
||||
const objectDelete = require('../../../lib/api/objectDelete');
|
||||
const objectPut = require('../../../lib/api/objectPut');
|
||||
const objectPutACL = require('../../../lib/api/objectPutACL');
|
||||
const objectPutTagging = require('../../../lib/api/objectPutTagging');
|
||||
const objectDeleteTagging = require('../../../lib/api/objectDeleteTagging');
|
||||
|
||||
const log = new DummyRequestLogger();
|
||||
const canonicalID = 'accessKey1';
|
||||
const authInfo = makeAuthInfo(canonicalID);
|
||||
const ownerID = authInfo.getCanonicalID();
|
||||
const namespace = 'default';
|
||||
const bucketName = 'source-bucket';
|
||||
const bucketARN = `arn:aws:s3:::${bucketName}`;
|
||||
const storageClassType = 'STANDARD';
|
||||
const keyA = 'key-A';
|
||||
const keyB = 'key-B';
|
||||
|
||||
const deleteReq = new DummyRequest({
|
||||
bucketName,
|
||||
namespace,
|
||||
objectKey: keyA,
|
||||
headers: {},
|
||||
url: `/${bucketName}/${keyA}`,
|
||||
});
|
||||
|
||||
const objectACLReq = {
|
||||
bucketName,
|
||||
namespace,
|
||||
objectKey: keyA,
|
||||
headers: {
|
||||
'x-amz-grant-read': `id=${ownerID}`,
|
||||
'x-amz-grant-read-acp': `id=${ownerID}`,
|
||||
},
|
||||
url: `/${bucketName}/${keyA}?acl`,
|
||||
query: { acl: '' },
|
||||
};
|
||||
|
||||
// Get an object request with the given key.
|
||||
function getObjectPutReq(key) {
|
||||
return new DummyRequest({
|
||||
bucketName,
|
||||
namespace,
|
||||
objectKey: key,
|
||||
headers: {},
|
||||
url: `/${bucketName}/${key}`,
|
||||
}, Buffer.from('body content', 'utf8'));
|
||||
}
|
||||
|
||||
const taggingPutReq = new TaggingConfigTester()
|
||||
.createObjectTaggingRequest('PUT', bucketName, keyA);
|
||||
const taggingDeleteReq = new TaggingConfigTester()
|
||||
.createObjectTaggingRequest('DELETE', bucketName, keyA);
|
||||
|
||||
const emptyReplicationMD = {
|
||||
status: '',
|
||||
content: [],
|
||||
destination: '',
|
||||
storageClass: '',
|
||||
};
|
||||
|
||||
// Check that the object key has the expected replication information.
|
||||
function checkObjectReplicationInfo(key, expected) {
|
||||
const objectMD = metadata.keyMaps.get(bucketName).get(key);
|
||||
assert.deepStrictEqual(objectMD.replicationInfo, expected);
|
||||
}
|
||||
|
||||
// Put the object key and check the replication information.
|
||||
function putObjectAndCheckMD(key, expected, cb) {
|
||||
return objectPut(authInfo, getObjectPutReq(key), undefined, log, err => {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
checkObjectReplicationInfo(key, expected);
|
||||
return cb();
|
||||
});
|
||||
}
|
||||
|
||||
// Create the bucket in metadata.
|
||||
function createBucket() {
|
||||
metadata
|
||||
.buckets.set(bucketName, new BucketInfo(bucketName, ownerID, '', ''));
|
||||
metadata.keyMaps.set(bucketName, new Map);
|
||||
}
|
||||
|
||||
// Create the bucket in metadata with versioning and a replication config.
|
||||
function createBucketWithReplication(hasStorageClass) {
|
||||
createBucket();
|
||||
const config = {
|
||||
role: 'arn:partition:service::account-id:resourcetype/resource',
|
||||
destination: 'arn:aws:s3:::source-bucket',
|
||||
rules: [{
|
||||
prefix: keyA,
|
||||
enabled: true,
|
||||
id: 'test-id',
|
||||
}],
|
||||
};
|
||||
if (hasStorageClass) {
|
||||
config.rules[0].storageClass = storageClassType;
|
||||
}
|
||||
Object.assign(metadata.buckets.get(bucketName), {
|
||||
_versioningConfiguration: { status: 'Enabled' },
|
||||
_replicationConfiguration: config,
|
||||
});
|
||||
}
|
||||
|
||||
describe('Replication object MD without bucket replication config', () => {
|
||||
beforeEach(() => {
|
||||
cleanup();
|
||||
createBucket();
|
||||
});
|
||||
|
||||
afterEach(() => cleanup());
|
||||
|
||||
it('should not update object metadata', done =>
|
||||
putObjectAndCheckMD(keyA, emptyReplicationMD, done));
|
||||
|
||||
it('should not update object metadata if putting object ACL', done =>
|
||||
async.series([
|
||||
next => putObjectAndCheckMD(keyA, emptyReplicationMD, next),
|
||||
next => objectPutACL(authInfo, objectACLReq, log, next),
|
||||
], err => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
checkObjectReplicationInfo(keyA, emptyReplicationMD);
|
||||
return done();
|
||||
}));
|
||||
|
||||
describe('Object tagging', () => {
|
||||
beforeEach(done => async.series([
|
||||
next => putObjectAndCheckMD(keyA, emptyReplicationMD, next),
|
||||
next => objectPutTagging(authInfo, taggingPutReq, log, next),
|
||||
], err => done(err)));
|
||||
|
||||
it('should not update object metadata if putting tag', done => {
|
||||
checkObjectReplicationInfo(keyA, emptyReplicationMD);
|
||||
return done();
|
||||
});
|
||||
|
||||
it('should not update object metadata if deleting tag', done =>
|
||||
async.series([
|
||||
// Put a new version to update replication MD content array.
|
||||
next => putObjectAndCheckMD(keyA, emptyReplicationMD, next),
|
||||
next => objectDeleteTagging(authInfo, taggingDeleteReq, log,
|
||||
next),
|
||||
], err => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
checkObjectReplicationInfo(keyA, emptyReplicationMD);
|
||||
return done();
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
[true, false].forEach(hasStorageClass => {
|
||||
describe('Replication object MD with bucket replication config ' +
|
||||
`${hasStorageClass ? 'with' : 'without'} storage class`, () => {
|
||||
const replicationMD = {
|
||||
status: 'PENDING',
|
||||
content: ['DATA', 'METADATA'],
|
||||
destination: bucketARN,
|
||||
storageClass: '',
|
||||
};
|
||||
const newReplicationMD = hasStorageClass ? Object.assign(replicationMD,
|
||||
{ storageClass: storageClassType }) : replicationMD;
|
||||
const replicateMetadataOnly = Object.assign({}, newReplicationMD,
|
||||
{ content: ['METADATA'] });
|
||||
|
||||
beforeEach(() => {
|
||||
cleanup();
|
||||
createBucketWithReplication(hasStorageClass);
|
||||
});
|
||||
|
||||
afterEach(() => cleanup());
|
||||
|
||||
it('should update metadata when replication config prefix matches ' +
|
||||
'an object key', done =>
|
||||
putObjectAndCheckMD(keyA, newReplicationMD, done));
|
||||
|
||||
it('should update metadata when replication config prefix matches ' +
|
||||
'the start of an object key', done =>
|
||||
putObjectAndCheckMD(`${keyA}abc`, newReplicationMD, done));
|
||||
|
||||
it('should not update metadata when replication config prefix does ' +
|
||||
'not match the start of an object key', done =>
|
||||
putObjectAndCheckMD(`abc${keyA}`, emptyReplicationMD, done));
|
||||
|
||||
it('should not update metadata when replication config prefix does ' +
|
||||
'not apply', done =>
|
||||
putObjectAndCheckMD(keyB, emptyReplicationMD, done));
|
||||
|
||||
it("should update status to 'PENDING' if putting a new version", done =>
|
||||
putObjectAndCheckMD(keyA, newReplicationMD, err => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
const objectMD = metadata.keyMaps.get(bucketName).get(keyA);
|
||||
// Update metadata to a status after replication has occurred.
|
||||
objectMD.replicationInfo.status = 'COMPLETED';
|
||||
return putObjectAndCheckMD(keyA, newReplicationMD, done);
|
||||
}));
|
||||
|
||||
it("should update status to 'PENDING' and content to '['METADATA']' " +
|
||||
'if putting object ACL', done =>
|
||||
async.series([
|
||||
next => putObjectAndCheckMD(keyA, newReplicationMD, next),
|
||||
next => objectPutACL(authInfo, objectACLReq, log, next),
|
||||
], err => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
checkObjectReplicationInfo(keyA, replicateMetadataOnly);
|
||||
return done();
|
||||
}));
|
||||
|
||||
it('should update metadata if putting a delete marker', done =>
|
||||
async.series([
|
||||
next => putObjectAndCheckMD(keyA, newReplicationMD, err => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
const objectMD = metadata.keyMaps.get(bucketName).get(keyA);
|
||||
// Set metadata to a status after replication has occurred.
|
||||
objectMD.replicationInfo.status = 'COMPLETED';
|
||||
return next();
|
||||
}),
|
||||
next => objectDelete(authInfo, deleteReq, log, next),
|
||||
], err => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
const objectMD = metadata.keyMaps.get(bucketName).get(keyA);
|
||||
assert.strictEqual(objectMD.isDeleteMarker, true);
|
||||
checkObjectReplicationInfo(keyA, replicateMetadataOnly);
|
||||
return done();
|
||||
}));
|
||||
|
||||
describe('Object tagging', () => {
|
||||
beforeEach(done => async.series([
|
||||
next => putObjectAndCheckMD(keyA, newReplicationMD, next),
|
||||
next => objectPutTagging(authInfo, taggingPutReq, log, next),
|
||||
], err => done(err)));
|
||||
|
||||
it("should update status to 'PENDING' and content to " +
|
||||
"'['METADATA']'if putting tag", done => {
|
||||
checkObjectReplicationInfo(keyA, replicateMetadataOnly);
|
||||
return done();
|
||||
});
|
||||
|
||||
it("should update status to 'PENDING' and content to " +
|
||||
"'['METADATA']' if deleting tag", done =>
|
||||
async.series([
|
||||
// Put a new version to update replication MD content array.
|
||||
next => putObjectAndCheckMD(keyA, newReplicationMD, next),
|
||||
next => objectDeleteTagging(authInfo, taggingDeleteReq, log,
|
||||
next),
|
||||
], err => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
checkObjectReplicationInfo(keyA, replicateMetadataOnly);
|
||||
return done();
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
|
@ -72,6 +72,19 @@ describe('ObjectMD class setters/getters', () => {
|
|||
['Tags', {
|
||||
key: 'value',
|
||||
}],
|
||||
['Tags', null, {}],
|
||||
['ReplicationInfo', null, {
|
||||
status: '',
|
||||
content: [],
|
||||
destination: '',
|
||||
storageClass: '',
|
||||
}],
|
||||
['ReplicationInfo', {
|
||||
status: 'PENDING',
|
||||
content: ['DATA', 'METADATA'],
|
||||
destination: 'destination-bucket',
|
||||
storageClass: 'STANDARD',
|
||||
}],
|
||||
].forEach(test => {
|
||||
const property = test[0];
|
||||
const testValue = test[1];
|
||||
|
|
Loading…
Reference in New Issue