Skip to content

Commit 779c0bf

Browse files
committed
add support for reserved bucket tags
Signed-off-by: Utkarsh Srivastava <srivastavautkarsh8097@gmail.com> fix put_bucket_tagging test - pass dummy objectsdk Signed-off-by: Utkarsh Srivastava <srivastavautkarsh8097@gmail.com>
1 parent 138b236 commit 779c0bf

File tree

8 files changed

+151
-27
lines changed

8 files changed

+151
-27
lines changed

config.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -956,6 +956,31 @@ config.NSFS_GLACIER_FORCE_EXPIRE_ON_GET = false;
956956
// interval
957957
config.NSFS_GLACIER_MIGRATE_LOG_THRESHOLD = 50 * 1024;
958958

959+
/**
960+
* NSFS_GLACIER_RESERVED_BUCKET_TAGS defines an object of bucket tags which will be reserved
961+
* by the system and PUT operations for them via S3 API would be disallowed even by the bucket
962+
* owners
963+
*
964+
* @type {Record<string, {
965+
* schema: Record<any, any> & { $id: string },
966+
* immutable: true | false | 'if-data',
967+
* default: any
968+
* }>}
969+
*
970+
* @example
971+
* {
972+
'deep-archive-copies': {
973+
schema: {
974+
$id: 'deep-archive-copies-schema-v0',
975+
enum: ['1', '2']
976+
}, // JSON Schema
977+
immutable: 'if-data',
978+
default: '1',
979+
}
980+
* }
981+
*/
982+
config.NSFS_GLACIER_RESERVED_BUCKET_TAGS = {};
983+
959984
// anonymous account name
960985
config.ANONYMOUS_ACCOUNT_NAME = 'anonymous';
961986

src/cmd/manage_nsfs.js

