Skip to content

Commit ee45f39

Browse files
tangledbytesromayalon
authored andcommitted
add support for reserved bucket tags
Signed-off-by: Utkarsh Srivastava <[email protected]> fix put_bucket_tagging test - pass dummy objectsdk Signed-off-by: Utkarsh Srivastava <[email protected]> add support for events, race safe tag manipulation and --merge_tag flag Signed-off-by: Utkarsh Srivastava <[email protected]> fix lint issues Signed-off-by: Utkarsh Srivastava <[email protected]> clarify the reserved tags config comment Signed-off-by: Utkarsh Srivastava <[email protected]> add docs for the CLI changes Signed-off-by: Utkarsh Srivastava <[email protected]> address PR comments Signed-off-by: Utkarsh Srivastava <[email protected]> add account name to the bucket create event Signed-off-by: Utkarsh Srivastava <[email protected]> address PR comments Signed-off-by: Utkarsh Srivastava <[email protected]> (cherry picked from commit f400465)
1 parent 7fe7ecb commit ee45f39

File tree

11 files changed

+317
-34
lines changed

11 files changed

+317
-34
lines changed

config.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -951,6 +951,33 @@ config.NSFS_GLACIER_FORCE_EXPIRE_ON_GET = false;
951951
// interval
952952
config.NSFS_GLACIER_MIGRATE_LOG_THRESHOLD = 50 * 1024;
953953

954+
/**
955+
* NSFS_GLACIER_RESERVED_BUCKET_TAGS defines an object of bucket tags which will be reserved
956+
* by the system and PUT operations for them via S3 API would be limited - as in they would be
957+
* mutable only if specified and only under certain conditions.
958+
*
959+
* @type {Record<string, {
960+
* schema: Record<any, any> & { $id: string },
961+
* immutable: true | false | 'if-data',
962+
* default: any,
963+
* event: boolean
964+
* }>}
965+
*
966+
* @example
967+
* {
968+
'deep-archive-copies': {
969+
schema: {
970+
$id: 'deep-archive-copies-schema-v0',
971+
enum: ['1', '2']
972+
}, // JSON Schema
973+
immutable: 'if-data',
974+
default: '1',
975+
event: true
976+
}
977+
* }
978+
*/
979+
config.NSFS_GLACIER_RESERVED_BUCKET_TAGS = {};
980+
954981
// anonymous account name
955982
config.ANONYMOUS_ACCOUNT_NAME = 'anonymous';
956983

docs/NooBaaNonContainerized/Events.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,10 @@ The following list includes events that indicate on a normal / successful operat
3232
- Description: NooBaa account was deleted successfully using NooBaa CLI.
3333

3434
#### 4. `noobaa_bucket_created`
35-
- Arguments: `bucket_name`
35+
- Arguments:
36+
- `bucket_name`
37+
- `account_name`
38+
- `<tag_value>` (if `event` is `true` for the reserved tag)
3639
- Description: NooBaa bucket was created successfully using NooBaa CLI or S3.
3740

3841
#### 5. `noobaa_bucket_deleted`
@@ -43,6 +46,11 @@ The following list includes events that indicate on a normal / successful operat
4346
- Arguments: `whitelist_ips`
4447
- Description: Whitelist Server IPs updated successfully using NooBaa CLI.
4548

49+
#### 7. `noobaa_bucket_reserved_tag_modified`
50+
- Arguments:
51+
- `bucket_name`
52+
- `<tag_value>` (if `event` is `true` for the reserved tag)
53+
- Description: NooBaa bucket reserved tag was modified successfully using NooBaa CLI or S3.
4654

4755
### Error Indicating Events
4856

@@ -219,4 +227,4 @@ The following list includes events that indicate on some sort of malfunction or
219227
- Reasons:
220228
- Free space in notification log dir FS is below threshold.
221229
- Resolutions:
222-
- Free up space is FS.
230+
- Free up space is FS.

docs/NooBaaNonContainerized/NooBaaCLI.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,13 @@ noobaa-cli bucket update --name <bucket_name> [--new_name] [--owner]
376376
- Type: Boolean
377377
- Description: Set the bucket to force md5 ETag calculation.
378378

379+
- `tag`
380+
- Type: String
381+
- Description: Set the bucket tags, type is a string of valid JSON. Behaviour is similar to `put-bucket-tagging` S3 API.
382+
383+
- `merge_tag`
384+
- Type: String
385+
- Description: Merge the bucket tags with previous bucket tags, type is a string of valid JSON.
379386

380387
### Bucket Status
381388

src/cmd/manage_nsfs.js

