Skip to content
5 changes: 5 additions & 0 deletions packages/manager/.changeset/pr-10597-fixed-1718893685492.md
Original file line number Diff line number Diff line change
@@ -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))
40 changes: 39 additions & 1 deletion packages/manager/src/features/Kubernetes/kubeUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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: '' });
});
});
});
40 changes: 33 additions & 7 deletions packages/manager/src/features/Kubernetes/kubeUtils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { sortByVersion } from 'src/utilities/sort-by';

import type { Account } from '@linode/api-v4/lib/account';
import type {
KubeNodePoolResponse,
Expand All @@ -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.`;
Expand Down Expand Up @@ -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}` };
};
38 changes: 38 additions & 0 deletions packages/manager/src/utilities/sort-by.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
Comment thread
cpathipa marked this conversation as resolved.

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);
});
});
53 changes: 53 additions & 0 deletions packages/manager/src/utilities/sort-by.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};