Skip to content

Commit af65596

Browse files
authored
Merge pull request #8867 from shirady/nc-upgrade-cli
NC | Online Upgrade CLI | Validate `expected_version` on `upgrade start`
2 parents b487b1a + 55ccd6b commit af65596

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)