Lines changed: 82 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ const { throw_cli_error, get_bucket_owner_account_by_name,
4040
const manage_nsfs_validations = require('../manage_nsfs/manage_nsfs_validations');
4141
const nc_mkm = require('../manage_nsfs/nc_master_key_manager').get_instance();
4242
const notifications_util = require('../util/notifications_util');
43+
const BucketSpaceFS = require('../sdk/bucketspace_fs');
44+
const NoobaaEvent = require('../manage_nsfs/manage_nsfs_events_utils').NoobaaEvent;
4345

4446
let config_fs;
4547

@@ -123,7 +125,6 @@ async function fetch_bucket_data(action, user_input) {
123125
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),
124126
notifications: user_input.notifications
125127
};
126-
127128
if (user_input.bucket_policy !== undefined) {
128129
if (typeof user_input.bucket_policy === 'string') {
129130
// bucket_policy deletion specified with empty string ''
@@ -142,6 +143,27 @@ async function fetch_bucket_data(action, user_input) {
142143
data = await merge_new_and_existing_config_data(data);
143144
}
144145

146+
if ((action === ACTIONS.UPDATE && user_input.tag) || (action === ACTIONS.ADD)) {
147+
const tags = JSON.parse(user_input.tag || '[]');
148+
data.tag = BucketSpaceFS._merge_reserved_tags(
149+
data.tag || BucketSpaceFS._default_bucket_tags(),
150+
tags,
151+
action === ACTIONS.ADD ? true : await _is_bucket_empty(data),
152+
);
153+
}
154+
155+
if ((action === ACTIONS.UPDATE && user_input.merge_tag) || (action === ACTIONS.ADD)) {
156+
const merge_tags = JSON.parse(user_input.merge_tag || '[]');
157+
data.tag = _.merge(
158+
data.tag,
159+
BucketSpaceFS._merge_reserved_tags(
160+
data.tag || BucketSpaceFS._default_bucket_tags(),
161+
merge_tags,
162+
action === ACTIONS.ADD ? true : await _is_bucket_empty(data),
163+
)
164+
);
165+
}
166+
145167
//if we're updating the owner, needs to override owner in file with the owner from user input.
146168
//if we're adding a bucket, need to set its owner id field
147169
if ((action === ACTIONS.UPDATE && user_input.owner) || (action === ACTIONS.ADD)) {
@@ -189,7 +211,14 @@ async function add_bucket(data) {
189211
data._id = mongo_utils.mongoObjectId();
190212
const parsed_bucket_data = await config_fs.create_bucket_config_file(data);
191213
await set_bucker_owner(parsed_bucket_data);
192-
return { code: ManageCLIResponse.BucketCreated, detail: parsed_bucket_data, event_arg: { bucket: data.name }};
214+
215+
const [reserved_tag_event_args] = BucketSpaceFS._generate_reserved_tag_event_args({}, data.tag);
216+
217+
return {
218+
code: ManageCLIResponse.BucketCreated,
219+
detail: parsed_bucket_data,
220+
event_arg: { ...(reserved_tag_event_args || {}), bucket: data.name, account: parsed_bucket_data.bucket_owner },
221+
};
193222
}
194223

195224
/**
@@ -245,25 +274,14 @@ async function update_bucket(data) {
245274
*/
246275
async function delete_bucket(data, force) {
247276
try {
248-
const temp_dir_name = native_fs_utils.get_bucket_tmpdir_name(data._id);
277+
const bucket_empty = await _is_bucket_empty(data);
278+
if (!bucket_empty && !force) {
279+
throw_cli_error(ManageCLIError.BucketDeleteForbiddenHasObjects, data.name);
280+
}
281+
249282
const bucket_temp_dir_path = native_fs_utils.get_bucket_tmpdir_full_path(data.path, data._id);
250-
// fs_contexts for bucket temp dir (storage path)
251283
const fs_context_fs_backend = native_fs_utils.get_process_fs_context(data.fs_backend);
252-
let entries;
253-
try {
254-
entries = await nb_native().fs.readdir(fs_context_fs_backend, data.path);
255-
} catch (err) {
256-
dbg.warn(`delete_bucket: bucket name ${data.name},` +
257-
`got an error on readdir with path: ${data.path}`, err);
258-
// if the bucket's path was deleted first (encounter ENOENT error) - continue deletion
259-
if (err.code !== 'ENOENT') throw err;
260-
}
261-
if (entries) {
262-
const object_entries = entries.filter(element => !element.name.endsWith(temp_dir_name));
263-
if (object_entries.length > 0 && !force) {
264-
throw_cli_error(ManageCLIError.BucketDeleteForbiddenHasObjects, data.name);
265-
}
266-
}
284+
267285
await native_fs_utils.folder_delete(bucket_temp_dir_path, fs_context_fs_backend, true);
268286
await config_fs.delete_bucket_config_file(data.name);
269287
return { code: ManageCLIResponse.BucketDeleted, detail: { name: data.name }, event_arg: { bucket: data.name } };
@@ -273,6 +291,33 @@ async function delete_bucket(data, force) {
273291
}
274292
}
275293

294+
/**
295+
* _is_bucket_empty returns true if the given bucket is empty
296+
*
297+
* @param {*} data
298+
* @returns {Promise<boolean>}
299+
*/
300+
async function _is_bucket_empty(data) {
301+
const temp_dir_name = native_fs_utils.get_bucket_tmpdir_name(data._id);
302+
// fs_contexts for bucket temp dir (storage path)
303+
const fs_context_fs_backend = native_fs_utils.get_process_fs_context(data.fs_backend);
304+
let entries;
305+
try {
306+
entries = await nb_native().fs.readdir(fs_context_fs_backend, data.path);
307+
} catch (err) {
308+
dbg.warn(`_is_bucket_empty: bucket name ${data.name},` +
309+
`got an error on readdir with path: ${data.path}`, err);
310+
// if the bucket's path was deleted first (encounter ENOENT error) - continue deletion
311+
if (err.code !== 'ENOENT') throw err;
312+
}
313+
if (entries) {
314+
const object_entries = entries.filter(element => !element.name.endsWith(temp_dir_name));
315+
return object_entries.length === 0;
316+
}
317+
318+
return true;
319+
}
320+
276321
/**
277322
* bucket_management does the following -
278323
* 1. fetches the bucket data if this is not a list operation
@@ -294,7 +339,24 @@ async function bucket_management(action, user_input) {
294339
} else if (action === ACTIONS.STATUS) {
295340
response = await get_bucket_status(data);
296341
} else if (action === ACTIONS.UPDATE) {
297-
response = await update_bucket(data);
342+
const bucket_path = config_fs.get_bucket_path_by_name(user_input.name);
343+
const bucket_lock_file = `${bucket_path}.lock`;
344+
await native_fs_utils.lock_and_run(config_fs.fs_context, bucket_lock_file, async () => {
345+
const prev_bucket_info = await fetch_bucket_data(action, _.omit(user_input, ['tag', 'merge_tag']));
346+
const bucket_info = await fetch_bucket_data(action, user_input);
347+
348+
const tagging_object = BucketSpaceFS._objectify_tagging_arr(prev_bucket_info.tag);
349+
const [
350+
reserved_tag_event_args,
351+
reserved_tag_modified,
352+
] = BucketSpaceFS._generate_reserved_tag_event_args(tagging_object, bucket_info.tag);
353+
354+
response = await update_bucket(bucket_info);
355+
if (reserved_tag_modified) {
356+
new NoobaaEvent(NoobaaEvent.BUCKET_RESERVED_TAG_MODIFIED)
357+
.create_event(undefined, { ...reserved_tag_event_args, bucket_name: user_input.name });
358+
}
359+
});
298360
} else if (action === ACTIONS.DELETE) {
299361
const force = get_boolean_or_string_value(user_input.force);
300362
response = await delete_bucket(data, force);

src/manage_nsfs/manage_nsfs_constants.js

Lines changed: 4 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', 'merge_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,9 @@ const OPTION_TYPE = {
171171
key: 'string',
172172
value: 'string',
173173
remove_key: 'boolean',
174+
// bucket tagging
175+
tag: 'string',
176+
merge_tag: 'string',
174177
};
175178

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

src/manage_nsfs/manage_nsfs_events_utils.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,16 @@ NoobaaEvent.UNAUTHORIZED = Object.freeze({
302302
severity: 'ERROR',
303303
state: 'HEALTHY',
304304
});
305+
NoobaaEvent.BUCKET_RESERVED_TAG_MODIFIED = Object.freeze({
306+
event_code: 'noobaa_bucket_reserved_tag_modified',
307+
message: 'Bucket reserved tag modified',
308+
description: 'Noobaa bucket reserved tag modified',
309+
entity_type: 'NODE',
310+
event_type: 'INFO',
311+
scope: 'NODE',
312+
severity: 'INFO',
313+
state: 'HEALTHY',
314+
});
305315

306316
NoobaaEvent.IO_STREAM_ITEM_TIMEOUT = Object.freeze({
307317
event_code: 'bucket_io_stream_item_timeout',

0 commit comments

Comments
 (0)