Skip to content

[NC | DBS3] Add support for reserved bucket tags #8967

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions config.js
Original file line number Diff line number Diff line change
Expand Up @@ -956,6 +956,33 @@ config.NSFS_GLACIER_FORCE_EXPIRE_ON_GET = false;
// interval
config.NSFS_GLACIER_MIGRATE_LOG_THRESHOLD = 50 * 1024;

/**
* NSFS_GLACIER_RESERVED_BUCKET_TAGS defines an object of bucket tags which will be reserved
* by the system and PUT operations for them via S3 API would be limited - as in they would be
* mutable only if specified and only under certain conditions.
*
* @type {Record<string, {
* schema: Record<any, any> & { $id: string },
* immutable: true | false | 'if-data',
* default: any,
* event: boolean
* }>}
*
* @example
* {
'deep-archive-copies': {
schema: {
$id: 'deep-archive-copies-schema-v0',
enum: ['1', '2']
}, // JSON Schema
immutable: 'if-data',
default: '1',
event: true
}
* }
*/
config.NSFS_GLACIER_RESERVED_BUCKET_TAGS = {};

// anonymous account name
config.ANONYMOUS_ACCOUNT_NAME = 'anonymous';

Expand Down
11 changes: 9 additions & 2 deletions docs/NooBaaNonContainerized/Events.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ The following list includes events that indicate on a normal / successful operat
- Description: NooBaa account was deleted successfully using NooBaa CLI.

#### 4. `noobaa_bucket_created`
- Arguments: `bucket_name`
- Arguments:
- `bucket_name`
- `<tag_value>` (if `event` is `true` for the reserved tag)
- Description: NooBaa bucket was created successfully using NooBaa CLI or S3.

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

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

### Error Indicating Events

Expand Down Expand Up @@ -219,4 +226,4 @@ The following list includes events that indicate on some sort of malfunction or
- Reasons:
- Free space in notification log dir FS is below threshold.
- Resolutions:
- Free up space is FS.
- Free up space is FS.
7 changes: 7 additions & 0 deletions docs/NooBaaNonContainerized/NooBaaCLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,13 @@ noobaa-cli bucket update --name <bucket_name> [--new_name] [--owner]
- Type: Boolean
- Description: Set the bucket to force md5 ETag calculation. Unset with ''.

- `tag`
- Type: String
- Description: Set the bucket tags, type is a string of valid JSON. Behaviour is similar to `put-bucket-tagging` S3 API.

- `merge_tag`
- Type: String
- Description: Merge the bucket tags with previous bucket tags, type is a string of valid JSON.

### Bucket Status

Expand Down
125 changes: 105 additions & 20 deletions src/cmd/manage_nsfs.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ const { throw_cli_error, get_bucket_owner_account_by_name,
const manage_nsfs_validations = require('../manage_nsfs/manage_nsfs_validations');
const nc_mkm = require('../manage_nsfs/nc_master_key_manager').get_instance();
const notifications_util = require('../util/notifications_util');
const BucketSpaceFS = require('../sdk/bucketspace_fs');
const NoobaaEvent = require('../manage_nsfs/manage_nsfs_events_utils').NoobaaEvent;

///////////////
//// GENERAL //
Expand Down Expand Up @@ -135,7 +137,6 @@ async function fetch_bucket_data(action, user_input) {
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),
notifications: user_input.notifications
};

