Skip to content

Commit 5761e58

Browse files
committed
fix(pci-kubernetes): calculate multiple node price
ref: #TAPC-3341 Signed-off-by: Pierre-Philippe <[email protected]>
1 parent 3bfb7e5 commit 5761e58

File tree

9 files changed

+274
-384
lines changed

9 files changed

+274
-384
lines changed

packages/manager/apps/pci-kubernetes/src/api/data/kubernetes.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,22 +19,22 @@ export const getAllKube = async (projectId: string): Promise<TKube[]> => {
1919
return data;
2020
};
2121

22-
export interface NodePool {
22+
export type NodePool = {
2323
name: string;
2424
antiAffinity: boolean;
2525
autoscale: boolean;
26-
availabilityZones: string[];
26+
availabilityZones?: string[];
2727
desiredNodes: number;
2828
minNodes: number;
29-
localisation: string; // will change with 3AZ
29+
localisation: string;
3030
flavorName: string;
3131
maxNodes: number;
3232
monthlyBilled: boolean;
33-
}
33+
};
3434

3535
export type NodePoolPrice = NodePool & { monthlyPrice: number };
3636

37-
export interface KubeClusterCreationParams {
37+
export type KubeClusterCreationParams = {
3838
name: string;
3939
region: string;
4040
version: string;
@@ -48,7 +48,7 @@ export interface KubeClusterCreationParams {
4848
defaultVrackGateway?: string;
4949
privateNetworkRoutingAsDefault?: boolean;
5050
};
51-
}
51+
};
5252

