mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[asset_manager] Add io-ts validations to asset endpoints (#157502)
closes https://github.com/elastic/kibana/issues/154147 ## Summary This PR introduces `io-ts` runtime validation to asset manager endpoints. The parameters will be validated as follows: - `maxDistance`: range from 1 to 5 - `size`: range from 1 to 100 - `type`: `k8s.pod` or `k8s.cluster` or `k8s.node` - `relation`: `ancestors` or `descendants` or `references` - any date parameter: Date ISO or DateMath formats ### How to test this PR - enable `xpack.assetManager.alphaEnabled` in kibana.dev.yml - Call the endpoints below and try to pass invalid parameters where they're applicable: - `GET kbn:/api/asset-manager/assets?from=2023-03-25T17:44:44.000Z&to=2023-03-25T18:44:44.000Z` - `GET kbn:/api/asset-manager/assets/diff?aFrom=2022-02-07T00:00:00.000Z&aTo=2022-02-07T01:30:00.000Z&bFrom=2022-02-07T01:00:00.000Z&bTo=2022-02-07T02:00:00.000Z` - `GET kbn:/api/asset-manager/assets/related?ean=k8s.node:node-101&relation=ancestors&maxDistance=1&from=2023-04-18T13:10:13.111Z&to=2023-04-18T15:10:13.111Z` --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
b3561b9328
commit
bc041fd467
17 changed files with 798 additions and 498 deletions
|
@ -21,10 +21,12 @@ export { toJsonSchema } from './src/to_json_schema';
|
|||
export { nonEmptyStringRt } from './src/non_empty_string_rt';
|
||||
export { createLiteralValueFromUndefinedRT } from './src/literal_value_from_undefined_rt';
|
||||
export { createRouteValidationFunction } from './src/route_validation';
|
||||
export { inRangeRt, type InRangeBrand, type InRange } from './src/in_range_rt';
|
||||
export { inRangeRt, type InRangeBrand, type InRange, inRangeFromStringRt } from './src/in_range_rt';
|
||||
export { dateRt } from './src/date_rt';
|
||||
export {
|
||||
isGreaterOrEqualRt,
|
||||
type IsGreaterOrEqualBrand,
|
||||
type IsGreaterOrEqual,
|
||||
} from './src/is_greater_or_equal';
|
||||
|
||||
export { datemathStringRt } from './src/datemath_string_rt';
|
||||
|
|
|
@ -10,15 +10,15 @@ import { dateRt } from '.';
|
|||
import { isRight } from 'fp-ts/lib/Either';
|
||||
|
||||
describe('dateRt', () => {
|
||||
it('passes if it is a valide date/time', () => {
|
||||
it('passes if it is a valid date/time', () => {
|
||||
expect(isRight(dateRt.decode('2019-08-20T11:18:31.407Z'))).toBe(true);
|
||||
});
|
||||
|
||||
it('passes if it is a valide date', () => {
|
||||
it('passes if it is a valid date', () => {
|
||||
expect(isRight(dateRt.decode('2019-08-20'))).toBe(true);
|
||||
});
|
||||
|
||||
it('fails if it is an invalide date/time', () => {
|
||||
it('fails if it is an invalid date/time', () => {
|
||||
expect(isRight(dateRt.decode('2019-02-30T11:18:31.407Z'))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { datemathStringRt } from '.';
|
||||
import { isRight } from 'fp-ts/lib/Either';
|
||||
|
||||
describe('datemathStringRt', () => {
|
||||
it('passes if it is a valid dateMath', () => {
|
||||
expect(isRight(datemathStringRt.decode('now'))).toBe(true);
|
||||
});
|
||||
|
||||
it('fails if it is an invalid dateMath', () => {
|
||||
expect(isRight(datemathStringRt.decode('now-1l'))).toBe(false);
|
||||
});
|
||||
|
||||
it('fails if it is a ISO date', () => {
|
||||
expect(isRight(datemathStringRt.decode('2019-02-30T11:18:31.407Z'))).toBe(false);
|
||||
});
|
||||
|
||||
it('fails if it is a letter', () => {
|
||||
expect(isRight(datemathStringRt.decode('a'))).toBe(false);
|
||||
});
|
||||
});
|
29
packages/kbn-io-ts-utils/src/datemath_string_rt/index.ts
Normal file
29
packages/kbn-io-ts-utils/src/datemath_string_rt/index.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import * as rt from 'io-ts';
|
||||
import dateMath from '@kbn/datemath';
|
||||
import { chain } from 'fp-ts/lib/Either';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
|
||||
const isValidDatemath = (value: string): boolean => {
|
||||
const parsedValue = dateMath.parse(value);
|
||||
return !!(parsedValue && parsedValue.isValid());
|
||||
};
|
||||
|
||||
export const datemathStringRt = new rt.Type<string, string, unknown>(
|
||||
'datemath',
|
||||
rt.string.is,
|
||||
(value, context) =>
|
||||
pipe(
|
||||
rt.string.validate(value, context),
|
||||
chain((stringValue) =>
|
||||
isValidDatemath(stringValue) ? rt.success(stringValue) : rt.failure(stringValue, context)
|
||||
)
|
||||
),
|
||||
String
|
||||
);
|
|
@ -7,6 +7,7 @@
|
|||
*/
|
||||
|
||||
import * as rt from 'io-ts';
|
||||
import { toNumberRt } from '../to_number_rt';
|
||||
|
||||
export interface InRangeBrand {
|
||||
readonly InRange: unique symbol;
|
||||
|
@ -21,3 +22,7 @@ export const inRangeRt = (start: number, end: number) =>
|
|||
// refinement of the number type
|
||||
'InRange' // name of this codec
|
||||
);
|
||||
|
||||
export const inRangeFromStringRt = (start: number, end: number) => {
|
||||
return toNumberRt.pipe(inRangeRt(start, end));
|
||||
};
|
||||
|
|
|
@ -12,7 +12,8 @@
|
|||
],
|
||||
"kbn_references": [
|
||||
"@kbn/config-schema",
|
||||
"@kbn/core"
|
||||
"@kbn/core",
|
||||
"@kbn/datemath"
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -5,14 +5,15 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import * as rt from 'io-ts';
|
||||
|
||||
export const assetTypeRT = schema.oneOf([
|
||||
schema.literal('k8s.pod'),
|
||||
schema.literal('k8s.cluster'),
|
||||
schema.literal('k8s.node'),
|
||||
export const assetTypeRT = rt.union([
|
||||
rt.literal('k8s.pod'),
|
||||
rt.literal('k8s.cluster'),
|
||||
rt.literal('k8s.node'),
|
||||
]);
|
||||
export type AssetType = typeof assetTypeRT.type;
|
||||
|
||||
export type AssetType = rt.TypeOf<typeof assetTypeRT>;
|
||||
|
||||
export type AssetKind = 'unknown' | 'node';
|
||||
export type AssetStatus =
|
||||
|
@ -130,13 +131,13 @@ export interface AssetFilters {
|
|||
to?: string;
|
||||
}
|
||||
|
||||
export const relationRT = schema.oneOf([
|
||||
schema.literal('ancestors'),
|
||||
schema.literal('descendants'),
|
||||
schema.literal('references'),
|
||||
export const relationRT = rt.union([
|
||||
rt.literal('ancestors'),
|
||||
rt.literal('descendants'),
|
||||
rt.literal('references'),
|
||||
]);
|
||||
|
||||
export type Relation = typeof relationRT.type;
|
||||
export type Relation = rt.TypeOf<typeof relationRT>;
|
||||
export type RelationField = keyof Pick<
|
||||
Asset,
|
||||
'asset.children' | 'asset.parents' | 'asset.references'
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { QueryDslQueryContainer, SearchRequest } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { debug } from '../../common/debug_log';
|
||||
import { Asset, AssetFilters } from '../../common/types_api';
|
||||
|
@ -26,6 +25,66 @@ export async function getAssets({
|
|||
// Maybe it makes the most sense to validate the filters here?
|
||||
|
||||
const { from = 'now-24h', to = 'now' } = filters;
|
||||
const must: QueryDslQueryContainer[] = [];
|
||||
|
||||
if (filters && Object.keys(filters).length > 0) {
|
||||
if (typeof filters.collectionVersion === 'number') {
|
||||
must.push({
|
||||
term: {
|
||||
['asset.collection_version']: filters.collectionVersion,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (filters.type?.length) {
|
||||
must.push({
|
||||
terms: {
|
||||
['asset.type']: Array.isArray(filters.type) ? filters.type : [filters.type],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (filters.kind) {
|
||||
must.push({
|
||||
term: {
|
||||
['asset.kind']: filters.kind,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (filters.ean) {
|
||||
must.push({
|
||||
terms: {
|
||||
['asset.ean']: Array.isArray(filters.ean) ? filters.ean : [filters.ean],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (filters.id) {
|
||||
must.push({
|
||||
term: {
|
||||
['asset.id']: filters.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (filters.typeLike) {
|
||||
must.push({
|
||||
wildcard: {
|
||||
['asset.type']: filters.typeLike,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (filters.eanLike) {
|
||||
must.push({
|
||||
wildcard: {
|
||||
['asset.ean']: filters.eanLike,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const dsl: SearchRequest = {
|
||||
index: ASSETS_INDEX_PREFIX + '*',
|
||||
size,
|
||||
|
@ -41,6 +100,7 @@ export async function getAssets({
|
|||
},
|
||||
},
|
||||
],
|
||||
must,
|
||||
},
|
||||
},
|
||||
collapse: {
|
||||
|
@ -53,74 +113,8 @@ export async function getAssets({
|
|||
},
|
||||
};
|
||||
|
||||
if (filters && Object.keys(filters).length > 0) {
|
||||
const musts: QueryDslQueryContainer[] = [];
|
||||
|
||||
if (typeof filters.collectionVersion === 'number') {
|
||||
musts.push({
|
||||
term: {
|
||||
['asset.collection_version']: filters.collectionVersion,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (filters.type?.length) {
|
||||
musts.push({
|
||||
terms: {
|
||||
['asset.type']: Array.isArray(filters.type) ? filters.type : [filters.type],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (filters.kind) {
|
||||
musts.push({
|
||||
term: {
|
||||
['asset.kind']: filters.kind,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (filters.ean) {
|
||||
musts.push({
|
||||
terms: {
|
||||
['asset.ean']: Array.isArray(filters.ean) ? filters.ean : [filters.ean],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (filters.id) {
|
||||
musts.push({
|
||||
term: {
|
||||
['asset.id']: filters.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (filters.typeLike) {
|
||||
musts.push({
|
||||
wildcard: {
|
||||
['asset.type']: filters.typeLike,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (filters.eanLike) {
|
||||
musts.push({
|
||||
wildcard: {
|
||||
['asset.ean']: filters.eanLike,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (musts.length > 0) {
|
||||
dsl.query = dsl.query || {};
|
||||
dsl.query.bool = dsl.query.bool || {};
|
||||
dsl.query.bool.must = musts;
|
||||
}
|
||||
}
|
||||
|
||||
debug('Performing Asset Query', '\n\n', JSON.stringify(dsl, null, 2));
|
||||
|
||||
const response = await esClient.search<{}>(dsl);
|
||||
return response.hits.hits.map((hit) => hit._source as Asset);
|
||||
const response = await esClient.search<Asset>(dsl);
|
||||
return response.hits.hits.map((hit) => hit._source).filter((asset): asset is Asset => !!asset);
|
||||
}
|
||||
|
|
|
@ -90,8 +90,8 @@ export async function getRelatedAssets({
|
|||
|
||||
debug('Performing Asset Query', '\n\n', JSON.stringify(dsl, null, 2));
|
||||
|
||||
const response = await esClient.search(dsl);
|
||||
return response.hits.hits.map((hit) => hit._source as Asset);
|
||||
const response = await esClient.search<Asset>(dsl);
|
||||
return response.hits.hits.map((hit) => hit._source).filter((asset): asset is Asset => !!asset);
|
||||
}
|
||||
|
||||
function relationToIndirectField(relation: Relation): RelationField {
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import moment from 'moment';
|
||||
|
||||
export function toArray<T>(maybeArray: T | T[] | undefined): T[] {
|
||||
if (!maybeArray) {
|
||||
return [];
|
||||
|
@ -14,3 +16,10 @@ export function toArray<T>(maybeArray: T | T[] | undefined): T[] {
|
|||
}
|
||||
return [maybeArray];
|
||||
}
|
||||
|
||||
export const isValidRange = (from: string, to: string): boolean => {
|
||||
if (moment(from).isAfter(to)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
|
|
@ -5,9 +5,16 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { RequestHandlerContext } from '@kbn/core/server';
|
||||
import { differenceBy, intersectionBy } from 'lodash';
|
||||
import * as rt from 'io-ts';
|
||||
import {
|
||||
dateRt,
|
||||
inRangeFromStringRt,
|
||||
datemathStringRt,
|
||||
createRouteValidationFunction,
|
||||
createLiteralValueFromUndefinedRT,
|
||||
} from '@kbn/io-ts-utils';
|
||||
import { debug } from '../../common/debug_log';
|
||||
import { AssetType, assetTypeRT, relationRT } from '../../common/types_api';
|
||||
import { ASSET_MANAGER_API_BASE } from '../constants';
|
||||
|
@ -16,47 +23,61 @@ import { getAllRelatedAssets } from '../lib/get_all_related_assets';
|
|||
import { SetupRouteOptions } from './types';
|
||||
import { getEsClientFromContext } from './utils';
|
||||
import { AssetNotFoundError } from '../lib/errors';
|
||||
import { toArray } from '../lib/utils';
|
||||
import { isValidRange, toArray } from '../lib/utils';
|
||||
|
||||
const assetType = schema.oneOf([
|
||||
schema.literal('k8s.pod'),
|
||||
schema.literal('k8s.cluster'),
|
||||
schema.literal('k8s.node'),
|
||||
]);
|
||||
const sizeRT = rt.union([inRangeFromStringRt(1, 100), createLiteralValueFromUndefinedRT(10)]);
|
||||
const assetDateRT = rt.union([dateRt, datemathStringRt]);
|
||||
const getAssetsQueryOptionsRT = rt.exact(
|
||||
rt.partial({
|
||||
from: assetDateRT,
|
||||
to: assetDateRT,
|
||||
type: rt.union([rt.array(assetTypeRT), assetTypeRT]),
|
||||
ean: rt.union([rt.array(rt.string), rt.string]),
|
||||
size: sizeRT,
|
||||
})
|
||||
);
|
||||
|
||||
const getAssetsQueryOptions = schema.object({
|
||||
from: schema.maybe(schema.string()),
|
||||
to: schema.maybe(schema.string()),
|
||||
type: schema.maybe(schema.oneOf([schema.arrayOf(assetTypeRT), assetTypeRT])),
|
||||
ean: schema.maybe(schema.oneOf([schema.arrayOf(schema.string()), schema.string()])),
|
||||
size: schema.maybe(schema.number()),
|
||||
});
|
||||
const getAssetsDiffQueryOptionsRT = rt.exact(
|
||||
rt.intersection([
|
||||
rt.type({
|
||||
aFrom: assetDateRT,
|
||||
aTo: assetDateRT,
|
||||
bFrom: assetDateRT,
|
||||
bTo: assetDateRT,
|
||||
}),
|
||||
rt.partial({
|
||||
type: rt.union([rt.array(assetTypeRT), assetTypeRT]),
|
||||
}),
|
||||
])
|
||||
);
|
||||
|
||||
const getAssetsDiffQueryOptions = schema.object({
|
||||
aFrom: schema.string(),
|
||||
aTo: schema.string(),
|
||||
bFrom: schema.string(),
|
||||
bTo: schema.string(),
|
||||
type: schema.maybe(schema.oneOf([schema.arrayOf(assetType), assetType])),
|
||||
});
|
||||
const getRelatedAssetsQueryOptionsRT = rt.exact(
|
||||
rt.intersection([
|
||||
rt.type({
|
||||
from: assetDateRT,
|
||||
ean: rt.string,
|
||||
relation: relationRT,
|
||||
size: sizeRT,
|
||||
maxDistance: rt.union([inRangeFromStringRt(1, 5), createLiteralValueFromUndefinedRT(1)]),
|
||||
}),
|
||||
rt.partial({
|
||||
to: assetDateRT,
|
||||
type: rt.union([rt.array(assetTypeRT), assetTypeRT]),
|
||||
}),
|
||||
])
|
||||
);
|
||||
|
||||
const getRelatedAssetsQueryOptions = schema.object({
|
||||
from: schema.string(), // ISO timestamp or ES datemath
|
||||
to: schema.maybe(schema.string()), // ISO timestamp or ES datemath
|
||||
ean: schema.string(),
|
||||
relation: relationRT,
|
||||
type: schema.maybe(schema.oneOf([assetTypeRT, schema.arrayOf(assetTypeRT)])),
|
||||
maxDistance: schema.maybe(schema.number()),
|
||||
size: schema.maybe(schema.number()),
|
||||
});
|
||||
export type GetAssetsQueryOptions = rt.TypeOf<typeof getAssetsQueryOptionsRT>;
|
||||
export type GetRelatedAssetsQueryOptions = rt.TypeOf<typeof getRelatedAssetsQueryOptionsRT>;
|
||||
export type GetAssetsDiffQueryOptions = rt.TypeOf<typeof getAssetsDiffQueryOptionsRT>;
|
||||
|
||||
export function assetsRoutes<T extends RequestHandlerContext>({ router }: SetupRouteOptions<T>) {
|
||||
// GET /assets
|
||||
router.get<unknown, typeof getAssetsQueryOptions.type, unknown>(
|
||||
router.get<unknown, GetAssetsQueryOptions, unknown>(
|
||||
{
|
||||
path: `${ASSET_MANAGER_API_BASE}/assets`,
|
||||
validate: {
|
||||
query: getAssetsQueryOptions,
|
||||
query: createRouteValidationFunction(getAssetsQueryOptionsRT),
|
||||
},
|
||||
},
|
||||
async (context, req, res) => {
|
||||
|
@ -81,24 +102,26 @@ export function assetsRoutes<T extends RequestHandlerContext>({ router }: SetupR
|
|||
);
|
||||
|
||||
// GET assets/related
|
||||
router.get<unknown, typeof getRelatedAssetsQueryOptions.type, unknown>(
|
||||
router.get<unknown, GetRelatedAssetsQueryOptions, unknown>(
|
||||
{
|
||||
path: `${ASSET_MANAGER_API_BASE}/assets/related`,
|
||||
validate: {
|
||||
query: getRelatedAssetsQueryOptions,
|
||||
query: createRouteValidationFunction(getRelatedAssetsQueryOptionsRT),
|
||||
},
|
||||
},
|
||||
async (context, req, res) => {
|
||||
// Add references into sample data and write integration tests
|
||||
|
||||
const { from, to, ean, relation } = req.query || {};
|
||||
const { from, to, ean, relation, maxDistance, size } = req.query || {};
|
||||
const esClient = await getEsClientFromContext(context);
|
||||
|
||||
// What if maxDistance is below 1?
|
||||
const maxDistance = req.query.maxDistance ? Math.min(req.query.maxDistance, 5) : 1; // Validate maxDistance not larger than 5
|
||||
const size = req.query.size ? Math.min(req.query.size, 100) : 10; // Do we need pagination and sorting? Yes.
|
||||
const type = toArray<AssetType>(req.query.type);
|
||||
// Validate from and to to be ISO string only. Or use io-ts to coerce.
|
||||
|
||||
if (to && !isValidRange(from, to)) {
|
||||
return res.badRequest({
|
||||
body: `Time range cannot move backwards in time. "to" (${to}) is before "from" (${from}).`,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
return res.ok({
|
||||
|
@ -125,24 +148,24 @@ export function assetsRoutes<T extends RequestHandlerContext>({ router }: SetupR
|
|||
);
|
||||
|
||||
// GET /assets/diff
|
||||
router.get<unknown, typeof getAssetsDiffQueryOptions.type, unknown>(
|
||||
router.get<unknown, GetAssetsDiffQueryOptions, unknown>(
|
||||
{
|
||||
path: `${ASSET_MANAGER_API_BASE}/assets/diff`,
|
||||
validate: {
|
||||
query: getAssetsDiffQueryOptions,
|
||||
query: createRouteValidationFunction(getAssetsDiffQueryOptionsRT),
|
||||
},
|
||||
},
|
||||
async (context, req, res) => {
|
||||
const { aFrom, aTo, bFrom, bTo } = req.query;
|
||||
const type = toArray<AssetType>(req.query.type);
|
||||
|
||||
if (new Date(aFrom) > new Date(aTo)) {
|
||||
if (!isValidRange(aFrom, aTo)) {
|
||||
return res.badRequest({
|
||||
body: `Time range cannot move backwards in time. "aTo" (${aTo}) is before "aFrom" (${aFrom}).`,
|
||||
});
|
||||
}
|
||||
|
||||
if (new Date(bFrom) > new Date(bTo)) {
|
||||
if (!isValidRange(bFrom, bTo)) {
|
||||
return res.badRequest({
|
||||
body: `Time range cannot move backwards in time. "bTo" (${bTo}) is before "bFrom" (${bFrom}).`,
|
||||
});
|
||||
|
|
|
@ -16,5 +16,6 @@
|
|||
"@kbn/config-schema",
|
||||
"@kbn/core-http-server",
|
||||
"@kbn/core-elasticsearch-client-server-mocks",
|
||||
"@kbn/io-ts-utils",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export const ASSETS_ENDPOINT = '/api/asset-manager/assets';
|
|
@ -11,5 +11,7 @@ export default function ({ loadTestFile }: FtrProviderContext) {
|
|||
loadTestFile(require.resolve('./tests/basics'));
|
||||
loadTestFile(require.resolve('./tests/sample_assets'));
|
||||
loadTestFile(require.resolve('./tests/assets'));
|
||||
loadTestFile(require.resolve('./tests/assets_diff'));
|
||||
loadTestFile(require.resolve('./tests/assets_related'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -5,16 +5,11 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { pick, sortBy } from 'lodash';
|
||||
|
||||
import { Asset, AssetWithoutTimestamp } from '@kbn/assetManager-plugin/common/types_api';
|
||||
import { AssetWithoutTimestamp } from '@kbn/assetManager-plugin/common/types_api';
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
import { createSampleAssets, deleteSampleAssets, viewSampleAssetDocs } from '../helpers';
|
||||
|
||||
const ASSETS_ENDPOINT = '/api/asset-manager/assets';
|
||||
const DIFF_ENDPOINT = `${ASSETS_ENDPOINT}/diff`;
|
||||
const RELATED_ASSETS_ENDPOINT = `${ASSETS_ENDPOINT}/related`;
|
||||
import { ASSETS_ENDPOINT } from '../constants';
|
||||
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
|
@ -182,380 +177,32 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
// The order of the expected assets is fixed
|
||||
expect(getResponse.body.results).to.eql(targetAssets);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /assets/diff', () => {
|
||||
it('should reject requests that do not include the two time ranges to compare', async () => {
|
||||
const timestamp = new Date().toISOString();
|
||||
it('should reject requests with negative size parameter', async () => {
|
||||
const getResponse = await supertest.get(ASSETS_ENDPOINT).query({ size: -1 }).expect(400);
|
||||
|
||||
let getResponse = await supertest.get(DIFF_ENDPOINT).expect(400);
|
||||
expect(getResponse.body.message).to.equal(
|
||||
'[request query.aFrom]: expected value of type [string] but got [undefined]'
|
||||
'[request query]: Failed to validate: \n in /size: -1 does not match expected type pipe(ToNumber, InRange)\n in /size: "-1" does not match expected type pipe(undefined, BooleanFromString)'
|
||||
);
|
||||
|
||||
getResponse = await supertest.get(DIFF_ENDPOINT).query({ aFrom: timestamp }).expect(400);
|
||||
expect(getResponse.body.message).to.equal(
|
||||
'[request query.aTo]: expected value of type [string] but got [undefined]'
|
||||
);
|
||||
|
||||
getResponse = await supertest
|
||||
.get(DIFF_ENDPOINT)
|
||||
.query({ aFrom: timestamp, aTo: timestamp })
|
||||
.expect(400);
|
||||
expect(getResponse.body.message).to.equal(
|
||||
'[request query.bFrom]: expected value of type [string] but got [undefined]'
|
||||
);
|
||||
|
||||
getResponse = await supertest
|
||||
.get(DIFF_ENDPOINT)
|
||||
.query({ aFrom: timestamp, aTo: timestamp, bFrom: timestamp })
|
||||
.expect(400);
|
||||
expect(getResponse.body.message).to.equal(
|
||||
'[request query.bTo]: expected value of type [string] but got [undefined]'
|
||||
);
|
||||
|
||||
await supertest
|
||||
.get(DIFF_ENDPOINT)
|
||||
.query({ aFrom: timestamp, aTo: timestamp, bFrom: timestamp, bTo: timestamp })
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
it('should reject requests where either time range is moving backwards in time', async () => {
|
||||
const now = new Date();
|
||||
const isoNow = now.toISOString();
|
||||
const oneHourAgo = new Date(now.getTime() - 1000 * 60 * 60 * 1).toISOString();
|
||||
it('should reject requests with size parameter greater than 100', async () => {
|
||||
const getResponse = await supertest.get(ASSETS_ENDPOINT).query({ size: 101 }).expect(400);
|
||||
|
||||
let getResponse = await supertest
|
||||
.get(DIFF_ENDPOINT)
|
||||
.query({
|
||||
aFrom: isoNow,
|
||||
aTo: oneHourAgo,
|
||||
bFrom: isoNow,
|
||||
bTo: isoNow,
|
||||
})
|
||||
.expect(400);
|
||||
expect(getResponse.body.message).to.equal(
|
||||
`Time range cannot move backwards in time. "aTo" (${oneHourAgo}) is before "aFrom" (${isoNow}).`
|
||||
'[request query]: Failed to validate: \n in /size: 101 does not match expected type pipe(ToNumber, InRange)\n in /size: "101" does not match expected type pipe(undefined, BooleanFromString)'
|
||||
);
|
||||
|
||||
getResponse = await supertest
|
||||
.get(DIFF_ENDPOINT)
|
||||
.query({
|
||||
aFrom: isoNow,
|
||||
aTo: isoNow,
|
||||
bFrom: isoNow,
|
||||
bTo: oneHourAgo,
|
||||
})
|
||||
.expect(400);
|
||||
expect(getResponse.body.message).to.equal(
|
||||
`Time range cannot move backwards in time. "bTo" (${oneHourAgo}) is before "bFrom" (${isoNow}).`
|
||||
);
|
||||
|
||||
await supertest
|
||||
.get(DIFF_ENDPOINT)
|
||||
.query({
|
||||
aFrom: oneHourAgo,
|
||||
aTo: isoNow,
|
||||
bFrom: oneHourAgo,
|
||||
bTo: isoNow,
|
||||
})
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
it('should return the difference in assets present between two time ranges', async () => {
|
||||
const onlyInA = sampleAssetDocs.slice(0, 2);
|
||||
const onlyInB = sampleAssetDocs.slice(sampleAssetDocs.length - 2);
|
||||
const inBoth = sampleAssetDocs.slice(2, sampleAssetDocs.length - 2);
|
||||
const now = new Date();
|
||||
const oneHourAgo = new Date(now.getTime() - 1000 * 60 * 60 * 1);
|
||||
const twoHoursAgo = new Date(now.getTime() - 1000 * 60 * 60 * 2);
|
||||
await createSampleAssets(supertest, {
|
||||
baseDateTime: twoHoursAgo.toISOString(),
|
||||
excludeEans: inBoth.concat(onlyInB).map((asset) => asset['asset.ean']),
|
||||
});
|
||||
await createSampleAssets(supertest, {
|
||||
baseDateTime: oneHourAgo.toISOString(),
|
||||
excludeEans: onlyInA.concat(onlyInB).map((asset) => asset['asset.ean']),
|
||||
});
|
||||
await createSampleAssets(supertest, {
|
||||
excludeEans: inBoth.concat(onlyInA).map((asset) => asset['asset.ean']),
|
||||
});
|
||||
|
||||
const twoHoursAndTenMinuesAgo = new Date(now.getTime() - 1000 * 60 * 130 * 1);
|
||||
const fiftyMinuesAgo = new Date(now.getTime() - 1000 * 60 * 50 * 1);
|
||||
const seventyMinuesAgo = new Date(now.getTime() - 1000 * 60 * 70 * 1);
|
||||
const tenMinutesAfterNow = new Date(now.getTime() + 1000 * 60 * 10);
|
||||
|
||||
it('should reject requests with invalid from and to parameters', async () => {
|
||||
const getResponse = await supertest
|
||||
.get(DIFF_ENDPOINT)
|
||||
.query({
|
||||
aFrom: twoHoursAndTenMinuesAgo,
|
||||
aTo: fiftyMinuesAgo,
|
||||
bFrom: seventyMinuesAgo,
|
||||
bTo: tenMinutesAfterNow,
|
||||
})
|
||||
.expect(200);
|
||||
.get(ASSETS_ENDPOINT)
|
||||
.query({ from: 'now_1p', to: 'now_1p' })
|
||||
.expect(400);
|
||||
|
||||
expect(getResponse.body).to.have.property('onlyInA');
|
||||
expect(getResponse.body).to.have.property('onlyInB');
|
||||
expect(getResponse.body).to.have.property('inBoth');
|
||||
|
||||
getResponse.body.onlyInA.forEach((asset: any) => {
|
||||
delete asset['@timestamp'];
|
||||
});
|
||||
getResponse.body.onlyInB.forEach((asset: any) => {
|
||||
delete asset['@timestamp'];
|
||||
});
|
||||
getResponse.body.inBoth.forEach((asset: any) => {
|
||||
delete asset['@timestamp'];
|
||||
});
|
||||
|
||||
const sortByEan = (assets: any[]) => sortBy(assets, (asset) => asset['asset.ean']);
|
||||
expect(sortByEan(getResponse.body.onlyInA)).to.eql(sortByEan(onlyInA));
|
||||
expect(sortByEan(getResponse.body.onlyInB)).to.eql(sortByEan(onlyInB));
|
||||
expect(sortByEan(getResponse.body.inBoth)).to.eql(sortByEan(inBoth));
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /assets/related', () => {
|
||||
describe('basic validation of all relations', () => {
|
||||
const relations = [
|
||||
{
|
||||
name: 'ancestors',
|
||||
ean: 'k8s.node:node-101',
|
||||
expectedRelatedEans: ['k8s.cluster:cluster-001'],
|
||||
},
|
||||
{
|
||||
name: 'descendants',
|
||||
ean: 'k8s.cluster:cluster-001',
|
||||
expectedRelatedEans: ['k8s.node:node-101', 'k8s.node:node-102', 'k8s.node:node-103'],
|
||||
},
|
||||
{
|
||||
name: 'references',
|
||||
ean: 'k8s.pod:pod-200xrg1',
|
||||
expectedRelatedEans: ['k8s.cluster:cluster-001'],
|
||||
},
|
||||
];
|
||||
|
||||
relations.forEach((relation) => {
|
||||
it(`should return the ${relation.name} assets`, async () => {
|
||||
await createSampleAssets(supertest);
|
||||
|
||||
const getResponse = await supertest
|
||||
.get(RELATED_ASSETS_ENDPOINT)
|
||||
.query({
|
||||
relation: relation.name,
|
||||
size: sampleAssetDocs.length,
|
||||
from: 'now-1d',
|
||||
ean: relation.ean,
|
||||
maxDistance: 1,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const relatedEans = getResponse.body.results[relation.name].map(
|
||||
(asset: Asset) => asset['asset.ean']
|
||||
);
|
||||
expect(relatedEans).to.eql(relation.expectedRelatedEans);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('response validation', () => {
|
||||
it('should return 404 if primary asset not found', async () => {
|
||||
await supertest
|
||||
.get(RELATED_ASSETS_ENDPOINT)
|
||||
.query({
|
||||
relation: 'descendants',
|
||||
size: sampleAssetDocs.length,
|
||||
from: 'now-1d',
|
||||
ean: 'non-existing-ean',
|
||||
maxDistance: 5,
|
||||
})
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
it('should return the primary asset', async () => {
|
||||
await createSampleAssets(supertest);
|
||||
|
||||
const sampleCluster = sampleAssetDocs.find(
|
||||
(asset) => asset['asset.id'] === 'cluster-002'
|
||||
);
|
||||
|
||||
const getResponse = await supertest
|
||||
.get(RELATED_ASSETS_ENDPOINT)
|
||||
.query({
|
||||
relation: 'descendants',
|
||||
size: sampleAssetDocs.length,
|
||||
from: 'now-1d',
|
||||
ean: sampleCluster!['asset.ean'],
|
||||
maxDistance: 5,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const {
|
||||
body: { results },
|
||||
} = getResponse;
|
||||
delete results.primary['@timestamp'];
|
||||
expect(results.primary).to.eql(sampleCluster);
|
||||
});
|
||||
|
||||
it('should return empty assets when none matching', async () => {
|
||||
await createSampleAssets(supertest);
|
||||
|
||||
const sampleCluster = sampleAssetDocs.find(
|
||||
(asset) => asset['asset.id'] === 'cluster-002'
|
||||
);
|
||||
|
||||
const getResponse = await supertest
|
||||
.get(RELATED_ASSETS_ENDPOINT)
|
||||
.query({
|
||||
relation: 'descendants',
|
||||
size: sampleAssetDocs.length,
|
||||
from: 'now-1d',
|
||||
ean: sampleCluster!['asset.ean'],
|
||||
maxDistance: 5,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const {
|
||||
body: { results },
|
||||
} = getResponse;
|
||||
expect(results).to.have.property('descendants');
|
||||
expect(results.descendants).to.have.length(0);
|
||||
});
|
||||
|
||||
it('breaks circular dependency', async () => {
|
||||
await createSampleAssets(supertest);
|
||||
|
||||
// pods reference a node that references the pods
|
||||
const sampleNode = sampleAssetDocs.find((asset) => asset['asset.id'] === 'pod-203ugg5');
|
||||
|
||||
const getResponse = await supertest
|
||||
.get(RELATED_ASSETS_ENDPOINT)
|
||||
.query({
|
||||
relation: 'references',
|
||||
size: sampleAssetDocs.length,
|
||||
from: 'now-1d',
|
||||
ean: sampleNode!['asset.ean'],
|
||||
maxDistance: 5,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const {
|
||||
body: { results },
|
||||
} = getResponse;
|
||||
expect(
|
||||
results.references.map((asset: Asset) => pick(asset, ['asset.ean', 'distance']))
|
||||
).to.eql([
|
||||
{ 'asset.ean': 'k8s.node:node-203', distance: 1 },
|
||||
{ 'asset.ean': 'k8s.pod:pod-203ugg9', distance: 2 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('no asset.type filters', () => {
|
||||
it('should return all descendants of a provided ean at maxDistance 1', async () => {
|
||||
await createSampleAssets(supertest);
|
||||
|
||||
const sampleCluster = sampleAssetDocs.find(
|
||||
(asset) => asset['asset.id'] === 'cluster-001'
|
||||
);
|
||||
|
||||
const getResponse = await supertest
|
||||
.get(RELATED_ASSETS_ENDPOINT)
|
||||
.query({
|
||||
relation: 'descendants',
|
||||
size: sampleAssetDocs.length,
|
||||
from: 'now-1d',
|
||||
ean: sampleCluster!['asset.ean'],
|
||||
maxDistance: 1,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const {
|
||||
body: { results },
|
||||
} = getResponse;
|
||||
expect(results.descendants).to.have.length(3);
|
||||
expect(results.descendants.every((asset: { distance: number }) => asset.distance === 1));
|
||||
});
|
||||
|
||||
it('should return all descendants of a provided ean at maxDistance 2', async () => {
|
||||
await createSampleAssets(supertest);
|
||||
|
||||
const sampleCluster = sampleAssetDocs.find(
|
||||
(asset) => asset['asset.id'] === 'cluster-001'
|
||||
);
|
||||
|
||||
const getResponse = await supertest
|
||||
.get(RELATED_ASSETS_ENDPOINT)
|
||||
.query({
|
||||
relation: 'descendants',
|
||||
size: sampleAssetDocs.length,
|
||||
from: 'now-1d',
|
||||
ean: sampleCluster!['asset.ean'],
|
||||
maxDistance: 2,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const {
|
||||
body: { results },
|
||||
} = getResponse;
|
||||
expect(results.descendants).to.have.length(12);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with asset.type filters', () => {
|
||||
it('should filter by the provided asset type', async () => {
|
||||
await createSampleAssets(supertest);
|
||||
|
||||
const sampleCluster = sampleAssetDocs.find(
|
||||
(asset) => asset['asset.id'] === 'cluster-001'
|
||||
);
|
||||
|
||||
const getResponse = await supertest
|
||||
.get(RELATED_ASSETS_ENDPOINT)
|
||||
.query({
|
||||
relation: 'descendants',
|
||||
size: sampleAssetDocs.length,
|
||||
from: 'now-1d',
|
||||
ean: sampleCluster!['asset.ean'],
|
||||
maxDistance: 1,
|
||||
type: ['k8s.pod'],
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const {
|
||||
body: { results },
|
||||
} = getResponse;
|
||||
expect(results.descendants).to.have.length(0);
|
||||
});
|
||||
|
||||
it('should return all descendants of a provided ean at maxDistance 2', async () => {
|
||||
await createSampleAssets(supertest);
|
||||
|
||||
const sampleCluster = sampleAssetDocs.find(
|
||||
(asset) => asset['asset.id'] === 'cluster-001'
|
||||
);
|
||||
|
||||
const getResponse = await supertest
|
||||
.get(RELATED_ASSETS_ENDPOINT)
|
||||
.query({
|
||||
relation: 'descendants',
|
||||
size: sampleAssetDocs.length,
|
||||
from: 'now-1d',
|
||||
ean: sampleCluster!['asset.ean'],
|
||||
maxDistance: 2,
|
||||
type: ['k8s.pod'],
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const {
|
||||
body: { results },
|
||||
} = getResponse;
|
||||
expect(results.descendants).to.have.length(9);
|
||||
expect(results.descendants.every((asset: { distance: number }) => asset.distance === 2));
|
||||
expect(results.descendants.every((asset: Asset) => asset['asset.type'] === 'k8s.pod'));
|
||||
});
|
||||
expect(getResponse.body.message).to.equal(
|
||||
'[request query]: Failed to validate: \n in /from: "now_1p" does not match expected type Date\n in /from: "now_1p" does not match expected type datemath\n in /to: "now_1p" does not match expected type Date\n in /to: "now_1p" does not match expected type datemath'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,175 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { sortBy } from 'lodash';
|
||||
|
||||
import { AssetWithoutTimestamp } from '@kbn/assetManager-plugin/common/types_api';
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
import { createSampleAssets, deleteSampleAssets, viewSampleAssetDocs } from '../helpers';
|
||||
import { ASSETS_ENDPOINT } from '../constants';
|
||||
|
||||
const DIFF_ENDPOINT = `${ASSETS_ENDPOINT}/diff`;
|
||||
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
|
||||
describe('asset management', () => {
|
||||
let sampleAssetDocs: AssetWithoutTimestamp[] = [];
|
||||
before(async () => {
|
||||
sampleAssetDocs = await viewSampleAssetDocs(supertest);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await deleteSampleAssets(supertest);
|
||||
});
|
||||
|
||||
describe('GET /assets/diff', () => {
|
||||
it('should reject requests that do not include the two time ranges to compare', async () => {
|
||||
const timestamp = new Date().toISOString();
|
||||
|
||||
let getResponse = await supertest.get(DIFF_ENDPOINT).expect(400);
|
||||
expect(getResponse.body.message).to.equal(
|
||||
'[request query]: Failed to validate: \n in /0/aFrom: undefined does not match expected type Date\n in /0/aFrom: undefined does not match expected type datemath\n in /0/aTo: undefined does not match expected type Date\n in /0/aTo: undefined does not match expected type datemath\n in /0/bFrom: undefined does not match expected type Date\n in /0/bFrom: undefined does not match expected type datemath\n in /0/bTo: undefined does not match expected type Date\n in /0/bTo: undefined does not match expected type datemath'
|
||||
);
|
||||
|
||||
getResponse = await supertest.get(DIFF_ENDPOINT).query({ aFrom: timestamp }).expect(400);
|
||||
expect(getResponse.body.message).to.equal(
|
||||
'[request query]: Failed to validate: \n in /0/aTo: undefined does not match expected type Date\n in /0/aTo: undefined does not match expected type datemath\n in /0/bFrom: undefined does not match expected type Date\n in /0/bFrom: undefined does not match expected type datemath\n in /0/bTo: undefined does not match expected type Date\n in /0/bTo: undefined does not match expected type datemath'
|
||||
);
|
||||
|
||||
getResponse = await supertest
|
||||
.get(DIFF_ENDPOINT)
|
||||
.query({ aFrom: timestamp, aTo: timestamp })
|
||||
.expect(400);
|
||||
expect(getResponse.body.message).to.equal(
|
||||
'[request query]: Failed to validate: \n in /0/bFrom: undefined does not match expected type Date\n in /0/bFrom: undefined does not match expected type datemath\n in /0/bTo: undefined does not match expected type Date\n in /0/bTo: undefined does not match expected type datemath'
|
||||
);
|
||||
|
||||
getResponse = await supertest
|
||||
.get(DIFF_ENDPOINT)
|
||||
.query({ aFrom: timestamp, aTo: timestamp, bFrom: timestamp })
|
||||
.expect(400);
|
||||
expect(getResponse.body.message).to.equal(
|
||||
'[request query]: Failed to validate: \n in /0/bTo: undefined does not match expected type Date\n in /0/bTo: undefined does not match expected type datemath'
|
||||
);
|
||||
|
||||
await supertest
|
||||
.get(DIFF_ENDPOINT)
|
||||
.query({ aFrom: timestamp, aTo: timestamp, bFrom: timestamp, bTo: timestamp })
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
it('should reject requests where either time range is moving backwards in time', async () => {
|
||||
const now = new Date();
|
||||
const isoNow = now.toISOString();
|
||||
const oneHourAgo = new Date(now.getTime() - 1000 * 60 * 60 * 1).toISOString();
|
||||
|
||||
let getResponse = await supertest
|
||||
.get(DIFF_ENDPOINT)
|
||||
.query({
|
||||
aFrom: isoNow,
|
||||
aTo: oneHourAgo,
|
||||
bFrom: isoNow,
|
||||
bTo: isoNow,
|
||||
})
|
||||
.expect(400);
|
||||
expect(getResponse.body.message).to.equal(
|
||||
`Time range cannot move backwards in time. "aTo" (${oneHourAgo}) is before "aFrom" (${isoNow}).`
|
||||
);
|
||||
|
||||
getResponse = await supertest
|
||||
.get(DIFF_ENDPOINT)
|
||||
.query({
|
||||
aFrom: isoNow,
|
||||
aTo: isoNow,
|
||||
bFrom: isoNow,
|
||||
bTo: oneHourAgo,
|
||||
})
|
||||
.expect(400);
|
||||
expect(getResponse.body.message).to.equal(
|
||||
`Time range cannot move backwards in time. "bTo" (${oneHourAgo}) is before "bFrom" (${isoNow}).`
|
||||
);
|
||||
|
||||
await supertest
|
||||
.get(DIFF_ENDPOINT)
|
||||
.query({
|
||||
aFrom: oneHourAgo,
|
||||
aTo: isoNow,
|
||||
bFrom: oneHourAgo,
|
||||
bTo: isoNow,
|
||||
})
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
it('should return the difference in assets present between two time ranges', async () => {
|
||||
const onlyInA = sampleAssetDocs.slice(0, 2);
|
||||
const onlyInB = sampleAssetDocs.slice(sampleAssetDocs.length - 2);
|
||||
const inBoth = sampleAssetDocs.slice(2, sampleAssetDocs.length - 2);
|
||||
const now = new Date();
|
||||
const oneHourAgo = new Date(now.getTime() - 1000 * 60 * 60 * 1);
|
||||
const twoHoursAgo = new Date(now.getTime() - 1000 * 60 * 60 * 2);
|
||||
await createSampleAssets(supertest, {
|
||||
baseDateTime: twoHoursAgo.toISOString(),
|
||||
excludeEans: inBoth.concat(onlyInB).map((asset) => asset['asset.ean']),
|
||||
});
|
||||
await createSampleAssets(supertest, {
|
||||
baseDateTime: oneHourAgo.toISOString(),
|
||||
excludeEans: onlyInA.concat(onlyInB).map((asset) => asset['asset.ean']),
|
||||
});
|
||||
await createSampleAssets(supertest, {
|
||||
excludeEans: inBoth.concat(onlyInA).map((asset) => asset['asset.ean']),
|
||||
});
|
||||
|
||||
const twoHoursAndTenMinuesAgo = new Date(now.getTime() - 1000 * 60 * 130 * 1);
|
||||
const fiftyMinuesAgo = new Date(now.getTime() - 1000 * 60 * 50 * 1);
|
||||
const seventyMinuesAgo = new Date(now.getTime() - 1000 * 60 * 70 * 1);
|
||||
const tenMinutesAfterNow = new Date(now.getTime() + 1000 * 60 * 10);
|
||||
|
||||
const getResponse = await supertest
|
||||
.get(DIFF_ENDPOINT)
|
||||
.query({
|
||||
aFrom: twoHoursAndTenMinuesAgo,
|
||||
aTo: fiftyMinuesAgo,
|
||||
bFrom: seventyMinuesAgo,
|
||||
bTo: tenMinutesAfterNow,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(getResponse.body).to.have.property('onlyInA');
|
||||
expect(getResponse.body).to.have.property('onlyInB');
|
||||
expect(getResponse.body).to.have.property('inBoth');
|
||||
|
||||
getResponse.body.onlyInA.forEach((asset: any) => {
|
||||
delete asset['@timestamp'];
|
||||
});
|
||||
getResponse.body.onlyInB.forEach((asset: any) => {
|
||||
delete asset['@timestamp'];
|
||||
});
|
||||
getResponse.body.inBoth.forEach((asset: any) => {
|
||||
delete asset['@timestamp'];
|
||||
});
|
||||
|
||||
const sortByEan = (assets: any[]) => sortBy(assets, (asset) => asset['asset.ean']);
|
||||
expect(sortByEan(getResponse.body.onlyInA)).to.eql(sortByEan(onlyInA));
|
||||
expect(sortByEan(getResponse.body.onlyInB)).to.eql(sortByEan(onlyInB));
|
||||
expect(sortByEan(getResponse.body.inBoth)).to.eql(sortByEan(inBoth));
|
||||
});
|
||||
|
||||
it('should reject requests with invalid datemath', async () => {
|
||||
const getResponse = await supertest
|
||||
.get(DIFF_ENDPOINT)
|
||||
.query({ aFrom: 'now_1p', aTo: 'now_1p', bFrom: 'now_1p', bTo: 'now_1p' })
|
||||
.expect(400);
|
||||
|
||||
expect(getResponse.body.message).to.equal(
|
||||
'[request query]: Failed to validate: \n in /0/aFrom: "now_1p" does not match expected type Date\n in /0/aFrom: "now_1p" does not match expected type datemath\n in /0/aTo: "now_1p" does not match expected type Date\n in /0/aTo: "now_1p" does not match expected type datemath\n in /0/bFrom: "now_1p" does not match expected type Date\n in /0/bFrom: "now_1p" does not match expected type datemath\n in /0/bTo: "now_1p" does not match expected type Date\n in /0/bTo: "now_1p" does not match expected type datemath'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,375 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { pick } from 'lodash';
|
||||
|
||||
import { Asset, AssetWithoutTimestamp } from '@kbn/assetManager-plugin/common/types_api';
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
import { createSampleAssets, deleteSampleAssets, viewSampleAssetDocs } from '../helpers';
|
||||
import { ASSETS_ENDPOINT } from '../constants';
|
||||
|
||||
const RELATED_ASSETS_ENDPOINT = `${ASSETS_ENDPOINT}/related`;
|
||||
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
|
||||
describe('asset management', () => {
|
||||
let sampleAssetDocs: AssetWithoutTimestamp[] = [];
|
||||
let relatedAssetBaseQuery = {};
|
||||
|
||||
before(async () => {
|
||||
sampleAssetDocs = await viewSampleAssetDocs(supertest);
|
||||
relatedAssetBaseQuery = {
|
||||
size: sampleAssetDocs.length,
|
||||
from: 'now-1d',
|
||||
maxDistance: 5,
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await deleteSampleAssets(supertest);
|
||||
});
|
||||
|
||||
describe('GET /assets/related', () => {
|
||||
describe('basic validation of all relations', () => {
|
||||
const relations = [
|
||||
{
|
||||
name: 'ancestors',
|
||||
ean: 'k8s.node:node-101',
|
||||
expectedRelatedEans: ['k8s.cluster:cluster-001'],
|
||||
},
|
||||
{
|
||||
name: 'descendants',
|
||||
ean: 'k8s.cluster:cluster-001',
|
||||
expectedRelatedEans: ['k8s.node:node-101', 'k8s.node:node-102', 'k8s.node:node-103'],
|
||||
},
|
||||
{
|
||||
name: 'references',
|
||||
ean: 'k8s.pod:pod-200xrg1',
|
||||
expectedRelatedEans: ['k8s.cluster:cluster-001'],
|
||||
},
|
||||
];
|
||||
|
||||
relations.forEach((relation) => {
|
||||
it(`should return the ${relation.name} assets`, async () => {
|
||||
await createSampleAssets(supertest);
|
||||
|
||||
const getResponse = await supertest
|
||||
.get(RELATED_ASSETS_ENDPOINT)
|
||||
.query({
|
||||
relation: relation.name,
|
||||
size: sampleAssetDocs.length,
|
||||
from: 'now-1d',
|
||||
ean: relation.ean,
|
||||
maxDistance: 1,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const relatedEans = getResponse.body.results[relation.name].map(
|
||||
(asset: Asset) => asset['asset.ean']
|
||||
);
|
||||
expect(relatedEans).to.eql(relation.expectedRelatedEans);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('response validation', () => {
|
||||
it('should return 404 if primary asset not found', async () => {
|
||||
await supertest
|
||||
.get(RELATED_ASSETS_ENDPOINT)
|
||||
.query({
|
||||
...relatedAssetBaseQuery,
|
||||
relation: 'descendants',
|
||||
ean: 'non-existing-ean',
|
||||
})
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
it('should return the primary asset', async () => {
|
||||
await createSampleAssets(supertest);
|
||||
|
||||
const sampleCluster = sampleAssetDocs.find(
|
||||
(asset) => asset['asset.id'] === 'cluster-002'
|
||||
);
|
||||
|
||||
const getResponse = await supertest
|
||||
.get(RELATED_ASSETS_ENDPOINT)
|
||||
.query({
|
||||
...relatedAssetBaseQuery,
|
||||
relation: 'descendants',
|
||||
ean: sampleCluster!['asset.ean'],
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const {
|
||||
body: { results },
|
||||
} = getResponse;
|
||||
delete results.primary['@timestamp'];
|
||||
expect(results.primary).to.eql(sampleCluster);
|
||||
});
|
||||
|
||||
it('should return empty assets when none matching', async () => {
|
||||
await createSampleAssets(supertest);
|
||||
|
||||
const sampleCluster = sampleAssetDocs.find(
|
||||
(asset) => asset['asset.id'] === 'cluster-002'
|
||||
);
|
||||
|
||||
const getResponse = await supertest
|
||||
.get(RELATED_ASSETS_ENDPOINT)
|
||||
.query({
|
||||
...relatedAssetBaseQuery,
|
||||
relation: 'descendants',
|
||||
ean: sampleCluster!['asset.ean'],
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const {
|
||||
body: { results },
|
||||
} = getResponse;
|
||||
|
||||
expect(results).to.have.property('descendants');
|
||||
expect(results.descendants).to.have.length(0);
|
||||
});
|
||||
|
||||
it('breaks circular dependency', async () => {
|
||||
await createSampleAssets(supertest);
|
||||
|
||||
// pods reference a node that references the pods
|
||||
const sampleNode = sampleAssetDocs.find((asset) => asset['asset.id'] === 'pod-203ugg5');
|
||||
|
||||
const getResponse = await supertest
|
||||
.get(RELATED_ASSETS_ENDPOINT)
|
||||
.query({
|
||||
...relatedAssetBaseQuery,
|
||||
relation: 'references',
|
||||
ean: sampleNode!['asset.ean'],
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const {
|
||||
body: { results },
|
||||
} = getResponse;
|
||||
expect(
|
||||
results.references.map((asset: Asset) => pick(asset, ['asset.ean', 'distance']))
|
||||
).to.eql([
|
||||
{ 'asset.ean': 'k8s.node:node-203', distance: 1 },
|
||||
{ 'asset.ean': 'k8s.pod:pod-203ugg9', distance: 2 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should reject requests with negative size parameter', async () => {
|
||||
const getResponse = await supertest
|
||||
.get(RELATED_ASSETS_ENDPOINT)
|
||||
.query({
|
||||
...relatedAssetBaseQuery,
|
||||
relation: 'descendants',
|
||||
ean: 'non-existing-ean',
|
||||
size: -1,
|
||||
})
|
||||
.expect(400);
|
||||
|
||||
expect(getResponse.body.message).to.equal(
|
||||
'[request query]: Failed to validate: \n in /0/size: -1 does not match expected type pipe(ToNumber, InRange)\n in /0/size: "-1" does not match expected type pipe(undefined, BooleanFromString)'
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject requests with size parameter greater than 100', async () => {
|
||||
const getResponse = await supertest
|
||||
.get(RELATED_ASSETS_ENDPOINT)
|
||||
.query({
|
||||
...relatedAssetBaseQuery,
|
||||
relation: 'descendants',
|
||||
ean: 'non-existing-ean',
|
||||
size: 101,
|
||||
})
|
||||
.expect(400);
|
||||
|
||||
expect(getResponse.body.message).to.equal(
|
||||
'[request query]: Failed to validate: \n in /0/size: 101 does not match expected type pipe(ToNumber, InRange)\n in /0/size: "101" does not match expected type pipe(undefined, BooleanFromString)'
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject requests with negative maxDistance parameter', async () => {
|
||||
const getResponse = await supertest
|
||||
.get(RELATED_ASSETS_ENDPOINT)
|
||||
.query({
|
||||
...relatedAssetBaseQuery,
|
||||
relation: 'descendants',
|
||||
ean: 'non-existing-ean',
|
||||
maxDistance: -1,
|
||||
})
|
||||
.expect(400);
|
||||
|
||||
expect(getResponse.body.message).to.equal(
|
||||
'[request query]: Failed to validate: \n in /0/maxDistance: -1 does not match expected type pipe(ToNumber, InRange)\n in /0/maxDistance: "-1" does not match expected type pipe(undefined, BooleanFromString)'
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject requests with size parameter maxDistance is greater than 5', async () => {
|
||||
const getResponse = await supertest
|
||||
.get(RELATED_ASSETS_ENDPOINT)
|
||||
.query({
|
||||
...relatedAssetBaseQuery,
|
||||
relation: 'descendants',
|
||||
ean: 'non-existing-ean',
|
||||
maxDistance: 6,
|
||||
})
|
||||
.expect(400);
|
||||
|
||||
expect(getResponse.body.message).to.equal(
|
||||
'[request query]: Failed to validate: \n in /0/maxDistance: 6 does not match expected type pipe(ToNumber, InRange)\n in /0/maxDistance: "6" does not match expected type pipe(undefined, BooleanFromString)'
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject requests with invalid from and to parameters', async () => {
|
||||
const getResponse = await supertest
|
||||
.get(RELATED_ASSETS_ENDPOINT)
|
||||
.query({
|
||||
...relatedAssetBaseQuery,
|
||||
relation: 'descendants',
|
||||
ean: 'non-existing-ean',
|
||||
from: 'now_1p',
|
||||
to: 'now_1p',
|
||||
})
|
||||
.expect(400);
|
||||
|
||||
expect(getResponse.body.message).to.equal(
|
||||
'[request query]: Failed to validate: \n in /0/from: "now_1p" does not match expected type Date\n in /0/from: "now_1p" does not match expected type datemath\n in /1/to: "now_1p" does not match expected type Date\n in /1/to: "now_1p" does not match expected type datemath'
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject requests where time range is moving backwards in time', async () => {
|
||||
const now = new Date();
|
||||
const isoNow = now.toISOString();
|
||||
const oneHourAgo = new Date(now.getTime() - 1000 * 60 * 60 * 1).toISOString();
|
||||
|
||||
const getResponse = await supertest
|
||||
.get(RELATED_ASSETS_ENDPOINT)
|
||||
.query({
|
||||
...relatedAssetBaseQuery,
|
||||
relation: 'descendants',
|
||||
from: isoNow,
|
||||
to: oneHourAgo,
|
||||
maxDistance: 1,
|
||||
ean: 'non-existing-ean',
|
||||
})
|
||||
.expect(400);
|
||||
expect(getResponse.body.message).to.equal(
|
||||
`Time range cannot move backwards in time. "to" (${oneHourAgo}) is before "from" (${isoNow}).`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('no asset.type filters', () => {
|
||||
it('should return all descendants of a provided ean at maxDistance 1', async () => {
|
||||
await createSampleAssets(supertest);
|
||||
|
||||
const sampleCluster = sampleAssetDocs.find(
|
||||
(asset) => asset['asset.id'] === 'cluster-001'
|
||||
);
|
||||
|
||||
const getResponse = await supertest
|
||||
.get(RELATED_ASSETS_ENDPOINT)
|
||||
.query({
|
||||
relation: 'descendants',
|
||||
size: sampleAssetDocs.length,
|
||||
from: 'now-1d',
|
||||
ean: sampleCluster!['asset.ean'],
|
||||
maxDistance: 1,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const {
|
||||
body: { results },
|
||||
} = getResponse;
|
||||
expect(results.descendants).to.have.length(3);
|
||||
expect(results.descendants.every((asset: { distance: number }) => asset.distance === 1));
|
||||
});
|
||||
|
||||
it('should return all descendants of a provided ean at maxDistance 2', async () => {
|
||||
await createSampleAssets(supertest);
|
||||
|
||||
const sampleCluster = sampleAssetDocs.find(
|
||||
(asset) => asset['asset.id'] === 'cluster-001'
|
||||
);
|
||||
|
||||
const getResponse = await supertest
|
||||
.get(RELATED_ASSETS_ENDPOINT)
|
||||
.query({
|
||||
relation: 'descendants',
|
||||
size: sampleAssetDocs.length,
|
||||
from: 'now-1d',
|
||||
ean: sampleCluster!['asset.ean'],
|
||||
maxDistance: 2,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const {
|
||||
body: { results },
|
||||
} = getResponse;
|
||||
expect(results.descendants).to.have.length(12);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with asset.type filters', () => {
|
||||
it('should filter by the provided asset type', async () => {
|
||||
await createSampleAssets(supertest);
|
||||
|
||||
const sampleCluster = sampleAssetDocs.find(
|
||||
(asset) => asset['asset.id'] === 'cluster-001'
|
||||
);
|
||||
|
||||
const getResponse = await supertest
|
||||
.get(RELATED_ASSETS_ENDPOINT)
|
||||
.query({
|
||||
relation: 'descendants',
|
||||
size: sampleAssetDocs.length,
|
||||
from: 'now-1d',
|
||||
ean: sampleCluster!['asset.ean'],
|
||||
maxDistance: 1,
|
||||
type: ['k8s.pod'],
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const {
|
||||
body: { results },
|
||||
} = getResponse;
|
||||
expect(results.descendants).to.have.length(0);
|
||||
});
|
||||
|
||||
it('should return all descendants of a provided ean at maxDistance 2', async () => {
|
||||
await createSampleAssets(supertest);
|
||||
|
||||
const sampleCluster = sampleAssetDocs.find(
|
||||
(asset) => asset['asset.id'] === 'cluster-001'
|
||||
);
|
||||
|
||||
const getResponse = await supertest
|
||||
.get(RELATED_ASSETS_ENDPOINT)
|
||||
.query({
|
||||
relation: 'descendants',
|
||||
size: sampleAssetDocs.length,
|
||||
from: 'now-1d',
|
||||
ean: sampleCluster!['asset.ean'],
|
||||
maxDistance: 2,
|
||||
type: ['k8s.pod'],
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const {
|
||||
body: { results },
|
||||
} = getResponse;
|
||||
expect(results.descendants).to.have.length(9);
|
||||
expect(results.descendants.every((asset: { distance: number }) => asset.distance === 2));
|
||||
expect(results.descendants.every((asset: Asset) => asset['asset.type'] === 'k8s.pod'));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue