diff --git a/packages/manager/.changeset/pr-10597-fixed-1718893685492.md b/packages/manager/.changeset/pr-10597-fixed-1718893685492.md new file mode 100644 index 00000000000..bce06d11bc7 --- /dev/null +++ b/packages/manager/.changeset/pr-10597-fixed-1718893685492.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Fixed +--- + +fix: [M3-8274] - Fix parsing issue causing in Kubernetes Version field ([#10597](https://github.com/linode/manager/pull/10597)) diff --git a/packages/manager/src/features/Kubernetes/kubeUtils.test.ts b/packages/manager/src/features/Kubernetes/kubeUtils.test.ts index 30f67b917be..35619fcce5f 100644 --- a/packages/manager/src/features/Kubernetes/kubeUtils.test.ts +++ b/packages/manager/src/features/Kubernetes/kubeUtils.test.ts @@ -5,7 +5,10 @@ import { } from 'src/factories'; import { extendType } from 'src/utilities/extendType'; -import { getTotalClusterMemoryCPUAndStorage } from './kubeUtils'; +import { + getLatestVersion, + getTotalClusterMemoryCPUAndStorage, +} from './kubeUtils'; describe('helper functions', () => { const badPool = nodePoolFactory.build({ @@ -64,4 +67,39 @@ describe('helper functions', () => { }); }); }); + describe('getLatestVersion', () => { + it('should return the correct latest version from a list of versions', () => { + const versions = [ + { label: '1.00', value: '1.00' }, + { label: '1.10', value: '1.10' }, + { label: '2.00', value: '2.00' }, + ]; + const result = getLatestVersion(versions); + expect(result).toEqual({ label: '2.00', value: '2.00' }); + }); + + it('should handle latest version minor version correctly', () => { + const versions = [ + { label: '1.22', value: '1.22' }, + { label: '1.23', value: '1.23' }, + { label: '1.30', value: '1.30' }, + ]; + const result = getLatestVersion(versions); + expect(result).toEqual({ label: '1.30', value: '1.30' }); + }); + it('should handle latest patch version correctly', () => { + const versions = [ + { label: '1.22', value: '1.30' }, + { label: '1.23', value: '1.15' }, + { label: '1.30', value: '1.50.1' }, + { label: '1.30', value: '1.50' }, + ]; + const result = getLatestVersion(versions); + expect(result).toEqual({ label: '1.50.1', value: '1.50.1' }); + }); + it('should return default fallback value when called with empty versions', () => { + const result = getLatestVersion([]); + expect(result).toEqual({ label: '', value: '' }); + }); + }); }); diff --git a/packages/manager/src/features/Kubernetes/kubeUtils.ts b/packages/manager/src/features/Kubernetes/kubeUtils.ts index 9b91404a0ec..0189a5b2dfd 100644 --- a/packages/manager/src/features/Kubernetes/kubeUtils.ts +++ b/packages/manager/src/features/Kubernetes/kubeUtils.ts @@ -1,3 +1,5 @@ +import { sortByVersion } from 'src/utilities/sort-by'; + import type { Account } from '@linode/api-v4/lib/account'; import type { KubeNodePoolResponse, @@ -6,7 +8,6 @@ import type { } from '@linode/api-v4/lib/kubernetes'; import type { Region } from '@linode/api-v4/lib/regions'; import type { ExtendedType } from 'src/utilities/extendType'; - export const nodeWarning = `We recommend a minimum of 3 nodes in each Node Pool to avoid downtime during upgrades and maintenance.`; export const nodesDeletionWarning = `All nodes will be deleted and new nodes will be created to replace them.`; export const localStorageWarning = `Any local storage (such as \u{2019}hostPath\u{2019} volumes) will be erased.`; @@ -111,15 +112,40 @@ export const getKubeHighAvailability = ( }; }; +/** + * Retrieves the latest version from an array of version objects. + * + * This function sorts an array of objects containing version information and returns the object + * with the highest version number. The sorting is performed in ascending order based on the + * `value` property of each object, and the last element of the sorted array, which represents + * the latest version, is returned. + * + * @param {{label: string, value: string}[]} versions - An array of objects with `label` and `value` + * properties where `value` is a version string. + * @returns {{label: string, value: string}} Returns the object with the highest version number. + * If the array is empty, returns an default fallback object. + * + * @example + * // Returns the latest version object + * getLatestVersion([ + * { label: 'Version 1.1', value: '1.1' }, + * { label: 'Version 2.0', value: '2.0' } + * ]); + * // Output: { label: '2.0', value: '2.0' } + */ export const getLatestVersion = ( versions: { label: string; value: string }[] -) => { - const versionsNumbersArray: number[] = []; +): { label: string; value: string } => { + const sortedVersions = versions.sort((a, b) => { + return sortByVersion(a.value, b.value, 'asc'); + }); + + const latestVersion = sortedVersions.pop(); - for (const element of versions) { - versionsNumbersArray.push(parseFloat(element.value)); + if (!latestVersion) { + // Return a default fallback object + return { label: '', value: '' }; } - const latestVersionValue = Math.max.apply(null, versionsNumbersArray); - return { label: `${latestVersionValue}`, value: `${latestVersionValue}` }; + return { label: `${latestVersion.value}`, value: `${latestVersion.value}` }; }; diff --git a/packages/manager/src/utilities/sort-by.test.ts b/packages/manager/src/utilities/sort-by.test.ts new file mode 100644 index 00000000000..dd1760f419e --- /dev/null +++ b/packages/manager/src/utilities/sort-by.test.ts @@ -0,0 +1,38 @@ +import { sortByVersion } from './sort-by'; + +describe('sortByVersion', () => { + it('should identify the later major version as greater', () => { + const result = sortByVersion('2.0.0', '1.0.0', 'asc'); + expect(result).toBeGreaterThan(0); + }); + + it('should identify the later minor version as greater', () => { + const result = sortByVersion('1.2.0', '1.1.0', 'asc'); + expect(result).toBeGreaterThan(0); + }); + + it('should identify the later patch version as greater', () => { + const result = sortByVersion('1.1.2', '1.1.1', 'asc'); + expect(result).toBeGreaterThan(0); + }); + + it('should identify the later minor version with differing number of digits', () => { + const result = sortByVersion('1.30', '1.3', 'asc'); + expect(result).toBeGreaterThan(0); + }); + + it('should return negative when the first version is earlier in ascending order', () => { + const result = sortByVersion('1.0.0', '2.0.0', 'asc'); + expect(result).toBeLessThan(0); + }); + + it('should return positive when the first version is earlier in descending order', () => { + const result = sortByVersion('1.0.0', '2.0.0', 'desc'); + expect(result).toBeGreaterThan(0); + }); + + it('should return zero when versions are equal', () => { + const result = sortByVersion('1.2.3', '1.2.3', 'asc'); + expect(result).toEqual(0); + }); +}); diff --git a/packages/manager/src/utilities/sort-by.ts b/packages/manager/src/utilities/sort-by.ts index 51753c9e809..8724e5ffbd5 100644 --- a/packages/manager/src/utilities/sort-by.ts +++ b/packages/manager/src/utilities/sort-by.ts @@ -45,3 +45,56 @@ export const sortByArrayLength = (a: any[], b: any[], order: SortOrder) => { return order === 'asc' ? result : -result; }; + +/** + * Compares two semantic version strings based on the specified order. + * + * This function splits each version string into its constituent parts (major, minor, patch), + * compares them numerically, and returns a positive number, zero, or a negative number + * based on the specified sorting order. If components are missing in either version, + * they are treated as zero. + * + * @param {string} a - The first version string to compare. + * @param {string} b - The second version string to compare. + * @param {SortOrder} order - The order to sort by, can be 'asc' for ascending or 'desc' for descending. + * @returns {number} Returns a positive number if version `a` is greater than `b` according to the sort order, + * zero if they are equal, and a negative number if `b` is greater than `a`. + * + * @example + * // returns a positive number + * sortByVersion('1.2.3', '1.2.2', 'asc'); + * + * @example + * // returns zero + * sortByVersion('1.2.3', '1.2.3', 'asc'); + * + * @example + * // returns a negative number + * sortByVersion('1.2.3', '1.2.4', 'asc'); + */ + +export const sortByVersion = ( + a: string, + b: string, + order: SortOrder +): number => { + const aParts = a.split('.'); + const bParts = b.split('.'); + + const result = (() => { + for (let i = 0; i < Math.max(aParts.length, bParts.length); i += 1) { + // If one version has a part and another doesn't (e.g. 3.1 vs 3.1.1), + // treat the missing part as 0. + const aNumber = Number(aParts[i]) || 0; + const bNumber = Number(bParts[i]) || 0; + const diff = aNumber - bNumber; + + if (diff !== 0) { + return diff; + } + } + return 0; + })(); + + return order === 'asc' ? result : -result; +};