Skip to content

Commit d790ca0

Browse files
fix: add version string checking (#2314)
Signed-off-by: Jeffrey Tang <[email protected]> Signed-off-by: JeffreyDallas <[email protected]> Co-authored-by: Copilot <[email protected]>
1 parent 1a6bf2c commit d790ca0

File tree

11 files changed

+96
-20
lines changed

11 files changed

+96
-20
lines changed

src/business/utils/version.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
// SPDX-License-Identifier: Apache-2.0
22

33
import {SemVer} from 'semver';
4+
import * as semver from 'semver';
5+
import {SoloError} from '../../core/errors/solo-error.js';
6+
import {IllegalArgumentError} from '../../core/errors/illegal-argument-error.js';
47

58
export class Version<T extends SemVer | number> {
69
public constructor(public readonly value: T) {
@@ -53,4 +56,37 @@ export class Version<T extends SemVer | number> {
5356
private static isNumeric<R extends SemVer | number>(v: R): boolean {
5457
return Number.isSafeInteger(v) && !Number.isNaN(v);
5558
}
59+
/**
60+
* Validates if a string is a valid semantic version and handles the 'v' prefix
61+
*
62+
* @param versionStr - The version string to validate
63+
* @param isNeedPrefix - If true, adds 'v' prefix if missing; if false, removes 'v' prefix if present
64+
* @param label - Label to use in error messages (e.g., 'Release tag', 'Version')
65+
* @returns The processed version string with proper prefix handling
66+
* @throws SoloError or IllegalArgumentError if the version string is invalid
67+
*/
68+
public static getValidSemanticVersion(
69+
versionString: string,
70+
isNeedPrefix: boolean = false,
71+
label: string = 'Version',
72+
): string {
73+
if (!versionString) {
74+
throw new SoloError(`${label} cannot be empty`);
75+
}
76+
77+
// Handle 'v' prefix based on isNeedPrefix parameter
78+
let processedVersion = versionString;
79+
if (isNeedPrefix && !versionString.startsWith('v')) {
80+
processedVersion = `v${versionString}`;
81+
} else if (!isNeedPrefix && versionString.startsWith('v')) {
82+
processedVersion = versionString.slice(1);
83+
}
84+
85+
// Validate the version string
86+
if (!semver.valid(processedVersion)) {
87+
throw new IllegalArgumentError(`Invalid ${label.toLowerCase()}: ${versionString}`, versionString);
88+
}
89+
90+
return processedVersion;
91+
}
5692
}

src/commands/block-node.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import {type ComponentFactoryApi} from '../core/config/remote/api/component-fact
4444
import {MINIMUM_HIERO_PLATFORM_VERSION_FOR_BLOCK_NODE} from '../../version.js';
4545
import {K8} from '../integration/kube/k8.js';
4646
import {BLOCK_NODE_IMAGE_NAME} from '../core/constants.js';
47+
import {Version} from '../business/utils/version.js';
4748

4849
interface BlockNodeDeployConfigClass {
4950
chartVersion: string;
@@ -163,6 +164,7 @@ export class BlockNodeCommand extends BaseCommand {
163164
}
164165

165166
if (config.imageTag) {
167+
config.imageTag = Version.getValidSemanticVersion(config.imageTag, false, 'Block node image tag');
166168
if (!checkDockerImageExists(BLOCK_NODE_IMAGE_NAME, config.imageTag)) {
167169
throw new SoloError(`Local block node image with tag "${config.imageTag}" does not exist.`);
168170
}
@@ -262,6 +264,12 @@ export class BlockNodeCommand extends BaseCommand {
262264
task: async (context_, task): Promise<void> => {
263265
const config: BlockNodeDeployConfigClass = context_.config;
264266

267+
config.chartVersion = Version.getValidSemanticVersion(
268+
config.chartVersion,
269+
false,
270+
'Block node chart version',
271+
);
272+
265273
await this.chartManager.install(
266274
config.namespace,
267275
config.releaseName,
@@ -539,12 +547,18 @@ export class BlockNodeCommand extends BaseCommand {
539547
task: async (context_): Promise<void> => {
540548
const {namespace, releaseName, context, upgradeVersion} = context_.config;
541549

550+
const validatedUpgradeVersion = Version.getValidSemanticVersion(
551+
upgradeVersion,
552+
false,
553+
'Block node chart version',
554+
);
555+
542556
await this.chartManager.upgrade(
543557
namespace,
544558
releaseName,
545559
constants.BLOCK_NODE_CHART,
546560
constants.BLOCK_NODE_CHART_URL,
547-
upgradeVersion,
561+
validatedUpgradeVersion,
548562
'',
549563
context,
550564
);

src/commands/cluster/tasks.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {LocalConfigRuntimeState} from '../../business/runtime-state/config/local
3232
import {StringFacade} from '../../business/runtime-state/facade/string-facade.js';
3333
import {Lock} from '../../core/lock/lock.js';
3434
import {RemoteConfigRuntimeState} from '../../business/runtime-state/config/remote/remote-config-runtime-state.js';
35+
import {Version} from '../../business/utils/version.js';
3536

3637
@injectable()
3738
export class ClusterCommandTasks {
@@ -110,7 +111,7 @@ export class ClusterCommandTasks {
110111
task.title = clusterRef;
111112

112113
if (self.localConfig.configuration.clusterRefs.get(clusterRef)) {
113-
throw new SoloError(`Cluster ref ${clusterRef} already exists inside local config`);
114+
this.logger.showUser(chalk.yellow(`Cluster ref ${clusterRef} already exists inside local config`));
114115
}
115116
},
116117
};
@@ -277,7 +278,7 @@ export class ClusterCommandTasks {
277278
title: `Install '${constants.SOLO_CLUSTER_SETUP_CHART}' chart`,
278279
task: async context_ => {
279280
const clusterSetupNamespace = context_.config.clusterSetupNamespace;
280-
const version = context_.config.soloChartVersion;
281+
const version = Version.getValidSemanticVersion(context_.config.soloChartVersion, false, 'Solo chart version');
281282
const valuesArgument = context_.valuesArg;
282283

283284
try {

src/commands/explorer.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {type ComponentFactoryApi} from '../core/config/remote/api/component-fact
4040
import {Lock} from '../core/lock/lock.js';
4141
import {PodReference} from '../integration/kube/resources/pod/pod-reference.js';
4242
import {Pod} from '../integration/kube/resources/pod/pod.js';
43+
import {Version} from '../business/utils/version.js';
4344

4445
interface ExplorerDeployConfigClass {
4546
cacheDir: string;
@@ -281,6 +282,11 @@ export class ExplorerCommand extends BaseCommand {
281282
title: 'Install cert manager',
282283
task: async context_ => {
283284
const config = context_.config;
285+
config.soloChartVersion = Version.getValidSemanticVersion(
286+
config.soloChartVersion,
287+
false,
288+
'Solo chart version',
289+
);
284290
const {soloChartVersion} = config;
285291

286292
const soloCertManagerValuesArgument = await self.prepareCertManagerChartValuesArg(config);
@@ -349,6 +355,8 @@ export class ExplorerCommand extends BaseCommand {
349355
let exploreValuesArgument = prepareValuesFiles(constants.EXPLORER_VALUES_FILE);
350356
exploreValuesArgument += await self.prepareHederaExplorerValuesArg(config);
351357

358+
config.explorerVersion = Version.getValidSemanticVersion(config.explorerVersion, false, 'Explorer version');
359+
352360
await self.chartManager.install(
353361
config.namespace,
354362
constants.EXPLORER_RELEASE_NAME,

src/commands/mirror-node.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import {SecretType} from '../integration/kube/resources/secret/secret-type.js';
5252
import * as semver from 'semver';
5353
import {Base64} from 'js-base64';
5454
import {Lock} from '../core/lock/lock.js';
55+
import {Version} from '../business/utils/version.js';
5556

5657
interface MirrorNodeDeployConfigClass {
5758
isChartInstalled: boolean;
@@ -171,6 +172,9 @@ export class MirrorNodeCommand extends BaseCommand {
171172
if (config.valuesFile) {
172173
valuesArgument += helpers.prepareValuesFiles(config.valuesFile);
173174
}
175+
176+
config.mirrorNodeVersion = Version.getValidSemanticVersion(config.mirrorNodeVersion, true, 'Mirror node version');
177+
174178
const chartNamespace: string = this.getChartNamespace(config.mirrorNodeVersion);
175179
const environmentVariablePrefix: string = this.getEnvironmentVariablePrefix(config.mirrorNodeVersion);
176180

src/commands/network.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ import {PvcReference} from '../integration/kube/resources/pvc/pvc-reference.js';
7575
import {NamespaceName} from '../types/namespace/namespace-name.js';
7676
import {ConsensusNode} from '../core/model/consensus-node.js';
7777
import {BlockNodeStateSchema} from '../data/schema/model/remote/state/block-node-state-schema.js';
78+
import {Version} from '../business/utils/version.js';
7879

7980
export interface NetworkDeployConfigClass {
8081
isUpgrade: boolean;
@@ -980,6 +981,12 @@ export class NetworkCommand extends BaseCommand {
980981
config.isUpgrade = true;
981982
}
982983

984+
config.soloChartVersion = Version.getValidSemanticVersion(
985+
config.soloChartVersion,
986+
false,
987+
'Solo chart version',
988+
);
989+
983990
await this.chartManager.upgrade(
984991
config.namespace,
985992
constants.SOLO_DEPLOYMENT_CHART,

src/commands/node/configs.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import {LocalConfigRuntimeState} from '../../business/runtime-state/config/local
5353
import {type RemoteConfigRuntimeStateApi} from '../../business/runtime-state/api/remote-config-runtime-state-api.js';
5454
import {SOLO_USER_AGENT_HEADER} from '../../core/constants.js';
5555
import {SemVer} from 'semver';
56+
import {Version} from '../../business/utils/version.js';
5657

5758
const PREPARE_UPGRADE_CONFIGS_NAME = 'prepareUpgradeConfig';
5859
const DOWNLOAD_GENERATED_FILES_CONFIGS_NAME = 'downloadGeneratedFilesConfig';
@@ -259,6 +260,13 @@ export class NodeCommandConfigs {
259260
);
260261
}
261262

263+
// check consensus releaseTag to make sure it is a valid semantic version string starting with 'v'
264+
context_.config.releaseTag = Version.getValidSemanticVersion(
265+
context_.config.releaseTag,
266+
true,
267+
'Consensus release tag',
268+
);
269+
262270
const freezeAdminAccountId: AccountId = this.accountManager.getFreezeAccountId(context_.config.deployment);
263271
const accountKeys = await this.accountManager.getAccountKeysFromSecret(
264272
freezeAdminAccountId.toString(),

src/commands/node/tasks.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ import {NodeServiceMapping} from '../../types/mappings/node-service-mapping.js';
128128
import {SemVer, lt} from 'semver';
129129
import {Pod} from '../../integration/kube/resources/pod/pod.js';
130130
import {type Container} from '../../integration/kube/resources/container/container.js';
131+
import {Version} from '../../business/utils/version.js';
131132

132133
export type LeaseWrapper = {lease: Lock};
133134

@@ -1191,6 +1192,10 @@ export class NodeCommandTasks {
11911192
const {podRefs, localBuildPath} = context_.config;
11921193
let {releaseTag} = context_.config;
11931194

1195+
if (releaseTag) {
1196+
releaseTag = Version.getValidSemanticVersion(releaseTag, true, 'Consensus release tag');
1197+
}
1198+
11941199
if ('upgradeVersion' in context_.config) {
11951200
if (!context_.config.upgradeVersion) {
11961201
this.logger.info('Skip, no need to update the platform software');
@@ -2289,6 +2294,11 @@ export class NodeCommandTasks {
22892294
const valuesArguments = valuesArgumentMap[clusterReference];
22902295
const context = this.localConfig.configuration.clusterRefs.get(clusterReference);
22912296

2297+
config.soloChartVersion = Version.getValidSemanticVersion(
2298+
config.soloChartVersion,
2299+
false,
2300+
'Solo chart version',
2301+
);
22922302
await self.chartManager.upgrade(
22932303
config.namespace,
22942304
constants.SOLO_DEPLOYMENT_CHART,

src/commands/relay.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {Lock} from '../core/lock/lock.js';
3434
import {PodReference} from '../integration/kube/resources/pod/pod-reference.js';
3535
import {Pod} from '../integration/kube/resources/pod/pod.js';
3636
import {Duration} from '../core/time/duration.js';
37+
import {Version} from '../business/utils/version.js';
3738

3839
interface RelayDestroyConfigClass {
3940
chartDirectory: string;
@@ -160,8 +161,9 @@ export class RelayCommand extends BaseCommand {
160161
}
161162

162163
if (relayRelease) {
163-
valuesArgument += ` --set relay.image.tag=${relayRelease.replace(/^v/, '')}`;
164-
valuesArgument += ` --set ws.image.tag=${relayRelease.replace(/^v/, '')}`;
164+
relayRelease = Version.getValidSemanticVersion(relayRelease, false, 'Relay release');
165+
valuesArgument += ` --set relay.image.tag=${relayRelease}`;
166+
valuesArgument += ` --set ws.image.tag=${relayRelease}`;
165167
}
166168

167169
if (replicaCount) {

test/e2e/commands/cluster.test.ts

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -160,20 +160,6 @@ describe('ClusterCommand', () => {
160160
return {argv, clusterRef: clusterReference, contextName};
161161
}
162162

163-
it('cluster-ref connect should fail with cluster ref that already exists', async () => {
164-
const clusterReference = 'duplicated';
165-
const {argv} = getClusterConnectDefaultArgv();
166-
argv.setArg(flags.clusterRef, clusterReference);
167-
168-
try {
169-
await clusterCmdHandlers.connect(argv.build());
170-
await clusterCmdHandlers.connect(argv.build());
171-
expect.fail();
172-
} catch (error) {
173-
expect(error.message).to.include(`Cluster ref ${clusterReference} already exists inside local config`);
174-
}
175-
});
176-
177163
it('cluster-ref connect should fail with invalid context name', async () => {
178164
const clusterReference = 'test-context-name';
179165
const contextName = 'INVALID_CONTEXT';

0 commit comments

Comments
 (0)