Lines changed: 37 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ const { throw_cli_error, get_bucket_owner_account_by_name,
3939
const manage_nsfs_validations = require('../manage_nsfs/manage_nsfs_validations');
4040
const nc_mkm = require('../manage_nsfs/nc_master_key_manager').get_instance();
4141
const notifications_util = require('../util/notifications_util');
42+
const BucketSpaceFS = require('../sdk/bucketspace_fs');
4243

4344
///////////////
4445
//// GENERAL //
@@ -135,7 +136,6 @@ async function fetch_bucket_data(action, user_input) {
135136
force_md5_etag: user_input.force_md5_etag === undefined || user_input.force_md5_etag === '' ? user_input.force_md5_etag : get_boolean_or_string_value(user_input.force_md5_etag),
136137
notifications: user_input.notifications
137138
};
138-
139139
if (user_input.bucket_policy !== undefined) {
140140
if (typeof user_input.bucket_policy === 'string') {
141141
// bucket_policy deletion specified with empty string ''
@@ -154,6 +154,15 @@ async function fetch_bucket_data(action, user_input) {
154154
data = await merge_new_and_existing_config_data(data);
155155
}
156156

157+
if ((action === ACTIONS.UPDATE && user_input.tag) || (action === ACTIONS.ADD)) {
158+
const tags = JSON.parse(user_input.tag || '[]');
159+
data.tag = BucketSpaceFS._merge_reserved_tags(
160+
data.tag || BucketSpaceFS._default_bucket_tags(),
161+
tags,
162+
action === ACTIONS.ADD ? true : await _is_bucket_empty(data),
163+
);
164+
}
165+
157166
//if we're updating the owner, needs to override owner in file with the owner from user input.
158167
//if we're adding a bucket, need to set its owner id field
159168
if ((action === ACTIONS.UPDATE && user_input.owner) || (action === ACTIONS.ADD)) {
@@ -256,25 +265,14 @@ async function update_bucket(data) {
256265
*/
257266
async function delete_bucket(data, force) {
258267
try {
259-
const temp_dir_name = native_fs_utils.get_bucket_tmpdir_name(data._id);
268+
const bucket_empty = await _is_bucket_empty(data);
269+
if (!bucket_empty && !force) {
270+
throw_cli_error(ManageCLIError.BucketDeleteForbiddenHasObjects, data.name);
271+
}
272+
260273
const bucket_temp_dir_path = native_fs_utils.get_bucket_tmpdir_full_path(data.path, data._id);
261-
// fs_contexts for bucket temp dir (storage path)
262274
const fs_context_fs_backend = native_fs_utils.get_process_fs_context(data.fs_backend);
263-
let entries;
264-
try {
265-
entries = await nb_native().fs.readdir(fs_context_fs_backend, data.path);
266-
} catch (err) {
267-
dbg.warn(`delete_bucket: bucket name ${data.name},` +
268-
`got an error on readdir with path: ${data.path}`, err);
269-
// if the bucket's path was deleted first (encounter ENOENT error) - continue deletion
270-
if (err.code !== 'ENOENT') throw err;
271-
}
272-
if (entries) {
273-
const object_entries = entries.filter(element => !element.name.endsWith(temp_dir_name));
274-
if (object_entries.length > 0 && !force) {
275-
throw_cli_error(ManageCLIError.BucketDeleteForbiddenHasObjects, data.name);
276-
}
277-
}
275+
278276
await native_fs_utils.folder_delete(bucket_temp_dir_path, fs_context_fs_backend, true);
279277
await config_fs.delete_bucket_config_file(data.name);
280278
return { code: ManageCLIResponse.BucketDeleted, detail: { name: data.name }, event_arg: { bucket: data.name } };
@@ -340,6 +338,27 @@ async function list_bucket_config_files(wide, filters = {}) {
340338
return config_files_list;
341339
}
342340

341+
async function _is_bucket_empty(data) {
342+
const temp_dir_name = native_fs_utils.get_bucket_tmpdir_name(data._id);
343+
// fs_contexts for bucket temp dir (storage path)
344+
const fs_context_fs_backend = native_fs_utils.get_process_fs_context(data.fs_backend);
345+
let entries;
346+
try {
347+
entries = await nb_native().fs.readdir(fs_context_fs_backend, data.path);
348+
} catch (err) {
349+
dbg.warn(`_is_bucket_empty: bucket name ${data.name},` +
350+
`got an error on readdir with path: ${data.path}`, err);
351+
// if the bucket's path was deleted first (encounter ENOENT error) - continue deletion
352+
if (err.code !== 'ENOENT') throw err;
353+
}
354+
if (entries) {
355+
const object_entries = entries.filter(element => !element.name.endsWith(temp_dir_name));
356+
return object_entries.length === 0;
357+
}
358+
359+
return true;
360+
}
361+
343362
/**
344363
* bucket_management does the following -
345364
* 1. fetches the bucket data if this is not a list operation

src/manage_nsfs/manage_nsfs_constants.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ const VALID_OPTIONS_ANONYMOUS_ACCOUNT = {
6262

6363
const VALID_OPTIONS_BUCKET = {
6464
'add': new Set(['name', 'owner', 'path', 'bucket_policy', 'fs_backend', 'force_md5_etag', 'notifications', FROM_FILE, ...CLI_MUTUAL_OPTIONS]),
65-
'update': new Set(['name', 'owner', 'path', 'bucket_policy', 'fs_backend', 'new_name', 'force_md5_etag', 'notifications', ...CLI_MUTUAL_OPTIONS]),
65+
'update': new Set(['name', 'owner', 'path', 'bucket_policy', 'fs_backend', 'new_name', 'force_md5_etag', 'notifications', 'tag', ...CLI_MUTUAL_OPTIONS]),
6666
'delete': new Set(['name', 'force', ...CLI_MUTUAL_OPTIONS]),
6767
'list': new Set(['wide', 'name', ...CLI_MUTUAL_OPTIONS]),
6868
'status': new Set(['name', ...CLI_MUTUAL_OPTIONS]),
@@ -171,6 +171,8 @@ const OPTION_TYPE = {
171171
key: 'string',
172172
value: 'string',
173173
remove_key: 'boolean',
174+
// bucket tagging
175+
tag: 'string'
174176
};
175177

176178
const BOOLEAN_STRING_VALUES = ['true', 'false'];

src/sdk/bucketspace_fs.js

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
const _ = require('lodash');
55
const util = require('util');
66
const path = require('path');
7+
const { default: Ajv } = require('ajv');
78
const P = require('../util/promise');
89
const config = require('../../config');
910
const RpcError = require('../rpc/rpc_error');
@@ -35,6 +36,7 @@ const NoobaaEvent = require('../manage_nsfs/manage_nsfs_events_utils').NoobaaEve
3536
const dbg = require('../util/debug_module')(__filename);
3637
const bucket_semaphore = new KeysSemaphore(1);
3738

39+
const ajv = new Ajv();
3840

3941
class BucketSpaceFS extends BucketSpaceSimpleFS {
4042
constructor({ config_root }, stats) {
@@ -339,7 +341,7 @@ class BucketSpaceFS extends BucketSpaceSimpleFS {
339341
return {
340342
_id: mongo_utils.mongoObjectId(),
341343
name,
342-
tag: js_utils.default_value(tag, undefined),
344+
tag: js_utils.default_value(tag, BucketSpaceFS._default_bucket_tags()),
343345
owner_account: account.owner ? account.owner : account._id, // The account is the owner of the buckets that were created by it or by its users.
344346
creator: account._id,
345347
versioning: config.NSFS_VERSIONING_ENABLED && lock_enabled ? 'ENABLED' : 'DISABLED',
@@ -485,12 +487,21 @@ class BucketSpaceFS extends BucketSpaceSimpleFS {
485487
// BUCKET TAGGING //
486488
////////////////////
487489

488-
async put_bucket_tagging(params) {
490+
/**
491+
* @param {*} params
492+
* @param {nb.ObjectSDK} object_sdk
493+
*/
494+
async put_bucket_tagging(params, object_sdk) {
489495
try {
490496
const { name, tagging } = params;
491497
dbg.log0('BucketSpaceFS.put_bucket_tagging: Bucket name, tagging', name, tagging);
492498
const bucket = await this.config_fs.get_bucket_by_name(name);
493-
bucket.tag = tagging;
499+
const { ns } = await object_sdk.read_bucket_full_info(name);
500+
const is_bucket_empty = await BucketSpaceFS.#_is_bucket_empty(name, null, ns, object_sdk);
501+
502+
bucket.tag = BucketSpaceFS._merge_reserved_tags(
503+
bucket.tag || BucketSpaceFS._default_bucket_tags(), tagging, is_bucket_empty
504+
);
494505
await this.config_fs.update_bucket_config_file(bucket);
495506
} catch (error) {
496507
throw translate_error_codes(error, entity_enum.BUCKET);
@@ -502,7 +513,16 @@ class BucketSpaceFS extends BucketSpaceSimpleFS {
502513
const { name } = params;
503514
dbg.log0('BucketSpaceFS.delete_bucket_tagging: Bucket name', name);
504515
const bucket = await this.config_fs.get_bucket_by_name(name);
505-
delete bucket.tag;
516+
517+
const preserved_tags = [];
518+
for (const tag of bucket.tag) {
519+
const tag_info = config.NSFS_GLACIER_RESERVED_BUCKET_TAGS[tag.key];
520+
if (tag_info?.immutable) {
521+
preserved_tags.push(tag);
522+
}
523+
}
524+
525+
bucket.tag = preserved_tags;
506526
await this.config_fs.update_bucket_config_file(bucket);
507527
} catch (error) {
508528
throw translate_error_codes(error, entity_enum.BUCKET);
@@ -520,6 +540,40 @@ class BucketSpaceFS extends BucketSpaceSimpleFS {
520540
}
521541
}
522542

543+
static _default_bucket_tags() {
544+
return Object.keys(
545+
config.NSFS_GLACIER_RESERVED_BUCKET_TAGS
546+
).map(key => ({ key, value: config.NSFS_GLACIER_RESERVED_BUCKET_TAGS[key].default }));
547+
}
548+
549+
static _merge_reserved_tags(original_tags, new_tags, is_bucket_empty) {
550+
const merged_tags = original_tags.reduce((curr, tag) => {
551+
if (!config.NSFS_GLACIER_RESERVED_BUCKET_TAGS[tag.key]) return curr;
552+
return Object.assign(curr, { [tag.key]: tag.value });
553+
}, {});
554+
555+
for (const tag of new_tags) {
556+
const tag_info = config.NSFS_GLACIER_RESERVED_BUCKET_TAGS[tag.key];
557+
if (!tag_info) {
558+
merged_tags[tag.key] = tag.value;
559+
continue;
560+
}
561+
if (!tag_info.immutable || (tag_info.immutable === 'if-data' && is_bucket_empty)) {
562+
let validator = ajv.getSchema(tag_info.schema.$id);
563+
if (!validator) {
564+
ajv.addSchema(tag_info.schema);
565+
validator = ajv.getSchema(tag_info.schema.$id);
566+
}
567+
568+
if (validator(tag.value)) {
569+
merged_tags[tag.key] = tag.value;
570+
}
571+
}
572+
}
573+
574+
return Object.keys(merged_tags).map(key => ({ key, value: merged_tags[key] }));
575+
}
576+
523577
////////////////////
524578
// BUCKET LOGGING //
525579
////////////////////
@@ -933,6 +987,25 @@ class BucketSpaceFS extends BucketSpaceSimpleFS {
933987

934988
return storage_classes;
935989
}
990+
991+
static async #_is_bucket_empty(name, params, ns, object_sdk) {
992+
params = params || {};
993+
994+
let list;
995+
try {
996+
if (ns._is_versioning_disabled()) {
997+
list = await ns.list_objects({ ...params, bucket: name, limit: 1 }, object_sdk);
998+
} else {
999+
list = await ns.list_object_versions({ ...params, bucket: name, limit: 1 }, object_sdk);
1000+
}
1001+
} catch (err) {
1002+
dbg.warn('#_is_bucket_empty: bucket name', name, 'got an error while trying to list_objects', err);
1003+
// in case the ULS was deleted - we will continue
1004+
if (err.rpc_code !== 'NO_SUCH_BUCKET') throw err;
1005+
}
1006+
1007+
return !(list && list.objects && list.objects.length > 0);
1008+
}
9361009
}
9371010

9381011
module.exports = BucketSpaceFS;

src/sdk/bucketspace_simple_fs.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,12 @@ class BucketSpaceSimpleFS {
168168
// BUCKET TAGGING //
169169
////////////////////
170170

171-
async put_bucket_tagging(params) {
171+
/**
172+
*
173+
* @param {*} params
174+
* @param {nb.ObjectSDK} object_sdk
175+
*/
176+
async put_bucket_tagging(params, object_sdk) {
172177
// TODO
173178
}
174179

src/sdk/nb.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -846,7 +846,7 @@ interface BucketSpace {
846846

847847
set_bucket_versioning(params: object, object_sdk: ObjectSDK): Promise<any>;
848848

849-
put_bucket_tagging(params: object): Promise<any>;
849+
put_bucket_tagging(params: object, object_sdk: ObjectSDK): Promise<any>;
850850
delete_bucket_tagging(params: object): Promise<any>;
851851
get_bucket_tagging(params: object): Promise<any>;
852852

src/sdk/object_sdk.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -986,7 +986,7 @@ class ObjectSDK {
986986

987987
async put_bucket_tagging(params) {
988988
const bs = this._get_bucketspace();
989-
return bs.put_bucket_tagging(params);
989+
return bs.put_bucket_tagging(params, this);
990990
}
991991

992992
async delete_bucket_tagging(params) {

src/test/unit_tests/test_bucketspace_fs.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -930,7 +930,7 @@ mocha.describe('bucketspace_fs', function() {
930930
mocha.describe('bucket tagging operations', function() {
931931
mocha.it('put_bucket_tagging', async function() {
932932
const param = { name: test_bucket, tagging: [{ key: 'k1', value: 'v1' }] };
933-
await bucketspace_fs.put_bucket_tagging(param);
933+
await bucketspace_fs.put_bucket_tagging(param, dummy_object_sdk);
934934
const tag = await bucketspace_fs.get_bucket_tagging(param);
935935
assert.deepEqual(tag, { tagging: param.tagging });
936936
});

0 commit comments

Comments
 (0)