Skip to content

Commit 55ccd6b

Browse files
committed
NC | Online Upgrade CLI | Validate expected_version on upgrade start
1. Add the function of validate_expected_version that would check that we have the flag of expected_version and it is from the supported versions we allow. Also, add specific errors to the CLI (MissingExpectedVersionFlag and InvalidArgumentType) so that eventually we would not report it as UpgradeFailed. 2. Move functions that are related to versions to versions_utils, and added a helper function is_valid_sematic_version to check the structure of the string. 3. Throw an error instead of the message "config_dir_version on system.json and config_fs.config_dir_version match, nothing to upgrade" and change the tests accordingly. 4. Add the skip_verification to BOOLEAN_STRING_OPTIONS and use it to in validate_expected_version. Signed-off-by: shirady <57721533+shirady@users.noreply.github.com>
1 parent b487b1a commit 55ccd6b

File tree

10 files changed

+157
-49
lines changed

10 files changed

+157
-49
lines changed

src/manage_nsfs/manage_nsfs_cli_errors.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,12 @@ ManageCLIError.InvalidUpgradeAction = Object.freeze({
472472
http_code: 400,
473473
});
474474

475+
ManageCLIError.MissingExpectedVersionFlag = Object.freeze({
476+
code: 'MissingExpectedVersionFlag',
477+
message: 'Expected version is mandatory, please use the --expected_version flag',
478+
http_code: 400,
479+
});
480+
475481
ManageCLIError.UpgradeFailed = Object.freeze({
476482
code: 'UpgradeFailed',
477483
message: 'Upgrade request failed',

src/manage_nsfs/manage_nsfs_constants.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,8 @@ const OPTION_TYPE = {
174174

175175
const BOOLEAN_STRING_VALUES = ['true', 'false'];
176176
const BOOLEAN_STRING_OPTIONS = new Set(['allow_bucket_creation', 'regenerate', 'wide', 'show_secrets', 'force',
177-
'force_md5_etag', 'iam_operate_on_root_account', 'all_account_details', 'all_bucket_details', 'anonymous', 'disable_service_validation', 'disable_runtime_validation', 'short_status']);
177+
'force_md5_etag', 'iam_operate_on_root_account', 'all_account_details', 'all_bucket_details', 'anonymous',
178+
'disable_service_validation', 'disable_runtime_validation', 'short_status', 'skip_verification']);
178179

179180
// CLI UNSET VALUES
180181
const CLI_EMPTY_STRING = '';

src/manage_nsfs/manage_nsfs_validations.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/* Copyright (C) 2024 NooBaa */
22
'use strict';
33

4+
const pkg = require('../../package.json');
45
const config = require('../../config');
56
const dbg = require('../util/debug_module')(__filename);
67
const net = require('net');
@@ -16,6 +17,7 @@ const { TYPES, ACTIONS, VALID_OPTIONS, OPTION_TYPE, FROM_FILE, BOOLEAN_STRING_VA
1617
const { check_root_account_owns_user } = require('../nc/nc_utils');
1718
const { validate_username } = require('../util/validation_utils');
1819
const notifications_util = require('../util/notifications_util');
20+
const version_utils = require('../util/versions_utils');
1921

2022
/////////////////////////////
2123
//// GENERAL VALIDATIONS ////
@@ -738,6 +740,43 @@ function validate_connection_args(user_input, action) {
738740
}
739741
}
740742

743+
////////////////////////////
744+
//// UPGRADE VALIDATION ////
745+
///////////////////////////
746+
747+
/**
748+
* validate_expected_version checks that expected_version is valid
749+
* if it's not valid it throws an error
750+
* Note: the cases where we used ManageCLIError errors will
751+
* not be reported as UpgradeFailed and without adding an event
752+
* @param {string} expected_version
753+
* @param {boolean} [skip_verification]
754+
*/
755+
function validate_expected_version(expected_version, skip_verification = false) {
756+
if (!expected_version) {
757+
throw_cli_error(ManageCLIError.MissingExpectedVersionFlag);
758+
}
759+
if (!version_utils.is_valid_semantic_version(expected_version)) {
760+
const detail = 'expected_version must have sematic version structure (major.minor.patch)';
761+
throw_cli_error(ManageCLIError.InvalidArgumentType, detail);
762+
}
763+
if (!skip_verification && !version_match_to_current_version(expected_version)) {
764+
throw new Error(`expected_version must match the package version ${pkg.version}`);
765+
}
766+
}
767+
768+
/**
769+
* version_match_to_current_version checks that the version
770+
* is the same as the one that was defined in the pkg
771+
* @param {string} expected_version
772+
* @returns {boolean}
773+
*/
774+
function version_match_to_current_version(expected_version) {
775+
const package_version = pkg.version;
776+
const diff_from_package_version = version_utils.version_compare(expected_version, package_version);
777+
return diff_from_package_version === 0;
778+
}
779+
741780

742781
// EXPORTS
743782
exports.validate_input_types = validate_input_types;
@@ -751,3 +790,4 @@ exports.validate_whitelist_ips = validate_whitelist_ips;
751790
exports.validate_flags_combination = validate_flags_combination;
752791
exports.validate_connection_args = validate_connection_args;
753792
exports.validate_no_extra_args = validate_no_extra_args;
793+
exports.validate_expected_version = validate_expected_version;

src/manage_nsfs/upgrade.js

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ const { ManageCLIError } = require('./manage_nsfs_cli_errors');
77
const { UPGRADE_ACTIONS } = require('./manage_nsfs_constants');
88
const { NCUpgradeManager } = require('../upgrade/nc_upgrade_manager');
99
const { ManageCLIResponse } = require('../manage_nsfs/manage_nsfs_cli_responses');
10-
const { throw_cli_error, write_stdout_response } = require('./manage_nsfs_cli_utils');
10+
const { throw_cli_error, write_stdout_response, get_boolean_or_string_value } = require('./manage_nsfs_cli_utils');
1111
const { NoobaaEvent } = require('./manage_nsfs_events_utils');
12+
const { validate_expected_version } = require('./manage_nsfs_validations');
1213

1314
/**
1415
* manage_upgrade_operations handles cli upgrade operations
@@ -41,23 +42,29 @@ async function manage_upgrade_operations(action, user_input, config_fs) {
4142
*/
4243
async function start_config_dir_upgrade(user_input, config_fs) {
4344
try {
44-
const skip_verification = user_input.skip_verification;
45+
const skip_verification = get_boolean_or_string_value(user_input.skip_verification);
4546
const expected_version = user_input.expected_version;
4647
const expected_hosts = user_input.expected_hosts && user_input.expected_hosts.split(',').filter(host => !_.isEmpty(host));
4748
const custom_upgrade_scripts_dir = user_input.custom_upgrade_scripts_dir;
4849

4950
new NoobaaEvent(NoobaaEvent.CONFIG_DIR_UPGRADE_STARTED).create_event(undefined, { expected_version, expected_hosts }, undefined);
5051

51-
if (!expected_version) throw new Error('expected_version flag is required');
52+
validate_expected_version(expected_version, skip_verification);
5253
if (!expected_hosts) dbg.warn('expected_hosts flag is empty, config dir upgrade will be performed without hosts version verification');
5354

5455
const nc_upgrade_manager = new NCUpgradeManager(config_fs, { custom_upgrade_scripts_dir });
5556
const upgrade_res = await nc_upgrade_manager.upgrade_config_dir(expected_version, expected_hosts, { skip_verification });
5657
if (!upgrade_res) throw new Error('Upgrade config directory failed', { cause: upgrade_res });
5758
write_stdout_response(ManageCLIResponse.UpgradeSuccessful, upgrade_res, { expected_version, expected_hosts });
5859
} catch (err) {
59-
dbg.error('could not upgrade config directory successfully - error', err);
60-
throw_cli_error({ ...ManageCLIError.UpgradeFailed, cause: err }, undefined, { error: err.stack || err });
60+
if (err instanceof ManageCLIError) {
61+
// those are errors that the admin made in the CLI, the upgrade didn't start
62+
// therefore, we won't report it as UpgradeFailed and without adding an event
63+
throw err;
64+
} else {
65+
dbg.error('could not upgrade config directory successfully - error', err);
66+
throw_cli_error({ ...ManageCLIError.UpgradeFailed, cause: err }, undefined, { error: err.stack || err });
67+
}
6168
}
6269
}
6370

src/sdk/config_fs.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const native_fs_utils = require('../util/native_fs_utils');
1616
const { RpcError } = require('../rpc');
1717
const nc_mkm = require('../manage_nsfs/nc_master_key_manager').get_instance();
1818
const nsfs_schema_utils = require('../manage_nsfs/nsfs_schema_utils');
19-
const { version_compare } = require('../upgrade/upgrade_utils');
19+
const { version_compare } = require('../util/versions_utils');
2020
const { anonymous_access_key } = require('./object_sdk');
2121

2222
/** @typedef {import('fs').Dirent} Dirent */

src/test/unit_tests/jest_tests/test_cli_upgrade.test.js

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -266,8 +266,39 @@ describe('noobaa cli - upgrade', () => {
266266
await fs_utils.replace_file(config_fs.system_json_path, JSON.stringify(old_rpm_expected_system_json));
267267
const res = await exec_manage_cli(TYPES.UPGRADE, UPGRADE_ACTIONS.START, { config_root, expected_hosts }, true);
268268
const parsed_res = JSON.parse(res.stdout);
269-
expect(parsed_res.error.message).toBe(ManageCLIError.UpgradeFailed.message);
270-
expect(parsed_res.error.cause).toContain('expected_version flag is required');
269+
expect(parsed_res.error.message).toBe(ManageCLIError.MissingExpectedVersionFlag.message);
270+
});
271+
272+
it('upgrade start - should fail on expected_version with wrong type (number instead of string)', async () => {
273+
await fs_utils.replace_file(config_fs.system_json_path, JSON.stringify(old_rpm_expected_system_json));
274+
const res = await exec_manage_cli(TYPES.UPGRADE, UPGRADE_ACTIONS.START, { config_root, expected_version: 5, expected_hosts }, true);
275+
const parsed_res = JSON.parse(res.stdout);
276+
expect(parsed_res.error.code).toBe(ManageCLIError.InvalidArgumentType.code);
277+
expect(parsed_res.error.detail).toContain('type of flag expected_version should be string');
278+
});
279+
280+
it('upgrade start - should fail on expected_version is a string not in the format on sematic version', async () => {
281+
await fs_utils.replace_file(config_fs.system_json_path, JSON.stringify(old_rpm_expected_system_json));
282+
const res = await exec_manage_cli(TYPES.UPGRADE, UPGRADE_ACTIONS.START, { config_root, expected_version: 'invalid-semversion', expected_hosts }, true);
283+
const parsed_res = JSON.parse(res.stdout);
284+
expect(parsed_res.error.code).toBe(ManageCLIError.InvalidArgumentType.code);
285+
expect(parsed_res.error.detail).toContain('expected_version must have sematic version structure');
286+
});
287+
288+
it('upgrade start - should fail on expected_version that is later that package version', async () => {
289+
await fs_utils.replace_file(config_fs.system_json_path, JSON.stringify(old_rpm_expected_system_json));
290+
const res = await exec_manage_cli(TYPES.UPGRADE, UPGRADE_ACTIONS.START, { config_root, expected_version: '6.18.0', expected_hosts }, true);
291+
const parsed_res = JSON.parse(res.stdout);
292+
expect(parsed_res.error.code).toBe(ManageCLIError.UpgradeFailed.code);
293+
expect(parsed_res.error.cause).toContain('expected_version must match the package version');
294+
});
295+
296+
it('upgrade start - should fail on expected_version that is older that package version', async () => {
297+
await fs_utils.replace_file(config_fs.system_json_path, JSON.stringify(old_rpm_expected_system_json));
298+
const res = await exec_manage_cli(TYPES.UPGRADE, UPGRADE_ACTIONS.START, { config_root, expected_version: '5.16.0', expected_hosts }, true);
299+
const parsed_res = JSON.parse(res.stdout);
300+
expect(parsed_res.error.code).toBe(ManageCLIError.UpgradeFailed.code);
301+
expect(parsed_res.error.cause).toContain('expected_version must match the package version');
271302
});
272303

273304
it('upgrade start - should succeed although missing expected hosts', async () => {
@@ -306,7 +337,7 @@ describe('noobaa cli - upgrade', () => {
306337
const res = await exec_manage_cli(TYPES.UPGRADE, UPGRADE_ACTIONS.START, { config_root, expected_version, expected_hosts }, true);
307338
const parsed_res = JSON.parse(res.stdout);
308339
expect(parsed_res.error.message).toBe(ManageCLIError.UpgradeFailed.message);
309-
expect(parsed_res.error.cause).toContain(`config dir upgrade can not be started - the host's package version=${pkg.version} does not match the user's expected version=${expected_version}`);
340+
expect(parsed_res.error.cause).toContain(`expected_version must match the package version`);
310341
});
311342

312343
it('upgrade start - should fail on old rpm version', async () => {
@@ -332,18 +363,17 @@ describe('noobaa cli - upgrade', () => {
332363
expect(system_data_after_upgrade.config_directory).toBeUndefined();
333364
});
334365

335-
it('upgrade start - should succeed - same version, nothing to upgrade - config directory property exists', async () => {
366+
it('upgrade start - should fail - same version, nothing to upgrade - config directory property exists', async () => {
336367
await fs_utils.replace_file(config_fs.system_json_path, JSON.stringify(old_expected_system_json2));
337368
const system_data_before_upgrade = await config_fs.get_system_config_file();
338369
const options = { config_root, expected_version: pkg.version, expected_hosts };
339370
const res = await exec_manage_cli(TYPES.UPGRADE, UPGRADE_ACTIONS.START, options, true);
340-
const parsed_res = JSON.parse(res);
341-
expect(parsed_res.response.code).toBe(ManageCLIResponse.UpgradeSuccessful.code);
342-
expect(parsed_res.response.reply.message).toBe('config_dir_version on system.json and config_fs.config_dir_version match, nothing to upgrade');
371+
const parsed_res = JSON.parse(res.stdout);
372+
expect(parsed_res.error.message).toBe(ManageCLIError.UpgradeFailed.message);
373+
expect(parsed_res.error.cause).toContain(`config_dir_version on system.json and config_fs.config_dir_version match, nothing to upgrade`);
343374
const system_data_after_upgrade = await config_fs.get_system_config_file();
344375
// check that in the hostname section nothing changed
345376
expect(system_data_before_upgrade[hostname]).toStrictEqual(system_data_after_upgrade[hostname]);
346-
expect(system_data_after_upgrade.config_directory).toStrictEqual(old_expected_system_json2.config_directory);
347377
});
348378

349379
it('upgrade start - should succeed - no old config directory', async () => {

src/test/unit_tests/jest_tests/test_nc_upgrade_manager.test.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -785,9 +785,9 @@ describe('nc upgrade manager - upgrade config directory', () => {
785785
const expected_version = pkg.version;
786786
const hosts_list = [hostname];
787787
await config_fs.create_system_config_file(JSON.stringify(system_data));
788-
const expected_msg = { "message": "config_dir_version on system.json and config_fs.config_dir_version match, nothing to upgrade" };
789-
const res = await nc_upgrade_manager.upgrade_config_dir(expected_version, hosts_list);
790-
expect(res).toStrictEqual(expected_msg);
788+
const expected_err_msg = 'config_dir_version on system.json and config_fs.config_dir_version match, nothing to upgrade';
789+
await expect(nc_upgrade_manager.upgrade_config_dir(expected_version, hosts_list))
790+
.rejects.toThrow(expected_err_msg);
791791
});
792792

793793
it('upgrade_config_dir - config_dir_version in system.json is newer than the hosts current config_dir_version', async () => {

src/upgrade/nc_upgrade_manager.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ const util = require('util');
77
const pkg = require('../../package.json');
88
const dbg = require('../util/debug_module')(__filename);
99
const { CONFIG_DIR_PHASES } = require('../sdk/config_fs');
10-
const { should_upgrade, run_upgrade_scripts, version_compare } = require('./upgrade_utils');
10+
const { should_upgrade, run_upgrade_scripts } = require('./upgrade_utils');
11+
const { version_compare } = require('../util/versions_utils');
1112

1213
const hostname = os.hostname();
1314
// prior to 5.18.0 - there is no config dir version, the config dir version to be used on the first upgrade is 0.0.0 (5.17.0 -> 5.18.0)
@@ -108,7 +109,11 @@ class NCUpgradeManager {
108109
const package_to_version = this.package_version;
109110
const this_upgrade_versions = { config_dir_from_version, config_dir_to_version, package_from_version, package_to_version };
110111

111-
if (!should_upgrade(config_dir_from_version, config_dir_to_version)) return { message: 'config_dir_version on system.json and config_fs.config_dir_version match, nothing to upgrade' };
112+
if (!should_upgrade(config_dir_from_version, config_dir_to_version)) {
113+
const err_message = 'config_dir_version on system.json and config_fs.config_dir_version match, nothing to upgrade';
114+
dbg.error(`upgrade_config_dir: ${err_message}`);
115+
throw new Error(err_message);
116+
}
112117

113118
if (!skip_verification) await this._verify_config_dir_upgrade(system_data, expected_version, hosts_list);
114119

src/upgrade/upgrade_utils.js

Lines changed: 1 addition & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,34 +5,7 @@ const fs = require('fs');
55
const _ = require('lodash');
66
const path = require('path');
77
const dbg = require('../util/debug_module')(__filename);
8-
9-
/**
10-
* @param {string} ver
11-
*/
12-
function parse_ver(ver) {
13-
const stripped_ver = ver.split('-')[0];
14-
return stripped_ver.split('.').map(i => Number.parseInt(i, 10));
15-
}
16-
17-
18-
/**
19-
* version_compare compares 2 versions. returns positive if ver1 is larger, negative if ver2, 0 if equal
20-
* @param {string} ver1
21-
* @param {string} ver2
22-
*/
23-
function version_compare(ver1, ver2) {
24-
const ver1_arr = parse_ver(ver1);
25-
const ver2_arr = parse_ver(ver2);
26-
const max_length = Math.max(ver1_arr.length, ver2_arr.length);
27-
for (let i = 0; i < max_length; ++i) {
28-
const comp1 = ver1_arr[i] || 0;
29-
const comp2 = ver2_arr[i] || 0;
30-
const diff = comp1 - comp2;
31-
// if version component is not the same, return the difference
32-
if (diff) return diff;
33-
}
34-
return 0;
35-
}
8+
const { version_compare } = require('../util/versions_utils');
369

3710
/**
3811
* @param {string} current_version
@@ -137,5 +110,4 @@ async function run_upgrade_scripts(this_upgrade, upgrade_scripts_dir, options) {
137110

138111
exports.should_upgrade = should_upgrade;
139112
exports.load_required_scripts = load_required_scripts;
140-
exports.version_compare = version_compare;
141113
exports.run_upgrade_scripts = run_upgrade_scripts;

src/util/versions_utils.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/* Copyright (C) 2025 NooBaa */
2+
"use strict";
3+
4+
/**
5+
* @param {string} ver
6+
*/
7+
function parse_ver(ver) {
8+
const stripped_ver = ver.split('-')[0];
9+
return stripped_ver.split('.').map(i => Number.parseInt(i, 10));
10+
}
11+
12+
/**
13+
* version_compare compares 2 versions. returns positive if ver1 is larger, negative if ver2, 0 if equal
14+
* @param {string} ver1
15+
* @param {string} ver2
16+
*/
17+
function version_compare(ver1, ver2) {
18+
const ver1_arr = parse_ver(ver1);
19+
const ver2_arr = parse_ver(ver2);
20+
const max_length = Math.max(ver1_arr.length, ver2_arr.length);
21+
for (let i = 0; i < max_length; ++i) {
22+
const comp1 = ver1_arr[i] || 0;
23+
const comp2 = ver2_arr[i] || 0;
24+
const diff = comp1 - comp2;
25+
// if version component is not the same, return the difference
26+
if (diff) return diff;
27+
}
28+
return 0;
29+
}
30+
31+
/**
32+
* is_valid_semantic_version checks that the version string is a valid sematic version
33+
* 1. strips the additional "-<something>" that might be added, for example: -alpha, -beta etc
34+
* 2. checks that the version has the sematic version is from the structure of version: major.minor.patch
35+
* @param {string} version
36+
* @returns {boolean}
37+
*/
38+
function is_valid_semantic_version(version) {
39+
const stripped_ver = version.split('-')[0];
40+
// sematic version is from the structure of version: major.minor.patch
41+
const semantic_version_regex = /^\d+\.\d+.\.\d+$/;
42+
return semantic_version_regex.test(stripped_ver);
43+
}
44+
45+
exports.version_compare = version_compare;
46+
exports.is_valid_semantic_version = is_valid_semantic_version;
47+

0 commit comments

Comments
 (0)