5353
export const createKubernetesCluster = async (
5454
projectId: string,
Lines changed: 62 additions & 205 deletions
Original file line numberDiff line numberDiff line change
@@ -1,208 +1,65 @@
1-
import { describe, it, expect } from 'vitest';
2-
import { z } from 'zod';
3-
import {
4-
compareFunction,
5-
formatIP,
6-
getFormatedKubeVersion,
7-
isIPValid,
8-
paginateResults,
9-
getColorByPercentage,
10-
camelToSnake,
11-
filterSchemaKeys,
12-
isBase64,
13-
parseCommaSeparated,
14-
generateUniqueName,
15-
} from '@/helpers/index';
16-
import { NodePool } from '@/api/data/kubernetes';
17-
18-
describe('helper', () => {
19-
it('compares two objects based on a key', () => {
20-
const obj1 = { name: 'Alice' };
21-
const obj2 = { name: 'Bob' };
22-
const result = compareFunction('name')(obj1, obj2);
23-
expect(result).toBeLessThan(0);
24-
});
25-
26-
it('paginates results correctly', () => {
27-
const items = Array.from({ length: 10 }, (_, i) => ({ id: i }));
28-
const pagination = { pageIndex: 1, pageSize: 5 };
29-
const result = paginateResults(items, pagination);
30-
expect(result.rows).toHaveLength(5);
31-
expect(result.pageCount).toBe(2);
32-
expect(result.totalRows).toBe(10);
33-
});
34-
35-
describe('getFormatedKubeVersion', () => {
36-
it.each([
37-
['1.32.1-1', '1.32'],
38-
['1.32', '1.32'],
39-
['1.32.0', '1.32'],
40-
['1.32.1', '1.32'],
41-
['1', '1'],
42-
])('retourne la version formatée pour %s', (version, expected) => {
43-
expect(getFormatedKubeVersion(version)).toBe(expected);
44-
});
45-
});
46-
47-
it('formats IP with default mask if not provided', () => {
48-
const ip = '192.168.1.1';
49-
const result = formatIP(ip);
50-
expect(result).toBe('192.168.1.1/32');
51-
});
52-
53-
it('formats IP with provided mask', () => {
54-
const ip = '192.168.1.1/24';
55-
const result = formatIP(ip);
56-
expect(result).toBe('192.168.1.1/24');
57-
});
58-
59-
it('validates correct IP without mask', () => {
60-
const ip = '192.168.1.1';
61-
const result = isIPValid(ip);
62-
expect(result).toBe(true);
63-
});
64-
65-
it('validates correct IP with mask', () => {
66-
const ip = '192.168.1.1/24';
67-
const result = isIPValid(ip);
68-
expect(result).toBe(true);
69-
});
70-
71-
it('invalidates incorrect IP', () => {
72-
const ip = '999.999.999.999';
73-
const result = isIPValid(ip);
74-
expect(result).toBe(false);
75-
});
76-
77-
it('invalidates IP with incorrect mask', () => {
78-
const ip = '192.168.1.1/999';
79-
const result = isIPValid(ip);
80-
expect(result).toBe(false);
81-
});
82-
});
83-
84-
describe('getColorByPercentage', () => {
85-
it('should return primary color for percentage <= 69', () => {
86-
expect(getColorByPercentage(50)).toBe('var(--ods-color-primary-500)');
87-
expect(getColorByPercentage(69)).toBe('var(--ods-color-primary-500)');
88-
});
89-
90-
it('should return warning color for percentage between 70 and 79', () => {
91-
expect(getColorByPercentage(75)).toBe('var(--ods-color-warning-500)');
92-
});
93-
94-
it('should return error color for percentage between 80 and 100', () => {
95-
expect(getColorByPercentage(85)).toBe('var(--ods-color-error-500)');
96-
expect(getColorByPercentage(100)).toBe('var(--ods-color-error-500)');
97-
});
98-
99-
it('should return last color in thresholds if percentage exceeds 100', () => {
100-
expect(getColorByPercentage(120)).toBe('var(--ods-color-error-500)');
101-
});
102-
});
103-
104-
describe('camelToSnake', () => {
105-
it('converts camelCase to snake_case', () => {
106-
expect(camelToSnake('camelCase')).toBe('camel_case');
107-
expect(camelToSnake('someLongVariableName')).toBe(
108-
'some_long_variable_name',
109-
);
110-
expect(camelToSnake('already_snake_case')).toBe('already_snake_case');
111-
});
112-
});
113-
114-
describe('filterSchemaKeys', () => {
115-
it('filters out keys from schema based on exclude list', () => {
116-
const schema = z.object({
117-
key1: z.string(),
118-
key2: z.number(),
119-
key3: z.boolean(),
120-
});
121-
const result = filterSchemaKeys(schema, ['key2']);
122-
expect(result).toEqual(['key1', 'key3']);
123-
});
124-
125-
it('returns all keys if exclude list is empty', () => {
126-
const schema = z.object({
127-
key1: z.string(),
128-
key2: z.number(),
129-
});
130-
const result = filterSchemaKeys(schema, []);
131-
expect(result).toEqual(['key1', 'key2']);
132-
});
133-
134-
it('returns no keys if all are excluded', () => {
135-
const schema = z.object({
136-
key1: z.string(),
137-
key2: z.number(),
1+
import { AutoscalingState } from '@/components/Autoscaling.component';
2+
import { KubeFlavor } from '@/components/flavor-selector/FlavorSelector.component';
3+
import { getPrice } from '.';
4+
5+
describe('getPrice', () => {
6+
const testCases = [
7+
{
8+
description: 'flavor et scaling with monthly',
9+
flavor: {
10+
pricingsHourly: { price: 0.1 },
11+
pricingsMonthly: { price: 50 },
12+
},
13+
scaling: {
14+
quantity: { desired: 2 },
15+
},
16+
expected: {
17+
hour: 0.2,
18+
month: 100,
19+
},
20+
},
21+
{
22+
description: 'flavor et scaling without monthly',
23+
flavor: {
24+
pricingsHourly: { price: 0.01 },
25+
pricingsMonthly: null,
26+
},
27+
scaling: {
28+
quantity: { desired: 3 },
29+
},
30+
expected: {
31+
hour: 0.03,
32+
month: undefined,
33+
},
34+
},
35+
{
36+
description: 'scaling null',
37+
flavor: {
38+
pricingsHourly: { price: 0.1 },
39+
pricingsMonthly: { price: 50 },
40+
},
41+
scaling: null,
42+
expected: undefined,
43+
},
44+
{
45+
description: 'flavor null',
46+
flavor: null,
47+
scaling: {
48+
quantity: { desired: 2 },
49+
},
50+
expected: undefined,
51+
},
52+
];
53+
54+
(testCases as {
55+
description: string;
56+
flavor: KubeFlavor | null;
57+
scaling: AutoscalingState | null;
58+
expected: { hour: number; month?: number } | undefined;
59+
}[]).forEach(({ description, flavor, scaling, expected }) => {
60+
it(`should return correct pricing when ${description}`, () => {
61+
const result = getPrice(flavor, scaling);
62+
expect(result).toEqual(expected);
13863
});
139-
const result = filterSchemaKeys(schema, ['key1', 'key2']);
140-
expect(result).toEqual([]);
141-
});
142-
});
143-
144-
describe('parseCommaSeparated', () => {
145-
it('parses a comma-separated string into an array', () => {
146-
expect(parseCommaSeparated('a,b,c')).toEqual(['a', 'b', 'c']);
147-
});
148-
149-
it('trims spaces around values', () => {
150-
expect(parseCommaSeparated(' a , b , c ')).toEqual(['a', 'b', 'c']);
15164
});
152-
153-
it('removes empty values', () => {
154-
expect(parseCommaSeparated('a,,b,c,,')).toEqual(['a', 'b', 'c']);
155-
});
156-
157-
it('handles arrays directly', () => {
158-
expect(parseCommaSeparated(['a', ' b ', 'c'])).toEqual(['a', 'b', 'c']);
159-
});
160-
161-
it('returns an empty array for undefined input', () => {
162-
expect(parseCommaSeparated(undefined)).toEqual([]);
163-
});
164-
});
165-
166-
describe('isBase64', () => {
167-
it('validates a correct Base64 string', () => {
168-
expect(isBase64('SGVsbG8gd29ybGQ=')).toBe(true);
169-
});
170-
171-
it('invalidates a non-Base64 string', () => {
172-
expect(isBase64('NotBase64')).toBe(false);
173-
});
174-
175-
it('invalidates a malformed Base64 string', () => {
176-
expect(isBase64('SGVsbG8gd29ybGQ')).toBe(false);
177-
});
178-
});
179-
180-
describe('generateUniqueName', () => {
181-
it.each([
182-
['NodePool-2', [{ name: 'NodePool-1' }], 'NodePool-2'],
183-
['NodePool-2', [{ name: 'NodePool-2' }], 'NodePool-2-1'],
184-
[
185-
'NodePool-2',
186-
[
187-
{ name: 'NodePool-2' },
188-
{ name: 'NodePool-2-1' },
189-
{ name: 'NodePool-2-2' },
190-
],
191-
'NodePool-2-3',
192-
],
193-
[
194-
'UniquePool',
195-
[{ name: 'AnotherPool' }, { name: 'NodePool-2' }],
196-
'UniquePool',
197-
],
198-
])(
199-
'should return %s for baseName "%s" with existing nodes %j',
200-
(baseName, existingNodePools, expectedResult) => {
201-
const result = generateUniqueName(
202-
baseName,
203-
existingNodePools as NodePool[],
204-
);
205-
expect(result).toBe(expectedResult);
206-
},
207-
);
20865
});

packages/manager/apps/pci-kubernetes/src/helpers/index.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import { ZodObject, ZodRawShape } from 'zod';
44
import { ClassValue, clsx } from 'clsx';
55
import { twMerge } from 'tailwind-merge';
66
import { DeploymentMode, SigningAlgorithms, TOidcProvider } from '@/types';
7-
import { NodePool } from '@/api/data/kubernetes';
7+
import { NodePool, NodePoolPrice } from '@/api/data/kubernetes';
8+
9+
import { KubeFlavor } from '@/components/flavor-selector/FlavorSelector.component';
10+
import { AutoscalingState } from '@/components/Autoscaling.component';
811

912
export const REFETCH_INTERVAL_DURATION = 15_000;
1013
export const QUOTA_ERROR_URL =
@@ -164,7 +167,7 @@ export const transformKey = (key: string): string =>
164167
key.replace(/([A-Z])/g, '_$1').toLowerCase();
165168

166169
const isNotEmptyString = (str: string): boolean => str.trim() !== '';
167-
const isNotEmptyArray = (arr: any[]): boolean => arr.length > 0;
170+
const isNotEmptyArray = (arr: unknown[]): boolean => arr.length > 0;
168171

169172
export const isOptionalValue = (
170173
value: string | string[] | SigningAlgorithms[] | null | undefined,
@@ -175,7 +178,7 @@ export const isOptionalValue = (
175178
return true;
176179
};
177180
export const getValidOptionalKeys = (oidcProvider: TOidcProvider) =>
178-
Object.entries(oidcProvider ?? {}).reduce((acc, [key, value]) => {
181+
Object.entries(oidcProvider ?? {}).reduce<string[]>((acc, [key, value]) => {
179182
if (isOptionalValue(value) && key !== 'issuerUrl' && key !== 'clientId') {
180183
acc.push(key);
181184
}
@@ -191,7 +194,7 @@ export const getValidOptionalKeys = (oidcProvider: TOidcProvider) =>
191194
*/
192195
export function generateUniqueName(
193196
baseName: string,
194-
existingNodePools: NodePool[],
197+
existingNodePools: NodePoolPrice[],
195198
) {
196199
let newName = baseName;
197200
let copyNumber = 1;
@@ -216,3 +219,16 @@ export const isMultiDeploymentZones = (type: DeploymentMode) =>
216219
type === DeploymentMode.MULTI_ZONES;
217220
export const isLocalDeploymentZone = (type: DeploymentMode) =>
218221
type === DeploymentMode.LOCAL_ZONE;
222+
223+
export const getPrice = (
224+
flavor?: KubeFlavor | null,
225+
scaling?: AutoscalingState | null,
226+
) => {
227+
if (!flavor || !scaling) return undefined;
228+
return {
229+
hour: (flavor.pricingsHourly?.price ?? 0) * scaling.quantity.desired,
230+
month: flavor.pricingsMonthly
231+
? flavor.pricingsMonthly.price * scaling.quantity.desired
232+
: undefined,
233+
};
234+
};

0 commit comments

Comments
 (0)