diff --git a/lib/routes/routeBackbeat.js b/lib/routes/routeBackbeat.js index 95354cf1c9..ab74e670de 100644 --- a/lib/routes/routeBackbeat.js +++ b/lib/routes/routeBackbeat.js @@ -491,14 +491,43 @@ function putMetadata(request, response, bucketInfo, objMd, log, callback) { omVal[headerName] = objMd[headerName]; }); } - const versionId = decodeVersionId(request.query); - const options = {}; - if (versionId || omVal.replicationInfo.isNFS) { - // specify both 'versioning' and 'versionId' to create a "new" - // version (updating master as well) but with specified - // versionId - options.versioning = bucketInfo.isVersioningEnabled(); - options.versionId = versionId; + + let versionId; + let versioning; + const decodedVidResult = decodeVersionId(request.query); + + if (decodedVidResult || omVal.replicationInfo.isNFS) { + versionId = decodedVidResult; + versioning = bucketInfo.isVersioningEnabled(); + } + + if (versionId === 'null') { + // Retrieve the null version id from the object metadata. + versionId = objMd && objMd.versionId; + if (!versionId) { + if (versioning) { + // If the null version does not have a version id, it is a current null version. + // To update the metadata of a current version, versioning is set to false. + + // This condition is to handle the case where a single null version looks like a master + // key and will not have a duplicate versioned key and no version ID. + // They are created when you have a non-versioned bucket with objects, + // and then convert bucket to versioned. + // If no new versioned objects are added for given object(s), they look like + // standalone master keys. + versioning = false; + } else { + const versioningConf = bucketInfo.getVersioningConfiguration(); + // The purpose of this condition is to address situations in which + // - versioning is "suspended" and + // - no existing object or no null version. + // In such scenarios, we generate a new null version and designate it as the master version. + if (versioningConf && versioningConf.Status === 'Suspended') { + versionId = ''; + omVal.isNull = true; + } + } + } } // If the object is from a source bucket without versioning (i.e. NFS), @@ -506,9 +535,23 @@ function putMetadata(request, response, bucketInfo, objMd, log, callback) { // none was provided in the object metadata value. if (omVal.replicationInfo.isNFS) { const isReplica = omVal.replicationInfo.status === 'REPLICA'; - options.versioning = isReplica; + versioning = isReplica; omVal.replicationInfo.isNFS = !isReplica; } + + const options = { + versionId, + }; + + // NOTE: When 'versioning' is set to true and no 'versionId' is specified, + // it results in the creation of a "new" version, which also updates the master. + // NOTE: Since option fields are converted to strings when they're sent to Metadata via the query string, + // Metadata interprets the value "false" as if it were true. + // Therefore, to avoid this confusion, we don't pass the versioning parameter at all if its value is false. + if (versioning) { + options.versioning = true; + } + log.trace('putting object version', { objectKey: request.objectKey, omVal, options }); return metadata.putObjectMD(bucketName, objectKey, omVal, options, log, diff --git a/tests/multipleBackend/routes/routeBackbeat.js b/tests/multipleBackend/routes/routeBackbeat.js index 42dcfc7e97..604d4f9893 100644 --- a/tests/multipleBackend/routes/routeBackbeat.js +++ b/tests/multipleBackend/routes/routeBackbeat.js @@ -3,7 +3,8 @@ const AWS = require('aws-sdk'); const async = require('async'); const crypto = require('crypto'); const { v4: uuidv4 } = require('uuid'); -const { versioning } = require('arsenal'); +const { models, versioning } = require('arsenal'); +const { ObjectMD } = models; const versionIdUtils = versioning.VersionID; const { makeid } = require('../../unit/helpers'); @@ -39,6 +40,7 @@ const TEST_BUCKET = 'backbeatbucket'; const TEST_ENCRYPTED_BUCKET = 'backbeatbucket-encrypted'; const TEST_KEY = 'fookey'; const NONVERSIONED_BUCKET = 'backbeatbucket-non-versioned'; +const BUCKET_FOR_NULL_VERSION = 'backbeatbucket-null-version'; const testArn = 'aws::iam:123456789012:user/bart'; const testKey = 'testkey'; @@ -132,6 +134,18 @@ function checkObjectData(s3, bucket, objectKey, dataValue, done) { }); } +function checkVersionData(s3, bucket, objectKey, versionId, dataValue, done) { + return s3.getObject({ + Bucket: bucket, + Key: objectKey, + VersionId: versionId, + }, (err, data) => { + assert.ifError(err); + assert.strictEqual(data.Body.toString(), dataValue); + return done(); + }); +} + /** makeBackbeatRequest - utility function to generate a request going * through backbeat route * @param {object} params - params for making request @@ -164,6 +178,21 @@ function makeBackbeatRequest(params, callback) { makeRequest(options, callback); } +function updateStorageClass(data, storageClass) { + let parsedBody; + try { + parsedBody = JSON.parse(data.body); + } catch (err) { + return { error: err }; + } + const { result, error } = ObjectMD.createFromBlob(parsedBody.Body); + if (error) { + return { error }; + } + result.setAmzStorageClass(storageClass); + return { result }; +} + describe.skip('backbeat DELETE routes', () => { it('abort MPU', done => { const awsKey = 'backbeat-mpu-test'; @@ -224,7 +253,7 @@ function getMetadataToPut(putDataResponse) { return mdToPut; } -describe.skip('backbeat routes', () => { +describe('backbeat routes', () => { let bucketUtil; let s3; @@ -274,7 +303,977 @@ describe.skip('backbeat routes', () => { .then(() => done()); }); - describe('backbeat PUT routes', () => { + describe('null version', () => { + const bucket = BUCKET_FOR_NULL_VERSION; + const keyName = 'key0'; + const storageClass = 'foo'; + + function assertVersionIsNullAndUpdated(version) { + const { Key, VersionId, StorageClass } = version; + assert.strictEqual(Key, keyName); + assert.strictEqual(VersionId, 'null'); + assert.strictEqual(StorageClass, storageClass); + } + + function assertVersionHasNotBeenUpdated(version, expectedVersionId) { + const { Key, VersionId, StorageClass } = version; + assert.strictEqual(Key, keyName); + assert.strictEqual(VersionId, expectedVersionId); + assert.strictEqual(StorageClass, 'STANDARD'); + } + + beforeEach(done => s3.createBucket({ Bucket: BUCKET_FOR_NULL_VERSION }, done)); + afterEach(done => { + bucketUtil.empty(BUCKET_FOR_NULL_VERSION) + .then(() => s3.deleteBucket({ Bucket: BUCKET_FOR_NULL_VERSION }).promise()) + .then(() => done()); + }); + + it('should update metadata of a current null version', done => { + let objMD; + return async.series([ + next => s3.putObject({ Bucket: bucket, Key: keyName, Body: new Buffer(testData) }, next), + next => s3.putBucketVersioning({ Bucket: bucket, VersioningConfiguration: { Status: 'Enabled' } }, + next), + next => makeBackbeatRequest({ + method: 'GET', + resourceType: 'metadata', + bucket, + objectKey: keyName, + queryObj: { + versionId: 'null', + }, + authCredentials: backbeatAuthCredentials, + }, (err, data) => { + if (err) { + return next(err); + } + const { error, result } = updateStorageClass(data, storageClass); + if (error) { + return next(error); + } + objMD = result; + return next(); + }), + next => makeBackbeatRequest({ + method: 'PUT', + resourceType: 'metadata', + bucket, + objectKey: keyName, + queryObj: { + versionId: 'null', + }, + authCredentials: backbeatAuthCredentials, + requestBody: objMD.getSerialized(), + }, next), + next => s3.headObject({ Bucket: bucket, Key: keyName, VersionId: 'null' }, next), + next => s3.listObjectVersions({ Bucket: bucket }, next), + ], (err, data) => { + if (err) { + return done(err); + } + const headObjectRes = data[4]; + assert.strictEqual(headObjectRes.VersionId, 'null'); + assert.strictEqual(headObjectRes.StorageClass, storageClass); + + const listObjectVersionsRes = data[5]; + const { Versions } = listObjectVersionsRes; + + assert.strictEqual(Versions.length, 1); + + const [currentVersion] = Versions; + assertVersionIsNullAndUpdated(currentVersion); + return done(); + }); + }); + + it('should update metadata of a non-current null version', done => { + let objMD; + let expectedVersionId; + return async.series([ + next => s3.putObject({ Bucket: bucket, Key: keyName, Body: new Buffer(testData) }, next), + next => s3.putBucketVersioning({ Bucket: bucket, VersioningConfiguration: { Status: 'Enabled' } }, + next), + next => s3.putObject({ Bucket: bucket, Key: keyName, Body: new Buffer(testData) }, (err, data) => { + if (err) { + return next(err); + } + expectedVersionId = data.VersionId; + return next(); + }), + next => makeBackbeatRequest({ + method: 'GET', + resourceType: 'metadata', + bucket, + objectKey: keyName, + queryObj: { + versionId: 'null', + }, + authCredentials: backbeatAuthCredentials, + }, (err, data) => { + if (err) { + return next(err); + } + const { error, result } = updateStorageClass(data, storageClass); + if (error) { + return next(error); + } + objMD = result; + return next(); + }), + next => makeBackbeatRequest({ + method: 'PUT', + resourceType: 'metadata', + bucket, + objectKey: keyName, + queryObj: { + versionId: 'null', + }, + authCredentials: backbeatAuthCredentials, + requestBody: objMD.getSerialized(), + }, next), + next => s3.headObject({ Bucket: bucket, Key: keyName, VersionId: 'null' }, next), + next => s3.listObjectVersions({ Bucket: bucket }, next), + ], (err, data) => { + if (err) { + return done(err); + } + const headObjectRes = data[5]; + assert.strictEqual(headObjectRes.VersionId, 'null'); + assert.strictEqual(headObjectRes.StorageClass, storageClass); + + const listObjectVersionsRes = data[6]; + const { Versions } = listObjectVersionsRes; + + assert.strictEqual(Versions.length, 2); + + const currentVersion = Versions.find(v => v.IsLatest); + assertVersionHasNotBeenUpdated(currentVersion, expectedVersionId); + + const nonCurrentVersion = Versions.find(v => !v.IsLatest); + assertVersionIsNullAndUpdated(nonCurrentVersion); + return done(); + }); + }); + + it('should update metadata of a non-version object', done => { + let objMD; + return async.series([ + next => s3.putObject({ Bucket: bucket, Key: keyName, Body: new Buffer(testData) }, next), + next => makeBackbeatRequest({ + method: 'GET', + resourceType: 'metadata', + bucket, + objectKey: keyName, + queryObj: { + versionId: 'null', + }, + authCredentials: backbeatAuthCredentials, + }, (err, data) => { + if (err) { + return next(err); + } + const { error, result } = updateStorageClass(data, storageClass); + if (error) { + return next(error); + } + objMD = result; + return next(); + }), + next => makeBackbeatRequest({ + method: 'PUT', + resourceType: 'metadata', + bucket, + objectKey: keyName, + queryObj: { + versionId: 'null', + }, + authCredentials: backbeatAuthCredentials, + requestBody: objMD.getSerialized(), + }, next), + next => s3.headObject({ Bucket: bucket, Key: keyName }, next), + next => s3.listObjectVersions({ Bucket: bucket }, next), + ], (err, data) => { + if (err) { + return done(err); + } + + const headObjectRes = data[3]; + assert(!headObjectRes.VersionId); + assert.strictEqual(headObjectRes.StorageClass, storageClass); + + const listObjectVersionsRes = data[4]; + const { DeleteMarkers, Versions } = listObjectVersionsRes; + + assert.strictEqual(DeleteMarkers.length, 0); + assert.strictEqual(Versions.length, 1); + + const currentVersion = Versions[0]; + assert(currentVersion.IsLatest); + assertVersionIsNullAndUpdated(currentVersion); + return done(); + }); + }); + + it('should create a new null version if versioning suspended and no version', done => { + let objMD; + return async.series([ + next => s3.putBucketVersioning({ Bucket: bucket, VersioningConfiguration: { Status: 'Suspended' } }, + next), + next => s3.putObject({ Bucket: bucket, Key: keyName, Body: new Buffer(testData) }, next), + next => makeBackbeatRequest({ + method: 'GET', + resourceType: 'metadata', + bucket, + objectKey: keyName, + queryObj: { + versionId: 'null', + }, + authCredentials: backbeatAuthCredentials, + }, (err, data) => { + if (err) { + return next(err); + } + const { error, result } = updateStorageClass(data, storageClass); + if (error) { + return next(error); + } + objMD = result; + return next(); + }), + next => s3.deleteObject({ Bucket: bucket, Key: keyName, VersionId: 'null' }, next), + next => makeBackbeatRequest({ + method: 'PUT', + resourceType: 'metadata', + bucket, + objectKey: keyName, + queryObj: { + versionId: 'null', + }, + authCredentials: backbeatAuthCredentials, + requestBody: objMD.getSerialized(), + }, next), + next => s3.headObject({ Bucket: bucket, Key: keyName }, next), + next => s3.listObjectVersions({ Bucket: bucket }, next), + ], (err, data) => { + if (err) { + return done(err); + } + const headObjectRes = data[5]; + assert.strictEqual(headObjectRes.VersionId, 'null'); + assert.strictEqual(headObjectRes.StorageClass, storageClass); + + const listObjectVersionsRes = data[6]; + const { DeleteMarkers, Versions } = listObjectVersionsRes; + + assert.strictEqual(DeleteMarkers.length, 0); + assert.strictEqual(Versions.length, 1); + + const currentVersion = Versions[0]; + assert(currentVersion.IsLatest); + + assertVersionIsNullAndUpdated(currentVersion); + + return done(); + }); + }); + + it('should create a new null version if versioning suspended and delete marker null version', done => { + let objMD; + return async.series([ + next => s3.putBucketVersioning({ Bucket: bucket, VersioningConfiguration: { Status: 'Suspended' } }, + next), + next => s3.putObject({ Bucket: bucket, Key: keyName, Body: new Buffer(testData) }, next), + next => makeBackbeatRequest({ + method: 'GET', + resourceType: 'metadata', + bucket, + objectKey: keyName, + queryObj: { + versionId: 'null', + }, + authCredentials: backbeatAuthCredentials, + }, (err, data) => { + if (err) { + return next(err); + } + const { error, result } = updateStorageClass(data, storageClass); + if (error) { + return next(error); + } + objMD = result; + return next(); + }), + next => s3.deleteObject({ Bucket: bucket, Key: keyName }, next), + next => makeBackbeatRequest({ + method: 'PUT', + resourceType: 'metadata', + bucket, + objectKey: keyName, + queryObj: { + versionId: 'null', + }, + authCredentials: backbeatAuthCredentials, + requestBody: objMD.getSerialized(), + }, next), + next => s3.headObject({ Bucket: bucket, Key: keyName }, next), + next => s3.listObjectVersions({ Bucket: bucket }, next), + ], (err, data) => { + if (err) { + return done(err); + } + const headObjectRes = data[5]; + assert.strictEqual(headObjectRes.VersionId, 'null'); + assert.strictEqual(headObjectRes.StorageClass, storageClass); + + const listObjectVersionsRes = data[6]; + const { DeleteMarkers, Versions } = listObjectVersionsRes; + + assert.strictEqual(DeleteMarkers.length, 0); + assert.strictEqual(Versions.length, 1); + + const currentVersion = Versions[0]; + assert(currentVersion.IsLatest); + assertVersionIsNullAndUpdated(currentVersion); + return done(); + }); + }); + + it('should create a new null version if versioning suspended and version has version id', done => { + let expectedVersionId; + let objMD; + return async.series([ + next => s3.putBucketVersioning({ Bucket: bucket, VersioningConfiguration: { Status: 'Enabled' } }, + next), + next => s3.putObject({ Bucket: bucket, Key: keyName, Body: new Buffer(testData) }, (err, data) => { + if (err) { + return next(err); + } + expectedVersionId = data.VersionId; + return next(); + }), + next => s3.putBucketVersioning({ Bucket: bucket, VersioningConfiguration: { Status: 'Suspended' } }, + next), + next => s3.putObject({ Bucket: bucket, Key: keyName, Body: new Buffer(testData) }, next), + next => makeBackbeatRequest({ + method: 'GET', + resourceType: 'metadata', + bucket, + objectKey: keyName, + queryObj: { + versionId: null, + }, + authCredentials: backbeatAuthCredentials, + }, (err, data) => { + if (err) { + return next(err); + } + const { error, result } = updateStorageClass(data, storageClass); + if (error) { + return next(error); + } + objMD = result; + return next(); + }), + next => s3.deleteObject({ Bucket: bucket, Key: keyName, VersionId: 'null' }, next), + next => makeBackbeatRequest({ + method: 'PUT', + resourceType: 'metadata', + bucket, + objectKey: keyName, + queryObj: { + versionId: 'null', + }, + authCredentials: backbeatAuthCredentials, + requestBody: objMD.getSerialized(), + }, next), + next => s3.headObject({ Bucket: bucket, Key: keyName }, next), + next => s3.listObjectVersions({ Bucket: bucket }, next), + ], (err, data) => { + if (err) { + return done(err); + } + const headObjectRes = data[7]; + assert.strictEqual(headObjectRes.VersionId, 'null'); + assert.strictEqual(headObjectRes.StorageClass, storageClass); + + const listObjectVersionsRes = data[8]; + const { DeleteMarkers, Versions } = listObjectVersionsRes; + + assert.strictEqual(DeleteMarkers.length, 0); + assert.strictEqual(Versions.length, 2); + + const currentVersion = Versions.find(v => v.IsLatest); + assertVersionIsNullAndUpdated(currentVersion); + + const nonCurrentVersion = Versions.find(v => !v.IsLatest); + assertVersionHasNotBeenUpdated(nonCurrentVersion, expectedVersionId); + + // give some time for the async deletes to complete + return setTimeout(() => checkVersionData(s3, bucket, keyName, expectedVersionId, testData, done), + 1000); + }); + }); + + it('should update null version with no version id and versioning suspended', done => { + let objMD; + return async.series([ + next => s3.putObject({ Bucket: bucket, Key: keyName, Body: new Buffer(testData) }, next), + next => s3.putBucketVersioning({ Bucket: bucket, VersioningConfiguration: { Status: 'Suspended' } }, + next), + next => makeBackbeatRequest({ + method: 'GET', + resourceType: 'metadata', + bucket, + objectKey: keyName, + queryObj: { + versionId: 'null', + }, + authCredentials: backbeatAuthCredentials, + }, (err, data) => { + if (err) { + return next(err); + } + const { error, result } = updateStorageClass(data, storageClass); + if (error) { + return next(error); + } + objMD = result; + return next(); + }), + next => makeBackbeatRequest({ + method: 'PUT', + resourceType: 'metadata', + bucket, + objectKey: keyName, + queryObj: { + versionId: 'null', + }, + authCredentials: backbeatAuthCredentials, + requestBody: objMD.getSerialized(), + }, next), + next => s3.headObject({ Bucket: bucket, Key: keyName }, next), + next => s3.listObjectVersions({ Bucket: bucket }, next), + ], (err, data) => { + if (err) { + return done(err); + } + const headObjectRes = data[4]; + assert.strictEqual(headObjectRes.VersionId, 'null'); + assert.strictEqual(headObjectRes.StorageClass, storageClass); + + const listObjectVersionsRes = data[5]; + const { DeleteMarkers, Versions } = listObjectVersionsRes; + assert.strictEqual(DeleteMarkers.length, 0); + assert.strictEqual(Versions.length, 1); + + const currentVersion = Versions[0]; + assert(currentVersion.IsLatest); + assertVersionIsNullAndUpdated(currentVersion); + + return done(); + }); + }); + + it('should update null version if versioning suspended and null version has a version id', done => { + let objMD; + return async.series([ + next => s3.putBucketVersioning({ Bucket: bucket, VersioningConfiguration: { Status: 'Suspended' } }, + next), + next => s3.putObject({ Bucket: bucket, Key: keyName, Body: new Buffer(testData) }, next), + next => makeBackbeatRequest({ + method: 'GET', + resourceType: 'metadata', + bucket, + objectKey: keyName, + queryObj: { + versionId: 'null', + }, + authCredentials: backbeatAuthCredentials, + }, (err, data) => { + if (err) { + return next(err); + } + const { error, result } = updateStorageClass(data, storageClass); + if (error) { + return next(error); + } + objMD = result; + return next(); + }), + next => makeBackbeatRequest({ + method: 'PUT', + resourceType: 'metadata', + bucket, + objectKey: keyName, + queryObj: { + versionId: 'null', + }, + authCredentials: backbeatAuthCredentials, + requestBody: objMD.getSerialized(), + }, next), + next => s3.headObject({ Bucket: bucket, Key: keyName, VersionId: 'null' }, next), + next => s3.listObjectVersions({ Bucket: bucket }, next), + ], (err, data) => { + if (err) { + return done(err); + } + + const headObjectRes = data[4]; + assert.strictEqual(headObjectRes.VersionId, 'null'); + assert.strictEqual(headObjectRes.StorageClass, storageClass); + + const listObjectVersionsRes = data[5]; + const { DeleteMarkers, Versions } = listObjectVersionsRes; + assert.strictEqual(Versions.length, 1); + assert.strictEqual(DeleteMarkers.length, 0); + + const currentVersion = Versions[0]; + assert(currentVersion.IsLatest); + assertVersionIsNullAndUpdated(currentVersion); + return done(); + }); + }); + + it('should update null version if versioning suspended and null version has a version id and' + + 'put object afterward', done => { + let objMD; + return async.series([ + next => s3.putBucketVersioning({ Bucket: bucket, VersioningConfiguration: { Status: 'Suspended' } }, + next), + next => s3.putObject({ Bucket: bucket, Key: keyName, Body: new Buffer(testData) }, next), + next => makeBackbeatRequest({ + method: 'GET', + resourceType: 'metadata', + bucket, + objectKey: keyName, + queryObj: { + versionId: 'null', + }, + authCredentials: backbeatAuthCredentials, + }, (err, data) => { + if (err) { + return next(err); + } + const { error, result } = updateStorageClass(data, storageClass); + if (error) { + return next(error); + } + objMD = result; + return next(); + }), + next => makeBackbeatRequest({ + method: 'PUT', + resourceType: 'metadata', + bucket, + objectKey: keyName, + queryObj: { + versionId: 'null', + }, + authCredentials: backbeatAuthCredentials, + requestBody: objMD.getSerialized(), + }, next), + next => s3.putObject({ Bucket: bucket, Key: keyName, Body: new Buffer(testData) }, next), + next => s3.headObject({ Bucket: bucket, Key: keyName, VersionId: 'null' }, next), + next => s3.listObjectVersions({ Bucket: bucket }, next), + ], (err, data) => { + if (err) { + return done(err); + } + + const headObjectRes = data[5]; + assert.strictEqual(headObjectRes.VersionId, 'null'); + assert(!headObjectRes.StorageClass); + + const listObjectVersionsRes = data[6]; + const { DeleteMarkers, Versions } = listObjectVersionsRes; + assert.strictEqual(DeleteMarkers.length, 0); + assert.strictEqual(Versions.length, 1); + + const currentVersion = Versions[0]; + assert(currentVersion.IsLatest); + assertVersionHasNotBeenUpdated(currentVersion, 'null'); + return done(); + }); + }); + + it('should update null version if versioning suspended and null version has a version id and' + + 'put version afterward', done => { + let objMD; + let expectedVersionId; + return async.series([ + next => s3.putBucketVersioning({ Bucket: bucket, VersioningConfiguration: { Status: 'Suspended' } }, + next), + next => s3.putObject({ Bucket: bucket, Key: keyName, Body: new Buffer(testData) }, next), + next => makeBackbeatRequest({ + method: 'GET', + resourceType: 'metadata', + bucket, + objectKey: keyName, + queryObj: { + versionId: 'null', + }, + authCredentials: backbeatAuthCredentials, + }, (err, data) => { + if (err) { + return next(err); + } + const { error, result } = updateStorageClass(data, storageClass); + if (error) { + return next(error); + } + objMD = result; + return next(); + }), + next => makeBackbeatRequest({ + method: 'PUT', + resourceType: 'metadata', + bucket, + objectKey: keyName, + queryObj: { + versionId: 'null', + }, + authCredentials: backbeatAuthCredentials, + requestBody: objMD.getSerialized(), + }, next), + next => s3.putBucketVersioning({ Bucket: bucket, VersioningConfiguration: { Status: 'Enabled' } }, + next), + next => s3.putObject({ Bucket: bucket, Key: keyName, Body: new Buffer(testData) }, (err, data) => { + if (err) { + return next(err); + } + expectedVersionId = data.VersionId; + return next(); + }), + next => s3.headObject({ Bucket: bucket, Key: keyName, VersionId: 'null' }, next), + next => s3.listObjectVersions({ Bucket: bucket }, 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, 2); + + const [currentVersion] = Versions.filter(v => v.IsLatest); + assertVersionHasNotBeenUpdated(currentVersion, expectedVersionId); + + const [nonCurrentVersion] = Versions.filter(v => !v.IsLatest); + assertVersionIsNullAndUpdated(nonCurrentVersion); + return done(); + }); + }); + + it('should update non-current null version if versioning suspended', done => { + let expectedVersionId; + let objMD; + return async.series([ + next => s3.putObject({ Bucket: bucket, Key: keyName, Body: new Buffer(testData) }, next), + next => s3.putBucketVersioning({ Bucket: bucket, VersioningConfiguration: { Status: 'Enabled' } }, + next), + next => s3.putObject({ Bucket: bucket, Key: keyName, Body: new Buffer(testData) }, (err, data) => { + if (err) { + return next(err); + } + expectedVersionId = data.VersionId; + return next(); + }), + next => s3.putBucketVersioning({ Bucket: bucket, VersioningConfiguration: { Status: 'Suspended' } }, + next), + next => makeBackbeatRequest({ + method: 'GET', + resourceType: 'metadata', + bucket, + objectKey: keyName, + queryObj: { + versionId: 'null', + }, + authCredentials: backbeatAuthCredentials, + }, (err, data) => { + if (err) { + return next(err); + } + const { error, result } = updateStorageClass(data, storageClass); + if (error) { + return next(error); + } + objMD = result; + return next(); + }), + next => makeBackbeatRequest({ + method: 'PUT', + resourceType: 'metadata', + bucket, + objectKey: keyName, + queryObj: { + versionId: 'null', + }, + authCredentials: backbeatAuthCredentials, + requestBody: objMD.getSerialized(), + }, next), + next => s3.headObject({ Bucket: bucket, Key: keyName, VersionId: 'null' }, next), + next => s3.listObjectVersions({ Bucket: bucket }, 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 deleteMarkers = listObjectVersionsRes.DeleteMarkers; + assert.strictEqual(deleteMarkers.length, 0); + const { Versions } = listObjectVersionsRes; + assert.strictEqual(Versions.length, 2); + + const [currentVersion] = Versions.filter(v => v.IsLatest); + assertVersionHasNotBeenUpdated(currentVersion, expectedVersionId); + + const [nonCurrentVersion] = Versions.filter(v => !v.IsLatest); + assertVersionIsNullAndUpdated(nonCurrentVersion); + + return done(); + }); + }); + + it('should update current null version if versioning suspended', done => { + let objMD; + let expectedVersionId; + return async.series([ + next => s3.putObject({ Bucket: bucket, Key: keyName, Body: new Buffer(testData) }, next), + next => s3.putBucketVersioning({ Bucket: bucket, VersioningConfiguration: { Status: 'Enabled' } }, + next), + next => s3.putObject({ Bucket: bucket, Key: keyName, Body: new Buffer(testData) }, (err, data) => { + if (err) { + return next(err); + } + expectedVersionId = data.VersionId; + return next(); + }), + next => s3.putBucketVersioning({ Bucket: bucket, VersioningConfiguration: { Status: 'Suspended' } }, + next), + next => s3.deleteObject({ Bucket: bucket, Key: keyName, VersionId: expectedVersionId }, next), + next => makeBackbeatRequest({ + method: 'GET', + resourceType: 'metadata', + bucket, + objectKey: keyName, + queryObj: { + versionId: 'null', + }, + authCredentials: backbeatAuthCredentials, + }, (err, data) => { + if (err) { + return next(err); + } + const { error, result } = updateStorageClass(data, storageClass); + if (error) { + return next(error); + } + objMD = result; + return next(); + }), + next => makeBackbeatRequest({ + method: 'PUT', + resourceType: 'metadata', + bucket, + objectKey: keyName, + queryObj: { + versionId: 'null', + }, + authCredentials: backbeatAuthCredentials, + requestBody: objMD.getSerialized(), + }, next), + next => s3.headObject({ Bucket: bucket, Key: keyName, VersionId: 'null' }, next), + next => s3.listObjectVersions({ Bucket: bucket }, next), + ], (err, data) => { + if (err) { + return done(err); + } + + const headObjectRes = data[7]; + assert.strictEqual(headObjectRes.VersionId, 'null'); + assert.strictEqual(headObjectRes.StorageClass, storageClass); + + const listObjectVersionsRes = data[8]; + const { DeleteMarkers, Versions } = listObjectVersionsRes; + assert.strictEqual(Versions.length, 1); + assert.strictEqual(DeleteMarkers.length, 0); + + const currentVersion = Versions[0]; + assert(currentVersion.IsLatest); + assertVersionIsNullAndUpdated(currentVersion); + return done(); + }); + }); + + it('should update current null version if versioning suspended and put a null version afterwards', done => { + let objMD; + let deletedVersionId; + return async.series([ + next => s3.putObject({ Bucket: bucket, Key: keyName, Body: new Buffer(testData) }, next), + next => s3.putBucketVersioning({ Bucket: bucket, VersioningConfiguration: { Status: 'Enabled' } }, + next), + next => s3.putObject({ Bucket: bucket, Key: keyName, Body: new Buffer(testData) }, (err, data) => { + if (err) { + return next(err); + } + deletedVersionId = data.VersionId; + return next(); + }), + next => s3.putBucketVersioning({ Bucket: bucket, VersioningConfiguration: { Status: 'Suspended' } }, + next), + next => s3.deleteObject({ Bucket: bucket, Key: keyName, VersionId: deletedVersionId }, next), + next => makeBackbeatRequest({ + method: 'GET', + resourceType: 'metadata', + bucket, + objectKey: keyName, + queryObj: { + versionId: 'null', + }, + authCredentials: backbeatAuthCredentials, + }, (err, data) => { + if (err) { + return next(err); + } + const { error, result } = updateStorageClass(data, storageClass); + if (error) { + return next(error); + } + objMD = result; + return next(); + }), + next => makeBackbeatRequest({ + method: 'PUT', + resourceType: 'metadata', + bucket, + objectKey: keyName, + queryObj: { + versionId: 'null', + }, + authCredentials: backbeatAuthCredentials, + requestBody: objMD.getSerialized(), + }, next), + next => s3.putObject({ Bucket: bucket, Key: keyName, Body: new Buffer(testData) }, next), + next => s3.headObject({ Bucket: bucket, Key: keyName, VersionId: 'null' }, next), + next => s3.listObjectVersions({ Bucket: bucket }, next), + ], (err, data) => { + if (err) { + return done(err); + } + + const headObjectRes = data[8]; + assert.strictEqual(headObjectRes.VersionId, 'null'); + assert(!headObjectRes.StorageClass); + + const listObjectVersionsRes = data[9]; + const { DeleteMarkers, Versions } = listObjectVersionsRes; + assert.strictEqual(DeleteMarkers.length, 0); + assert.strictEqual(Versions.length, 1); + + const currentVersion = Versions[0]; + assert(currentVersion.IsLatest); + assertVersionHasNotBeenUpdated(currentVersion, 'null'); + + return done(); + }); + }); + + it('should update current null version if versioning suspended and put a version afterwards', done => { + let objMD; + let deletedVersionId; + let expectedVersionId; + return async.series([ + next => s3.putObject({ Bucket: bucket, Key: keyName, Body: new Buffer(testData) }, next), + next => s3.putBucketVersioning({ Bucket: bucket, VersioningConfiguration: { Status: 'Enabled' } }, + next), + next => s3.putObject({ Bucket: bucket, Key: keyName, Body: new Buffer(testData) }, (err, data) => { + if (err) { + return next(err); + } + deletedVersionId = data.VersionId; + return next(); + }), + next => s3.putBucketVersioning({ Bucket: bucket, VersioningConfiguration: { Status: 'Suspended' } }, + next), + next => s3.deleteObject({ Bucket: bucket, Key: keyName, VersionId: deletedVersionId }, next), + next => makeBackbeatRequest({ + method: 'GET', + resourceType: 'metadata', + bucket, + objectKey: keyName, + queryObj: { + versionId: 'null', + }, + authCredentials: backbeatAuthCredentials, + }, (err, data) => { + if (err) { + return next(err); + } + const { error, result } = updateStorageClass(data, storageClass); + if (error) { + return next(error); + } + objMD = result; + return next(); + }), + next => makeBackbeatRequest({ + method: 'PUT', + resourceType: 'metadata', + bucket, + objectKey: keyName, + queryObj: { + versionId: 'null', + }, + authCredentials: backbeatAuthCredentials, + requestBody: objMD.getSerialized(), + }, next), + next => s3.putBucketVersioning({ Bucket: bucket, VersioningConfiguration: { Status: 'Enabled' } }, + next), + next => s3.putObject({ Bucket: bucket, Key: keyName, Body: new Buffer(testData) }, (err, data) => { + if (err) { + return next(err); + } + expectedVersionId = data.VersionId; + return next(); + }), + next => s3.headObject({ Bucket: bucket, Key: keyName, VersionId: 'null' }, next), + next => s3.listObjectVersions({ Bucket: bucket }, next), + ], (err, data) => { + if (err) { + return done(err); + } + + const headObjectRes = data[9]; + assert.strictEqual(headObjectRes.VersionId, 'null'); + assert.strictEqual(headObjectRes.StorageClass, storageClass); + + const listObjectVersionsRes = data[10]; + const { DeleteMarkers, Versions } = listObjectVersionsRes; + assert.strictEqual(DeleteMarkers.length, 0); + assert.strictEqual(Versions.length, 2); + + const [currentVersion] = Versions.filter(v => v.IsLatest); + assertVersionHasNotBeenUpdated(currentVersion, expectedVersionId); + + const [nonCurrentVersion] = Versions.filter(v => !v.IsLatest); + assertVersionIsNullAndUpdated(nonCurrentVersion); + + return done(); + }); + }); + }); + + // TODO: CLDSRV-394 unskip routeBackbeat tests + describe.skip('backbeat PUT routes', () => { describe('PUT data + metadata should create a new complete object', () => { [{ @@ -902,7 +1901,7 @@ describe.skip('backbeat routes', () => { }); }); }); - describe('backbeat authorization checks', () => { + describe.skip('backbeat authorization checks', () => { [{ method: 'PUT', resourceType: 'metadata' }, { method: 'PUT', resourceType: 'data' }].forEach(test => { const queryObj = test.resourceType === 'data' ? { v2: '' } : {}; @@ -1020,7 +2019,7 @@ describe.skip('backbeat routes', () => { }); }); - describe('GET Metadata route', () => { + describe.skip('GET Metadata route', () => { beforeEach(done => makeBackbeatRequest({ method: 'PUT', bucket: TEST_BUCKET, objectKey: TEST_KEY, @@ -1078,7 +2077,7 @@ describe.skip('backbeat routes', () => { }); }); }); - describe('backbeat multipart upload operations', function test() { + describe.skip('backbeat multipart upload operations', function test() { this.timeout(10000); // The ceph image does not support putting tags during initiate MPU. @@ -1239,7 +2238,7 @@ describe.skip('backbeat routes', () => { ], done); }); }); - describe('Batch Delete Route', function test() { + describe.skip('Batch Delete Route', function test() { this.timeout(30000); it('should batch delete a local location', done => { let versionId;