diff --git a/.buildkite/ftr_configs.yml b/.buildkite/ftr_configs.yml index b388545ce0e32..616f32285c99a 100644 --- a/.buildkite/ftr_configs.yml +++ b/.buildkite/ftr_configs.yml @@ -68,13 +68,11 @@ disabled: - x-pack/test/plugin_api_perf/config.js - x-pack/test/screenshot_creation/config.ts - x-pack/test/fleet_packages/config.ts + - x-pack/test/api_integration/apis/asset_manager/config_with_assets_source.ts # Scalability testing config that we run in its own pipeline - x-pack/test/scalability/config.ts - # Asset Manager configs, in tech preview, will move to enabled after more stability introduced - - x-pack/test/api_integration/apis/asset_manager/config.ts - # Serverless base config files - x-pack/test_serverless/api_integration/config.base.ts - x-pack/test_serverless/functional/config.base.ts @@ -176,6 +174,7 @@ enabled: - x-pack/test/api_integration/config_security_trial.ts - x-pack/test/api_integration/apis/aiops/config.ts - x-pack/test/api_integration/apis/asset_manager/config_when_disabled.ts + - x-pack/test/api_integration/apis/asset_manager/config_with_signals_source.ts - x-pack/test/api_integration/apis/cases/config.ts - x-pack/test/api_integration/apis/cloud_security_posture/config.ts - x-pack/test/api_integration/apis/console/config.ts diff --git a/x-pack/plugins/asset_manager/common/types_api.ts b/x-pack/plugins/asset_manager/common/types_api.ts index e1eb6e61f83c3..8e9e9181a29e4 100644 --- a/x-pack/plugins/asset_manager/common/types_api.ts +++ b/x-pack/plugins/asset_manager/common/types_api.ts @@ -15,7 +15,17 @@ export const assetTypeRT = rt.union([ export type AssetType = rt.TypeOf; -export type AssetKind = 'unknown' | 'node'; +export const assetKindRT = rt.union([ + rt.literal('cluster'), + rt.literal('host'), + rt.literal('pod'), + rt.literal('container'), + rt.literal('service'), + rt.literal('alert'), +]); + +export type AssetKind = rt.TypeOf; + export type AssetStatus = | 'CREATING' | 'ACTIVE' @@ -47,17 +57,20 @@ export interface ECSDocument extends WithTimestamp { 'orchestrator.cluster.version'?: string; 'cloud.provider'?: CloudProviderName; + 'cloud.instance.id'?: string; 'cloud.region'?: string; 'cloud.service.name'?: string; + + 'service.environment'?: string; } export interface Asset extends ECSDocument { 'asset.collection_version'?: string; 'asset.ean': string; 'asset.id': string; - 'asset.kind'?: AssetKind; + 'asset.kind': AssetKind; 'asset.name'?: string; - 'asset.type': AssetType; + 'asset.type'?: AssetType; 'asset.status'?: AssetStatus; 'asset.parents'?: string | string[]; 'asset.children'?: string | string[]; @@ -121,14 +134,15 @@ export interface K8sCluster extends WithTimestamp { export interface AssetFilters { type?: AssetType | AssetType[]; - kind?: AssetKind; + kind?: AssetKind | AssetKind[]; ean?: string | string[]; id?: string; typeLike?: string; + kindLike?: string; eanLike?: string; collectionVersion?: number | 'latest' | 'all'; - from?: string; - to?: string; + from?: string | number; + to?: string | number; } export const relationRT = rt.union([ diff --git a/x-pack/plugins/asset_manager/server/index.ts b/x-pack/plugins/asset_manager/server/index.ts index bb594df162e0e..d6eafa380b857 100644 --- a/x-pack/plugins/asset_manager/server/index.ts +++ b/x-pack/plugins/asset_manager/server/index.ts @@ -6,8 +6,9 @@ */ import { PluginInitializerContext } from '@kbn/core-plugins-server'; -import { AssetManagerServerPlugin, config, AssetManagerConfig } from './plugin'; +import { AssetManagerServerPlugin, config } from './plugin'; import type { WriteSamplesPostBody } from './routes/sample_assets'; +import { AssetManagerConfig } from './types'; export type { AssetManagerConfig, WriteSamplesPostBody }; export { config }; diff --git a/x-pack/plugins/asset_manager/server/lib/accessors/hosts/get_hosts_by_assets.ts b/x-pack/plugins/asset_manager/server/lib/accessors/hosts/get_hosts_by_assets.ts new file mode 100644 index 0000000000000..13e2d00a82083 --- /dev/null +++ b/x-pack/plugins/asset_manager/server/lib/accessors/hosts/get_hosts_by_assets.ts @@ -0,0 +1,27 @@ +/* + * 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 { Asset } from '../../../../common/types_api'; +import { GetHostsOptionsInjected } from '.'; +import { getAssets } from '../../get_assets'; + +export async function getHostsByAssets( + options: GetHostsOptionsInjected +): Promise<{ hosts: Asset[] }> { + const hosts = await getAssets({ + esClient: options.esClient, + filters: { + kind: 'host', + from: options.from, + to: options.to, + }, + }); + + return { + hosts, + }; +} diff --git a/x-pack/plugins/asset_manager/server/lib/accessors/hosts/get_hosts_by_signals.ts b/x-pack/plugins/asset_manager/server/lib/accessors/hosts/get_hosts_by_signals.ts new file mode 100644 index 0000000000000..a58bfac79c3d3 --- /dev/null +++ b/x-pack/plugins/asset_manager/server/lib/accessors/hosts/get_hosts_by_signals.ts @@ -0,0 +1,24 @@ +/* + * 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 { Asset } from '../../../../common/types_api'; +import { GetHostsOptionsInjected } from '.'; +import { collectHosts } from '../../collectors/hosts'; + +export async function getHostsBySignals( + options: GetHostsOptionsInjected +): Promise<{ hosts: Asset[] }> { + const { assets } = await collectHosts({ + client: options.esClient, + from: options.from, + to: options.to, + sourceIndices: options.sourceIndices, + }); + return { + hosts: assets, + }; +} diff --git a/x-pack/plugins/asset_manager/server/lib/accessors/hosts/index.ts b/x-pack/plugins/asset_manager/server/lib/accessors/hosts/index.ts new file mode 100644 index 0000000000000..6198824ba8a02 --- /dev/null +++ b/x-pack/plugins/asset_manager/server/lib/accessors/hosts/index.ts @@ -0,0 +1,20 @@ +/* + * 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 { AccessorOptions, OptionsWithInjectedValues } from '..'; + +export interface GetHostsOptions extends AccessorOptions { + from: number; + to: number; +} +export type GetHostsOptionsInjected = OptionsWithInjectedValues; + +export interface HostIdentifier { + 'asset.ean': string; + 'asset.id': string; + 'asset.name'?: string; +} diff --git a/x-pack/plugins/asset_manager/server/lib/accessors/index.ts b/x-pack/plugins/asset_manager/server/lib/accessors/index.ts new file mode 100644 index 0000000000000..a097703fb3b51 --- /dev/null +++ b/x-pack/plugins/asset_manager/server/lib/accessors/index.ts @@ -0,0 +1,19 @@ +/* + * 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-elasticsearch-server'; +import { AssetManagerConfig } from '../../types'; + +export interface InjectedValues { + sourceIndices: AssetManagerConfig['sourceIndices']; +} + +export type OptionsWithInjectedValues = T & InjectedValues; + +export interface AccessorOptions { + esClient: ElasticsearchClient; +} diff --git a/x-pack/plugins/asset_manager/server/lib/asset_accessor.ts b/x-pack/plugins/asset_manager/server/lib/asset_accessor.ts new file mode 100644 index 0000000000000..d31173d8d8c69 --- /dev/null +++ b/x-pack/plugins/asset_manager/server/lib/asset_accessor.ts @@ -0,0 +1,38 @@ +/* + * 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 { Asset } from '../../common/types_api'; +import { AssetManagerConfig } from '../types'; +import { OptionsWithInjectedValues } from './accessors'; +import { GetHostsOptions } from './accessors/hosts'; +import { getHostsByAssets } from './accessors/hosts/get_hosts_by_assets'; +import { getHostsBySignals } from './accessors/hosts/get_hosts_by_signals'; + +interface AssetAccessorClassOptions { + sourceIndices: AssetManagerConfig['sourceIndices']; + source: AssetManagerConfig['lockedSource']; +} + +export class AssetAccessor { + constructor(private options: AssetAccessorClassOptions) {} + + injectOptions(options: T): OptionsWithInjectedValues { + return { + ...options, + sourceIndices: this.options.sourceIndices, + }; + } + + async getHosts(options: GetHostsOptions): Promise<{ hosts: Asset[] }> { + const withInjected = this.injectOptions(options); + if (this.options.source === 'assets') { + return await getHostsByAssets(withInjected); + } else { + return await getHostsBySignals(withInjected); + } + } +} diff --git a/x-pack/plugins/asset_manager/server/lib/collectors/containers.ts b/x-pack/plugins/asset_manager/server/lib/collectors/containers.ts new file mode 100644 index 0000000000000..18a55af232dfb --- /dev/null +++ b/x-pack/plugins/asset_manager/server/lib/collectors/containers.ts @@ -0,0 +1,91 @@ +/* + * 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 { estypes } from '@elastic/elasticsearch'; +import { Asset } from '../../../common/types_api'; +import { CollectorOptions, QUERY_MAX_SIZE } from '.'; + +export async function collectContainers({ + client, + from, + to, + sourceIndices, + afterKey, +}: CollectorOptions) { + const { metrics, logs, traces } = sourceIndices; + const dsl: estypes.SearchRequest = { + index: [traces, logs, metrics], + size: QUERY_MAX_SIZE, + collapse: { + field: 'container.id', + }, + sort: [{ 'container.id': 'asc' }], + _source: false, + fields: [ + 'kubernetes.*', + 'cloud.provider', + 'orchestrator.cluster.name', + 'host.name', + 'host.hostname', + ], + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + gte: from, + lte: to, + }, + }, + }, + ], + should: [ + { exists: { field: 'kubernetes.container.id' } }, + { exists: { field: 'kubernetes.pod.uid' } }, + { exists: { field: 'kubernetes.node.name' } }, + { exists: { field: 'host.hostname' } }, + ], + }, + }, + }; + + if (afterKey) { + dsl.search_after = afterKey; + } + + const esResponse = await client.search(dsl); + + const assets = esResponse.hits.hits.reduce((acc: Asset[], hit: any) => { + const { fields = {} } = hit; + const containerId = fields['container.id']; + const podUid = fields['kubernetes.pod.uid']; + const nodeName = fields['kubernetes.node.name']; + + const parentEan = podUid ? `pod:${podUid}` : `host:${fields['host.hostname']}`; + + const container: Asset = { + '@timestamp': new Date().toISOString(), + 'asset.kind': 'container', + 'asset.id': containerId, + 'asset.ean': `container:${containerId}`, + 'asset.parents': [parentEan], + }; + + if (nodeName) { + container['asset.references'] = [`host:${nodeName}`]; + } + + acc.push(container); + + return acc; + }, []); + + const hitsLen = esResponse.hits.hits.length; + const next = hitsLen === QUERY_MAX_SIZE ? esResponse.hits.hits[hitsLen - 1].sort : undefined; + return { assets, afterKey: next }; +} diff --git a/x-pack/plugins/asset_manager/server/lib/collectors/hosts.ts b/x-pack/plugins/asset_manager/server/lib/collectors/hosts.ts new file mode 100644 index 0000000000000..0151567a36982 --- /dev/null +++ b/x-pack/plugins/asset_manager/server/lib/collectors/hosts.ts @@ -0,0 +1,110 @@ +/* + * 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 { estypes } from '@elastic/elasticsearch'; +import { Asset } from '../../../common/types_api'; +import { CollectorOptions, QUERY_MAX_SIZE } from '.'; + +export async function collectHosts({ + client, + from, + to, + sourceIndices, + afterKey, +}: CollectorOptions) { + const { metrics, logs, traces } = sourceIndices; + const dsl: estypes.SearchRequest = { + index: [metrics, logs, traces], + size: QUERY_MAX_SIZE, + collapse: { field: 'host.hostname' }, + sort: [{ 'host.hostname': 'asc' }], + _source: false, + fields: [ + '@timestamp', + 'cloud.*', + 'container.*', + 'host.hostname', + 'kubernetes.*', + 'orchestrator.cluster.name', + ], + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + gte: from, + lte: to, + }, + }, + }, + ], + must: [{ exists: { field: 'host.hostname' } }], + should: [ + { exists: { field: 'kubernetes.node.name' } }, + { exists: { field: 'kubernetes.pod.uid' } }, + { exists: { field: 'container.id' } }, + ], + }, + }, + }; + + if (afterKey) { + dsl.search_after = afterKey; + } + + const esResponse = await client.search(dsl); + + const assets = esResponse.hits.hits.reduce((acc: Asset[], hit: any) => { + const { fields = {} } = hit; + const hostName = fields['host.hostname']; + const k8sNode = fields['kubernetes.node.name']; + const k8sPod = fields['kubernetes.pod.uid']; + + const hostEan = `host:${k8sNode || hostName}`; + + const host: Asset = { + '@timestamp': new Date().toISOString(), + 'asset.kind': 'host', + 'asset.id': k8sNode || hostName, + 'asset.name': k8sNode || hostName, + 'asset.ean': hostEan, + }; + + if (fields['cloud.provider']) { + host['cloud.provider'] = fields['cloud.provider']; + } + + if (fields['cloud.instance.id']) { + host['cloud.instance.id'] = fields['cloud.instance.id']; + } + + if (fields['cloud.service.name']) { + host['cloud.service.name'] = fields['cloud.service.name']; + } + + if (fields['cloud.region']) { + host['cloud.region'] = fields['cloud.region']; + } + + if (fields['orchestrator.cluster.name']) { + host['orchestrator.cluster.name'] = fields['orchestrator.cluster.name']; + } + + if (k8sPod) { + host['asset.children'] = [`pod:${k8sPod}`]; + } + + acc.push(host); + + return acc; + }, []); + + const hitsLen = esResponse.hits.hits.length; + const next = hitsLen === QUERY_MAX_SIZE ? esResponse.hits.hits[hitsLen - 1].sort : undefined; + return { assets, afterKey: next }; +} diff --git a/x-pack/plugins/asset_manager/server/lib/collectors/index.ts b/x-pack/plugins/asset_manager/server/lib/collectors/index.ts new file mode 100644 index 0000000000000..0fef13d74b7fc --- /dev/null +++ b/x-pack/plugins/asset_manager/server/lib/collectors/index.ts @@ -0,0 +1,33 @@ +/* + * 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 { estypes } from '@elastic/elasticsearch'; +import { ElasticsearchClient } from '@kbn/core/server'; +import { AssetManagerConfig } from '../../types'; +import { Asset } from '../../../common/types_api'; + +export const QUERY_MAX_SIZE = 10000; + +export type Collector = (opts: CollectorOptions) => Promise; + +export interface CollectorOptions { + client: ElasticsearchClient; + from: number; + to: number; + sourceIndices: AssetManagerConfig['sourceIndices']; + afterKey?: estypes.SortResults; +} + +export interface CollectorResult { + assets: Asset[]; + afterKey?: estypes.SortResults; +} + +export { collectContainers } from './containers'; +export { collectHosts } from './hosts'; +export { collectPods } from './pods'; +export { collectServices } from './services'; diff --git a/x-pack/plugins/asset_manager/server/lib/collectors/pods.ts b/x-pack/plugins/asset_manager/server/lib/collectors/pods.ts new file mode 100644 index 0000000000000..d3d1af91326c7 --- /dev/null +++ b/x-pack/plugins/asset_manager/server/lib/collectors/pods.ts @@ -0,0 +1,85 @@ +/* + * 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 { estypes } from '@elastic/elasticsearch'; +import { Asset } from '../../../common/types_api'; +import { CollectorOptions, QUERY_MAX_SIZE } from '.'; + +export async function collectPods({ client, from, to, sourceIndices, afterKey }: CollectorOptions) { + const { metrics, logs, traces } = sourceIndices; + const dsl: estypes.SearchRequest = { + index: [metrics, logs, traces], + size: QUERY_MAX_SIZE, + collapse: { + field: 'kubernetes.pod.uid', + }, + sort: [{ 'kubernetes.pod.uid': 'asc' }], + _source: false, + fields: [ + 'kubernetes.*', + 'cloud.provider', + 'orchestrator.cluster.name', + 'host.name', + 'host.hostname', + ], + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + gte: from, + lte: to, + }, + }, + }, + ], + must: [ + { exists: { field: 'kubernetes.pod.uid' } }, + { exists: { field: 'kubernetes.node.name' } }, + ], + }, + }, + }; + + if (afterKey) { + dsl.search_after = afterKey; + } + + const esResponse = await client.search(dsl); + + const assets = esResponse.hits.hits.reduce((acc: Asset[], hit: any) => { + const { fields = {} } = hit; + const podUid = fields['kubernetes.pod.uid']; + const nodeName = fields['kubernetes.node.name']; + const clusterName = fields['orchestrator.cluster.name']; + + const pod: Asset = { + '@timestamp': new Date().toISOString(), + 'asset.kind': 'pod', + 'asset.id': podUid, + 'asset.ean': `pod:${podUid}`, + 'asset.parents': [`host:${nodeName}`], + }; + + if (fields['cloud.provider']) { + pod['cloud.provider'] = fields['cloud.provider']; + } + + if (clusterName) { + pod['orchestrator.cluster.name'] = clusterName; + } + + acc.push(pod); + + return acc; + }, []); + + const hitsLen = esResponse.hits.hits.length; + const next = hitsLen === QUERY_MAX_SIZE ? esResponse.hits.hits[hitsLen - 1].sort : undefined; + return { assets, afterKey: next }; +} diff --git a/x-pack/plugins/asset_manager/server/lib/collectors/services.ts b/x-pack/plugins/asset_manager/server/lib/collectors/services.ts new file mode 100644 index 0000000000000..04d3b9c472a3e --- /dev/null +++ b/x-pack/plugins/asset_manager/server/lib/collectors/services.ts @@ -0,0 +1,131 @@ +/* + * 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 { estypes } from '@elastic/elasticsearch'; +import { Asset } from '../../../common/types_api'; +import { CollectorOptions, QUERY_MAX_SIZE } from '.'; + +export async function collectServices({ + client, + from, + to, + sourceIndices, + afterKey, +}: CollectorOptions) { + const { traces, serviceMetrics, serviceLogs } = sourceIndices; + const dsl: estypes.SearchRequest = { + index: [traces, serviceMetrics, serviceLogs], + size: 0, + _source: false, + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + gte: from, + lte: to, + }, + }, + }, + ], + must: [ + { + exists: { + field: 'service.name', + }, + }, + ], + }, + }, + aggs: { + services: { + composite: { + size: QUERY_MAX_SIZE, + sources: [ + { + serviceName: { + terms: { + field: 'service.name', + }, + }, + }, + { + serviceEnvironment: { + terms: { + field: 'service.environment', + }, + }, + }, + ], + }, + aggs: { + container_and_hosts: { + multi_terms: { + terms: [ + { + field: 'host.hostname', + }, + { + field: 'container.id', + }, + ], + }, + }, + }, + }, + }, + }; + + if (afterKey) { + dsl.aggs!.services!.composite!.after = afterKey; + } + + const esResponse = await client.search(dsl); + + const { after_key: nextKey, buckets = [] } = (esResponse.aggregations?.services || {}) as any; + const assets = buckets.reduce((acc: Asset[], bucket: any) => { + const { + key: { serviceName, serviceEnvironment }, + container_and_hosts: containerHosts, + } = bucket; + + if (!serviceName) { + return acc; + } + + const service: Asset = { + '@timestamp': new Date().toISOString(), + 'asset.kind': 'service', + 'asset.id': serviceName, + 'asset.ean': `service:${serviceName}`, + 'asset.references': [], + 'asset.parents': [], + }; + + if (serviceEnvironment) { + service['service.environment'] = serviceEnvironment; + } + + containerHosts.buckets?.forEach((containerBucket: any) => { + const [containerId, hostname] = containerBucket.key; + if (containerId) { + (service['asset.parents'] as string[]).push(`container:${containerId}`); + } + + if (hostname) { + (service['asset.references'] as string[]).push(`host:${hostname}`); + } + }); + + acc.push(service); + + return acc; + }, []); + + return { assets, afterKey: buckets.length === QUERY_MAX_SIZE ? nextKey : undefined }; +} diff --git a/x-pack/plugins/asset_manager/server/lib/get_all_related_assets.test.ts b/x-pack/plugins/asset_manager/server/lib/get_all_related_assets.test.ts index 7eaa05dbd35bb..47006590a9238 100644 --- a/x-pack/plugins/asset_manager/server/lib/get_all_related_assets.test.ts +++ b/x-pack/plugins/asset_manager/server/lib/get_all_related_assets.test.ts @@ -6,13 +6,13 @@ */ jest.mock('./get_assets', () => ({ getAssets: jest.fn() })); -jest.mock('./get_related_assets', () => ({ getRelatedAssets: jest.fn() })); +jest.mock('./get_indirectly_related_assets', () => ({ getIndirectlyRelatedAssets: 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 { getIndirectlyRelatedAssets } from './get_indirectly_related_assets'; // Mocked import { getAllRelatedAssets } from './get_all_related_assets'; const esClientMock = elasticsearchClientMock.createScopedClusterClient().asCurrentUser; @@ -20,13 +20,14 @@ const esClientMock = elasticsearchClientMock.createScopedClusterClient().asCurre describe('getAllRelatedAssets', () => { beforeEach(() => { (getAssets as jest.Mock).mockReset(); - (getRelatedAssets as jest.Mock).mockReset(); + (getIndirectlyRelatedAssets 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.kind': 'pod', 'asset.id': uuid(), }; @@ -34,7 +35,9 @@ describe('getAllRelatedAssets', () => { (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')); + (getIndirectlyRelatedAssets as jest.Mock).mockRejectedValueOnce( + new Error('Should respect maxDistance') + ); await expect( getAllRelatedAssets(esClientMock, { @@ -53,16 +56,19 @@ describe('getAllRelatedAssets', () => { const primaryAssetWithoutParents: AssetWithoutTimestamp = { 'asset.ean': 'primary-ean', 'asset.type': 'k8s.pod', + 'asset.kind': 'pod', 'asset.id': uuid(), 'asset.parents': [], }; (getAssets as jest.Mock).mockResolvedValueOnce([primaryAssetWithoutParents]); // Distance 1 - (getRelatedAssets as jest.Mock).mockResolvedValueOnce([]); + (getIndirectlyRelatedAssets 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')); + (getIndirectlyRelatedAssets as jest.Mock).mockRejectedValueOnce( + new Error('Should respect maxDistance') + ); await expect( getAllRelatedAssets(esClientMock, { @@ -80,14 +86,16 @@ describe('getAllRelatedAssets', () => { it('returns the primary and a directly referenced parent', async () => { const parentAsset: AssetWithoutTimestamp = { - 'asset.ean': 'primary-ean', + 'asset.ean': 'parent-ean', 'asset.type': 'k8s.pod', + 'asset.kind': 'pod', 'asset.id': uuid(), }; const primaryAssetWithDirectParent: AssetWithoutTimestamp = { 'asset.ean': 'primary-ean', 'asset.type': 'k8s.pod', + 'asset.kind': 'pod', 'asset.id': uuid(), 'asset.parents': [parentAsset['asset.ean']], }; @@ -96,10 +104,12 @@ describe('getAllRelatedAssets', () => { (getAssets as jest.Mock).mockResolvedValueOnce([primaryAssetWithDirectParent]); // Distance 1 (getAssets as jest.Mock).mockResolvedValueOnce([parentAsset]); - (getRelatedAssets as jest.Mock).mockResolvedValueOnce([]); + (getIndirectlyRelatedAssets 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')); + (getIndirectlyRelatedAssets as jest.Mock).mockRejectedValueOnce( + new Error('Should respect maxDistance') + ); await expect( getAllRelatedAssets(esClientMock, { @@ -124,6 +134,7 @@ describe('getAllRelatedAssets', () => { const primaryAssetWithIndirectParent: AssetWithoutTimestamp = { 'asset.ean': 'primary-ean', 'asset.type': 'k8s.pod', + 'asset.kind': 'pod', 'asset.id': uuid(), 'asset.parents': [], }; @@ -131,6 +142,7 @@ describe('getAllRelatedAssets', () => { const parentAsset: AssetWithoutTimestamp = { 'asset.ean': 'primary-ean', 'asset.type': 'k8s.pod', + 'asset.kind': 'pod', 'asset.id': uuid(), 'asset.children': [primaryAssetWithIndirectParent['asset.ean']], }; @@ -139,10 +151,12 @@ describe('getAllRelatedAssets', () => { (getAssets as jest.Mock).mockResolvedValueOnce([primaryAssetWithIndirectParent]); // Distance 1 (getAssets as jest.Mock).mockResolvedValueOnce([]); - (getRelatedAssets as jest.Mock).mockResolvedValueOnce([parentAsset]); + (getIndirectlyRelatedAssets 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')); + (getIndirectlyRelatedAssets as jest.Mock).mockRejectedValueOnce( + new Error('Should respect maxDistance') + ); await expect( getAllRelatedAssets(esClientMock, { @@ -167,6 +181,7 @@ describe('getAllRelatedAssets', () => { const directlyReferencedParent: AssetWithoutTimestamp = { 'asset.ean': 'directly-referenced-parent-ean', 'asset.type': 'k8s.pod', + 'asset.kind': 'pod', 'asset.id': uuid(), 'asset.children': [], }; @@ -174,6 +189,7 @@ describe('getAllRelatedAssets', () => { const primaryAsset: AssetWithoutTimestamp = { 'asset.ean': 'primary-ean', 'asset.type': 'k8s.pod', + 'asset.kind': 'pod', 'asset.id': uuid(), 'asset.parents': [directlyReferencedParent['asset.ean']], }; @@ -181,6 +197,7 @@ describe('getAllRelatedAssets', () => { const indirectlyReferencedParent: AssetWithoutTimestamp = { 'asset.ean': 'indirectly-referenced-parent-ean', 'asset.type': 'k8s.pod', + 'asset.kind': 'pod', 'asset.id': uuid(), 'asset.children': [primaryAsset['asset.ean']], }; @@ -189,10 +206,12 @@ describe('getAllRelatedAssets', () => { (getAssets as jest.Mock).mockResolvedValueOnce([primaryAsset]); // Distance 1 (getAssets as jest.Mock).mockResolvedValueOnce([directlyReferencedParent]); - (getRelatedAssets as jest.Mock).mockResolvedValueOnce([indirectlyReferencedParent]); + (getIndirectlyRelatedAssets 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')); + (getIndirectlyRelatedAssets as jest.Mock).mockRejectedValueOnce( + new Error('Should respect maxDistance') + ); await expect( getAllRelatedAssets(esClientMock, { @@ -221,12 +240,14 @@ describe('getAllRelatedAssets', () => { const parentAsset: AssetWithoutTimestamp = { 'asset.ean': 'parent-ean', 'asset.type': 'k8s.pod', + 'asset.kind': 'pod', 'asset.id': uuid(), }; const primaryAsset: AssetWithoutTimestamp = { 'asset.ean': 'primary-ean', 'asset.type': 'k8s.pod', + 'asset.kind': 'pod', 'asset.id': uuid(), }; @@ -238,10 +259,12 @@ describe('getAllRelatedAssets', () => { // 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([]); + (getIndirectlyRelatedAssets 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')); + (getIndirectlyRelatedAssets as jest.Mock).mockRejectedValueOnce( + new Error('Should respect maxDistance') + ); await expect( getAllRelatedAssets(esClientMock, { @@ -266,12 +289,14 @@ describe('getAllRelatedAssets', () => { const distance6Parent: AssetWithoutTimestamp = { 'asset.ean': 'parent-5-ean', 'asset.type': 'k8s.pod', + 'asset.kind': 'pod', 'asset.id': uuid(), }; const distance5Parent: AssetWithoutTimestamp = { 'asset.ean': 'parent-5-ean', 'asset.type': 'k8s.pod', + 'asset.kind': 'pod', 'asset.id': uuid(), 'asset.parents': [distance6Parent['asset.ean']], }; @@ -279,6 +304,7 @@ describe('getAllRelatedAssets', () => { const distance4Parent: AssetWithoutTimestamp = { 'asset.ean': 'parent-4-ean', 'asset.type': 'k8s.pod', + 'asset.kind': 'pod', 'asset.id': uuid(), 'asset.parents': [distance5Parent['asset.ean']], }; @@ -286,6 +312,7 @@ describe('getAllRelatedAssets', () => { const distance3Parent: AssetWithoutTimestamp = { 'asset.ean': 'parent-3-ean', 'asset.type': 'k8s.pod', + 'asset.kind': 'pod', 'asset.id': uuid(), 'asset.parents': [distance4Parent['asset.ean']], }; @@ -293,6 +320,7 @@ describe('getAllRelatedAssets', () => { const distance2Parent: AssetWithoutTimestamp = { 'asset.ean': 'parent-2-ean', 'asset.type': 'k8s.pod', + 'asset.kind': 'pod', 'asset.id': uuid(), 'asset.parents': [distance3Parent['asset.ean']], }; @@ -300,6 +328,7 @@ describe('getAllRelatedAssets', () => { const distance1Parent: AssetWithoutTimestamp = { 'asset.ean': 'parent-1-ean', 'asset.type': 'k8s.pod', + 'asset.kind': 'pod', 'asset.id': uuid(), 'asset.parents': [distance2Parent['asset.ean']], }; @@ -307,12 +336,13 @@ describe('getAllRelatedAssets', () => { const primaryAsset: AssetWithoutTimestamp = { 'asset.ean': 'primary-ean', 'asset.type': 'k8s.pod', + 'asset.kind': 'pod', 'asset.id': uuid(), 'asset.parents': [distance1Parent['asset.ean']], }; // Only using directly referenced parents - (getRelatedAssets as jest.Mock).mockResolvedValue([]); + (getIndirectlyRelatedAssets as jest.Mock).mockResolvedValue([]); // Primary (getAssets as jest.Mock).mockResolvedValueOnce([primaryAsset]); @@ -368,12 +398,14 @@ describe('getAllRelatedAssets', () => { const distance3Parent: AssetWithoutTimestamp = { 'asset.ean': 'parent-3-ean', 'asset.type': 'k8s.pod', + 'asset.kind': 'pod', 'asset.id': uuid(), }; const distance2Parent: AssetWithoutTimestamp = { 'asset.ean': 'parent-2-ean', 'asset.type': 'k8s.pod', + 'asset.kind': 'pod', 'asset.id': uuid(), 'asset.parents': [distance3Parent['asset.ean']], }; @@ -381,6 +413,7 @@ describe('getAllRelatedAssets', () => { const distance1Parent: AssetWithoutTimestamp = { 'asset.ean': 'parent-1-ean', 'asset.type': 'k8s.pod', + 'asset.kind': 'pod', 'asset.id': uuid(), 'asset.parents': [distance2Parent['asset.ean']], }; @@ -388,12 +421,13 @@ describe('getAllRelatedAssets', () => { const primaryAsset: AssetWithoutTimestamp = { 'asset.ean': 'primary-ean', 'asset.type': 'k8s.pod', + 'asset.kind': 'pod', 'asset.id': uuid(), 'asset.parents': [distance1Parent['asset.ean']], }; // Only using directly referenced parents - (getRelatedAssets as jest.Mock).mockResolvedValue([]); + (getIndirectlyRelatedAssets as jest.Mock).mockResolvedValue([]); // Primary (getAssets as jest.Mock).mockResolvedValueOnce([primaryAsset]); @@ -435,30 +469,35 @@ describe('getAllRelatedAssets', () => { const distance2ParentA: AssetWithoutTimestamp = { 'asset.ean': 'parent-2-ean-a', 'asset.type': 'k8s.pod', + 'asset.kind': 'pod', 'asset.id': uuid(), }; const distance2ParentB: AssetWithoutTimestamp = { 'asset.ean': 'parent-2-ean-b', 'asset.type': 'k8s.pod', + 'asset.kind': 'pod', 'asset.id': uuid(), }; const distance2ParentC: AssetWithoutTimestamp = { 'asset.ean': 'parent-2-ean-c', 'asset.type': 'k8s.pod', + 'asset.kind': 'pod', 'asset.id': uuid(), }; const distance2ParentD: AssetWithoutTimestamp = { 'asset.ean': 'parent-2-ean-d', 'asset.type': 'k8s.pod', + 'asset.kind': 'pod', 'asset.id': uuid(), }; const distance1ParentA: AssetWithoutTimestamp = { 'asset.ean': 'parent-1-ean-a', 'asset.type': 'k8s.pod', + 'asset.kind': 'pod', 'asset.id': uuid(), 'asset.parents': [distance2ParentA['asset.ean'], distance2ParentB['asset.ean']], }; @@ -466,6 +505,7 @@ describe('getAllRelatedAssets', () => { const distance1ParentB: AssetWithoutTimestamp = { 'asset.ean': 'parent-1-ean-b', 'asset.type': 'k8s.pod', + 'asset.kind': 'pod', 'asset.id': uuid(), 'asset.parents': [distance2ParentC['asset.ean'], distance2ParentD['asset.ean']], }; @@ -473,12 +513,13 @@ describe('getAllRelatedAssets', () => { const primaryAsset: AssetWithoutTimestamp = { 'asset.ean': 'primary-ean', 'asset.type': 'k8s.pod', + 'asset.kind': 'pod', 'asset.id': uuid(), 'asset.parents': [distance1ParentA['asset.ean'], distance1ParentB['asset.ean']], }; // Only using directly referenced parents - (getRelatedAssets as jest.Mock).mockResolvedValue([]); + (getIndirectlyRelatedAssets as jest.Mock).mockResolvedValue([]); // Primary (getAssets as jest.Mock).mockResolvedValueOnce([primaryAsset]); diff --git a/x-pack/plugins/asset_manager/server/lib/get_all_related_assets.ts b/x-pack/plugins/asset_manager/server/lib/get_all_related_assets.ts index f297881e1aec6..ad8aff78cbb18 100644 --- a/x-pack/plugins/asset_manager/server/lib/get_all_related_assets.ts +++ b/x-pack/plugins/asset_manager/server/lib/get_all_related_assets.ts @@ -7,9 +7,10 @@ import { ElasticsearchClient } from '@kbn/core/server'; import { flatten, without } from 'lodash'; -import { Asset, AssetType, Relation, RelationField } from '../../common/types_api'; +import { debug } from '../../common/debug_log'; +import { Asset, AssetType, AssetKind, Relation, RelationField } from '../../common/types_api'; import { getAssets } from './get_assets'; -import { getRelatedAssets } from './get_related_assets'; +import { getIndirectlyRelatedAssets } from './get_indirectly_related_assets'; import { AssetNotFoundError } from './errors'; import { toArray } from './utils'; @@ -19,6 +20,7 @@ interface GetAllRelatedAssetsOptions { to?: string; relation: Relation; type?: AssetType[]; + kind?: AssetKind[]; maxDistance: number; size: number; } @@ -28,7 +30,7 @@ export async function getAllRelatedAssets( options: GetAllRelatedAssetsOptions ) { // How to put size into this? - const { ean, from, to, relation, maxDistance, type = [] } = options; + const { ean, from, to, relation, maxDistance, kind = [] } = options; const primary = await findPrimary(esClient, { ean, from, to }); @@ -42,10 +44,10 @@ export async function getAllRelatedAssets( 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; + // if we enforce the kind filter before the last query we'll miss nodes with + // possible edges to the requested kind values + if (currentDistance === maxDistance && kind.length) { + queryOptions.kind = kind; } const results = flatten( @@ -66,8 +68,8 @@ export async function getAllRelatedAssets( return { primary, - [relation]: type.length - ? relatedAssets.filter((asset) => type.includes(asset['asset.type'])) + [relation]: kind.length + ? relatedAssets.filter((asset) => asset['asset.kind'] && kind.includes(asset['asset.kind'])) : relatedAssets, }; } @@ -95,36 +97,44 @@ async function findPrimary( type FindRelatedAssetsOptions = Pick< GetAllRelatedAssetsOptions, - 'relation' | 'type' | 'from' | 'to' + 'relation' | 'kind' | 'from' | 'to' > & { visitedEans: string[] }; async function findRelatedAssets( esClient: ElasticsearchClient, primary: Asset, - { relation, from, to, type, visitedEans }: FindRelatedAssetsOptions + { relation, from, to, kind, visitedEans }: FindRelatedAssetsOptions ): Promise { const relationField = relationToDirectField(relation); - const directlyRelatedEans = toArray(primary[relationField]); + const directlyRelatedEans = toArray(primary[relationField]); + + debug('Directly Related EAN values found on primary asset', directlyRelatedEans); let directlyRelatedAssets: Asset[] = []; - if (directlyRelatedEans.length) { - // get the directly related assets we haven't visited already + + // get the directly related assets we haven't visited already + const remainingEansToFind = without(directlyRelatedEans, ...visitedEans); + if (remainingEansToFind.length > 0) { directlyRelatedAssets = await getAssets({ esClient, - filters: { ean: without(directlyRelatedEans, ...visitedEans), from, to, type }, + filters: { ean: remainingEansToFind, from, to, kind }, }); } - const indirectlyRelatedAssets = await getRelatedAssets({ + debug('Directly related assets found:', JSON.stringify(directlyRelatedAssets)); + + const indirectlyRelatedAssets = await getIndirectlyRelatedAssets({ esClient, ean: primary['asset.ean'], excludeEans: visitedEans.concat(directlyRelatedEans), relation, from, to, - type, + kind, }); + debug('Indirectly related assets found:', JSON.stringify(indirectlyRelatedAssets)); + return [...directlyRelatedAssets, ...indirectlyRelatedAssets]; } diff --git a/x-pack/plugins/asset_manager/server/lib/get_assets.ts b/x-pack/plugins/asset_manager/server/lib/get_assets.ts index a5c797cdfdfd3..12f87e4b398fc 100644 --- a/x-pack/plugins/asset_manager/server/lib/get_assets.ts +++ b/x-pack/plugins/asset_manager/server/lib/get_assets.ts @@ -4,11 +4,12 @@ * 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 { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import { debug } from '../../common/debug_log'; import { Asset, AssetFilters } from '../../common/types_api'; import { ASSETS_INDEX_PREFIX } from '../constants'; import { ElasticsearchAccessorOptions } from '../types'; +import { isStringOrNonEmptyArray } from './utils'; interface GetAssetsOptions extends ElasticsearchAccessorOptions { size?: number; @@ -23,6 +24,7 @@ export async function getAssets({ filters = {}, }: GetAssetsOptions): Promise { // Maybe it makes the most sense to validate the filters here? + debug('Get Assets Filters:', JSON.stringify(filters)); const { from = 'now-24h', to = 'now' } = filters; const must: QueryDslQueryContainer[] = []; @@ -36,7 +38,7 @@ export async function getAssets({ }); } - if (filters.type?.length) { + if (isStringOrNonEmptyArray(filters.type)) { must.push({ terms: { ['asset.type']: Array.isArray(filters.type) ? filters.type : [filters.type], @@ -44,15 +46,15 @@ export async function getAssets({ }); } - if (filters.kind) { + if (isStringOrNonEmptyArray(filters.kind)) { must.push({ - term: { - ['asset.kind']: filters.kind, + terms: { + ['asset.kind']: Array.isArray(filters.kind) ? filters.kind : [filters.kind], }, }); } - if (filters.ean) { + if (isStringOrNonEmptyArray(filters.ean)) { must.push({ terms: { ['asset.ean']: Array.isArray(filters.ean) ? filters.ean : [filters.ean], @@ -76,6 +78,14 @@ export async function getAssets({ }); } + if (filters.kindLike) { + must.push({ + wildcard: { + ['asset.kind']: filters.kindLike, + }, + }); + } + if (filters.eanLike) { must.push({ wildcard: { @@ -85,7 +95,7 @@ export async function getAssets({ } } - const dsl: SearchRequest = { + const dsl = { index: ASSETS_INDEX_PREFIX + '*', size, query: { @@ -113,7 +123,7 @@ export async function getAssets({ }, }; - debug('Performing Asset Query', '\n\n', JSON.stringify(dsl, null, 2)); + debug('Performing Get Assets Query', '\n\n', JSON.stringify(dsl, null, 2)); const response = await esClient.search(dsl); return response.hits.hits.map((hit) => hit._source).filter((asset): asset is Asset => !!asset); diff --git a/x-pack/plugins/asset_manager/server/lib/get_related_assets.ts b/x-pack/plugins/asset_manager/server/lib/get_indirectly_related_assets.ts similarity index 84% rename from x-pack/plugins/asset_manager/server/lib/get_related_assets.ts rename to x-pack/plugins/asset_manager/server/lib/get_indirectly_related_assets.ts index 3e65ed565e5ac..fa9f3279ec497 100644 --- a/x-pack/plugins/asset_manager/server/lib/get_related_assets.ts +++ b/x-pack/plugins/asset_manager/server/lib/get_indirectly_related_assets.ts @@ -7,9 +7,10 @@ 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 { Asset, AssetKind, Relation, RelationField } from '../../common/types_api'; import { ASSETS_INDEX_PREFIX } from '../constants'; import { ElasticsearchAccessorOptions } from '../types'; +import { isStringOrNonEmptyArray } from './utils'; interface GetRelatedAssetsOptions extends ElasticsearchAccessorOptions { size?: number; @@ -18,10 +19,10 @@ interface GetRelatedAssetsOptions extends ElasticsearchAccessorOptions { from?: string; to?: string; relation: Relation; - type?: AssetType[]; + kind?: AssetKind | AssetKind[]; } -export async function getRelatedAssets({ +export async function getIndirectlyRelatedAssets({ esClient, size = 100, from = 'now-24h', @@ -29,7 +30,7 @@ export async function getRelatedAssets({ ean, excludeEans, relation, - type, + kind, }: GetRelatedAssetsOptions): Promise { const relationField = relationToIndirectField(relation); const must: QueryDslQueryContainer[] = [ @@ -40,10 +41,10 @@ export async function getRelatedAssets({ }, ]; - if (type?.length) { + if (isStringOrNonEmptyArray(kind)) { must.push({ terms: { - ['asset.type']: type, + ['asset.kind']: Array.isArray(kind) ? kind : [kind], }, }); } @@ -88,7 +89,7 @@ export async function getRelatedAssets({ }, }; - debug('Performing Asset Query', '\n\n', JSON.stringify(dsl, null, 2)); + debug('Performing Indirectly Related Asset Query', '\n\n', JSON.stringify(dsl, null, 2)); const response = await esClient.search(dsl); return response.hits.hits.map((hit) => hit._source).filter((asset): asset is Asset => !!asset); diff --git a/x-pack/plugins/asset_manager/server/lib/sample_assets.ts b/x-pack/plugins/asset_manager/server/lib/sample_assets.ts index f49300e4663e6..2e9fbc2bcd5be 100644 --- a/x-pack/plugins/asset_manager/server/lib/sample_assets.ts +++ b/x-pack/plugins/asset_manager/server/lib/sample_assets.ts @@ -30,9 +30,10 @@ export function getSampleAssetDocs({ const sampleK8sClusters: AssetWithoutTimestamp[] = [ { 'asset.type': 'k8s.cluster', + 'asset.kind': 'cluster', 'asset.id': 'cluster-001', 'asset.name': 'Cluster 001 (AWS EKS)', - 'asset.ean': 'k8s.cluster:cluster-001', + 'asset.ean': 'cluster:cluster-001', 'orchestrator.type': 'kubernetes', 'orchestrator.cluster.name': 'Cluster 001 (AWS EKS)', 'orchestrator.cluster.id': 'cluster-001', @@ -42,9 +43,10 @@ const sampleK8sClusters: AssetWithoutTimestamp[] = [ }, { 'asset.type': 'k8s.cluster', + 'asset.kind': 'cluster', 'asset.id': 'cluster-002', 'asset.name': 'Cluster 002 (Azure AKS)', - 'asset.ean': 'k8s.cluster:cluster-002', + 'asset.ean': 'cluster:cluster-002', 'orchestrator.type': 'kubernetes', 'orchestrator.cluster.name': 'Cluster 002 (Azure AKS)', 'orchestrator.cluster.id': 'cluster-002', @@ -57,10 +59,11 @@ const sampleK8sClusters: AssetWithoutTimestamp[] = [ const sampleK8sNodes: AssetWithoutTimestamp[] = [ { 'asset.type': 'k8s.node', + 'asset.kind': 'host', 'asset.id': 'node-101', 'asset.name': 'k8s-node-101-aws', - 'asset.ean': 'k8s.node:node-101', - 'asset.parents': ['k8s.cluster:cluster-001'], + 'asset.ean': 'host:node-101', + 'asset.parents': ['cluster:cluster-001'], 'orchestrator.type': 'kubernetes', 'orchestrator.cluster.name': 'Cluster 001 (AWS EKS)', 'orchestrator.cluster.id': 'cluster-001', @@ -70,10 +73,11 @@ const sampleK8sNodes: AssetWithoutTimestamp[] = [ }, { 'asset.type': 'k8s.node', + 'asset.kind': 'host', 'asset.id': 'node-102', 'asset.name': 'k8s-node-102-aws', - 'asset.ean': 'k8s.node:node-102', - 'asset.parents': ['k8s.cluster:cluster-001'], + 'asset.ean': 'host:node-102', + 'asset.parents': ['cluster:cluster-001'], 'orchestrator.type': 'kubernetes', 'orchestrator.cluster.name': 'Cluster 001 (AWS EKS)', 'orchestrator.cluster.id': 'cluster-001', @@ -83,10 +87,11 @@ const sampleK8sNodes: AssetWithoutTimestamp[] = [ }, { 'asset.type': 'k8s.node', + 'asset.kind': 'host', 'asset.id': 'node-103', 'asset.name': 'k8s-node-103-aws', - 'asset.ean': 'k8s.node:node-103', - 'asset.parents': ['k8s.cluster:cluster-001'], + 'asset.ean': 'host:node-103', + 'asset.parents': ['cluster:cluster-001'], 'orchestrator.type': 'kubernetes', 'orchestrator.cluster.name': 'Cluster 001 (AWS EKS)', 'orchestrator.cluster.id': 'cluster-001', @@ -99,97 +104,109 @@ const sampleK8sNodes: AssetWithoutTimestamp[] = [ const sampleK8sPods: AssetWithoutTimestamp[] = [ { 'asset.type': 'k8s.pod', + 'asset.kind': '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'], + 'asset.ean': 'pod:pod-200xrg1', + 'asset.parents': ['host:node-101'], + 'asset.references': ['cluster:cluster-001'], }, { 'asset.type': 'k8s.pod', + 'asset.kind': 'pod', 'asset.id': 'pod-200dfp2', 'asset.name': 'k8s-pod-200dfp2-aws', - 'asset.ean': 'k8s.pod:pod-200dfp2', - 'asset.parents': ['k8s.node:node-101'], + 'asset.ean': 'pod:pod-200dfp2', + 'asset.parents': ['host:node-101'], }, { 'asset.type': 'k8s.pod', + 'asset.kind': 'pod', 'asset.id': 'pod-200wwc3', 'asset.name': 'k8s-pod-200wwc3-aws', - 'asset.ean': 'k8s.pod:pod-200wwc3', - 'asset.parents': ['k8s.node:node-101'], + 'asset.ean': 'pod:pod-200wwc3', + 'asset.parents': ['host:node-101'], }, { 'asset.type': 'k8s.pod', + 'asset.kind': 'pod', 'asset.id': 'pod-200naq4', 'asset.name': 'k8s-pod-200naq4-aws', - 'asset.ean': 'k8s.pod:pod-200naq4', - 'asset.parents': ['k8s.node:node-102'], + 'asset.ean': 'pod:pod-200naq4', + 'asset.parents': ['host:node-102'], }, { 'asset.type': 'k8s.pod', + 'asset.kind': 'pod', 'asset.id': 'pod-200ohr5', 'asset.name': 'k8s-pod-200ohr5-aws', - 'asset.ean': 'k8s.pod:pod-200ohr5', - 'asset.parents': ['k8s.node:node-102'], + 'asset.ean': 'pod:pod-200ohr5', + 'asset.parents': ['host:node-102'], }, { 'asset.type': 'k8s.pod', + 'asset.kind': 'pod', 'asset.id': 'pod-200yyx6', 'asset.name': 'k8s-pod-200yyx6-aws', - 'asset.ean': 'k8s.pod:pod-200yyx6', - 'asset.parents': ['k8s.node:node-103'], + 'asset.ean': 'pod:pod-200yyx6', + 'asset.parents': ['host:node-103'], }, { 'asset.type': 'k8s.pod', + 'asset.kind': 'pod', 'asset.id': 'pod-200psd7', 'asset.name': 'k8s-pod-200psd7-aws', - 'asset.ean': 'k8s.pod:pod-200psd7', - 'asset.parents': ['k8s.node:node-103'], + 'asset.ean': 'pod:pod-200psd7', + 'asset.parents': ['host:node-103'], }, { 'asset.type': 'k8s.pod', + 'asset.kind': 'pod', 'asset.id': 'pod-200wmc8', 'asset.name': 'k8s-pod-200wmc8-aws', - 'asset.ean': 'k8s.pod:pod-200wmc8', - 'asset.parents': ['k8s.node:node-103'], + 'asset.ean': 'pod:pod-200wmc8', + 'asset.parents': ['host:node-103'], }, { 'asset.type': 'k8s.pod', + 'asset.kind': 'pod', 'asset.id': 'pod-200ugg9', 'asset.name': 'k8s-pod-200ugg9-aws', - 'asset.ean': 'k8s.pod:pod-200ugg9', - 'asset.parents': ['k8s.node:node-103'], + 'asset.ean': 'pod:pod-200ugg9', + 'asset.parents': ['host:node-103'], }, ]; const sampleCircularReferences: AssetWithoutTimestamp[] = [ { 'asset.type': 'k8s.node', + 'asset.kind': 'host', 'asset.id': 'node-203', 'asset.name': 'k8s-node-203-aws', - 'asset.ean': 'k8s.node:node-203', + 'asset.ean': 'host: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.references': ['pod:pod-203ugg9', 'pod:pod-203ugg5'], }, { 'asset.type': 'k8s.pod', + 'asset.kind': 'pod', 'asset.id': 'pod-203ugg5', 'asset.name': 'k8s-pod-203ugg5-aws', - 'asset.ean': 'k8s.pod:pod-203ugg5', - 'asset.references': ['k8s.node:node-203'], + 'asset.ean': 'pod:pod-203ugg5', + 'asset.references': ['host:node-203'], }, { 'asset.type': 'k8s.pod', + 'asset.kind': 'pod', 'asset.id': 'pod-203ugg9', 'asset.name': 'k8s-pod-203ugg9-aws', - 'asset.ean': 'k8s.pod:pod-203ugg9', - 'asset.references': ['k8s.node:node-203'], + 'asset.ean': 'pod:pod-203ugg9', + 'asset.references': ['host:node-203'], }, ]; diff --git a/x-pack/plugins/asset_manager/server/lib/utils.ts b/x-pack/plugins/asset_manager/server/lib/utils.ts index 9db0d573a6221..fa9cf965771ef 100644 --- a/x-pack/plugins/asset_manager/server/lib/utils.ts +++ b/x-pack/plugins/asset_manager/server/lib/utils.ts @@ -23,3 +23,15 @@ export const isValidRange = (from: string, to: string): boolean => { } return true; }; + +export function isStringOrNonEmptyArray( + value: string | string[] | undefined +): value is string | string[] { + if (typeof value === 'undefined') { + return false; + } + if (Array.isArray(value) && value.length === 0) { + return false; + } + return true; +} diff --git a/x-pack/plugins/asset_manager/server/plugin.ts b/x-pack/plugins/asset_manager/server/plugin.ts index d81d3f1f87b63..56a880b0da080 100644 --- a/x-pack/plugins/asset_manager/server/plugin.ts +++ b/x-pack/plugins/asset_manager/server/plugin.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { schema } from '@kbn/config-schema'; import { Plugin, CoreSetup, @@ -15,22 +14,23 @@ import { PluginConfigDescriptor, Logger, } from '@kbn/core/server'; + import { upsertTemplate } from './lib/manage_index_templates'; import { setupRoutes } from './routes'; import { assetsIndexTemplateConfig } from './templates/assets_template'; +import { AssetManagerConfig, configSchema } from './types'; +import { AssetAccessor } from './lib/asset_accessor'; export type AssetManagerServerPluginSetup = ReturnType; -export interface AssetManagerConfig { - alphaEnabled?: boolean; -} +export type AssetManagerServerPluginStart = ReturnType; export const config: PluginConfigDescriptor = { - schema: schema.object({ - alphaEnabled: schema.maybe(schema.boolean()), - }), + schema: configSchema, }; -export class AssetManagerServerPlugin implements Plugin { +export class AssetManagerServerPlugin + implements Plugin +{ public config: AssetManagerConfig; public logger: Logger; @@ -48,10 +48,17 @@ export class AssetManagerServerPlugin implements Plugin({ router }); + setupRoutes({ router, assetAccessor }); - return {}; + return { + assetAccessor, + }; } public start(core: CoreStart) { @@ -61,11 +68,15 @@ export class AssetManagerServerPlugin implements Plugin; + +export function hostsRoutes({ + router, + assetAccessor, +}: SetupRouteOptions) { + // GET /assets/hosts + router.get( + { + path: `${ASSET_MANAGER_API_BASE}/assets/hosts`, + validate: { + query: createRouteValidationFunction(getHostAssetsQueryOptionsRT), + }, + }, + async (context, req, res) => { + const { from = 'now-24h', to = 'now' } = req.query || {}; + const esClient = await getEsClientFromContext(context); + + try { + const response = await assetAccessor.getHosts({ + from: datemath.parse(from)!.valueOf(), + to: datemath.parse(to)!.valueOf(), + esClient, + }); + + return res.ok({ body: response }); + } catch (error: unknown) { + debug('Error while looking up HOST asset records', error); + return res.customError({ + statusCode: 500, + body: { message: 'Error while looking up host asset records - ' + `${error}` }, + }); + } + } + ); +} diff --git a/x-pack/plugins/asset_manager/server/routes/assets.ts b/x-pack/plugins/asset_manager/server/routes/assets/index.ts similarity index 78% rename from x-pack/plugins/asset_manager/server/routes/assets.ts rename to x-pack/plugins/asset_manager/server/routes/assets/index.ts index 5c554f2454fd4..b8b6d7ab0fa3a 100644 --- a/x-pack/plugins/asset_manager/server/routes/assets.ts +++ b/x-pack/plugins/asset_manager/server/routes/assets/index.ts @@ -15,15 +15,19 @@ import { 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'; -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 { isValidRange, toArray } from '../lib/utils'; +import { debug } from '../../../common/debug_log'; +import { assetTypeRT, assetKindRT, 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 { isValidRange } from '../../lib/utils'; + +function maybeArrayRT(t: rt.Mixed) { + return rt.union([rt.array(t), t]); +} const sizeRT = rt.union([inRangeFromStringRt(1, 100), createLiteralValueFromUndefinedRT(10)]); const assetDateRT = rt.union([dateRt, datemathStringRt]); @@ -31,8 +35,9 @@ 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]), + type: maybeArrayRT(assetTypeRT), + kind: maybeArrayRT(assetKindRT), + ean: maybeArrayRT(rt.string), size: sizeRT, }) ); @@ -46,7 +51,8 @@ const getAssetsDiffQueryOptionsRT = rt.exact( bTo: assetDateRT, }), rt.partial({ - type: rt.union([rt.array(assetTypeRT), assetTypeRT]), + type: maybeArrayRT(assetTypeRT), + kind: maybeArrayRT(assetKindRT), }), ]) ); @@ -62,7 +68,8 @@ const getRelatedAssetsQueryOptionsRT = rt.exact( }), rt.partial({ to: assetDateRT, - type: rt.union([rt.array(assetTypeRT), assetTypeRT]), + type: maybeArrayRT(assetTypeRT), + kind: maybeArrayRT(assetKindRT), }), ]) ); @@ -89,6 +96,12 @@ export function assetsRoutes({ router }: SetupR }); } + if (filters.kind && filters.ean) { + return res.badRequest({ + body: 'Filters "kind" and "ean" are mutually exclusive but found both.', + }); + } + const esClient = await getEsClientFromContext(context); try { @@ -96,7 +109,10 @@ export function assetsRoutes({ router }: SetupR return res.ok({ body: { results } }); } catch (error: unknown) { debug('error looking up asset records', error); - return res.customError({ statusCode: 500 }); + return res.customError({ + statusCode: 500, + body: { message: 'Error while looking up asset records - ' + `${error}` }, + }); } } ); @@ -112,11 +128,9 @@ export function assetsRoutes({ router }: SetupR async (context, req, res) => { // Add references into sample data and write integration tests - const { from, to, ean, relation, maxDistance, size } = req.query || {}; + const { from, to, ean, relation, maxDistance, size, type, kind } = req.query || {}; const esClient = await getEsClientFromContext(context); - const type = toArray(req.query.type); - if (to && !isValidRange(from, to)) { return res.badRequest({ body: `Time range cannot move backwards in time. "to" (${to}) is before "from" (${from}).`, @@ -131,6 +145,7 @@ export function assetsRoutes({ router }: SetupR from, to, type, + kind, maxDistance, size, relation, @@ -156,8 +171,9 @@ export function assetsRoutes({ router }: SetupR }, }, async (context, req, res) => { - const { aFrom, aTo, bFrom, bTo } = req.query; - const type = toArray(req.query.type); + const { aFrom, aTo, bFrom, bTo, type, kind } = req.query; + // const type = toArray(req.query.type); + // const kind = toArray(req.query.kind); if (!isValidRange(aFrom, aTo)) { return res.badRequest({ @@ -180,6 +196,7 @@ export function assetsRoutes({ router }: SetupR from: aFrom, to: aTo, type, + kind, }, }); @@ -189,6 +206,7 @@ export function assetsRoutes({ router }: SetupR from: bFrom, to: bTo, type, + kind, }, }); diff --git a/x-pack/plugins/asset_manager/server/routes/index.ts b/x-pack/plugins/asset_manager/server/routes/index.ts index 8225e996c247b..4c80215ae5b85 100644 --- a/x-pack/plugins/asset_manager/server/routes/index.ts +++ b/x-pack/plugins/asset_manager/server/routes/index.ts @@ -10,9 +10,14 @@ import { SetupRouteOptions } from './types'; import { pingRoute } from './ping'; import { assetsRoutes } from './assets'; import { sampleAssetsRoutes } from './sample_assets'; +import { hostsRoutes } from './assets/hosts'; -export function setupRoutes({ router }: SetupRouteOptions) { - pingRoute({ router }); - assetsRoutes({ router }); - sampleAssetsRoutes({ router }); +export function setupRoutes({ + router, + assetAccessor, +}: SetupRouteOptions) { + pingRoute({ router, assetAccessor }); + assetsRoutes({ router, assetAccessor }); + sampleAssetsRoutes({ router, assetAccessor }); + hostsRoutes({ router, assetAccessor }); } diff --git a/x-pack/plugins/asset_manager/server/routes/sample_assets.ts b/x-pack/plugins/asset_manager/server/routes/sample_assets.ts index 64c975d46a812..98f7f32051f3f 100644 --- a/x-pack/plugins/asset_manager/server/routes/sample_assets.ts +++ b/x-pack/plugins/asset_manager/server/routes/sample_assets.ts @@ -103,38 +103,38 @@ export function sampleAssetsRoutes({ async (context, req, res) => { const esClient = await getEsClientFromContext(context); - const sampleDataIndices = await esClient.indices.get({ - index: 'assets-*-sample_data', + const sampleDataStreams = await esClient.indices.getDataStream({ + name: 'assets-*-sample_data', expand_wildcards: 'all', }); - const deletedIndices: string[] = []; + const deletedDataStreams: string[] = []; let errorWhileDeleting: string | null = null; - const indicesToDelete = Object.keys(sampleDataIndices); + const dataStreamsToDelete = sampleDataStreams.data_streams.map((ds) => ds.name); - for (let i = 0; i < indicesToDelete.length; i++) { - const index = indicesToDelete[i]; + for (let i = 0; i < dataStreamsToDelete.length; i++) { + const dsName = dataStreamsToDelete[i]; try { - await esClient.indices.delete({ index }); - deletedIndices.push(index); + await esClient.indices.deleteDataStream({ name: dsName }); + deletedDataStreams.push(dsName); } catch (error: any) { errorWhileDeleting = typeof error.message === 'string' ? error.message - : `Unknown error occurred while deleting indices, at index ${index}`; + : `Unknown error occurred while deleting sample data streams, at data stream name: ${dsName}`; break; } } - if (deletedIndices.length === indicesToDelete.length) { - return res.ok({ body: { deleted: deletedIndices } }); + if (!errorWhileDeleting && deletedDataStreams.length === dataStreamsToDelete.length) { + return res.ok({ body: { deleted: deletedDataStreams } }); } else { return res.custom({ statusCode: 500, body: { - message: ['Not all matching indices were deleted', errorWhileDeleting].join(' - '), - deleted: deletedIndices, - matching: indicesToDelete, + message: ['Not all found data streams were deleted', errorWhileDeleting].join(' - '), + deleted: deletedDataStreams, + matching: dataStreamsToDelete, }, }); } diff --git a/x-pack/plugins/asset_manager/server/routes/types.ts b/x-pack/plugins/asset_manager/server/routes/types.ts index 341d4cb3cc18a..2a0cf91f47df7 100644 --- a/x-pack/plugins/asset_manager/server/routes/types.ts +++ b/x-pack/plugins/asset_manager/server/routes/types.ts @@ -6,7 +6,9 @@ */ import { IRouter, RequestHandlerContextBase } from '@kbn/core-http-server'; +import { AssetAccessor } from '../lib/asset_accessor'; export interface SetupRouteOptions { router: IRouter; + assetAccessor: AssetAccessor; } diff --git a/x-pack/plugins/asset_manager/server/templates/assets_template.ts b/x-pack/plugins/asset_manager/server/templates/assets_template.ts index 14ea4819a6631..71d4058eba4c0 100644 --- a/x-pack/plugins/asset_manager/server/templates/assets_template.ts +++ b/x-pack/plugins/asset_manager/server/templates/assets_template.ts @@ -10,6 +10,7 @@ import { IndicesPutIndexTemplateRequest } from '@elastic/elasticsearch/lib/api/t export const assetsIndexTemplateConfig: IndicesPutIndexTemplateRequest = { name: 'assets', priority: 100, + data_stream: {}, template: { settings: {}, mappings: { diff --git a/x-pack/plugins/asset_manager/server/types.ts b/x-pack/plugins/asset_manager/server/types.ts index ebbe18ec95996..a20ad1981b548 100644 --- a/x-pack/plugins/asset_manager/server/types.ts +++ b/x-pack/plugins/asset_manager/server/types.ts @@ -5,8 +5,45 @@ * 2.0. */ +import { schema, TypeOf } from '@kbn/config-schema'; import { ElasticsearchClient } from '@kbn/core/server'; export interface ElasticsearchAccessorOptions { esClient: ElasticsearchClient; } + +export const INDEX_DEFAULTS = { + metrics: 'metricbeat-*,metrics-*', + logs: 'filebeat-*,logs-*', + traces: 'traces-*', + serviceMetrics: 'metrics-apm*', + serviceLogs: 'logs-apm*', +}; + +export const configSchema = schema.object({ + alphaEnabled: schema.maybe(schema.boolean()), + // Designate where various types of data live. + // NOTE: this should be handled in a centralized way for observability, so + // that when a user configures these differently from the known defaults, + // that value is propagated everywhere. For now, we duplicate the value here. + sourceIndices: schema.object( + { + metrics: schema.string({ defaultValue: INDEX_DEFAULTS.metrics }), + logs: schema.string({ defaultValue: INDEX_DEFAULTS.logs }), + traces: schema.string({ defaultValue: INDEX_DEFAULTS.traces }), + serviceMetrics: schema.string({ defaultValue: INDEX_DEFAULTS.serviceMetrics }), + serviceLogs: schema.string({ defaultValue: INDEX_DEFAULTS.serviceLogs }), + }, + { defaultValue: INDEX_DEFAULTS } + ), + // Choose an explicit source for asset queries. + // NOTE: This will eventually need to be able to cleverly switch + // between these values based on the availability of data in the + // indices, and possibly for each asset kind/type value. + // For now, we set this explicitly. + lockedSource: schema.oneOf([schema.literal('assets'), schema.literal('signals')], { + defaultValue: 'signals', + }), +}); + +export type AssetManagerConfig = TypeOf; diff --git a/x-pack/plugins/asset_manager/tsconfig.json b/x-pack/plugins/asset_manager/tsconfig.json index 99d21abf14ba1..68bc19d31204e 100644 --- a/x-pack/plugins/asset_manager/tsconfig.json +++ b/x-pack/plugins/asset_manager/tsconfig.json @@ -17,5 +17,8 @@ "@kbn/core-http-server", "@kbn/core-elasticsearch-client-server-mocks", "@kbn/io-ts-utils", + "@kbn/core-elasticsearch-server", + "@kbn/core-http-request-handler-context-server", + "@kbn/datemath" ] } diff --git a/x-pack/test/api_integration/apis/asset_manager/config_when_disabled.ts b/x-pack/test/api_integration/apis/asset_manager/config_when_disabled.ts index 517b904d7850d..af9d3d0d206a4 100644 --- a/x-pack/test/api_integration/apis/asset_manager/config_when_disabled.ts +++ b/x-pack/test/api_integration/apis/asset_manager/config_when_disabled.ts @@ -12,6 +12,6 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { return { ...baseIntegrationTestsConfig.getAll(), - testFiles: [require.resolve('./when_disabled.ts')], + testFiles: [require.resolve('./tests/when_disabled.ts')], }; } diff --git a/x-pack/test/api_integration/apis/asset_manager/config.ts b/x-pack/test/api_integration/apis/asset_manager/config_with_assets_source.ts similarity index 96% rename from x-pack/test/api_integration/apis/asset_manager/config.ts rename to x-pack/test/api_integration/apis/asset_manager/config_with_assets_source.ts index 647ebb8a1c5b2..9e1c7a6305a10 100644 --- a/x-pack/test/api_integration/apis/asset_manager/config.ts +++ b/x-pack/test/api_integration/apis/asset_manager/config_with_assets_source.ts @@ -39,7 +39,7 @@ export default async function createTestConfig({ return { ...baseIntegrationTestsConfig.getAll(), - testFiles: [require.resolve('.')], + testFiles: [require.resolve('./tests/with_assets_source')], services: { ...services, assetsSynthtraceEsClient: (context: InheritedFtrProviderContext) => { @@ -88,6 +88,7 @@ export default async function createTestConfig({ serverArgs: [ ...baseIntegrationTestsConfig.get('kbnTestServer.serverArgs'), '--xpack.assetManager.alphaEnabled=true', + `--xpack.assetManager.lockedSource=assets`, ], }, }; diff --git a/x-pack/test/api_integration/apis/asset_manager/config_with_signals_source.ts b/x-pack/test/api_integration/apis/asset_manager/config_with_signals_source.ts new file mode 100644 index 0000000000000..395886f321294 --- /dev/null +++ b/x-pack/test/api_integration/apis/asset_manager/config_with_signals_source.ts @@ -0,0 +1,99 @@ +/* + * 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 { APM_TEST_PASSWORD } from '@kbn/apm-plugin/server/test_helpers/create_apm_users/authentication'; +import { + ApmSynthtraceEsClient, + ApmSynthtraceKibanaClient, + AssetsSynthtraceEsClient, + createLogger, + InfraSynthtraceEsClient, + LogLevel, +} from '@kbn/apm-synthtrace'; +import { FtrConfigProviderContext } from '@kbn/test'; +import url from 'url'; +import { FtrProviderContext as InheritedFtrProviderContext } from '../../ftr_provider_context'; +import { InheritedServices } from './types'; + +interface AssetManagerConfig { + services: InheritedServices & { + assetsSynthtraceEsClient: ( + context: InheritedFtrProviderContext + ) => Promise; + infraSynthtraceEsClient: ( + context: InheritedFtrProviderContext + ) => Promise; + apmSynthtraceEsClient: (context: InheritedFtrProviderContext) => Promise; + }; +} + +export default async function createTestConfig({ + readConfigFile, +}: FtrConfigProviderContext): Promise { + const baseIntegrationTestsConfig = await readConfigFile(require.resolve('../../config.ts')); + const services = baseIntegrationTestsConfig.get('services'); + + return { + ...baseIntegrationTestsConfig.getAll(), + testFiles: [require.resolve('./tests/with_signals_source')], + services: { + ...services, + assetsSynthtraceEsClient: (context: InheritedFtrProviderContext) => { + return new AssetsSynthtraceEsClient({ + client: context.getService('es'), + logger: createLogger(LogLevel.info), + refreshAfterIndex: true, + }); + }, + infraSynthtraceEsClient: (context: InheritedFtrProviderContext) => { + return new InfraSynthtraceEsClient({ + client: context.getService('es'), + logger: createLogger(LogLevel.info), + refreshAfterIndex: true, + }); + }, + apmSynthtraceEsClient: async (context: InheritedFtrProviderContext) => { + const servers = baseIntegrationTestsConfig.get('servers'); + + const kibanaServer = servers.kibana as url.UrlObject; + const kibanaServerUrl = url.format(kibanaServer); + const kibanaServerUrlWithAuth = url + .format({ + ...url.parse(kibanaServerUrl), + auth: `elastic:${APM_TEST_PASSWORD}`, + }) + .slice(0, -1); + + const kibanaClient = new ApmSynthtraceKibanaClient({ + target: kibanaServerUrlWithAuth, + logger: createLogger(LogLevel.debug), + }); + const kibanaVersion = await kibanaClient.fetchLatestApmPackageVersion(); + await kibanaClient.installApmPackage(kibanaVersion); + + return new ApmSynthtraceEsClient({ + client: context.getService('es'), + logger: createLogger(LogLevel.info), + version: kibanaVersion, + refreshAfterIndex: true, + }); + }, + }, + kbnTestServer: { + ...baseIntegrationTestsConfig.get('kbnTestServer'), + serverArgs: [ + ...baseIntegrationTestsConfig.get('kbnTestServer.serverArgs'), + '--xpack.assetManager.alphaEnabled=true', + `--xpack.assetManager.lockedSource=signals`, + ], + }, + }; +} + +export type CreateTestConfig = Awaited>; + +export type AssetManagerServices = CreateTestConfig['services']; diff --git a/x-pack/test/api_integration/apis/asset_manager/index.ts b/x-pack/test/api_integration/apis/asset_manager/index.ts deleted file mode 100644 index a5c9bd0227ab4..0000000000000 --- a/x-pack/test/api_integration/apis/asset_manager/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * 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 { FtrProviderContext } from '../../ftr_provider_context'; - -export default function ({ loadTestFile }: FtrProviderContext) { - describe('Asset Manager API Endpoints - when enabled', () => { - 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')); - }); -} diff --git a/x-pack/test/api_integration/apis/asset_manager/constants.ts b/x-pack/test/api_integration/apis/asset_manager/tests/constants.ts similarity index 100% rename from x-pack/test/api_integration/apis/asset_manager/constants.ts rename to x-pack/test/api_integration/apis/asset_manager/tests/constants.ts diff --git a/x-pack/test/api_integration/apis/asset_manager/helpers.ts b/x-pack/test/api_integration/apis/asset_manager/tests/helpers.ts similarity index 100% rename from x-pack/test/api_integration/apis/asset_manager/helpers.ts rename to x-pack/test/api_integration/apis/asset_manager/tests/helpers.ts diff --git a/x-pack/test/api_integration/apis/asset_manager/when_disabled.ts b/x-pack/test/api_integration/apis/asset_manager/tests/when_disabled.ts similarity index 92% rename from x-pack/test/api_integration/apis/asset_manager/when_disabled.ts rename to x-pack/test/api_integration/apis/asset_manager/tests/when_disabled.ts index 5ed03476ddc55..d8b556a959f8d 100644 --- a/x-pack/test/api_integration/apis/asset_manager/when_disabled.ts +++ b/x-pack/test/api_integration/apis/asset_manager/tests/when_disabled.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); diff --git a/x-pack/test/api_integration/apis/asset_manager/tests/assets.ts b/x-pack/test/api_integration/apis/asset_manager/tests/with_assets_source/assets.ts similarity index 74% rename from x-pack/test/api_integration/apis/asset_manager/tests/assets.ts rename to x-pack/test/api_integration/apis/asset_manager/tests/with_assets_source/assets.ts index 3afe434a530ec..f5069a274ebb3 100644 --- a/x-pack/test/api_integration/apis/asset_manager/tests/assets.ts +++ b/x-pack/test/api_integration/apis/asset_manager/tests/with_assets_source/assets.ts @@ -7,7 +7,7 @@ import { AssetWithoutTimestamp } from '@kbn/assetManager-plugin/common/types_api'; import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; import { createSampleAssets, deleteSampleAssets, viewSampleAssetDocs } from '../helpers'; import { ASSETS_ENDPOINT } from '../constants'; @@ -93,7 +93,7 @@ export default function ({ getService }: FtrProviderContext) { // Dynamically grab all types from the sample asset data set const sampleTypeSet: Set = new Set(); for (let i = 0; i < sampleAssetDocs.length; i++) { - sampleTypeSet.add(sampleAssetDocs[i]['asset.type']); + sampleTypeSet.add(sampleAssetDocs[i]['asset.type']!); } const sampleTypes = Array.from(sampleTypeSet); if (sampleTypes.length <= 2) { @@ -107,7 +107,7 @@ export default function ({ getService }: FtrProviderContext) { // Track a reference to how many docs should be returned for these two types const samplesForFilteredTypes = sampleAssetDocs.filter((doc) => - filterByTypes.includes(doc['asset.type']) + filterByTypes.includes(doc['asset.type']!) ); expect(samplesForFilteredTypes.length).to.be.lessThan(sampleAssetDocs.length); @@ -136,6 +136,72 @@ export default function ({ getService }: FtrProviderContext) { ); }); + it('should return assets filtered by a single asset.kind value', async () => { + await createSampleAssets(supertest); + + const singleSampleKind = sampleAssetDocs[0]['asset.kind']; + const samplesForKind = sampleAssetDocs.filter( + (doc) => doc['asset.kind'] === singleSampleKind + ); + + const getResponse = await supertest + .get(ASSETS_ENDPOINT) + .query({ size: sampleAssetDocs.length, from: 'now-1d', kind: singleSampleKind }) + .expect(200); + + expect(getResponse.body).to.have.property('results'); + expect(getResponse.body.results.length).to.equal(samplesForKind.length); + }); + + it('should return assets filtered by multiple asset.kind values (OR)', async () => { + await createSampleAssets(supertest); + + // Dynamically grab all asset.kind values from the sample asset data set + const sampleKindSet: Set = new Set(); + for (let i = 0; i < sampleAssetDocs.length; i++) { + sampleKindSet.add(sampleAssetDocs[i]['asset.kind']!); + } + const sampleKinds = Array.from(sampleKindSet); + if (sampleKinds.length <= 2) { + throw new Error( + 'Not enough asset kind values in sample asset documents, need more than two to test filtering by multiple kinds' + ); + } + + // Pick the first two unique kinds from the sample data set + const filterByKinds = sampleKinds.slice(0, 2); + + // Track a reference to how many docs should be returned for these two kinds + const samplesForFilteredKinds = sampleAssetDocs.filter((doc) => + filterByKinds.includes(doc['asset.kind']!) + ); + + expect(samplesForFilteredKinds.length).to.be.lessThan(sampleAssetDocs.length); + + // Request assets for multiple types (with a size matching the number of total sample asset docs) + const getResponse = await supertest + .get(ASSETS_ENDPOINT) + .query({ size: sampleAssetDocs.length, from: 'now-1d', kind: filterByKinds }) + .expect(200); + + expect(getResponse.body).to.have.property('results'); + expect(getResponse.body.results.length).to.equal(samplesForFilteredKinds.length); + }); + + it('should reject requests that try to filter by both kind and ean', async () => { + const sampleKind = sampleAssetDocs[0]['asset.kind']; + const sampleEan = sampleAssetDocs[0]['asset.ean']; + + const getResponse = await supertest + .get(ASSETS_ENDPOINT) + .query({ kind: sampleKind, ean: sampleEan }) + .expect(400); + + expect(getResponse.body.message).to.equal( + 'Filters "kind" and "ean" are mutually exclusive but found both.' + ); + }); + it('should return the asset matching a single ean', async () => { await createSampleAssets(supertest); diff --git a/x-pack/test/api_integration/apis/asset_manager/tests/assets_diff.ts b/x-pack/test/api_integration/apis/asset_manager/tests/with_assets_source/assets_diff.ts similarity index 92% rename from x-pack/test/api_integration/apis/asset_manager/tests/assets_diff.ts rename to x-pack/test/api_integration/apis/asset_manager/tests/with_assets_source/assets_diff.ts index 8ae204989b910..15cf1a64dafaa 100644 --- a/x-pack/test/api_integration/apis/asset_manager/tests/assets_diff.ts +++ b/x-pack/test/api_integration/apis/asset_manager/tests/with_assets_source/assets_diff.ts @@ -9,7 +9,7 @@ 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 { FtrProviderContext } from '../../../../ftr_provider_context'; import { createSampleAssets, deleteSampleAssets, viewSampleAssetDocs } from '../helpers'; import { ASSETS_ENDPOINT } from '../constants'; @@ -58,7 +58,7 @@ export default function ({ getService }: FtrProviderContext) { '[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 + return await supertest .get(DIFF_ENDPOINT) .query({ aFrom: timestamp, aTo: timestamp, bFrom: timestamp, bTo: timestamp }) .expect(200); @@ -95,7 +95,7 @@ export default function ({ getService }: FtrProviderContext) { `Time range cannot move backwards in time. "bTo" (${oneHourAgo}) is before "bFrom" (${isoNow}).` ); - await supertest + return await supertest .get(DIFF_ENDPOINT) .query({ aFrom: oneHourAgo, @@ -107,20 +107,26 @@ export default function ({ getService }: FtrProviderContext) { }); 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 onlyInA = sampleAssetDocs.slice(0, 2); // first two sample assets + const onlyInB = sampleAssetDocs.slice(sampleAssetDocs.length - 2); // last two sample assets + const inBoth = sampleAssetDocs.slice(2, sampleAssetDocs.length - 2); // everything between first 2, last 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); + + // Set 1: Two hours ago, excludes inBoth + onlyInB (leaves just onlyInA) await createSampleAssets(supertest, { baseDateTime: twoHoursAgo.toISOString(), excludeEans: inBoth.concat(onlyInB).map((asset) => asset['asset.ean']), }); + + // Set 2: One hour ago, excludes onlyInA + onlyInB (leaves just inBoth) await createSampleAssets(supertest, { baseDateTime: oneHourAgo.toISOString(), excludeEans: onlyInA.concat(onlyInB).map((asset) => asset['asset.ean']), }); + + // Set 3: Right now, excludes inBoth + onlyInA (leaves just onlyInB) await createSampleAssets(supertest, { excludeEans: inBoth.concat(onlyInA).map((asset) => asset['asset.ean']), }); @@ -130,6 +136,8 @@ export default function ({ getService }: FtrProviderContext) { const seventyMinuesAgo = new Date(now.getTime() - 1000 * 60 * 70 * 1); const tenMinutesAfterNow = new Date(now.getTime() + 1000 * 60 * 10); + // Range A: 2h10m ago - 50m ago (Sets 1 and 2) + // Range B: 70m ago - 10m after now (Sets 2 and 3) const getResponse = await supertest .get(DIFF_ENDPOINT) .query({ diff --git a/x-pack/test/api_integration/apis/asset_manager/tests/assets_related.ts b/x-pack/test/api_integration/apis/asset_manager/tests/with_assets_source/assets_related.ts similarity index 94% rename from x-pack/test/api_integration/apis/asset_manager/tests/assets_related.ts rename to x-pack/test/api_integration/apis/asset_manager/tests/with_assets_source/assets_related.ts index edabcb734893b..2cb05d89ed618 100644 --- a/x-pack/test/api_integration/apis/asset_manager/tests/assets_related.ts +++ b/x-pack/test/api_integration/apis/asset_manager/tests/with_assets_source/assets_related.ts @@ -9,7 +9,7 @@ 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 { FtrProviderContext } from '../../../../ftr_provider_context'; import { createSampleAssets, deleteSampleAssets, viewSampleAssetDocs } from '../helpers'; import { ASSETS_ENDPOINT } from '../constants'; @@ -40,18 +40,18 @@ export default function ({ getService }: FtrProviderContext) { const relations = [ { name: 'ancestors', - ean: 'k8s.node:node-101', - expectedRelatedEans: ['k8s.cluster:cluster-001'], + ean: 'host:node-101', + expectedRelatedEans: ['cluster:cluster-001'], }, { name: 'descendants', - ean: 'k8s.cluster:cluster-001', - expectedRelatedEans: ['k8s.node:node-101', 'k8s.node:node-102', 'k8s.node:node-103'], + ean: 'cluster:cluster-001', + expectedRelatedEans: ['host:node-101', 'host:node-102', 'host:node-103'], }, { name: 'references', - ean: 'k8s.pod:pod-200xrg1', - expectedRelatedEans: ['k8s.cluster:cluster-001'], + ean: 'pod:pod-200xrg1', + expectedRelatedEans: ['cluster:cluster-001'], }, ]; @@ -158,8 +158,8 @@ export default function ({ getService }: FtrProviderContext) { 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 }, + { 'asset.ean': 'host:node-203', distance: 1 }, + { 'asset.ean': 'pod:pod-203ugg9', distance: 2 }, ]); }); @@ -317,8 +317,8 @@ export default function ({ getService }: FtrProviderContext) { }); }); - describe('with asset.type filters', () => { - it('should filter by the provided asset type', async () => { + describe('with asset.kind filters', () => { + it('should filter by the provided asset kind', async () => { await createSampleAssets(supertest); const sampleCluster = sampleAssetDocs.find( @@ -333,7 +333,7 @@ export default function ({ getService }: FtrProviderContext) { from: 'now-1d', ean: sampleCluster!['asset.ean'], maxDistance: 1, - type: ['k8s.pod'], + kind: ['pod'], }) .expect(200); @@ -358,7 +358,7 @@ export default function ({ getService }: FtrProviderContext) { from: 'now-1d', ean: sampleCluster!['asset.ean'], maxDistance: 2, - type: ['k8s.pod'], + kind: ['pod'], }) .expect(200); @@ -367,7 +367,7 @@ export default function ({ getService }: FtrProviderContext) { } = 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(results.descendants.every((asset: Asset) => asset['asset.kind'] === 'pod')); }); }); }); diff --git a/x-pack/test/api_integration/apis/asset_manager/tests/basics.ts b/x-pack/test/api_integration/apis/asset_manager/tests/with_assets_source/basics.ts similarity index 93% rename from x-pack/test/api_integration/apis/asset_manager/tests/basics.ts rename to x-pack/test/api_integration/apis/asset_manager/tests/with_assets_source/basics.ts index 4dca3a812d484..451d49605a213 100644 --- a/x-pack/test/api_integration/apis/asset_manager/tests/basics.ts +++ b/x-pack/test/api_integration/apis/asset_manager/tests/with_assets_source/basics.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); diff --git a/x-pack/test/api_integration/apis/asset_manager/tests/with_assets_source/index.ts b/x-pack/test/api_integration/apis/asset_manager/tests/with_assets_source/index.ts new file mode 100644 index 0000000000000..d92db4cff9958 --- /dev/null +++ b/x-pack/test/api_integration/apis/asset_manager/tests/with_assets_source/index.ts @@ -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. + */ +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('Asset Manager API Endpoints - with assets source', () => { + loadTestFile(require.resolve('./basics')); + loadTestFile(require.resolve('./sample_assets')); + loadTestFile(require.resolve('./assets')); + loadTestFile(require.resolve('./assets_diff')); + loadTestFile(require.resolve('./assets_related')); + }); +} diff --git a/x-pack/test/api_integration/apis/asset_manager/tests/sample_assets.ts b/x-pack/test/api_integration/apis/asset_manager/tests/with_assets_source/sample_assets.ts similarity index 98% rename from x-pack/test/api_integration/apis/asset_manager/tests/sample_assets.ts rename to x-pack/test/api_integration/apis/asset_manager/tests/with_assets_source/sample_assets.ts index 1c8faff74f006..618f5f49f8892 100644 --- a/x-pack/test/api_integration/apis/asset_manager/tests/sample_assets.ts +++ b/x-pack/test/api_integration/apis/asset_manager/tests/with_assets_source/sample_assets.ts @@ -7,7 +7,7 @@ import { Asset } from '@kbn/assetManager-plugin/common/types_api'; import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; import { createSampleAssets, deleteSampleAssets, viewSampleAssetDocs } from '../helpers'; export default function ({ getService }: FtrProviderContext) { diff --git a/x-pack/test/api_integration/apis/asset_manager/tests/with_signals_source/basics.ts b/x-pack/test/api_integration/apis/asset_manager/tests/with_signals_source/basics.ts new file mode 100644 index 0000000000000..ea64d5898b265 --- /dev/null +++ b/x-pack/test/api_integration/apis/asset_manager/tests/with_signals_source/basics.ts @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esSupertest = getService('esSupertest'); + + describe('during basic startup', () => { + describe('ping endpoint', () => { + it('returns a successful response', async () => { + const response = await supertest.get('/api/asset-manager/ping').expect(200); + expect(response.body).to.eql({ message: 'Asset Manager OK' }); + }); + }); + + describe('assets index templates', () => { + it('should not be installed', async () => { + await esSupertest.get('/_index_template/assets').expect(404); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/asset_manager/tests/with_signals_source/index.ts b/x-pack/test/api_integration/apis/asset_manager/tests/with_signals_source/index.ts new file mode 100644 index 0000000000000..72e685549dc29 --- /dev/null +++ b/x-pack/test/api_integration/apis/asset_manager/tests/with_signals_source/index.ts @@ -0,0 +1,13 @@ +/* + * 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 { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('Asset Manager API Endpoints - with signals source', () => { + loadTestFile(require.resolve('./basics')); + }); +} diff --git a/x-pack/test/api_integration/apis/asset_manager/types.ts b/x-pack/test/api_integration/apis/asset_manager/types.ts index 2da488241ee47..ba52dcb88a5b6 100644 --- a/x-pack/test/api_integration/apis/asset_manager/types.ts +++ b/x-pack/test/api_integration/apis/asset_manager/types.ts @@ -7,7 +7,7 @@ import { GenericFtrProviderContext } from '@kbn/test'; import { FtrProviderContext as InheritedFtrProviderContext } from '../../ftr_provider_context'; -import { AssetManagerServices } from './config'; +import { AssetManagerServices } from './config_with_signals_source'; export type InheritedServices = InheritedFtrProviderContext extends GenericFtrProviderContext< infer TServices,