if (user_input.bucket_policy !== undefined) {
if (typeof user_input.bucket_policy === 'string') {
// bucket_policy deletion specified with empty string ''
Expand All @@ -154,6 +155,27 @@ async function fetch_bucket_data(action, user_input) {
data = await merge_new_and_existing_config_data(data);
}

if ((action === ACTIONS.UPDATE && user_input.tag) || (action === ACTIONS.ADD)) {
const tags = JSON.parse(user_input.tag || '[]');
data.tag = BucketSpaceFS._merge_reserved_tags(
data.tag || BucketSpaceFS._default_bucket_tags(),
tags,
action === ACTIONS.ADD ? true : await _is_bucket_empty(data),
);
}

if ((action === ACTIONS.UPDATE && user_input.merge_tag) || (action === ACTIONS.ADD)) {
const merge_tags = JSON.parse(user_input.merge_tag || '[]');
data.tag = _.merge(
data.tag,
BucketSpaceFS._merge_reserved_tags(
data.tag || BucketSpaceFS._default_bucket_tags(),
merge_tags,
action === ACTIONS.ADD ? true : await _is_bucket_empty(data),
)
);
}

//if we're updating the owner, needs to override owner in file with the owner from user input.
//if we're adding a bucket, need to set its owner id field
if ((action === ACTIONS.UPDATE && user_input.owner) || (action === ACTIONS.ADD)) {
Expand Down Expand Up @@ -200,7 +222,24 @@ async function add_bucket(data) {
data._id = mongo_utils.mongoObjectId();
const parsed_bucket_data = await config_fs.create_bucket_config_file(data);
await set_bucker_owner(parsed_bucket_data);
return { code: ManageCLIResponse.BucketCreated, detail: parsed_bucket_data, event_arg: { bucket: data.name }};

const reserved_tag_event_args = data.tag?.reduce((curr, tag) => {
const tag_info = config.NSFS_GLACIER_RESERVED_BUCKET_TAGS[tag.key];

// If not a reserved tag - skip
if (!tag_info) return curr;

// If no event is requested - skip
if (!tag_info.event) return curr;

return Object.assign(curr, { [tag.key]: tag.value });
}, {});

return {
code: ManageCLIResponse.BucketCreated,
detail: parsed_bucket_data,
event_arg: { ...(reserved_tag_event_args || {}), bucket: data.name },
};
}

/**
Expand Down Expand Up @@ -256,25 +295,14 @@ async function update_bucket(data) {
*/
async function delete_bucket(data, force) {
try {
const temp_dir_name = native_fs_utils.get_bucket_tmpdir_name(data._id);
const bucket_empty = await _is_bucket_empty(data);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is a bit confusing, suggesting to change it to:

Suggested change
const bucket_empty = await _is_bucket_empty(data);
const is_bucket_empty = await empty_bucket(data);

From what I understand the operation is to empty the bucket and return true when it ends.

if (!bucket_empty && !force) {
throw_cli_error(ManageCLIError.BucketDeleteForbiddenHasObjects, data.name);
}

const bucket_temp_dir_path = native_fs_utils.get_bucket_tmpdir_full_path(data.path, data._id);
// fs_contexts for bucket temp dir (storage path)
const fs_context_fs_backend = native_fs_utils.get_process_fs_context(data.fs_backend);
let entries;
try {
entries = await nb_native().fs.readdir(fs_context_fs_backend, data.path);
} catch (err) {
dbg.warn(`delete_bucket: bucket name ${data.name},` +
`got an error on readdir with path: ${data.path}`, err);
// if the bucket's path was deleted first (encounter ENOENT error) - continue deletion
if (err.code !== 'ENOENT') throw err;
}
if (entries) {
const object_entries = entries.filter(element => !element.name.endsWith(temp_dir_name));
if (object_entries.length > 0 && !force) {
throw_cli_error(ManageCLIError.BucketDeleteForbiddenHasObjects, data.name);
}
}

await native_fs_utils.folder_delete(bucket_temp_dir_path, fs_context_fs_backend, true);
await config_fs.delete_bucket_config_file(data.name);
return { code: ManageCLIResponse.BucketDeleted, detail: { name: data.name }, event_arg: { bucket: data.name } };
Expand Down Expand Up @@ -340,6 +368,33 @@ async function list_bucket_config_files(wide, filters = {}) {
return config_files_list;
}

/**
* _is_bucket_empty returns true if the given bucket is empty
*
* @param {*} data
* @returns {Promise<boolean>}
*/
async function _is_bucket_empty(data) {
const temp_dir_name = native_fs_utils.get_bucket_tmpdir_name(data._id);
// fs_contexts for bucket temp dir (storage path)
const fs_context_fs_backend = native_fs_utils.get_process_fs_context(data.fs_backend);
let entries;
try {
entries = await nb_native().fs.readdir(fs_context_fs_backend, data.path);
} catch (err) {
dbg.warn(`_is_bucket_empty: bucket name ${data.name},` +
`got an error on readdir with path: ${data.path}`, err);
// if the bucket's path was deleted first (encounter ENOENT error) - continue deletion
if (err.code !== 'ENOENT') throw err;
}
if (entries) {
const object_entries = entries.filter(element => !element.name.endsWith(temp_dir_name));
return object_entries.length === 0;
}

return true;
}

/**
* bucket_management does the following -
* 1. fetches the bucket data if this is not a list operation
Expand All @@ -361,7 +416,37 @@ async function bucket_management(action, user_input) {
} else if (action === ACTIONS.STATUS) {
response = await get_bucket_status(data);
} else if (action === ACTIONS.UPDATE) {
response = await update_bucket(data);
const bucket_path = config_fs.get_bucket_path_by_name(user_input.name);
const bucket_lock_file = `${bucket_path}.lock`;
await native_fs_utils.lock_and_run(config_fs.fs_context, bucket_lock_file, async () => {
const prev_bucket_info = await fetch_bucket_data(action, _.omit(user_input, ['tag', 'merge_tag']));
const bucket_info = await fetch_bucket_data(action, user_input);

const tagging_object = prev_bucket_info.tag?.reduce((curr, tag) => Object.assign(curr, { [tag.key]: tag.value }), {});

let reserved_tag_modified = false;
const reserved_tag_event_args = bucket_info.tag?.reduce((curr, tag) => {
const tag_info = config.NSFS_GLACIER_RESERVED_BUCKET_TAGS[tag.key];

// If not a reserved tag - skip
if (!tag_info) return curr;

// If no event is requested - skip
if (!tag_info.event) return curr;

// If value didn't change - skip
if (_.isEqual(tagging_object[tag.key], tag.value)) return curr;

reserved_tag_modified = true;
return Object.assign(curr, { [tag.key]: tag.value });
}, {});

response = await update_bucket(bucket_info);
if (reserved_tag_modified) {
new NoobaaEvent(NoobaaEvent.BUCKET_RESERVED_TAG_MODIFIED)
.create_event(undefined, { ...reserved_tag_event_args, bucket_name: user_input.name });
}
});
} else if (action === ACTIONS.DELETE) {
const force = get_boolean_or_string_value(user_input.force);
response = await delete_bucket(data, force);
Expand Down
5 changes: 4 additions & 1 deletion src/manage_nsfs/manage_nsfs_constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ const VALID_OPTIONS_ANONYMOUS_ACCOUNT = {

const VALID_OPTIONS_BUCKET = {
'add': new Set(['name', 'owner', 'path', 'bucket_policy', 'fs_backend', 'force_md5_etag', 'notifications', FROM_FILE, ...CLI_MUTUAL_OPTIONS]),
'update': new Set(['name', 'owner', 'path', 'bucket_policy', 'fs_backend', 'new_name', 'force_md5_etag', 'notifications', ...CLI_MUTUAL_OPTIONS]),
'update': new Set(['name', 'owner', 'path', 'bucket_policy', 'fs_backend', 'new_name', 'force_md5_etag', 'notifications', 'tag', 'merge_tag', ...CLI_MUTUAL_OPTIONS]),
'delete': new Set(['name', 'force', ...CLI_MUTUAL_OPTIONS]),
'list': new Set(['wide', 'name', ...CLI_MUTUAL_OPTIONS]),
'status': new Set(['name', ...CLI_MUTUAL_OPTIONS]),
Expand Down Expand Up @@ -171,6 +171,9 @@ const OPTION_TYPE = {
key: 'string',
value: 'string',
remove_key: 'boolean',
// bucket tagging
tag: 'string',
merge_tag: 'string',
};

const BOOLEAN_STRING_VALUES = ['true', 'false'];
Expand Down
10 changes: 10 additions & 0 deletions src/manage_nsfs/manage_nsfs_events_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,16 @@ NoobaaEvent.UNAUTHORIZED = Object.freeze({
severity: 'ERROR',
state: 'HEALTHY',
});
NoobaaEvent.BUCKET_RESERVED_TAG_MODIFIED = Object.freeze({
event_code: 'noobaa_bucket_reserved_tag_modified',
message: 'Bucket reserved tag modified',
description: 'Noobaa bucket reserved tag modified',
entity_type: 'NODE',
event_type: 'INFO',
scope: 'NODE',
severity: 'INFO',
state: 'HEALTHY',
});

NoobaaEvent.IO_STREAM_ITEM_TIMEOUT = Object.freeze({
event_code: 'bucket_io_stream_item_timeout',
Expand Down
Loading