Skip to content

Commit 6c8a935

Browse files
Merge pull request #8881 from romayalon/romy-backports-5.18
NC | Backports - 5.18.z
2 parents efefe8e + 625cbe6 commit 6c8a935

15 files changed

+552
-65
lines changed

config.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -950,6 +950,9 @@ config.ANONYMOUS_ACCOUNT_NAME = 'anonymous';
950950

951951
config.NFSF_UPLOAD_STREAM_MEM_THRESHOLD = 8 * 1024 * 1024;
952952

953+
// we want to change our handling related to EACCESS error
954+
config.NSFS_LIST_IGNORE_ENTRY_ON_EACCES = false;
955+
953956
////////////////////////////
954957
// NSFS NON CONTAINERIZED //
955958
////////////////////////////

docs/NooBaaNonContainerized/NooBaaCLI.md

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -838,4 +838,57 @@ sudo noobaa-cli connection add --from_file <options_connection_JSON_file_path>
838838
### White List Server IP command example
839839
```
840840
sudo noobaa-cli whitelist --ips ["127.0.0.1", "192.0.10.0", "3002:0bd6:0000:0000:0000:ee00:0033:6778"]'
841-
```
841+
```
842+
843+
### Known issues
844+
845+
1. Bucket with suffix `.json`
846+
847+
When the customer creates a bucket with the suffix `.json`, suffix is removed from the bucket config file name.
848+
849+
For example
850+
```
851+
sudo noobaa-cli bucket add --name my-bucket1.json --owner <owner_name> --path <path>
852+
```
853+
Actual file name : `{config_root}\buckets\my-test-bucket.json`
854+
855+
Expected file name : `{config_root}\buckets\my-test-bucket.json.json`
856+
857+
I/O is not possible in this bucket. This issue is only reproducible in older versions(5.18.0 and older).
858+
Issue fixed version is <TBD>
859+
860+
solution :
861+
* Recreate the bucket with same name in new version
862+
```
863+
sudo noobaa-cli bucket add --name my-bucket1.json --owner <owner_name> --path <path>
864+
```
865+
* Delete invalid bucket
866+
```
867+
bucket delete --name my-bucket1 (bucket name without suffix `.json`)
868+
```
869+
870+
2. Account with suffix `.symlink`
871+
872+
When the customer creates an account with the suffix `.symlink`, suffix is removed from the account config file name.
873+
874+
For example
875+
```
876+
sudo noobaa-cli account add --name acc_symlink.symlink --new_buckets_path <path> --uid <uid> --gid <gid>
877+
```
878+
Actual file name : `{config_root}\accounts_by_name\acc_symlink.symlink`
879+
880+
Expected file name : `{config_root}\accounts_by_name\acc_symlink.symlink.symlink`
881+
882+
This issue is only reproducible in older versions(5.18.0 and older).
883+
Issue fixed version is <TBD>
884+
885+
solution :
886+
* Recreate the account with same name in new version
887+
```
888+
sudo noobaa-cli account add --name acc_symlink.symlink --new_buckets_path <path> --uid <uid> --gid <gid>
889+
```
890+
* Delete invalid account
891+
```
892+
sudo noobaa-cli account delete --name acc_symlink (account name without suffix `.symlink`)
893+
```
894+

src/endpoint/s3/ops/s3_get_object.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ async function get_object(req, res) {
4848
throw new S3Error(S3Error.InvalidObjectState);
4949
}
5050
}
51-
51+
http_utils.set_response_headers_from_request(req, res);
5252
const obj_size = object_md.size;
5353
const params = {
5454
object_md,

src/endpoint/s3/ops/s3_head_object.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ async function head_object(req, res) {
2828

2929
s3_utils.set_response_object_md(res, object_md);
3030
s3_utils.set_encryption_response_headers(req, res, object_md.encryption);
31+
http_utils.set_response_headers_from_request(req, res);
3132
}
3233

3334
module.exports = {

src/native/fs/fs_napi.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -772,6 +772,11 @@ struct FSWrapWorker : public FSWorker
772772

773773
/**
774774
* Stat is an fs op
775+
*
776+
* Note: this stat operation contains the system call of open.
777+
* Currently, we use it in list objects, but might want to create a different stat call
778+
* (or add changes inside this) to avoid permission check during list objects
779+
* while we stat each file (to avoid EACCES error)
775780
*/
776781
struct Stat : public FSWorker
777782
{

src/sdk/config_fs.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,13 +103,13 @@ class ConfigFS {
103103

104104
/**
105105
* add_config_file_suffix returns the config_file_name follwed by the given suffix
106+
* Bucket name can have suffix `.json`. This will make sure the bucket name and bucket config file have the same name.
106107
* @param {string} config_file_name
107108
* @param {string} suffix
108109
* @returns {string}
109110
*/
110111
add_config_file_suffix(config_file_name, suffix) {
111112
if (!config_file_name) dbg.warn(`Config file name is missing - ${config_file_name}`);
112-
if (String(config_file_name).endsWith(suffix)) return config_file_name;
113113
return config_file_name + suffix;
114114
}
115115

src/sdk/namespace_fs.js

Lines changed: 67 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -713,10 +713,19 @@ class NamespaceFS {
713713
if (!delimiter && r.common_prefix) {
714714
await process_dir(r.key);
715715
} else {
716-
if (pos < results.length) {
717-
results.splice(pos, 0, r);
718-
} else {
719-
results.push(r);
716+
const entry_path = path.join(this.bucket_path, r.key);
717+
// If entry is outside of bucket, returns stat of symbolic link
718+
const use_lstat = !(await this._is_path_in_bucket_boundaries(fs_context, entry_path));
719+
const stat = await native_fs_utils.stat_if_exists(fs_context, entry_path,
720+
use_lstat, config.NSFS_LIST_IGNORE_ENTRY_ON_EACCES);
721+
if (stat) {
722+
r.stat = stat;
723+
// add the result only if we have the stat information
724+
if (pos < results.length) {
725+
results.splice(pos, 0, r);
726+
} else {
727+
results.push(r);
728+
}
720729
}
721730
if (results.length > limit) {
722731
results.length = limit;
@@ -756,6 +765,11 @@ class NamespaceFS {
756765
await insert_entry_to_results_arr(r);
757766
};
758767

768+
// our current mechanism - list the files and skipping inaccessible directory (invisible in the list).
769+
// We use this check_access in case the directory is not accessible inside a bucket.
770+
// In a directory if we don’t have access to the directory, we want to skip the directory and its sub directories from the list.
771+
// We did it outside to avoid undefined values in the cache.
772+
// Note: It is not the same case as a file without permission.
759773
if (!(await this.check_access(fs_context, dir_path))) return;
760774
try {
761775
if (list_versions) {
@@ -877,13 +891,6 @@ class NamespaceFS {
877891

878892
const prefix_dir_key = prefix.slice(0, prefix.lastIndexOf('/') + 1);
879893
await process_dir(prefix_dir_key);
880-
await Promise.all(results.map(async r => {
881-
if (r.common_prefix) return;
882-
const entry_path = path.join(this.bucket_path, r.key);
883-
//If entry is outside of bucket, returns stat of symbolic link
884-
const use_lstat = !(await this._is_path_in_bucket_boundaries(fs_context, entry_path));
885-
r.stat = await nb_native().fs.stat(fs_context, entry_path, { use_lstat });
886-
}));
887894
const res = {
888895
objects: [],
889896
common_prefixes: [],
@@ -1007,10 +1014,15 @@ class NamespaceFS {
10071014
file_path = await this._find_version_path(fs_context, params);
10081015
await this._check_path_in_bucket_boundaries(fs_context, file_path);
10091016

1010-
// NOTE: don't move this code after the open
1011-
// this can lead to ENOENT failures due to file not exists when content size is 0
1012-
// if entry is a directory object and its content size = 0 - return empty response
1013-
if (await this._is_empty_directory_content(file_path, fs_context, params)) return null;
1017+
// NOTE: don't move this code after the open
1018+
// this can lead to ENOENT failures due to file not exists when content size is 0
1019+
// if entry is a directory object and its content size = 0 - return empty response
1020+
if (await this._is_empty_directory_content(file_path, fs_context, params)) {
1021+
res.end();
1022+
// since we don't write anything to the stream wait_finished might not be needed. added just in case there is a delay
1023+
await stream_utils.wait_finished(res, { signal: object_sdk.abort_controller.signal });
1024+
return null;
1025+
}
10141026

10151027
file = await nb_native().fs.open(
10161028
fs_context,
@@ -1427,10 +1439,10 @@ class NamespaceFS {
14271439
// will retry renaming a file in case of parallel deleting of the destination path
14281440
for (;;) {
14291441
try {
1430-
await native_fs_utils._make_path_dirs(dest_path, fs_context);
14311442
if (this._is_versioning_disabled() ||
14321443
(this._is_versioning_enabled() && is_dir_content)) {
1433-
// dir_content is not supported in versioning, hence we will treat it like versioning disabled
1444+
// dir_content is not supported in versioning, hence we will treat it like versioning disabled
1445+
await native_fs_utils._make_path_dirs(dest_path, fs_context);
14341446
if (open_mode === 'wt') {
14351447
await target_file.linkfileat(fs_context, dest_path);
14361448
} else {
@@ -1478,6 +1490,8 @@ class NamespaceFS {
14781490
try {
14791491
let new_ver_info;
14801492
let latest_ver_info;
1493+
// dir might be deleted by other thread. will recreacte if missing
1494+
await native_fs_utils._make_path_dirs(latest_ver_path, fs_context);
14811495
if (is_gpfs) {
14821496
const latest_ver_info_exist = await native_fs_utils.is_path_exists(fs_context, latest_ver_path);
14831497
gpfs_options = await this._open_files_gpfs(fs_context, new_ver_tmp_path, latest_ver_path, upload_file,
@@ -2558,12 +2572,32 @@ class NamespaceFS {
25582572
}
25592573
}
25602574

2575+
/**
2576+
* _delete_path_dirs deletes all the paths in the hierarchy that are empty after a successful delete
2577+
* if the original file_path to be deleted is a regular object which means file_path is not a directory and it's not a directory object path -
2578+
* before deletion of the parent directory -
2579+
* if the parent directory is a directory object (has CONTENT_DIR xattr) - stop the deletion loop
2580+
* else - delete the directory - if dir is not empty it will stop at the first non empty dir
2581+
* NOTE - the directory object check is needed because when object size is zero we won't create a .folder file and the dir will be empty
2582+
* therefore the deletion will succeed although we shouldn't delete the directory object
2583+
* @param {String} file_path
2584+
* @param {nb.NativeFSContext} fs_context
2585+
*/
25612586
async _delete_path_dirs(file_path, fs_context) {
25622587
try {
2563-
let dir = path.dirname(file_path);
2564-
while (dir !== this.bucket_path) {
2565-
await nb_native().fs.rmdir(fs_context, dir);
2566-
dir = path.dirname(dir);
2588+
let dir_path = path.dirname(file_path);
2589+
const deleted_file_is_dir = file_path.endsWith('/');
2590+
const deleted_file_is_dir_object = file_path.endsWith(config.NSFS_FOLDER_OBJECT_NAME);
2591+
let should_check_dir_path_is_content_dir = !deleted_file_is_dir && !deleted_file_is_dir_object;
2592+
while (dir_path !== this.bucket_path) {
2593+
if (should_check_dir_path_is_content_dir) {
2594+
const dir_stat = await nb_native().fs.stat(fs_context, dir_path);
2595+
const file_is_disabled_dir_content = dir_stat.xattr && dir_stat.xattr[XATTR_DIR_CONTENT] !== undefined;
2596+
if (file_is_disabled_dir_content) break;
2597+
}
2598+
await nb_native().fs.rmdir(fs_context, dir_path);
2599+
dir_path = path.dirname(dir_path);
2600+
should_check_dir_path_is_content_dir = true;
25672601
}
25682602
} catch (err) {
25692603
if (err.code !== 'ENOTEMPTY' &&
@@ -2992,10 +3026,18 @@ class NamespaceFS {
29923026
return res;
29933027
}
29943028

2995-
// delete version_id -
2996-
// 1. get version info, if it's empty - return
2997-
// 2. unlink key
2998-
// 3. if version is latest version - promote second latest -> latest
3029+
/**
3030+
* delete version_id does the following -
3031+
* 1. get version info, if it's empty - return
3032+
* 2. unlink key
3033+
* 3. if version is latest version - promote second latest -> latest
3034+
* 4. if it's the latest version - delete the directory hirerachy of the key if it's empty
3035+
* if it's a past version - delete .versions/ and the directory hirerachy if it's empty
3036+
* @param {nb.NativeFSContext} fs_context
3037+
* @param {String} file_path
3038+
* @param {Object} params
3039+
* @returns {Promise<{deleted_delete_marker?: string, version_id?: string}>}
3040+
*/
29993041
async _delete_version_id(fs_context, file_path, params) {
30003042
// TODO optimization - GPFS link overrides, no need to unlink before promoting, but if there is nothing to promote we should unlink
30013043
const del_obj_version_info = await this._delete_single_object_versioned(fs_context, params.key, params.version_id);

src/test/unit_tests/jest_tests/test_nc_nsfs_account_cli.test.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -570,6 +570,46 @@ describe('manage nsfs cli account flow', () => {
570570
const res = await exec_manage_cli(type, action, account_options);
571571
expect(JSON.parse(res.stdout).error.code).toBe(ManageCLIError.InvalidSupplementalGroupsList.code);
572572
});
573+
574+
it('cli account add - with account name suffix `.json`', async function() {
575+
const action = ACTIONS.ADD;
576+
const { type, new_buckets_path, uid, gid } = defaults;
577+
await fs_utils.create_fresh_path(new_buckets_path);
578+
await fs_utils.file_must_exist(new_buckets_path);
579+
const name = 'account_suffix.json';
580+
const account_options = { config_root, name, new_buckets_path, uid, gid };
581+
const res = await exec_manage_cli(type, action, account_options);
582+
const parse_res = JSON.parse(res);
583+
expect(parse_res.response.code).toEqual(ManageCLIResponse.AccountCreated.code);
584+
expect(parse_res.response.reply.name).toEqual(name);
585+
const account = await config_fs.get_account_by_name(name);
586+
expect(account.name).toBe(name);
587+
const res1 = await exec_manage_cli(TYPES.ACCOUNT, ACTIONS.STATUS, { name, config_root });
588+
expect(JSON.parse(res1).response.reply.name).toBe(name);
589+
const res_list = await exec_manage_cli(TYPES.ACCOUNT, ACTIONS.LIST, { config_root });
590+
expect(JSON.parse(res_list).response.reply.map(item => item.name))
591+
.toEqual(expect.arrayContaining([name]));
592+
});
593+
594+
it('cli account add - with account name suffix `.symlink`', async function() {
595+
const action = ACTIONS.ADD;
596+
const { type, new_buckets_path, uid, gid } = defaults;
597+
await fs_utils.create_fresh_path(new_buckets_path);
598+
await fs_utils.file_must_exist(new_buckets_path);
599+
const name = 'account_suffix.symlink';
600+
const account_options = { config_root, name, new_buckets_path, uid, gid };
601+
const res = await exec_manage_cli(type, action, account_options);
602+
const parse_res = JSON.parse(res);
603+
expect(parse_res.response.code).toEqual(ManageCLIResponse.AccountCreated.code);
604+
expect(parse_res.response.reply.name).toEqual(name);
605+
const account = await config_fs.get_account_by_name(name);
606+
expect(account.name).toBe(name);
607+
const res1 = await exec_manage_cli(TYPES.ACCOUNT, ACTIONS.STATUS, { name, config_root });
608+
expect(JSON.parse(res1).response.reply.name).toBe(name);
609+
const res_list = await exec_manage_cli(TYPES.ACCOUNT, ACTIONS.LIST, { config_root });
610+
expect(JSON.parse(res_list).response.reply.map(item => item.name))
611+
.toEqual(expect.arrayContaining([name]));
612+
});
573613
});
574614

575615
describe('cli update account', () => {

src/test/unit_tests/jest_tests/test_nc_nsfs_bucket_cli.test.js

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ describe('manage nsfs cli bucket flow', () => {
2727
const config_root = path.join(tmp_fs_path, 'config_root_manage_nsfs');
2828
const config_fs = new ConfigFS(config_root);
2929
const root_path = path.join(tmp_fs_path, 'root_path_manage_nsfs/');
30-
const bucket_storage_path = path.join(tmp_fs_path, 'root_path_manage_nsfs', 'bucket1');
30+
const bucket_storage_path = path.join(root_path, 'bucket1');
31+
const bucket_json_storage_path = path.join(root_path, 'bucket3.json');
3132
set_nc_config_dir_in_config(config_root);
3233

3334
const account_defaults = {
@@ -54,6 +55,12 @@ describe('manage nsfs cli bucket flow', () => {
5455
path: bucket_storage_path,
5556
};
5657

58+
const bucket_suffix_json = {
59+
name: 'bucket3.json',
60+
owner: account_defaults.name,
61+
path: bucket_json_storage_path,
62+
};
63+
5764
beforeEach(async () => {
5865
await fs_utils.create_fresh_path(root_path);
5966
await fs_utils.create_fresh_path(bucket_storage_path);
@@ -242,6 +249,22 @@ describe('manage nsfs cli bucket flow', () => {
242249
await assert_bucket(bucket, bucket_options, config_fs);
243250
expect(bucket.s3_policy).toStrictEqual(bucket_policy);
244251
});
252+
253+
it('cli create bucket - with bucket name suffix `.json`', async () => {
254+
await fs_utils.create_fresh_path(bucket_json_storage_path);
255+
await fs_utils.file_must_exist(bucket_json_storage_path);
256+
const action = ACTIONS.ADD;
257+
const bucket_options = { config_root, ...bucket_suffix_json };
258+
await exec_manage_cli(TYPES.BUCKET, action, bucket_options);
259+
const bucket = await config_fs.get_bucket_by_name(bucket_suffix_json.name);
260+
expect(bucket).toBeDefined();
261+
expect(bucket.name).toBe(bucket_options.name);
262+
const res = await exec_manage_cli(TYPES.BUCKET, ACTIONS.STATUS, { name: bucket_suffix_json.name, config_root });
263+
expect(JSON.parse(res).response.reply.name).toBe(bucket_options.name);
264+
const res_list = await exec_manage_cli(TYPES.BUCKET, ACTIONS.LIST, { config_root });
265+
expect(JSON.parse(res_list).response.reply.map(item => item.name))
266+
.toEqual(expect.arrayContaining([bucket_options.name]));
267+
});
245268
});
246269

247270
describe('cli create bucket using from_file', () => {

0 commit comments

Comments
 (0)