diff --git a/manifests/integrations/cloud-metadata-divide-boavizta.yml b/manifests/integrations/cloud-metadata-divide-boavizta.yml index f0a1f1d4a..cafd1467b 100644 --- a/manifests/integrations/cloud-metadata-divide-boavizta.yml +++ b/manifests/integrations/cloud-metadata-divide-boavizta.yml @@ -8,7 +8,7 @@ initialize: path: "@grnsft/if-plugins" divide: method: Divide - path: "@grnsft/if-plugins" + path: "builtin" global-config: numerator: vcpus-allocated denominator: 2 diff --git a/manifests/plugins/divide/failure-denominator-equal-zero.yml b/manifests/plugins/divide/failure-denominator-equal-zero.yml index 9a5f8d46a..af5ed2ae0 100644 --- a/manifests/plugins/divide/failure-denominator-equal-zero.yml +++ b/manifests/plugins/divide/failure-denominator-equal-zero.yml @@ -9,7 +9,7 @@ initialize: path: "@grnsft/if-plugins" divide: method: Divide - path: "@grnsft/if-plugins" + path: "builtin" global-config: numerator: vcpus-allocated denominator: 0 diff --git a/manifests/plugins/divide/failure-invalid-config-denominator.yml b/manifests/plugins/divide/failure-invalid-config-denominator.yml index a4bc1b218..063274bc0 100644 --- a/manifests/plugins/divide/failure-invalid-config-denominator.yml +++ b/manifests/plugins/divide/failure-invalid-config-denominator.yml @@ -9,7 +9,7 @@ initialize: path: "@grnsft/if-plugins" divide: method: Divide - path: "@grnsft/if-plugins" + path: "builtin" global-config: numerator: vcpus-allocated denominator: 'vcpus' diff --git a/manifests/plugins/divide/failure-missing-numerator.yml b/manifests/plugins/divide/failure-missing-numerator.yml index a15b49ebd..7f03d5eba 100644 --- a/manifests/plugins/divide/failure-missing-numerator.yml +++ b/manifests/plugins/divide/failure-missing-numerator.yml @@ -9,7 +9,7 @@ initialize: path: "@grnsft/if-plugins" divide: method: Divide - path: "@grnsft/if-plugins" + path: "builtin" global-config: #numerator: vcpus-allocated denominator: 2 diff --git a/manifests/plugins/divide/success.yml b/manifests/plugins/divide/success.yml index 86472671b..ed75a7c5c 100644 --- a/manifests/plugins/divide/success.yml +++ b/manifests/plugins/divide/success.yml @@ -9,7 +9,7 @@ initialize: path: "@grnsft/if-plugins" divide: method: Divide - path: "@grnsft/if-plugins" + path: "builtin" global-config: numerator: vcpus-allocated denominator: 2 diff --git a/src/__tests__/unit/builtins/divide.test.ts b/src/__tests__/unit/builtins/divide.test.ts new file mode 100644 index 000000000..8f56bb19d --- /dev/null +++ b/src/__tests__/unit/builtins/divide.test.ts @@ -0,0 +1,175 @@ +import {Divide} from '../../../builtins'; + +import {ERRORS} from '../../../util/errors'; + +const {InputValidationError, ConfigNotFoundError} = ERRORS; + +describe('builtins/divide: ', () => { + describe('Divide: ', () => { + const globalConfig = { + numerator: 'vcpus-allocated', + denominator: 2, + output: 'cpu/number-cores', + }; + const divide = Divide(globalConfig); + + describe('init: ', () => { + it('successfully initalized.', () => { + expect(divide).toHaveProperty('metadata'); + expect(divide).toHaveProperty('execute'); + }); + }); + + describe('execute(): ', () => { + it('successfully applies Divide strategy to given input.', async () => { + expect.assertions(1); + + const expectedResult = [ + { + duration: 3600, + 'vcpus-allocated': 24, + 'cpu/number-cores': 12, + timestamp: '2021-01-01T00:00:00Z', + }, + ]; + + const result = await divide.execute([ + { + duration: 3600, + 'vcpus-allocated': 24, + timestamp: '2021-01-01T00:00:00Z', + }, + ]); + + expect(result).toStrictEqual(expectedResult); + }); + + it('returns a result when `denominator` is provded in input.', async () => { + expect.assertions(1); + const globalConfig = { + numerator: 'vcpus-allocated', + denominator: 'duration', + output: 'vcpus-allocated-per-second', + }; + const divide = Divide(globalConfig); + + const input = [ + { + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + 'vcpus-allocated': 24, + }, + ]; + const response = await divide.execute(input); + + const expectedResult = [ + { + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + 'vcpus-allocated': 24, + 'vcpus-allocated-per-second': 24 / 3600, + }, + ]; + + expect(response).toEqual(expectedResult); + }); + + it('throws an error on missing params in input.', async () => { + const expectedMessage = + '"vcpus-allocated" parameter is required. Error code: invalid_type.'; + + const globalConfig = { + numerator: 'vcpus-allocated', + denominator: 3600, + output: 'vcpus-allocated-per-second', + }; + const divide = Divide(globalConfig); + + expect.assertions(1); + + try { + await divide.execute([ + { + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + }, + ]); + } catch (error) { + expect(error).toStrictEqual( + new InputValidationError(expectedMessage) + ); + } + }); + }); + + it('throws an error on missing global config.', async () => { + const expectedMessage = 'Global config is not provided.'; + const config = undefined; + const divide = Divide(config!); + + expect.assertions(1); + + try { + await divide.execute([ + { + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + }, + ]); + } catch (error) { + expect(error).toStrictEqual(new ConfigNotFoundError(expectedMessage)); + } + }); + + it('throws an error when `denominator` is 0.', async () => { + const expectedMessage = + '"denominator" parameter is number must be greater than 0. Error code: too_small.'; + + const globalConfig = { + numerator: 'vcpus-allocated', + denominator: 0, + output: 'vcpus-allocated-per-second', + }; + const divide = Divide(globalConfig); + + expect.assertions(1); + + try { + await divide.execute([ + { + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + 'vcpus-allocated': 24, + }, + ]); + } catch (error) { + expect(error).toStrictEqual(new InputValidationError(expectedMessage)); + } + }); + + it('throws an error when `denominator` is string.', async () => { + const expectedMessage = '`10` is missing from the input.'; + + const globalConfig = { + numerator: 'vcpus-allocated', + denominator: '10', + output: 'vcpus-allocated-per-second', + }; + const divide = Divide(globalConfig); + + expect.assertions(1); + + try { + await divide.execute([ + { + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + 'vcpus-allocated': 24, + }, + ]); + } catch (error) { + expect(error).toStrictEqual(new InputValidationError(expectedMessage)); + } + }); + }); +}); diff --git a/src/builtins/divide/README.md b/src/builtins/divide/README.md new file mode 100644 index 000000000..d403d0361 --- /dev/null +++ b/src/builtins/divide/README.md @@ -0,0 +1,95 @@ +# Divide + +`divide` is a generic plugin for doing arithmetic division of two values in an `input` array. + +You provide the names of the values you want to divide, and a name to use to add the divide to the output array. + +For example, `boavizta-cpu` need `cpu/number-cores` to work, however `cloud-metadata` returns `vcpus-allocated`, to get number of cores you divide `vcpus-allocated` by 2. + +## Parameters + +### Plugin config + +- `numerator` - a parameter by a specific configured number +- `denominator` - a parameter by a specific configured number or the number by which `numerator` is divided +- `output` - the number to a configured output parameter + +### Inputs + +- `numerator` - as input parameter, must be available in the input array +- `denominator` - must be available in the input array if is an input parameter +- `output` - as input parameter, must be available in the input array + +## Returns + +- `output`: the division of `numerator` with the parameter name into `denominator` with the parameter name defined by `output` in global config. + +The plugin throws an exception if the division result is not a number. + +## Calculation + +```pseudocode +output = input0 / input1 +``` + +## Implementation + +To run the plugin, you must first create an instance of `Divide`. Then, you can call `execute()`. + +```typescript +const globalConfig = { + numerator: 'vcpus-allocated', + denominator: 2, + output: 'cpu/number-cores', +}; +const divide = Divide(globalConfig); + +const input = [ + { + timestamp: '2021-01-01T00:00:00Z', + duration: 3600, + 'vcpus-allocated': 24, + }, +]; +``` + +## Example manifest + +IF users will typically call the plugin as part of a pipeline defined in a manifest file. In this case, instantiating the plugin is handled by `if` and does not have to be done explicitly by the user. The following is an example manifest that calls `divide`: + +```yaml +name: divide-demo +description: +tags: +initialize: + outputs: + - yaml + plugins: + divide: + method: Divide + path: 'builtin' + global-config: + numerator: vcpus-allocated + denominator: 2 + output: cpu/number-cores +tree: + children: + child: + pipeline: + - divide + config: + divide: + inputs: + - timestamp: 2023-08-06T00:00 + duration: 3600 + vcpus-allocated: 24 +``` + +You can run this example by saving it as `./examples/manifests/divide.yml` and executing the following command from the project root: + +```sh +npm i -g @grnsft/if +ie --manifest ./examples/manifests/divide.yml --output ./examples/outputs/divide.yml +``` + +The results will be saved to a new `yaml` file in `./examples/outputs`. diff --git a/src/builtins/divide/index.ts b/src/builtins/divide/index.ts new file mode 100644 index 000000000..b91d4c9fa --- /dev/null +++ b/src/builtins/divide/index.ts @@ -0,0 +1,91 @@ +import {z} from 'zod'; + +import {ERRORS} from '../../util/errors'; +import {validate} from '../../util/validations'; + +import {ExecutePlugin, PluginParams, ConfigParams} from '../../types/interface'; + +const {InputValidationError, ConfigNotFoundError} = ERRORS; + +export const Divide = (globalConfig: ConfigParams): ExecutePlugin => { + const metadata = { + kind: 'execute', + }; + + /** + * Calculate the division of each input parameter. + */ + const execute = (inputs: PluginParams[]) => { + const safeGlobalConfig = validateGlobalConfig(); + const {numerator, denominator, output} = safeGlobalConfig; + + return inputs.map(input => { + const safeInput = Object.assign( + {}, + input, + validateSingleInput(input, numerator, denominator) + ); + + return { + ...input, + [output]: calculateDivide(safeInput, numerator, denominator), + }; + }); + }; + + /** + * Checks global config value are valid. + */ + const validateGlobalConfig = () => { + if (!globalConfig) { + throw new ConfigNotFoundError('Global config is not provided.'); + } + + const schema = z.object({ + numerator: z.string().min(1), + denominator: z.string().or(z.number().gt(0)), + output: z.string(), + }); + + return validate>(schema, globalConfig); + }; + + /** + * Checks for required fields in input. + */ + const validateSingleInput = ( + input: PluginParams, + numerator: string, + denominator: number | string + ) => { + const schema = z + .object({ + [numerator]: z.number(), + [denominator]: z.number().optional(), + }) + .refine(() => { + if (typeof denominator === 'string' && !input[denominator]) { + throw new InputValidationError( + `\`${denominator}\` is missing from the input.` + ); + } + return true; + }); + + return validate>(schema, input); + }; + + /** + * Calculates the division of the given parameter. + */ + const calculateDivide = ( + input: PluginParams, + numerator: string, + denominator: number | string + ) => input[numerator] / (input[denominator] || denominator); + + return { + metadata, + execute, + }; +}; diff --git a/src/builtins/index.ts b/src/builtins/index.ts index 5efcded90..492713ee5 100644 --- a/src/builtins/index.ts +++ b/src/builtins/index.ts @@ -1,5 +1,6 @@ export {GroupBy} from './group-by'; export {TimeSync} from './time-sync'; +export {Divide} from './divide'; export {Subtract} from './subtract'; export {Coefficient} from './coefficient'; export {Multiply} from './multiply';