mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[asset_manager] Add /related endpoint (#154541)
Exposes a `/related` endpoint that can be called to retrieve any supported asset relationship (`ancestors|descendants|references`). Note that this is a first draft to get a functioning endpoint available. Further optimizations (performances, typing..) will be implemented as follow ups or when we get feedback from actual use cases. Follow ups: - We're currently doing two sequential requests to retrieve the related assets, one for the _directly_ referenced of the primary (in `assets.children|parents..`) and another for _indirectly_ referenced that lists the primary in `asset.children|parents...`. These two predicates can be packed in a single query - The size is difficult to enforce at the query level if a `type` filter is provided. If we're looking at a `depth > 1` and we apply the size limit to the queries at `depth < maxDistance` we may miss edges to the requested type. Similarly the `type` filter can't be enforced at `depth < maxDistance` To do: - [x] Add filtering by type - [ ] ~Limit by size~ - [x] Add sample assets which use references - [x] Add integration tests that validate each type of relation query - [x] Add documentation and sample requests/responses for each relation type - [x] Handle circular references. In what situations can that happens, references ? Closes #153471 Closes #153473 Closes #153482 --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: klacabane <kevin.lacabane@elastic.co> Co-authored-by: Kevin Lacabane <klacabane@gmail.com> Co-authored-by: Carlos Crespo <crespocarlos@users.noreply.github.com>
This commit is contained in:
parent
dab1409fef
commit
9b2562e5db
13 changed files with 1532 additions and 24 deletions
|
@ -5,8 +5,16 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
|
||||
export const assetTypeRT = schema.oneOf([
|
||||
schema.literal('k8s.pod'),
|
||||
schema.literal('k8s.cluster'),
|
||||
schema.literal('k8s.node'),
|
||||
]);
|
||||
export type AssetType = typeof assetTypeRT.type;
|
||||
|
||||
export type AssetKind = 'unknown' | 'node';
|
||||
export type AssetType = 'k8s.pod' | 'k8s.cluster' | 'k8s.node';
|
||||
export type AssetStatus =
|
||||
| 'CREATING'
|
||||
| 'ACTIVE'
|
||||
|
@ -52,6 +60,7 @@ export interface Asset extends ECSDocument {
|
|||
'asset.status'?: AssetStatus;
|
||||
'asset.parents'?: string | string[];
|
||||
'asset.children'?: string | string[];
|
||||
'asset.references'?: string | string[];
|
||||
'asset.namespace'?: string;
|
||||
}
|
||||
|
||||
|
@ -120,3 +129,15 @@ export interface AssetFilters {
|
|||
from?: string;
|
||||
to?: string;
|
||||
}
|
||||
|
||||
export const relationRT = schema.oneOf([
|
||||
schema.literal('ancestors'),
|
||||
schema.literal('descendants'),
|
||||
schema.literal('references'),
|
||||
]);
|
||||
|
||||
export type Relation = typeof relationRT.type;
|
||||
export type RelationField = keyof Pick<
|
||||
Asset,
|
||||
'asset.children' | 'asset.parents' | 'asset.references'
|
||||
>;
|
||||
|
|
|
@ -78,7 +78,7 @@ _Notes:_
|
|||
<summary>Invalid request with type and ean both specified</summary>
|
||||
|
||||
```curl
|
||||
GET /assets?from=2023-03-25T17:44:44.000Z&type=k8s.pod&ean=k8s.pod:123
|
||||
GET kbn:/api/asset-manager/assets?from=2023-03-25T17:44:44.000Z&type=k8s.pod&ean=k8s.pod:123
|
||||
|
||||
{
|
||||
"statusCode": 400,
|
||||
|
@ -94,7 +94,7 @@ GET /assets?from=2023-03-25T17:44:44.000Z&type=k8s.pod&ean=k8s.pod:123
|
|||
<summary>All assets JSON response</summary>
|
||||
|
||||
```curl
|
||||
GET /assets?from=2023-03-25T17:44:44.000Z&to=2023-03-25T18:44:44.000Z
|
||||
GET kbn:/api/asset-manager/assets?from=2023-03-25T17:44:44.000Z&to=2023-03-25T18:44:44.000Z
|
||||
|
||||
{
|
||||
"results": [
|
||||
|
@ -273,7 +273,7 @@ GET /assets?from=2023-03-25T17:44:44.000Z&to=2023-03-25T18:44:44.000Z
|
|||
<summary>Assets by type JSON response</summary>
|
||||
|
||||
```curl
|
||||
GET /assets?from=2023-03-25T17:44:44.000Z&to=2023-03-25T18:44:44.000Z&type=k8s.pod
|
||||
GET kbn:/api/asset-manager/assets?from=2023-03-25T17:44:44.000Z&to=2023-03-25T18:44:44.000Z&type=k8s.pod
|
||||
|
||||
{
|
||||
"results": [
|
||||
|
@ -378,7 +378,7 @@ GET /assets?from=2023-03-25T17:44:44.000Z&to=2023-03-25T18:44:44.000Z&type=k8s.p
|
|||
<summary>Assets by EAN JSON response</summary>
|
||||
|
||||
```curl
|
||||
GET /assets?from=2023-03-25T17:44:44.000Z&to=2023-03-25T18:44:44.000Z&ean=k8s.node:node-101&ean=k8s.pod:pod-6r1z
|
||||
GET kbn:/api/asset-manager/assets?from=2023-03-25T17:44:44.000Z&to=2023-03-25T18:44:44.000Z&ean=k8s.node:node-101&ean=k8s.pod:pod-6r1z
|
||||
|
||||
{
|
||||
"results": [
|
||||
|
@ -427,7 +427,7 @@ Returns assets found in the two time ranges, split by what occurs in only either
|
|||
<summary>Request where comparison range is missing assets that are found in the baseline range</summary>
|
||||
|
||||
```curl
|
||||
GET /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/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
|
||||
|
||||
{
|
||||
"onlyInA": [
|
||||
|
@ -589,7 +589,7 @@ GET /assets/diff?aFrom=2022-02-07T00:00:00.000Z&aTo=2022-02-07T01:30:00.000Z&bFr
|
|||
<summary>Request where baseline range is missing assets that are found in the comparison range</summary>
|
||||
|
||||
```curl
|
||||
GET /assets/diff?aFrom=2022-02-07T01:00:00.000Z&aTo=2022-02-07T01:30:00.000Z&bFrom=2022-02-07T01:00:00.000Z&bTo=2022-02-07T03:00:00.000Z
|
||||
GET kbn:/api/asset-manager/assets/diff?aFrom=2022-02-07T01:00:00.000Z&aTo=2022-02-07T01:30:00.000Z&bFrom=2022-02-07T01:00:00.000Z&bTo=2022-02-07T03:00:00.000Z
|
||||
|
||||
{
|
||||
"onlyInA": [],
|
||||
|
@ -751,7 +751,7 @@ GET /assets/diff?aFrom=2022-02-07T01:00:00.000Z&aTo=2022-02-07T01:30:00.000Z&bFr
|
|||
<summary>Request where each range is missing assets found in the other range</summary>
|
||||
|
||||
```curl
|
||||
GET /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-07T03:00:00.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-07T03:00:00.000Z
|
||||
|
||||
{
|
||||
"onlyInA": [
|
||||
|
@ -934,7 +934,7 @@ GET /assets/diff?aFrom=2022-02-07T00:00:00.000Z&aTo=2022-02-07T01:30:00.000Z&bFr
|
|||
<summary>Request where each range is missing assets found in the other range, but restricted by type</summary>
|
||||
|
||||
```curl
|
||||
GET /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-07T03:00:00.000Z&type=k8s.pod
|
||||
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-07T03:00:00.000Z&type=k8s.pod
|
||||
|
||||
{
|
||||
"onlyInA": [
|
||||
|
@ -1038,6 +1038,326 @@ GET /assets/diff?aFrom=2022-02-07T00:00:00.000Z&aTo=2022-02-07T01:30:00.000Z&bFr
|
|||
|
||||
</details>
|
||||
|
||||
#### GET /assets/related
|
||||
|
||||
Returns assets related to the provided ean. The relation can be one of ancestors, descendants or references.
|
||||
|
||||
#### Request
|
||||
|
||||
| Option | Type | Required? | Default | Description |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| relation | string | Yes | N/A | The type of related assets we're looking for. One of (ancestors|descendants|references) |
|
||||
| from | RangeDate | Yes | N/A | Starting point for date range to search for assets within |
|
||||
| to | RangeDate | No | "now" | End point for date range to search for assets |
|
||||
| ean | AssetEan | Yes | N/A | Single Elastic Asset Name representing the asset for which the related assets are being requested |
|
||||
| type | AssetType[] | No | all | Restrict results to one or more asset.type value |
|
||||
| maxDistance | number (1-5) | No | 1 | Maximum number of "hops" to search away from specified asset |
|
||||
|
||||
#### Responses
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Request looking for ancestors</summary>
|
||||
|
||||
```curl
|
||||
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
|
||||
{
|
||||
"results": {
|
||||
"primary": {
|
||||
"@timestamp": "2023-04-18T14:10:13.111Z",
|
||||
"asset.type": "k8s.node",
|
||||
"asset.id": "node-101",
|
||||
"asset.name": "k8s-node-101-aws",
|
||||
"asset.ean": "k8s.node:node-101",
|
||||
"asset.parents": [
|
||||
"k8s.cluster:cluster-001"
|
||||
],
|
||||
"orchestrator.type": "kubernetes",
|
||||
"orchestrator.cluster.name": "Cluster 001 (AWS EKS)",
|
||||
"orchestrator.cluster.id": "cluster-001",
|
||||
"cloud.provider": "aws",
|
||||
"cloud.region": "us-east-1",
|
||||
"cloud.service.name": "eks"
|
||||
},
|
||||
"ancestors": [
|
||||
{
|
||||
"@timestamp": "2023-04-18T14:10:13.111Z",
|
||||
"asset.type": "k8s.cluster",
|
||||
"asset.id": "cluster-001",
|
||||
"asset.name": "Cluster 001 (AWS EKS)",
|
||||
"asset.ean": "k8s.cluster:cluster-001",
|
||||
"orchestrator.type": "kubernetes",
|
||||
"orchestrator.cluster.name": "Cluster 001 (AWS EKS)",
|
||||
"orchestrator.cluster.id": "cluster-001",
|
||||
"cloud.provider": "aws",
|
||||
"cloud.region": "us-east-1",
|
||||
"cloud.service.name": "eks",
|
||||
"distance": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Request looking for descendants</summary>
|
||||
|
||||
```curl
|
||||
GET kbn:/api/asset-manager/assets/related?ean=k8s.cluster:cluster-001&relation=descendants&maxDistance=1&from=2023-04-18T13:10:13.111Z&to=2023-04-18T15:10:13.111Z
|
||||
|
||||
{
|
||||
"results": {
|
||||
"primary": {
|
||||
"@timestamp": "2023-04-18T14:10:13.111Z",
|
||||
"asset.type": "k8s.cluster",
|
||||
"asset.id": "cluster-001",
|
||||
"asset.name": "Cluster 001 (AWS EKS)",
|
||||
"asset.ean": "k8s.cluster:cluster-001",
|
||||
"orchestrator.type": "kubernetes",
|
||||
"orchestrator.cluster.name": "Cluster 001 (AWS EKS)",
|
||||
"orchestrator.cluster.id": "cluster-001",
|
||||
"cloud.provider": "aws",
|
||||
"cloud.region": "us-east-1",
|
||||
"cloud.service.name": "eks"
|
||||
},
|
||||
"descendants": [
|
||||
{
|
||||
"@timestamp": "2023-04-18T14:10:13.111Z",
|
||||
"asset.type": "k8s.node",
|
||||
"asset.id": "node-101",
|
||||
"asset.name": "k8s-node-101-aws",
|
||||
"asset.ean": "k8s.node:node-101",
|
||||
"asset.parents": [
|
||||
"k8s.cluster:cluster-001"
|
||||
],
|
||||
"orchestrator.type": "kubernetes",
|
||||
"orchestrator.cluster.name": "Cluster 001 (AWS EKS)",
|
||||
"orchestrator.cluster.id": "cluster-001",
|
||||
"cloud.provider": "aws",
|
||||
"cloud.region": "us-east-1",
|
||||
"cloud.service.name": "eks",
|
||||
"distance": 1
|
||||
},
|
||||
{
|
||||
"@timestamp": "2023-04-18T14:10:13.111Z",
|
||||
"asset.type": "k8s.node",
|
||||
"asset.id": "node-102",
|
||||
"asset.name": "k8s-node-102-aws",
|
||||
"asset.ean": "k8s.node:node-102",
|
||||
"asset.parents": [
|
||||
"k8s.cluster:cluster-001"
|
||||
],
|
||||
"orchestrator.type": "kubernetes",
|
||||
"orchestrator.cluster.name": "Cluster 001 (AWS EKS)",
|
||||
"orchestrator.cluster.id": "cluster-001",
|
||||
"cloud.provider": "aws",
|
||||
"cloud.region": "us-east-1",
|
||||
"cloud.service.name": "eks",
|
||||
"distance": 1
|
||||
},
|
||||
{
|
||||
"@timestamp": "2023-04-18T14:10:13.111Z",
|
||||
"asset.type": "k8s.node",
|
||||
"asset.id": "node-103",
|
||||
"asset.name": "k8s-node-103-aws",
|
||||
"asset.ean": "k8s.node:node-103",
|
||||
"asset.parents": [
|
||||
"k8s.cluster:cluster-001"
|
||||
],
|
||||
"orchestrator.type": "kubernetes",
|
||||
"orchestrator.cluster.name": "Cluster 001 (AWS EKS)",
|
||||
"orchestrator.cluster.id": "cluster-001",
|
||||
"cloud.provider": "aws",
|
||||
"cloud.region": "us-east-1",
|
||||
"cloud.service.name": "eks",
|
||||
"distance": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Request looking for references</summary>
|
||||
|
||||
```curl
|
||||
GET kbn:/api/asset-manager/assets/related?ean=k8s.pod:pod-200xrg1&relation=references&maxDistance=1&from=2023-04-18T13:10:13.111Z&to=2023-04-18T15:10:13.111Z
|
||||
|
||||
{
|
||||
"results": {
|
||||
"primary": {
|
||||
"@timestamp": "2023-04-18T14:10:13.111Z",
|
||||
"asset.type": "k8s.pod",
|
||||
"asset.id": "pod-200xrg1",
|
||||
"asset.name": "k8s-pod-200xrg1-aws",
|
||||
"asset.ean": "k8s.pod:pod-200xrg1",
|
||||
"asset.parents": [
|
||||
"k8s.node:node-101"
|
||||
],
|
||||
"asset.references": [
|
||||
"k8s.cluster:cluster-001"
|
||||
]
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"@timestamp": "2023-04-18T14:10:13.111Z",
|
||||
"asset.type": "k8s.cluster",
|
||||
"asset.id": "cluster-001",
|
||||
"asset.name": "Cluster 001 (AWS EKS)",
|
||||
"asset.ean": "k8s.cluster:cluster-001",
|
||||
"orchestrator.type": "kubernetes",
|
||||
"orchestrator.cluster.name": "Cluster 001 (AWS EKS)",
|
||||
"orchestrator.cluster.id": "cluster-001",
|
||||
"cloud.provider": "aws",
|
||||
"cloud.region": "us-east-1",
|
||||
"cloud.service.name": "eks",
|
||||
"distance": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Request with type filter and non-default maxDistance</summary>
|
||||
|
||||
```curl
|
||||
GET kbn:/api/asset-manager/assets/related?ean=k8s.cluster:cluster-001&relation=descendants&maxDistance=2&from=2023-04-18T13:10:13.111Z&to=2023-04-18T15:10:13.111Z&type=k8s.pod
|
||||
|
||||
{
|
||||
"results": {
|
||||
"primary": {
|
||||
"@timestamp": "2023-04-18T14:10:13.111Z",
|
||||
"asset.type": "k8s.cluster",
|
||||
"asset.id": "cluster-001",
|
||||
"asset.name": "Cluster 001 (AWS EKS)",
|
||||
"asset.ean": "k8s.cluster:cluster-001",
|
||||
"orchestrator.type": "kubernetes",
|
||||
"orchestrator.cluster.name": "Cluster 001 (AWS EKS)",
|
||||
"orchestrator.cluster.id": "cluster-001",
|
||||
"cloud.provider": "aws",
|
||||
"cloud.region": "us-east-1",
|
||||
"cloud.service.name": "eks"
|
||||
},
|
||||
"descendants": [
|
||||
{
|
||||
"@timestamp": "2023-04-18T14:10:13.111Z",
|
||||
"asset.type": "k8s.pod",
|
||||
"asset.id": "pod-200xrg1",
|
||||
"asset.name": "k8s-pod-200xrg1-aws",
|
||||
"asset.ean": "k8s.pod:pod-200xrg1",
|
||||
"asset.parents": [
|
||||
"k8s.node:node-101"
|
||||
],
|
||||
"asset.references": [
|
||||
"k8s.cluster:cluster-001"
|
||||
],
|
||||
"distance": 2
|
||||
},
|
||||
{
|
||||
"@timestamp": "2023-04-18T14:10:13.111Z",
|
||||
"asset.type": "k8s.pod",
|
||||
"asset.id": "pod-200dfp2",
|
||||
"asset.name": "k8s-pod-200dfp2-aws",
|
||||
"asset.ean": "k8s.pod:pod-200dfp2",
|
||||
"asset.parents": [
|
||||
"k8s.node:node-101"
|
||||
],
|
||||
"distance": 2
|
||||
},
|
||||
{
|
||||
"@timestamp": "2023-04-18T14:10:13.111Z",
|
||||
"asset.type": "k8s.pod",
|
||||
"asset.id": "pod-200wwc3",
|
||||
"asset.name": "k8s-pod-200wwc3-aws",
|
||||
"asset.ean": "k8s.pod:pod-200wwc3",
|
||||
"asset.parents": [
|
||||
"k8s.node:node-101"
|
||||
],
|
||||
"distance": 2
|
||||
},
|
||||
{
|
||||
"@timestamp": "2023-04-18T14:10:13.111Z",
|
||||
"asset.type": "k8s.pod",
|
||||
"asset.id": "pod-200naq4",
|
||||
"asset.name": "k8s-pod-200naq4-aws",
|
||||
"asset.ean": "k8s.pod:pod-200naq4",
|
||||
"asset.parents": [
|
||||
"k8s.node:node-102"
|
||||
],
|
||||
"distance": 2
|
||||
},
|
||||
{
|
||||
"@timestamp": "2023-04-18T14:10:13.111Z",
|
||||
"asset.type": "k8s.pod",
|
||||
"asset.id": "pod-200ohr5",
|
||||
"asset.name": "k8s-pod-200ohr5-aws",
|
||||
"asset.ean": "k8s.pod:pod-200ohr5",
|
||||
"asset.parents": [
|
||||
"k8s.node:node-102"
|
||||
],
|
||||
"distance": 2
|
||||
},
|
||||
{
|
||||
"@timestamp": "2023-04-18T14:10:13.111Z",
|
||||
"asset.type": "k8s.pod",
|
||||
"asset.id": "pod-200yyx6",
|
||||
"asset.name": "k8s-pod-200yyx6-aws",
|
||||
"asset.ean": "k8s.pod:pod-200yyx6",
|
||||
"asset.parents": [
|
||||
"k8s.node:node-103"
|
||||
],
|
||||
"distance": 2
|
||||
},
|
||||
{
|
||||
"@timestamp": "2023-04-18T14:10:13.111Z",
|
||||
"asset.type": "k8s.pod",
|
||||
"asset.id": "pod-200psd7",
|
||||
"asset.name": "k8s-pod-200psd7-aws",
|
||||
"asset.ean": "k8s.pod:pod-200psd7",
|
||||
"asset.parents": [
|
||||
"k8s.node:node-103"
|
||||
],
|
||||
"distance": 2
|
||||
},
|
||||
{
|
||||
"@timestamp": "2023-04-18T14:10:13.111Z",
|
||||
"asset.type": "k8s.pod",
|
||||
"asset.id": "pod-200wmc8",
|
||||
"asset.name": "k8s-pod-200wmc8-aws",
|
||||
"asset.ean": "k8s.pod:pod-200wmc8",
|
||||
"asset.parents": [
|
||||
"k8s.node:node-103"
|
||||
],
|
||||
"distance": 2
|
||||
},
|
||||
{
|
||||
"@timestamp": "2023-04-18T14:10:13.111Z",
|
||||
"asset.type": "k8s.pod",
|
||||
"asset.id": "pod-200ugg9",
|
||||
"asset.name": "k8s-pod-200ugg9-aws",
|
||||
"asset.ean": "k8s.pod:pod-200ugg9",
|
||||
"asset.parents": [
|
||||
"k8s.node:node-103"
|
||||
],
|
||||
"distance": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
#### GET /assets/sample
|
||||
|
||||
Returns the list of pre-defined sample asset documents that would be indexed
|
||||
|
|
17
x-pack/plugins/asset_manager/jest.config.js
Normal file
17
x-pack/plugins/asset_manager/jest.config.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../../..',
|
||||
roots: ['<rootDir>/x-pack/plugins/asset_manager'],
|
||||
coverageDirectory: '<rootDir>/target/kibana-coverage/jest/x-pack/plugins/asset_manager',
|
||||
coverageReporters: ['text', 'html'],
|
||||
collectCoverageFrom: [
|
||||
'<rootDir>/x-pack/plugins/asset_manager/{common,public,server}/**/*.{js,ts,tsx}',
|
||||
],
|
||||
};
|
14
x-pack/plugins/asset_manager/server/lib/errors.ts
Normal file
14
x-pack/plugins/asset_manager/server/lib/errors.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* 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 class AssetNotFoundError extends Error {
|
||||
constructor(ean: string) {
|
||||
super(`Asset with ean (${ean}) not found in the provided time range`);
|
||||
Object.setPrototypeOf(this, new.target.prototype);
|
||||
this.name = 'AssetNotFoundError';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,529 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
jest.mock('./get_assets', () => ({ getAssets: jest.fn() }));
|
||||
jest.mock('./get_related_assets', () => ({ getRelatedAssets: jest.fn() }));
|
||||
|
||||
import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { AssetWithoutTimestamp } from '../../common/types_api';
|
||||
import { getAssets } from './get_assets'; // Mocked
|
||||
import { getRelatedAssets } from './get_related_assets'; // Mocked
|
||||
import { getAllRelatedAssets } from './get_all_related_assets';
|
||||
|
||||
const esClientMock = elasticsearchClientMock.createScopedClusterClient().asCurrentUser;
|
||||
|
||||
describe('getAllRelatedAssets', () => {
|
||||
beforeEach(() => {
|
||||
(getAssets as jest.Mock).mockReset();
|
||||
(getRelatedAssets as jest.Mock).mockReset();
|
||||
});
|
||||
|
||||
it('throws if it cannot find the primary asset', async () => {
|
||||
const primaryAsset: AssetWithoutTimestamp = {
|
||||
'asset.ean': 'primary-which-does-not-exist',
|
||||
'asset.type': 'k8s.pod',
|
||||
'asset.id': uuid(),
|
||||
};
|
||||
|
||||
// Mock that we cannot find the primary
|
||||
(getAssets as jest.Mock).mockResolvedValueOnce([]);
|
||||
// Ensure maxDistance is respected
|
||||
(getAssets as jest.Mock).mockRejectedValueOnce(new Error('Should respect maxDistance'));
|
||||
(getRelatedAssets as jest.Mock).mockRejectedValueOnce(new Error('Should respect maxDistance'));
|
||||
|
||||
await expect(
|
||||
getAllRelatedAssets(esClientMock, {
|
||||
ean: primaryAsset['asset.ean'],
|
||||
relation: 'ancestors',
|
||||
from: new Date().toISOString(),
|
||||
maxDistance: 1,
|
||||
size: 10,
|
||||
})
|
||||
).rejects.toThrow(
|
||||
`Asset with ean (${primaryAsset['asset.ean']}) not found in the provided time range`
|
||||
);
|
||||
});
|
||||
|
||||
it('returns only the primary if it does not have any ancestors', async () => {
|
||||
const primaryAssetWithoutParents: AssetWithoutTimestamp = {
|
||||
'asset.ean': 'primary-ean',
|
||||
'asset.type': 'k8s.pod',
|
||||
'asset.id': uuid(),
|
||||
'asset.parents': [],
|
||||
};
|
||||
|
||||
(getAssets as jest.Mock).mockResolvedValueOnce([primaryAssetWithoutParents]);
|
||||
// Distance 1
|
||||
(getRelatedAssets as jest.Mock).mockResolvedValueOnce([]);
|
||||
// Ensure maxDistance is respected
|
||||
(getAssets as jest.Mock).mockRejectedValueOnce(new Error('Should respect maxDistance'));
|
||||
(getRelatedAssets as jest.Mock).mockRejectedValueOnce(new Error('Should respect maxDistance'));
|
||||
|
||||
await expect(
|
||||
getAllRelatedAssets(esClientMock, {
|
||||
ean: primaryAssetWithoutParents['asset.ean'],
|
||||
from: new Date().toISOString(),
|
||||
relation: 'ancestors',
|
||||
maxDistance: 1,
|
||||
size: 10,
|
||||
})
|
||||
).resolves.toStrictEqual({
|
||||
primary: primaryAssetWithoutParents,
|
||||
ancestors: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the primary and a directly referenced parent', async () => {
|
||||
const parentAsset: AssetWithoutTimestamp = {
|
||||
'asset.ean': 'primary-ean',
|
||||
'asset.type': 'k8s.pod',
|
||||
'asset.id': uuid(),
|
||||
};
|
||||
|
||||
const primaryAssetWithDirectParent: AssetWithoutTimestamp = {
|
||||
'asset.ean': 'primary-ean',
|
||||
'asset.type': 'k8s.pod',
|
||||
'asset.id': uuid(),
|
||||
'asset.parents': [parentAsset['asset.ean']],
|
||||
};
|
||||
|
||||
// Primary
|
||||
(getAssets as jest.Mock).mockResolvedValueOnce([primaryAssetWithDirectParent]);
|
||||
// Distance 1
|
||||
(getAssets as jest.Mock).mockResolvedValueOnce([parentAsset]);
|
||||
(getRelatedAssets as jest.Mock).mockResolvedValueOnce([]);
|
||||
// Ensure maxDistance is respected
|
||||
(getAssets as jest.Mock).mockRejectedValueOnce(new Error('Should respect maxDistance'));
|
||||
(getRelatedAssets as jest.Mock).mockRejectedValueOnce(new Error('Should respect maxDistance'));
|
||||
|
||||
await expect(
|
||||
getAllRelatedAssets(esClientMock, {
|
||||
ean: primaryAssetWithDirectParent['asset.ean'],
|
||||
from: new Date().toISOString(),
|
||||
relation: 'ancestors',
|
||||
maxDistance: 1,
|
||||
size: 10,
|
||||
})
|
||||
).resolves.toStrictEqual({
|
||||
primary: primaryAssetWithDirectParent,
|
||||
ancestors: [
|
||||
{
|
||||
...parentAsset,
|
||||
distance: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the primary and an indirectly referenced parent', async () => {
|
||||
const primaryAssetWithIndirectParent: AssetWithoutTimestamp = {
|
||||
'asset.ean': 'primary-ean',
|
||||
'asset.type': 'k8s.pod',
|
||||
'asset.id': uuid(),
|
||||
'asset.parents': [],
|
||||
};
|
||||
|
||||
const parentAsset: AssetWithoutTimestamp = {
|
||||
'asset.ean': 'primary-ean',
|
||||
'asset.type': 'k8s.pod',
|
||||
'asset.id': uuid(),
|
||||
'asset.children': [primaryAssetWithIndirectParent['asset.ean']],
|
||||
};
|
||||
|
||||
// Primary
|
||||
(getAssets as jest.Mock).mockResolvedValueOnce([primaryAssetWithIndirectParent]);
|
||||
// Distance 1
|
||||
(getAssets as jest.Mock).mockResolvedValueOnce([]);
|
||||
(getRelatedAssets as jest.Mock).mockResolvedValueOnce([parentAsset]);
|
||||
// Ensure maxDistance is respected
|
||||
(getAssets as jest.Mock).mockRejectedValueOnce(new Error('Should respect maxDistance'));
|
||||
(getRelatedAssets as jest.Mock).mockRejectedValueOnce(new Error('Should respect maxDistance'));
|
||||
|
||||
await expect(
|
||||
getAllRelatedAssets(esClientMock, {
|
||||
ean: primaryAssetWithIndirectParent['asset.ean'],
|
||||
from: new Date().toISOString(),
|
||||
relation: 'ancestors',
|
||||
maxDistance: 1,
|
||||
size: 10,
|
||||
})
|
||||
).resolves.toStrictEqual({
|
||||
primary: primaryAssetWithIndirectParent,
|
||||
ancestors: [
|
||||
{
|
||||
...parentAsset,
|
||||
distance: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the primary and all distance 1 parents', async () => {
|
||||
const directlyReferencedParent: AssetWithoutTimestamp = {
|
||||
'asset.ean': 'directly-referenced-parent-ean',
|
||||
'asset.type': 'k8s.pod',
|
||||
'asset.id': uuid(),
|
||||
'asset.children': [],
|
||||
};
|
||||
|
||||
const primaryAsset: AssetWithoutTimestamp = {
|
||||
'asset.ean': 'primary-ean',
|
||||
'asset.type': 'k8s.pod',
|
||||
'asset.id': uuid(),
|
||||
'asset.parents': [directlyReferencedParent['asset.ean']],
|
||||
};
|
||||
|
||||
const indirectlyReferencedParent: AssetWithoutTimestamp = {
|
||||
'asset.ean': 'indirectly-referenced-parent-ean',
|
||||
'asset.type': 'k8s.pod',
|
||||
'asset.id': uuid(),
|
||||
'asset.children': [primaryAsset['asset.ean']],
|
||||
};
|
||||
|
||||
// Primary
|
||||
(getAssets as jest.Mock).mockResolvedValueOnce([primaryAsset]);
|
||||
// Distance 1
|
||||
(getAssets as jest.Mock).mockResolvedValueOnce([directlyReferencedParent]);
|
||||
(getRelatedAssets as jest.Mock).mockResolvedValueOnce([indirectlyReferencedParent]);
|
||||
// Ensure maxDistance is respected
|
||||
(getAssets as jest.Mock).mockRejectedValueOnce(new Error('Should respect maxDistance'));
|
||||
(getRelatedAssets as jest.Mock).mockRejectedValueOnce(new Error('Should respect maxDistance'));
|
||||
|
||||
await expect(
|
||||
getAllRelatedAssets(esClientMock, {
|
||||
ean: primaryAsset['asset.ean'],
|
||||
from: new Date().toISOString(),
|
||||
relation: 'ancestors',
|
||||
maxDistance: 1,
|
||||
size: 10,
|
||||
})
|
||||
).resolves.toStrictEqual({
|
||||
primary: primaryAsset,
|
||||
ancestors: [
|
||||
{
|
||||
...directlyReferencedParent,
|
||||
distance: 1,
|
||||
},
|
||||
{
|
||||
...indirectlyReferencedParent,
|
||||
distance: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the primary and one parent even with a two way relation defined', async () => {
|
||||
const parentAsset: AssetWithoutTimestamp = {
|
||||
'asset.ean': 'parent-ean',
|
||||
'asset.type': 'k8s.pod',
|
||||
'asset.id': uuid(),
|
||||
};
|
||||
|
||||
const primaryAsset: AssetWithoutTimestamp = {
|
||||
'asset.ean': 'primary-ean',
|
||||
'asset.type': 'k8s.pod',
|
||||
'asset.id': uuid(),
|
||||
};
|
||||
|
||||
primaryAsset['asset.parents'] = [parentAsset['asset.ean']];
|
||||
parentAsset['asset.children'] = [primaryAsset['asset.ean']];
|
||||
|
||||
// Primary
|
||||
(getAssets as jest.Mock).mockResolvedValueOnce([primaryAsset]);
|
||||
// Distance 1
|
||||
(getAssets as jest.Mock).mockResolvedValueOnce([parentAsset]);
|
||||
// Code should filter out any directly referenced parent from the indirectly referenced parents query
|
||||
(getRelatedAssets as jest.Mock).mockResolvedValueOnce([]);
|
||||
// Ensure maxDistance is respected
|
||||
(getAssets as jest.Mock).mockRejectedValueOnce(new Error('Should respect maxDistance'));
|
||||
(getRelatedAssets as jest.Mock).mockRejectedValueOnce(new Error('Should respect maxDistance'));
|
||||
|
||||
await expect(
|
||||
getAllRelatedAssets(esClientMock, {
|
||||
ean: primaryAsset['asset.ean'],
|
||||
from: new Date().toISOString(),
|
||||
relation: 'ancestors',
|
||||
maxDistance: 1,
|
||||
size: 10,
|
||||
})
|
||||
).resolves.toStrictEqual({
|
||||
primary: primaryAsset,
|
||||
ancestors: [
|
||||
{
|
||||
...parentAsset,
|
||||
distance: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns relations from 5 jumps', async () => {
|
||||
const distance6Parent: AssetWithoutTimestamp = {
|
||||
'asset.ean': 'parent-5-ean',
|
||||
'asset.type': 'k8s.pod',
|
||||
'asset.id': uuid(),
|
||||
};
|
||||
|
||||
const distance5Parent: AssetWithoutTimestamp = {
|
||||
'asset.ean': 'parent-5-ean',
|
||||
'asset.type': 'k8s.pod',
|
||||
'asset.id': uuid(),
|
||||
'asset.parents': [distance6Parent['asset.ean']],
|
||||
};
|
||||
|
||||
const distance4Parent: AssetWithoutTimestamp = {
|
||||
'asset.ean': 'parent-4-ean',
|
||||
'asset.type': 'k8s.pod',
|
||||
'asset.id': uuid(),
|
||||
'asset.parents': [distance5Parent['asset.ean']],
|
||||
};
|
||||
|
||||
const distance3Parent: AssetWithoutTimestamp = {
|
||||
'asset.ean': 'parent-3-ean',
|
||||
'asset.type': 'k8s.pod',
|
||||
'asset.id': uuid(),
|
||||
'asset.parents': [distance4Parent['asset.ean']],
|
||||
};
|
||||
|
||||
const distance2Parent: AssetWithoutTimestamp = {
|
||||
'asset.ean': 'parent-2-ean',
|
||||
'asset.type': 'k8s.pod',
|
||||
'asset.id': uuid(),
|
||||
'asset.parents': [distance3Parent['asset.ean']],
|
||||
};
|
||||
|
||||
const distance1Parent: AssetWithoutTimestamp = {
|
||||
'asset.ean': 'parent-1-ean',
|
||||
'asset.type': 'k8s.pod',
|
||||
'asset.id': uuid(),
|
||||
'asset.parents': [distance2Parent['asset.ean']],
|
||||
};
|
||||
|
||||
const primaryAsset: AssetWithoutTimestamp = {
|
||||
'asset.ean': 'primary-ean',
|
||||
'asset.type': 'k8s.pod',
|
||||
'asset.id': uuid(),
|
||||
'asset.parents': [distance1Parent['asset.ean']],
|
||||
};
|
||||
|
||||
// Only using directly referenced parents
|
||||
(getRelatedAssets as jest.Mock).mockResolvedValue([]);
|
||||
|
||||
// Primary
|
||||
(getAssets as jest.Mock).mockResolvedValueOnce([primaryAsset]);
|
||||
// Distance 1
|
||||
(getAssets as jest.Mock).mockResolvedValueOnce([distance1Parent]);
|
||||
// Distance 2
|
||||
(getAssets as jest.Mock).mockResolvedValueOnce([distance2Parent]);
|
||||
// Distance 3
|
||||
(getAssets as jest.Mock).mockResolvedValueOnce([distance3Parent]);
|
||||
// Distance 4
|
||||
(getAssets as jest.Mock).mockResolvedValueOnce([distance4Parent]);
|
||||
// Distance 5
|
||||
(getAssets as jest.Mock).mockResolvedValueOnce([distance5Parent]);
|
||||
// Should not exceed maxDistance
|
||||
(getAssets as jest.Mock).mockRejectedValueOnce(new Error('Should respect maxDistance'));
|
||||
|
||||
await expect(
|
||||
getAllRelatedAssets(esClientMock, {
|
||||
ean: primaryAsset['asset.ean'],
|
||||
from: new Date().toISOString(),
|
||||
relation: 'ancestors',
|
||||
maxDistance: 5,
|
||||
size: 10,
|
||||
})
|
||||
).resolves.toStrictEqual({
|
||||
primary: primaryAsset,
|
||||
ancestors: [
|
||||
{
|
||||
...distance1Parent,
|
||||
distance: 1,
|
||||
},
|
||||
{
|
||||
...distance2Parent,
|
||||
distance: 2,
|
||||
},
|
||||
{
|
||||
...distance3Parent,
|
||||
distance: 3,
|
||||
},
|
||||
{
|
||||
...distance4Parent,
|
||||
distance: 4,
|
||||
},
|
||||
{
|
||||
...distance5Parent,
|
||||
distance: 5,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns relations from only 3 jumps if there are no more parents', async () => {
|
||||
const distance3Parent: AssetWithoutTimestamp = {
|
||||
'asset.ean': 'parent-3-ean',
|
||||
'asset.type': 'k8s.pod',
|
||||
'asset.id': uuid(),
|
||||
};
|
||||
|
||||
const distance2Parent: AssetWithoutTimestamp = {
|
||||
'asset.ean': 'parent-2-ean',
|
||||
'asset.type': 'k8s.pod',
|
||||
'asset.id': uuid(),
|
||||
'asset.parents': [distance3Parent['asset.ean']],
|
||||
};
|
||||
|
||||
const distance1Parent: AssetWithoutTimestamp = {
|
||||
'asset.ean': 'parent-1-ean',
|
||||
'asset.type': 'k8s.pod',
|
||||
'asset.id': uuid(),
|
||||
'asset.parents': [distance2Parent['asset.ean']],
|
||||
};
|
||||
|
||||
const primaryAsset: AssetWithoutTimestamp = {
|
||||
'asset.ean': 'primary-ean',
|
||||
'asset.type': 'k8s.pod',
|
||||
'asset.id': uuid(),
|
||||
'asset.parents': [distance1Parent['asset.ean']],
|
||||
};
|
||||
|
||||
// Only using directly referenced parents
|
||||
(getRelatedAssets as jest.Mock).mockResolvedValue([]);
|
||||
|
||||
// Primary
|
||||
(getAssets as jest.Mock).mockResolvedValueOnce([primaryAsset]);
|
||||
// Distance 1
|
||||
(getAssets as jest.Mock).mockResolvedValueOnce([distance1Parent]);
|
||||
// Distance 2
|
||||
(getAssets as jest.Mock).mockResolvedValueOnce([distance2Parent]);
|
||||
// Distance 3
|
||||
(getAssets as jest.Mock).mockResolvedValueOnce([distance3Parent]);
|
||||
|
||||
await expect(
|
||||
getAllRelatedAssets(esClientMock, {
|
||||
ean: primaryAsset['asset.ean'],
|
||||
from: new Date().toISOString(),
|
||||
relation: 'ancestors',
|
||||
maxDistance: 5,
|
||||
size: 10,
|
||||
})
|
||||
).resolves.toStrictEqual({
|
||||
primary: primaryAsset,
|
||||
ancestors: [
|
||||
{
|
||||
...distance1Parent,
|
||||
distance: 1,
|
||||
},
|
||||
{
|
||||
...distance2Parent,
|
||||
distance: 2,
|
||||
},
|
||||
{
|
||||
...distance3Parent,
|
||||
distance: 3,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns relations by distance even if there are multiple parents in each jump', async () => {
|
||||
const distance2ParentA: AssetWithoutTimestamp = {
|
||||
'asset.ean': 'parent-2-ean-a',
|
||||
'asset.type': 'k8s.pod',
|
||||
'asset.id': uuid(),
|
||||
};
|
||||
|
||||
const distance2ParentB: AssetWithoutTimestamp = {
|
||||
'asset.ean': 'parent-2-ean-b',
|
||||
'asset.type': 'k8s.pod',
|
||||
'asset.id': uuid(),
|
||||
};
|
||||
|
||||
const distance2ParentC: AssetWithoutTimestamp = {
|
||||
'asset.ean': 'parent-2-ean-c',
|
||||
'asset.type': 'k8s.pod',
|
||||
'asset.id': uuid(),
|
||||
};
|
||||
|
||||
const distance2ParentD: AssetWithoutTimestamp = {
|
||||
'asset.ean': 'parent-2-ean-d',
|
||||
'asset.type': 'k8s.pod',
|
||||
'asset.id': uuid(),
|
||||
};
|
||||
|
||||
const distance1ParentA: AssetWithoutTimestamp = {
|
||||
'asset.ean': 'parent-1-ean-a',
|
||||
'asset.type': 'k8s.pod',
|
||||
'asset.id': uuid(),
|
||||
'asset.parents': [distance2ParentA['asset.ean'], distance2ParentB['asset.ean']],
|
||||
};
|
||||
|
||||
const distance1ParentB: AssetWithoutTimestamp = {
|
||||
'asset.ean': 'parent-1-ean-b',
|
||||
'asset.type': 'k8s.pod',
|
||||
'asset.id': uuid(),
|
||||
'asset.parents': [distance2ParentC['asset.ean'], distance2ParentD['asset.ean']],
|
||||
};
|
||||
|
||||
const primaryAsset: AssetWithoutTimestamp = {
|
||||
'asset.ean': 'primary-ean',
|
||||
'asset.type': 'k8s.pod',
|
||||
'asset.id': uuid(),
|
||||
'asset.parents': [distance1ParentA['asset.ean'], distance1ParentB['asset.ean']],
|
||||
};
|
||||
|
||||
// Only using directly referenced parents
|
||||
(getRelatedAssets as jest.Mock).mockResolvedValue([]);
|
||||
|
||||
// Primary
|
||||
(getAssets as jest.Mock).mockResolvedValueOnce([primaryAsset]);
|
||||
// Distance 1
|
||||
(getAssets as jest.Mock).mockResolvedValueOnce([distance1ParentA, distance1ParentB]);
|
||||
// Distance 2 (the order matters)
|
||||
(getAssets as jest.Mock).mockResolvedValueOnce([distance2ParentA, distance2ParentB]);
|
||||
(getAssets as jest.Mock).mockResolvedValueOnce([distance2ParentC, distance2ParentD]);
|
||||
|
||||
await expect(
|
||||
getAllRelatedAssets(esClientMock, {
|
||||
ean: primaryAsset['asset.ean'],
|
||||
from: new Date().toISOString(),
|
||||
relation: 'ancestors',
|
||||
maxDistance: 5,
|
||||
size: 10,
|
||||
})
|
||||
).resolves.toStrictEqual({
|
||||
primary: primaryAsset,
|
||||
ancestors: [
|
||||
{
|
||||
...distance1ParentA,
|
||||
distance: 1,
|
||||
},
|
||||
{
|
||||
...distance1ParentB,
|
||||
distance: 1,
|
||||
},
|
||||
{
|
||||
...distance2ParentA,
|
||||
distance: 2,
|
||||
},
|
||||
{
|
||||
...distance2ParentB,
|
||||
distance: 2,
|
||||
},
|
||||
{
|
||||
...distance2ParentC,
|
||||
distance: 2,
|
||||
},
|
||||
{
|
||||
...distance2ParentD,
|
||||
distance: 2,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,145 @@
|
|||
/*
|
||||
* 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 { ElasticsearchClient } from '@kbn/core/server';
|
||||
import { flatten, without } from 'lodash';
|
||||
import { Asset, AssetType, Relation, RelationField } from '../../common/types_api';
|
||||
import { getAssets } from './get_assets';
|
||||
import { getRelatedAssets } from './get_related_assets';
|
||||
import { AssetNotFoundError } from './errors';
|
||||
import { toArray } from './utils';
|
||||
|
||||
interface GetAllRelatedAssetsOptions {
|
||||
ean: string;
|
||||
from: string;
|
||||
to?: string;
|
||||
relation: Relation;
|
||||
type?: AssetType[];
|
||||
maxDistance: number;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export async function getAllRelatedAssets(
|
||||
esClient: ElasticsearchClient,
|
||||
options: GetAllRelatedAssetsOptions
|
||||
) {
|
||||
// How to put size into this?
|
||||
const { ean, from, to, relation, maxDistance, type = [] } = options;
|
||||
|
||||
const primary = await findPrimary(esClient, { ean, from, to });
|
||||
|
||||
let assetsToFetch = [primary];
|
||||
let currentDistance = 1;
|
||||
const relatedAssets = [];
|
||||
while (currentDistance <= maxDistance) {
|
||||
const queryOptions: FindRelatedAssetsOptions = {
|
||||
relation,
|
||||
from,
|
||||
to,
|
||||
visitedEans: [primary['asset.ean'], ...relatedAssets.map((asset) => asset['asset.ean'])],
|
||||
};
|
||||
// if we enforce the type filter before the last query we'll miss nodes with
|
||||
// possible edges to the requested types
|
||||
if (currentDistance === maxDistance && type.length) {
|
||||
queryOptions.type = type;
|
||||
}
|
||||
|
||||
const results = flatten(
|
||||
await Promise.all(
|
||||
assetsToFetch.map((asset) => findRelatedAssets(esClient, asset, queryOptions))
|
||||
)
|
||||
);
|
||||
|
||||
if (results.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
relatedAssets.push(...results.map(withDistance(currentDistance)));
|
||||
|
||||
assetsToFetch = results;
|
||||
currentDistance++;
|
||||
}
|
||||
|
||||
return {
|
||||
primary,
|
||||
[relation]: type.length
|
||||
? relatedAssets.filter((asset) => type.includes(asset['asset.type']))
|
||||
: relatedAssets,
|
||||
};
|
||||
}
|
||||
|
||||
async function findPrimary(
|
||||
esClient: ElasticsearchClient,
|
||||
{ ean, from, to }: Pick<GetAllRelatedAssetsOptions, 'ean' | 'from' | 'to'>
|
||||
): Promise<Asset> {
|
||||
const primaryResults = await getAssets({
|
||||
esClient,
|
||||
size: 1,
|
||||
filters: { ean, from, to },
|
||||
});
|
||||
|
||||
if (primaryResults.length === 0) {
|
||||
throw new AssetNotFoundError(ean);
|
||||
}
|
||||
|
||||
if (primaryResults.length > 1) {
|
||||
throw new Error(`Illegal state: Found more than one asset with the same ean (ean=${ean}).`);
|
||||
}
|
||||
|
||||
return primaryResults[0];
|
||||
}
|
||||
|
||||
type FindRelatedAssetsOptions = Pick<
|
||||
GetAllRelatedAssetsOptions,
|
||||
'relation' | 'type' | 'from' | 'to'
|
||||
> & { visitedEans: string[] };
|
||||
|
||||
async function findRelatedAssets(
|
||||
esClient: ElasticsearchClient,
|
||||
primary: Asset,
|
||||
{ relation, from, to, type, visitedEans }: FindRelatedAssetsOptions
|
||||
): Promise<Asset[]> {
|
||||
const relationField = relationToDirectField(relation);
|
||||
const directlyRelatedEans = toArray(primary[relationField]);
|
||||
|
||||
let directlyRelatedAssets: Asset[] = [];
|
||||
if (directlyRelatedEans.length) {
|
||||
// get the directly related assets we haven't visited already
|
||||
directlyRelatedAssets = await getAssets({
|
||||
esClient,
|
||||
filters: { ean: without(directlyRelatedEans, ...visitedEans), from, to, type },
|
||||
});
|
||||
}
|
||||
|
||||
const indirectlyRelatedAssets = await getRelatedAssets({
|
||||
esClient,
|
||||
ean: primary['asset.ean'],
|
||||
excludeEans: visitedEans.concat(directlyRelatedEans),
|
||||
relation,
|
||||
from,
|
||||
to,
|
||||
type,
|
||||
});
|
||||
|
||||
return [...directlyRelatedAssets, ...indirectlyRelatedAssets];
|
||||
}
|
||||
|
||||
function relationToDirectField(relation: Relation): RelationField {
|
||||
if (relation === 'ancestors') {
|
||||
return 'asset.parents';
|
||||
} else if (relation === 'descendants') {
|
||||
return 'asset.children';
|
||||
} else {
|
||||
return 'asset.references';
|
||||
}
|
||||
}
|
||||
|
||||
function withDistance(
|
||||
distance: number
|
||||
): (value: Asset, index: number, array: Asset[]) => Asset & { distance: number } {
|
||||
return (asset: Asset) => ({ ...asset, distance });
|
||||
}
|
|
@ -23,6 +23,8 @@ export async function getAssets({
|
|||
size = 100,
|
||||
filters = {},
|
||||
}: GetAssetsOptions): Promise<Asset[]> {
|
||||
// Maybe it makes the most sense to validate the filters here?
|
||||
|
||||
const { from = 'now-24h', to = 'now' } = filters;
|
||||
const dsl: SearchRequest = {
|
||||
index: ASSETS_INDEX_PREFIX + '*',
|
||||
|
@ -62,7 +64,7 @@ export async function getAssets({
|
|||
});
|
||||
}
|
||||
|
||||
if (filters.type) {
|
||||
if (filters.type?.length) {
|
||||
musts.push({
|
||||
terms: {
|
||||
['asset.type']: Array.isArray(filters.type) ? filters.type : [filters.type],
|
||||
|
|
105
x-pack/plugins/asset_manager/server/lib/get_related_assets.ts
Normal file
105
x-pack/plugins/asset_manager/server/lib/get_related_assets.ts
Normal file
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* 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 { QueryDslQueryContainer, SearchRequest } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { debug } from '../../common/debug_log';
|
||||
import { Asset, AssetType, Relation, RelationField } from '../../common/types_api';
|
||||
import { ASSETS_INDEX_PREFIX } from '../constants';
|
||||
import { ElasticsearchAccessorOptions } from '../types';
|
||||
|
||||
interface GetRelatedAssetsOptions extends ElasticsearchAccessorOptions {
|
||||
size?: number;
|
||||
ean: string;
|
||||
excludeEans?: string[];
|
||||
from?: string;
|
||||
to?: string;
|
||||
relation: Relation;
|
||||
type?: AssetType[];
|
||||
}
|
||||
|
||||
export async function getRelatedAssets({
|
||||
esClient,
|
||||
size = 100,
|
||||
from = 'now-24h',
|
||||
to = 'now',
|
||||
ean,
|
||||
excludeEans,
|
||||
relation,
|
||||
type,
|
||||
}: GetRelatedAssetsOptions): Promise<Asset[]> {
|
||||
const relationField = relationToIndirectField(relation);
|
||||
const must: QueryDslQueryContainer[] = [
|
||||
{
|
||||
terms: {
|
||||
[relationField]: [ean],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
if (type?.length) {
|
||||
must.push({
|
||||
terms: {
|
||||
['asset.type']: type,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const mustNot: QueryDslQueryContainer[] =
|
||||
excludeEans && excludeEans.length
|
||||
? [
|
||||
{
|
||||
terms: {
|
||||
'asset.ean': excludeEans,
|
||||
},
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const dsl: SearchRequest = {
|
||||
index: ASSETS_INDEX_PREFIX + '*',
|
||||
size,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: from,
|
||||
lte: to,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
must,
|
||||
must_not: mustNot,
|
||||
},
|
||||
},
|
||||
collapse: {
|
||||
field: 'asset.ean',
|
||||
},
|
||||
sort: {
|
||||
'@timestamp': {
|
||||
order: 'desc',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
function relationToIndirectField(relation: Relation): RelationField {
|
||||
if (relation === 'ancestors') {
|
||||
return 'asset.children';
|
||||
} else if (relation === 'descendants') {
|
||||
return 'asset.parents';
|
||||
} else {
|
||||
return 'asset.references';
|
||||
}
|
||||
}
|
|
@ -103,6 +103,7 @@ const sampleK8sPods: AssetWithoutTimestamp[] = [
|
|||
'asset.name': 'k8s-pod-200xrg1-aws',
|
||||
'asset.ean': 'k8s.pod:pod-200xrg1',
|
||||
'asset.parents': ['k8s.node:node-101'],
|
||||
'asset.references': ['k8s.cluster:cluster-001'],
|
||||
},
|
||||
{
|
||||
'asset.type': 'k8s.pod',
|
||||
|
@ -162,8 +163,39 @@ const sampleK8sPods: AssetWithoutTimestamp[] = [
|
|||
},
|
||||
];
|
||||
|
||||
const sampleCircularReferences: AssetWithoutTimestamp[] = [
|
||||
{
|
||||
'asset.type': 'k8s.node',
|
||||
'asset.id': 'node-203',
|
||||
'asset.name': 'k8s-node-203-aws',
|
||||
'asset.ean': 'k8s.node:node-203',
|
||||
'orchestrator.type': 'kubernetes',
|
||||
'orchestrator.cluster.name': 'Cluster 001 (AWS EKS)',
|
||||
'orchestrator.cluster.id': 'cluster-001',
|
||||
'cloud.provider': 'aws',
|
||||
'cloud.region': 'us-east-1',
|
||||
'cloud.service.name': 'eks',
|
||||
'asset.references': ['k8s.pod:pod-203ugg9', 'k8s.pod:pod-203ugg5'],
|
||||
},
|
||||
{
|
||||
'asset.type': 'k8s.pod',
|
||||
'asset.id': 'pod-203ugg5',
|
||||
'asset.name': 'k8s-pod-203ugg5-aws',
|
||||
'asset.ean': 'k8s.pod:pod-203ugg5',
|
||||
'asset.references': ['k8s.node:node-203'],
|
||||
},
|
||||
{
|
||||
'asset.type': 'k8s.pod',
|
||||
'asset.id': 'pod-203ugg9',
|
||||
'asset.name': 'k8s-pod-203ugg9-aws',
|
||||
'asset.ean': 'k8s.pod:pod-203ugg9',
|
||||
'asset.references': ['k8s.node:node-203'],
|
||||
},
|
||||
];
|
||||
|
||||
export const sampleAssets: AssetWithoutTimestamp[] = [
|
||||
...sampleK8sClusters,
|
||||
...sampleK8sNodes,
|
||||
...sampleK8sPods,
|
||||
...sampleCircularReferences,
|
||||
];
|
||||
|
|
16
x-pack/plugins/asset_manager/server/lib/utils.ts
Normal file
16
x-pack/plugins/asset_manager/server/lib/utils.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* 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 function toArray<T>(maybeArray: T | T[] | undefined): T[] {
|
||||
if (!maybeArray) {
|
||||
return [];
|
||||
}
|
||||
if (Array.isArray(maybeArray)) {
|
||||
return maybeArray;
|
||||
}
|
||||
return [maybeArray];
|
||||
}
|
|
@ -9,10 +9,14 @@ import { schema } from '@kbn/config-schema';
|
|||
import { RequestHandlerContext } from '@kbn/core/server';
|
||||
import { differenceBy, intersectionBy } from 'lodash';
|
||||
import { debug } from '../../common/debug_log';
|
||||
import { AssetType, assetTypeRT, relationRT } from '../../common/types_api';
|
||||
import { ASSET_MANAGER_API_BASE } from '../constants';
|
||||
import { getAssets } from '../lib/get_assets';
|
||||
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';
|
||||
|
||||
const assetType = schema.oneOf([
|
||||
schema.literal('k8s.pod'),
|
||||
|
@ -23,11 +27,29 @@ const assetType = schema.oneOf([
|
|||
const getAssetsQueryOptions = schema.object({
|
||||
from: schema.maybe(schema.string()),
|
||||
to: schema.maybe(schema.string()),
|
||||
type: schema.maybe(schema.oneOf([schema.arrayOf(assetType), assetType])),
|
||||
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 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 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 function assetsRoutes<T extends RequestHandlerContext>({ router }: SetupRouteOptions<T>) {
|
||||
// GET /assets
|
||||
router.get<unknown, typeof getAssetsQueryOptions.type, unknown>(
|
||||
|
@ -58,14 +80,51 @@ export function assetsRoutes<T extends RequestHandlerContext>({ router }: SetupR
|
|||
}
|
||||
);
|
||||
|
||||
// GET assets/related
|
||||
router.get<unknown, typeof getRelatedAssetsQueryOptions.type, unknown>(
|
||||
{
|
||||
path: `${ASSET_MANAGER_API_BASE}/assets/related`,
|
||||
validate: {
|
||||
query: getRelatedAssetsQueryOptions,
|
||||
},
|
||||
},
|
||||
async (context, req, res) => {
|
||||
// Add references into sample data and write integration tests
|
||||
|
||||
const { from, to, ean, relation } = 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.
|
||||
|
||||
try {
|
||||
return res.ok({
|
||||
body: {
|
||||
results: await getAllRelatedAssets(esClient, {
|
||||
ean,
|
||||
from,
|
||||
to,
|
||||
type,
|
||||
maxDistance,
|
||||
size,
|
||||
relation,
|
||||
}),
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
debug('error looking up asset records', error);
|
||||
if (error instanceof AssetNotFoundError) {
|
||||
return res.customError({ statusCode: 404, body: error.message });
|
||||
}
|
||||
return res.customError({ statusCode: 500, body: error.message });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// GET /assets/diff
|
||||
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])),
|
||||
});
|
||||
router.get<unknown, typeof getAssetsDiffQueryOptions.type, unknown>(
|
||||
{
|
||||
path: `${ASSET_MANAGER_API_BASE}/assets/diff`,
|
||||
|
@ -74,7 +133,8 @@ export function assetsRoutes<T extends RequestHandlerContext>({ router }: SetupR
|
|||
},
|
||||
},
|
||||
async (context, req, res) => {
|
||||
const { aFrom, aTo, bFrom, bTo, type } = req.query;
|
||||
const { aFrom, aTo, bFrom, bTo } = req.query;
|
||||
const type = toArray<AssetType>(req.query.type);
|
||||
|
||||
if (new Date(aFrom) > new Date(aTo)) {
|
||||
return res.badRequest({
|
||||
|
|
|
@ -15,5 +15,6 @@
|
|||
"@kbn/core",
|
||||
"@kbn/config-schema",
|
||||
"@kbn/core-http-server",
|
||||
"@kbn/core-elasticsearch-client-server-mocks",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -5,13 +5,16 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { AssetWithoutTimestamp } from '@kbn/assetManager-plugin/common/types_api';
|
||||
import { pick, sortBy } 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';
|
||||
|
||||
const ASSETS_ENDPOINT = '/api/asset-manager/assets';
|
||||
const DIFF_ENDPOINT = ASSETS_ENDPOINT + '/diff';
|
||||
const DIFF_ENDPOINT = `${ASSETS_ENDPOINT}/diff`;
|
||||
const RELATED_ASSETS_ENDPOINT = `${ASSETS_ENDPOINT}/related`;
|
||||
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
|
@ -307,9 +310,252 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
delete asset['@timestamp'];
|
||||
});
|
||||
|
||||
expect(getResponse.body.onlyInA).to.eql(onlyInA);
|
||||
expect(getResponse.body.onlyInB).to.eql(onlyInB);
|
||||
expect(getResponse.body.inBoth).to.eql(inBoth);
|
||||
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'));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue