Skip to content

Commit

Permalink
[APM] Agent remote config: validation for Java agent configs (#63956)
Browse files Browse the repository at this point in the history
* validating java settings

* adding min max support to duration

* Agent config cleanup

* refactoring

* refactoring

* refactoring

* fixing i18n

* validating min and max bytes

* refactoring

* refactoring

* refactoring

* accept number and string on amountAndUnitToString

Co-authored-by: Elastic Machine <[email protected]>
Co-authored-by: Søren Louv-Jansen <[email protected]>
  • Loading branch information
3 people authored May 4, 2020
1 parent a532a91 commit f8349f6
Show file tree
Hide file tree
Showing 22 changed files with 539 additions and 344 deletions.
11 changes: 7 additions & 4 deletions x-pack/plugins/apm/common/agent_configuration/amount_and_unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,20 @@
* you may not use this file except in compliance with the Elastic License.
*/

interface AmountAndUnit {
amount: string;
export interface AmountAndUnit {
amount: number;
unit: string;
}

export function amountAndUnitToObject(value: string): AmountAndUnit {
// matches any postive and negative number and its unit.
const [, amount = '', unit = ''] = value.match(/(^-?\d+)?(\w+)?/) || [];
return { amount, unit };
return { amount: parseInt(amount, 10), unit };
}

export function amountAndUnitToString({ amount, unit }: AmountAndUnit) {
export function amountAndUnitToString({
amount,
unit
}: Omit<AmountAndUnit, 'amount'> & { amount: string | number }) {
return `${amount}${unit}`;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@

import * as t from 'io-ts';
import { settingDefinitions } from '../setting_definitions';
import { SettingValidation } from '../setting_definitions/types';

// retrieve validation from config definitions settings and validate on the server
const knownSettings = settingDefinitions.reduce<
// TODO: is it possible to get rid of any?
Record<string, t.Type<any, string, unknown>>
Record<string, SettingValidation>
>((acc, { key, validation }) => {
acc[key] = validation;
return acc;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,88 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { bytesRt } from './bytes_rt';
import { getBytesRt } from './bytes_rt';
import { isRight } from 'fp-ts/lib/Either';
import { PathReporter } from 'io-ts/lib/PathReporter';

describe('bytesRt', () => {
describe('it should not accept', () => {
[
undefined,
null,
'',
0,
'foo',
true,
false,
'100',
'mb',
'0kb',
'5gb',
'6tb'
].map(input => {
it(`${JSON.stringify(input)}`, () => {
expect(isRight(bytesRt.decode(input))).toBe(false);
describe('must accept any amount and unit', () => {
const bytesRt = getBytesRt({});
describe('it should not accept', () => {
['mb', 1, '1', '5gb', '6tb'].map(input => {
it(`${JSON.stringify(input)}`, () => {
expect(isRight(bytesRt.decode(input))).toBe(false);
});
});
});

describe('it should accept', () => {
['-1b', '0mb', '1b', '2kb', '3mb', '1000mb'].map(input => {
it(`${JSON.stringify(input)}`, () => {
expect(isRight(bytesRt.decode(input))).toBe(true);
});
});
});
});
describe('must be at least 0b', () => {
const bytesRt = getBytesRt({
min: '0b'
});

describe('it should not accept', () => {
['mb', '-1kb', '5gb', '6tb'].map(input => {
it(`${JSON.stringify(input)}`, () => {
expect(isRight(bytesRt.decode(input))).toBe(false);
});
});
});

describe('it should return correct error message', () => {
['-1kb', '5gb', '6tb'].map(input => {
it(`${JSON.stringify(input)}`, () => {
const result = bytesRt.decode(input);
const message = PathReporter.report(result)[0];
expect(message).toEqual('Must be greater than 0b');
expect(isRight(result)).toBeFalsy();
});
});
});

describe('it should accept', () => {
['1b', '2kb', '3mb'].map(input => {
it(`${JSON.stringify(input)}`, () => {
expect(isRight(bytesRt.decode(input))).toBe(true);
describe('it should accept', () => {
['1b', '2kb', '3mb'].map(input => {
it(`${JSON.stringify(input)}`, () => {
expect(isRight(bytesRt.decode(input))).toBe(true);
});
});
});
});
describe('must be between 500b and 1kb', () => {
const bytesRt = getBytesRt({
min: '500b',
max: '1kb'
});
describe('it should not accept', () => {
['mb', '-1b', '1b', '499b', '1025b', '2kb', '1mb'].map(input => {
it(`${JSON.stringify(input)}`, () => {
expect(isRight(bytesRt.decode(input))).toBe(false);
});
});
});
describe('it should return correct error message', () => {
['-1b', '1b', '499b', '1025b', '2kb', '1mb'].map(input => {
it(`${JSON.stringify(input)}`, () => {
const result = bytesRt.decode(input);
const message = PathReporter.report(result)[0];
expect(message).toEqual('Must be between 500b and 1kb');
expect(isRight(result)).toBeFalsy();
});
});
});
describe('it should accept', () => {
['500b', '1024b', '1kb'].map(input => {
it(`${JSON.stringify(input)}`, () => {
expect(isRight(bytesRt.decode(input))).toBe(true);
});
});
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,50 @@
import * as t from 'io-ts';
import { either } from 'fp-ts/lib/Either';
import { amountAndUnitToObject } from '../amount_and_unit';
import { getRangeTypeMessage } from './get_range_type_message';

export const BYTE_UNITS = ['b', 'kb', 'mb'];
function toBytes(amount: number, unit: string) {
switch (unit) {
case 'b':
return amount;
case 'kb':
return amount * 2 ** 10;
case 'mb':
return amount * 2 ** 20;
}
}

export const bytesRt = new t.Type<string, string, unknown>(
'bytesRt',
t.string.is,
(input, context) => {
return either.chain(t.string.validate(input, context), inputAsString => {
const { amount, unit } = amountAndUnitToObject(inputAsString);
const amountAsInt = parseInt(amount, 10);
const isValidUnit = BYTE_UNITS.includes(unit);
const isValid = amountAsInt > 0 && isValidUnit;
function amountAndUnitToBytes(value?: string): number | undefined {
if (value) {
const { amount, unit } = amountAndUnitToObject(value);
if (isFinite(amount) && unit) {
return toBytes(amount, unit);
}
}
}

return isValid
? t.success(inputAsString)
: t.failure(
input,
context,
`Must have numeric amount and a valid unit (${BYTE_UNITS})`
);
});
},
t.identity
);
export function getBytesRt({ min, max }: { min?: string; max?: string }) {
const minAsBytes = amountAndUnitToBytes(min) ?? -Infinity;
const maxAsBytes = amountAndUnitToBytes(max) ?? Infinity;
const message = getRangeTypeMessage(min, max);

return new t.Type<string, string, unknown>(
'bytesRt',
t.string.is,
(input, context) => {
return either.chain(t.string.validate(input, context), inputAsString => {
const inputAsBytes = amountAndUnitToBytes(inputAsString);

const isValidAmount =
inputAsBytes !== undefined &&
inputAsBytes >= minAsBytes &&
inputAsBytes <= maxAsBytes;

return isValidAmount
? t.success(inputAsString)
: t.failure(input, context, message);
});
},
t.identity
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,62 +4,122 @@
* you may not use this file except in compliance with the Elastic License.
*/

/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { durationRt, getDurationRt } from './duration_rt';
import { getDurationRt } from './duration_rt';
import { isRight } from 'fp-ts/lib/Either';
import { PathReporter } from 'io-ts/lib/PathReporter';

describe('durationRt', () => {
describe('it should not accept', () => {
[
undefined,
null,
'',
0,
'foo',
true,
false,
'100',
's',
'm',
'0ms',
'-1ms'
].map(input => {
it(`${JSON.stringify(input)}`, () => {
expect(isRight(durationRt.decode(input))).toBe(false);
describe('getDurationRt', () => {
describe('must be at least 1m', () => {
const customDurationRt = getDurationRt({ min: '1m' });
describe('it should not accept', () => {
[
undefined,
null,
'',
0,
'foo',
true,
false,
'0m',
'-1m',
'1ms',
'1s'
].map(input => {
it(`${JSON.stringify(input)}`, () => {
expect(isRight(customDurationRt.decode(input))).toBeFalsy();
});
});
});
});

describe('it should accept', () => {
['1000ms', '2s', '3m', '1s'].map(input => {
it(`${JSON.stringify(input)}`, () => {
expect(isRight(durationRt.decode(input))).toBe(true);
describe('it should return correct error message', () => {
['0m', '-1m', '1ms', '1s'].map(input => {
it(`${JSON.stringify(input)}`, () => {
const result = customDurationRt.decode(input);
const message = PathReporter.report(result)[0];
expect(message).toEqual('Must be greater than 1m');
expect(isRight(result)).toBeFalsy();
});
});
});
describe('it should accept', () => {
['1m', '2m', '1000m'].map(input => {
it(`${JSON.stringify(input)}`, () => {
expect(isRight(customDurationRt.decode(input))).toBeTruthy();
});
});
});
});
});

describe('getDurationRt', () => {
const customDurationRt = getDurationRt({ min: -1 });
describe('it should not accept', () => {
[undefined, null, '', 0, 'foo', true, false, '100', 's', 'm', '-2ms'].map(
input => {
describe('must be between 1ms and 1s', () => {
const customDurationRt = getDurationRt({ min: '1ms', max: '1s' });

describe('it should not accept', () => {
[
undefined,
null,
'',
0,
'foo',
true,
false,
'-1s',
'0s',
'2s',
'1001ms',
'0ms',
'-1ms',
'0m',
'1m'
].map(input => {
it(`${JSON.stringify(input)}`, () => {
expect(isRight(customDurationRt.decode(input))).toBeFalsy();
});
});
});
describe('it should return correct error message', () => {
['-1s', '0s', '2s', '1001ms', '0ms', '-1ms', '0m', '1m'].map(input => {
it(`${JSON.stringify(input)}`, () => {
const result = customDurationRt.decode(input);
const message = PathReporter.report(result)[0];
expect(message).toEqual('Must be between 1ms and 1s');
expect(isRight(result)).toBeFalsy();
});
});
});
describe('it should accept', () => {
['1s', '1ms', '50ms', '1000ms'].map(input => {
it(`${JSON.stringify(input)}`, () => {
expect(isRight(customDurationRt.decode(input))).toBe(false);
expect(isRight(customDurationRt.decode(input))).toBeTruthy();
});
}
);
});
});
});
describe('must be max 1m', () => {
const customDurationRt = getDurationRt({ max: '1m' });

describe('it should accept', () => {
['1000ms', '2s', '3m', '1s', '-1s', '0ms'].map(input => {
it(`${JSON.stringify(input)}`, () => {
expect(isRight(customDurationRt.decode(input))).toBe(true);
describe('it should not accept', () => {
[undefined, null, '', 0, 'foo', true, false, '2m', '61s', '60001ms'].map(
input => {
it(`${JSON.stringify(input)}`, () => {
expect(isRight(customDurationRt.decode(input))).toBeFalsy();
});
}
);
});
describe('it should return correct error message', () => {
['2m', '61s', '60001ms'].map(input => {
it(`${JSON.stringify(input)}`, () => {
const result = customDurationRt.decode(input);
const message = PathReporter.report(result)[0];
expect(message).toEqual('Must be less than 1m');
expect(isRight(result)).toBeFalsy();
});
});
});
describe('it should accept', () => {
['1m', '0m', '-1m', '60s', '6000ms', '1ms', '1s'].map(input => {
it(`${JSON.stringify(input)}`, () => {
expect(isRight(customDurationRt.decode(input))).toBeTruthy();
});
});
});
});
Expand Down
Loading

0 comments on commit f8349f6

Please sign in to comment.