From 86f30f750cad88e79b1c7c8cc74dcd0a29fa9877 Mon Sep 17 00:00:00 2001 From: Jason Rhodes Date: Wed, 2 Nov 2022 15:44:44 -0400 Subject: [PATCH 1/5] Baseline boilerplate for asset_inventory plugin This includes the server and public folders with a simple page template in place. --- .../asset_inventory/common/types_api.ts | 59 +++++++++ x-pack/plugins/asset_inventory/kibana.json | 30 +++++ .../asset_inventory/public/app/index.tsx | 87 ++++++++++++++ .../asset_inventory/public/app/routes.ts | 16 +++ .../public/components/assets_table.tsx | 113 ++++++++++++++++++ .../plugins/asset_inventory/public/index.ts | 32 +++++ .../public/lib/page_template.tsx | 13 ++ .../pages/asset_inventory_list_page.tsx | 36 ++++++ .../plugins/asset_inventory/public/plugin.ts | 111 +++++++++++++++++ .../plugins/asset_inventory/server/index.ts | 12 ++ .../asset_inventory/server/lib/es_client.ts | 26 ++++ .../asset_inventory/server/lib/get_assets.ts | 19 +++ .../plugins/asset_inventory/server/plugin.ts | 51 ++++++++ x-pack/plugins/asset_inventory/tsconfig.json | 35 ++++++ 14 files changed, 640 insertions(+) create mode 100644 x-pack/plugins/asset_inventory/common/types_api.ts create mode 100644 x-pack/plugins/asset_inventory/kibana.json create mode 100644 x-pack/plugins/asset_inventory/public/app/index.tsx create mode 100644 x-pack/plugins/asset_inventory/public/app/routes.ts create mode 100644 x-pack/plugins/asset_inventory/public/components/assets_table.tsx create mode 100644 x-pack/plugins/asset_inventory/public/index.ts create mode 100644 x-pack/plugins/asset_inventory/public/lib/page_template.tsx create mode 100644 x-pack/plugins/asset_inventory/public/pages/asset_inventory_list_page.tsx create mode 100644 x-pack/plugins/asset_inventory/public/plugin.ts create mode 100644 x-pack/plugins/asset_inventory/server/index.ts create mode 100644 x-pack/plugins/asset_inventory/server/lib/es_client.ts create mode 100644 x-pack/plugins/asset_inventory/server/lib/get_assets.ts create mode 100644 x-pack/plugins/asset_inventory/server/plugin.ts create mode 100644 x-pack/plugins/asset_inventory/tsconfig.json diff --git a/x-pack/plugins/asset_inventory/common/types_api.ts b/x-pack/plugins/asset_inventory/common/types_api.ts new file mode 100644 index 0000000000000..8775496303ffb --- /dev/null +++ b/x-pack/plugins/asset_inventory/common/types_api.ts @@ -0,0 +1,59 @@ +/* + * 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 type AssetKind = 'unknown' | 'node'; +export type AssetType = 'k8s.pod' | 'k8s.cluster' | 'k8s.node'; + +export interface Asset { + '@timestamp': string; + 'asset.ean': string; + 'asset.id': string; + 'asset.kind': AssetKind; + 'asset.name'?: string; + 'asset.type': AssetType; + 'asset.parents'?: string | string[]; + 'asset.children'?: string | string[]; + 'asset.namespace'?: string; + kubernetes?: EcsKubernetesFieldset; + orchestrator?: EcsOrchestratorFieldset; +} + +export interface EcsKubernetesFieldset { + namespace?: string; + pod?: { + name: string; + uid: string; + start_time?: Date; + }; + node?: { + name: string; + start_time?: Date; + }; +} + +// See: https://www.elastic.co/guide/en/ecs/current/ecs-orchestrator.html +export interface EcsOrchestratorFieldset { + api_version?: string; + namespace?: string; + organization?: string; + type?: string; + cluster?: { + id?: string; + name?: string; + url?: string; + version?: string; + }; + resource?: { + id?: string; + ip?: string; + name?: string; + type?: string; + parent?: { + type?: string; + }; + }; +} diff --git a/x-pack/plugins/asset_inventory/kibana.json b/x-pack/plugins/asset_inventory/kibana.json new file mode 100644 index 0000000000000..e1001df0d804d --- /dev/null +++ b/x-pack/plugins/asset_inventory/kibana.json @@ -0,0 +1,30 @@ +{ + "id": "assetInventory", + "owner": { + "name": "TBD" + }, + "version": "8.0.0", + "kibanaVersion": "kibana", + "configPath": [ + "xpack", + "assetInventory" + ], + "optionalPlugins": [ + ], + "requiredPlugins": [ + "alerting", + "cases", + "data", + "dataViews", + "features", + "inspector", + "unifiedSearch", + "usageCollection" + ], + "ui": true, + "server": true, + "requiredBundles": [ + "kibanaReact", + "kibanaUtils" + ] +} diff --git a/x-pack/plugins/asset_inventory/public/app/index.tsx b/x-pack/plugins/asset_inventory/public/app/index.tsx new file mode 100644 index 0000000000000..f0e5a69d1b4d8 --- /dev/null +++ b/x-pack/plugins/asset_inventory/public/app/index.tsx @@ -0,0 +1,87 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import { Route, Router, Switch } from 'react-router-dom'; +import { Storage } from '@kbn/kibana-utils-plugin/public'; + +import { AppMountParameters, APP_WRAPPER_CLASS, CoreStart } from '@kbn/core/public'; +import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; +import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common'; + +import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; +import { AssetInventoryPublicPluginsStart } from '../plugin'; +import { routes } from './routes'; + +function App() { + return ( + <> + + {routes.map((routeProps) => { + return ; + })} + + + ); +} + +export function renderApp({ + core, + config, + plugins, + appMountParameters, + usageCollection, + isDev, +}: { + core: CoreStart; + config: {}; + plugins: AssetInventoryPublicPluginsStart; + appMountParameters: AppMountParameters; + usageCollection: UsageCollectionSetup; + isDev?: boolean; +}) { + const { element, history, theme$ } = appMountParameters; + const i18nCore = core.i18n; + const isDarkMode = core.uiSettings.get('theme:darkMode'); + + core.chrome.setHelpExtension({ + appName: i18n.translate('xpack.observability.feedbackMenu.appName', { + defaultMessage: 'Observability', + }), + links: [{ linkType: 'discuss', href: 'https://ela.st/observability-discuss' }], + }); + + // ensure all divs are .kbnAppWrappers + element.classList.add(APP_WRAPPER_CLASS); + + const ApplicationUsageTrackingProvider = + usageCollection?.components.ApplicationUsageTrackingProvider ?? React.Fragment; + + ReactDOM.render( + + + + + + + + + + + + + , + element + ); + return () => { + ReactDOM.unmountComponentAtNode(element); + }; +} diff --git a/x-pack/plugins/asset_inventory/public/app/routes.ts b/x-pack/plugins/asset_inventory/public/app/routes.ts new file mode 100644 index 0000000000000..555a5e0d9b3fb --- /dev/null +++ b/x-pack/plugins/asset_inventory/public/app/routes.ts @@ -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. + */ + +import { RouteProps } from 'react-router-dom'; +import { AssetInventoryListPage } from '../pages/asset_inventory_list_page'; + +export const routes: RouteProps[] = [ + { + path: '/', + component: AssetInventoryListPage, + }, +]; diff --git a/x-pack/plugins/asset_inventory/public/components/assets_table.tsx b/x-pack/plugins/asset_inventory/public/components/assets_table.tsx new file mode 100644 index 0000000000000..77555687039d3 --- /dev/null +++ b/x-pack/plugins/asset_inventory/public/components/assets_table.tsx @@ -0,0 +1,113 @@ +/* + * 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 React from 'react'; +import { EuiBasicTable } from '@elastic/eui'; +import { Asset } from '../../common/types_api'; + +/* +Example user object: + +{ + id: '1', + firstName: 'john', + lastName: 'doe', + github: 'johndoe', + dateOfBirth: Date.now(), + nationality: 'NL', + online: true +} + +Example country object: + +{ + code: 'NL', + name: 'Netherlands', + flag: '🇳🇱' +} +*/ + +interface AssetsTableProps { + assets: Asset[]; +} + +export function AssetsTable({ assets }: AssetsTableProps) { + const columns = [ + { + field: '@timestamp', + name: 'Timestamp', + render: (date: Asset['@timestamp']) => { + const d = new Date(date); + return d.toLocaleString(); + }, + width: '200px', + }, + { + field: 'asset.ean', + name: 'EAN (Elastic Asset Name)', + sortable: true, + width: '300px', + }, + { + field: 'asset.kind', + name: 'Asset Kind', + sortable: true, + }, + { + field: 'asset.type', + name: 'Asset Type', + sortable: true, + }, + { + field: 'asset.id', + name: 'Asset Original ID', + sortable: true, + }, + { + field: 'asset.parents', + name: '# of parents', + render: (parents: Asset['asset.parents']) => parents?.length || 0, + width: '100px', + }, + { + field: 'asset.children', + name: '# of children', + render: (children: Asset['asset.children']) => children?.length || 0, + width: '100px', + }, + ]; + + // const getRowProps = (item) => { + // const { id } = item; + // return { + // 'data-test-subj': `row-${id}`, + // className: 'customRowClass', + // onClick: () => {}, + // }; + // }; + + // const getCellProps = (item, column) => { + // const { id } = item; + // const { field } = column; + // return { + // className: 'customCellClass', + // 'data-test-subj': `cell-${id}-${field}`, + // textOnly: true, + // }; + // }; + + return ( + + tableCaption="Asset Inventory Demo" + items={assets} + rowHeader="firstName" + columns={columns} + // rowProps={getRowProps} + // cellProps={getCellProps} + /> + ); +} diff --git a/x-pack/plugins/asset_inventory/public/index.ts b/x-pack/plugins/asset_inventory/public/index.ts new file mode 100644 index 0000000000000..919c10b2b78ec --- /dev/null +++ b/x-pack/plugins/asset_inventory/public/index.ts @@ -0,0 +1,32 @@ +/* + * 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 { PluginInitializer, PluginInitializerContext } from '@kbn/core/public'; + +import { + Plugin, + AssetInventoryPublicPluginsStart, + AssetInventoryPublicPluginsSetup, + AssetInventoryPublicStart, + AssetInventoryPublicSetup, +} from './plugin'; + +export type { + AssetInventoryPublicSetup, + AssetInventoryPublicStart, + AssetInventoryPublicPluginsSetup, + AssetInventoryPublicPluginsStart, +}; + +export const plugin: PluginInitializer< + AssetInventoryPublicSetup, + AssetInventoryPublicStart, + AssetInventoryPublicPluginsSetup, + AssetInventoryPublicPluginsStart +> = (initializerContext: PluginInitializerContext) => { + return new Plugin(initializerContext); +}; diff --git a/x-pack/plugins/asset_inventory/public/lib/page_template.tsx b/x-pack/plugins/asset_inventory/public/lib/page_template.tsx new file mode 100644 index 0000000000000..c595bb84e1de0 --- /dev/null +++ b/x-pack/plugins/asset_inventory/public/lib/page_template.tsx @@ -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 React from 'react'; +import { EuiPageTemplate, EuiPageTemplateProps } from '@elastic/eui'; + +export function PageTemplate(props: EuiPageTemplateProps) { + return ; +} diff --git a/x-pack/plugins/asset_inventory/public/pages/asset_inventory_list_page.tsx b/x-pack/plugins/asset_inventory/public/pages/asset_inventory_list_page.tsx new file mode 100644 index 0000000000000..2f1a34cbc3dec --- /dev/null +++ b/x-pack/plugins/asset_inventory/public/pages/asset_inventory_list_page.tsx @@ -0,0 +1,36 @@ +/* + * 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 React, { useEffect, useState } from 'react'; +import { EuiPageTemplate } from '@elastic/eui'; +import axios from 'axios'; +import { PageTemplate } from '../lib/page_template'; +import { Asset } from '../../common/types_api'; +import { AssetsTable } from '../components/assets_table'; + +export function AssetInventoryListPage() { + const [assets, setAssets] = useState([]); + + useEffect(() => { + async function retrieve() { + const response = await axios.get('/local/api/asset-inventory'); + if (response.data && response.data.assets) { + setAssets(response.data.assets); + } + } + retrieve(); + }, []); + + return ( + + + + + + + ); +} diff --git a/x-pack/plugins/asset_inventory/public/plugin.ts b/x-pack/plugins/asset_inventory/public/plugin.ts new file mode 100644 index 0000000000000..e4e2df757289f --- /dev/null +++ b/x-pack/plugins/asset_inventory/public/plugin.ts @@ -0,0 +1,111 @@ +/* + * 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. + */ + +// NOTE: Originally copied from plugins/observability/public/plugin.ts, heavily modified + +import { i18n } from '@kbn/i18n'; +import { BehaviorSubject } from 'rxjs'; +import { + AppMountParameters, + AppUpdater, + CoreSetup, + CoreStart, + DEFAULT_APP_CATEGORIES, + Plugin as PluginClass, + PluginInitializerContext, +} from '@kbn/core/public'; + +import type { DataPublicPluginSetup, DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; +import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; + +export type AssetInventoryPublicSetup = ReturnType; + +export interface AssetInventoryPublicPluginsSetup { + data: DataPublicPluginSetup; + usageCollection: UsageCollectionSetup; +} + +export interface AssetInventoryPublicPluginsStart { + data: DataPublicPluginStart; + dataViews: DataViewsPublicPluginStart; +} + +export type AssetInventoryPublicStart = ReturnType; + +export class Plugin + implements + PluginClass< + AssetInventoryPublicSetup, + AssetInventoryPublicStart, + AssetInventoryPublicPluginsSetup, + AssetInventoryPublicPluginsStart + > +{ + private readonly appUpdater$ = new BehaviorSubject(() => ({})); + + constructor(private readonly initContext: PluginInitializerContext<{}>) {} + + public setup( + coreSetup: CoreSetup, + pluginsSetup: AssetInventoryPublicPluginsSetup + ) { + const category = DEFAULT_APP_CATEGORIES.observability; + const euiIconType = 'logoObservability'; + const config = this.initContext.config.get(); + + const mount = async (params: AppMountParameters) => { + // Load application bundle + const { renderApp } = await import('./app'); + // Get start services + const [coreStart, pluginsStart] = await coreSetup.getStartServices(); + + return renderApp({ + core: coreStart, + config, + plugins: pluginsStart, + appMountParameters: params, + // ObservabilityPageTemplate: navigation.PageTemplate, + usageCollection: pluginsSetup.usageCollection, + isDev: this.initContext.env.mode.dev, + }); + }; + + const appUpdater$ = this.appUpdater$; + const app = { + appRoute: '/app/asset-inventory', + category, + euiIconType, + id: 'assetInventory', + mount, + order: 8000, + title: i18n.translate('xpack.observability.overviewLinkTitle', { + defaultMessage: 'Overview', + }), + updater$: appUpdater$, + keywords: [ + 'observability', + 'monitor', + 'logs', + 'metrics', + 'apm', + 'performance', + 'trace', + 'agent', + 'rum', + 'user', + 'experience', + ], + }; + + coreSetup.application.register(app); + + return {}; + } + + public start(coreStart: CoreStart, pluginsStart: AssetInventoryPublicPluginsStart) {} +} diff --git a/x-pack/plugins/asset_inventory/server/index.ts b/x-pack/plugins/asset_inventory/server/index.ts new file mode 100644 index 0000000000000..d2f12f290c994 --- /dev/null +++ b/x-pack/plugins/asset_inventory/server/index.ts @@ -0,0 +1,12 @@ +/* + * 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. + */ + +// TODO: https://github.com/elastic/kibana/issues/110905 + +import { AssetInventoryServerPlugin } from './plugin'; + +export const plugin = () => new AssetInventoryServerPlugin(); diff --git a/x-pack/plugins/asset_inventory/server/lib/es_client.ts b/x-pack/plugins/asset_inventory/server/lib/es_client.ts new file mode 100644 index 0000000000000..54fb630d3ee51 --- /dev/null +++ b/x-pack/plugins/asset_inventory/server/lib/es_client.ts @@ -0,0 +1,26 @@ +/* + * 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. + */ + +// TODO: Replace all of this with Kibana ES client and/or data plugin + +import { Client } from '@elastic/elasticsearch'; +const { + ELASTICSEARCH_HOSTS = 'https://localhost:9200', + ELASTICSEARCH_USERNAME = 'elastic', + ELASTICSEARCH_PASSWORD = 'changeme', +} = process.env; + +export const esClient = new Client({ + node: ELASTICSEARCH_HOSTS, + auth: { + username: ELASTICSEARCH_USERNAME, + password: ELASTICSEARCH_PASSWORD, + }, + tls: { + rejectUnauthorized: false, + }, +}); diff --git a/x-pack/plugins/asset_inventory/server/lib/get_assets.ts b/x-pack/plugins/asset_inventory/server/lib/get_assets.ts new file mode 100644 index 0000000000000..bf6bf1eb43b16 --- /dev/null +++ b/x-pack/plugins/asset_inventory/server/lib/get_assets.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 { SearchRequest } from '@elastic/elasticsearch/lib/api/types'; +import { Asset } from '../../common/types_api'; +import { esClient } from './es_client'; + +export async function getAssets(): Promise { + const query: SearchRequest = { + index: 'assets', + }; + + const response = await esClient.search<{}>(query); + return response.hits.hits.map((hit) => hit._source as Asset); +} diff --git a/x-pack/plugins/asset_inventory/server/plugin.ts b/x-pack/plugins/asset_inventory/server/plugin.ts new file mode 100644 index 0000000000000..dce68b6f21541 --- /dev/null +++ b/x-pack/plugins/asset_inventory/server/plugin.ts @@ -0,0 +1,51 @@ +/* + * 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 { Plugin, CoreSetup } from '@kbn/core/server'; +import { getAssets } from './lib/get_assets'; + +export type AssetInventoryServerPluginSetup = ReturnType; + +export class AssetInventoryServerPlugin implements Plugin { + public async setup(core: CoreSetup) { + const router = core.http.createRouter(); + + router.get( + { + path: '/api/asset-inventory/ping', + validate: false, + }, + (context, req, res) => { + return res.ok({ + body: { message: 'Asset Inventory OK' }, + headers: { 'content-type': 'application/json' }, + }); + } + ); + + router.get( + { + path: '/api/asset-inventory', + validate: false, + }, + async (context, req, res) => { + try { + const assets = await getAssets(); + return res.ok({ body: { assets } }); + } catch (error: unknown) { + return res.customError({ statusCode: 500 }); + } + } + ); + + return {}; + } + + public start() {} + + public stop() {} +} diff --git a/x-pack/plugins/asset_inventory/tsconfig.json b/x-pack/plugins/asset_inventory/tsconfig.json new file mode 100644 index 0000000000000..163160f555669 --- /dev/null +++ b/x-pack/plugins/asset_inventory/tsconfig.json @@ -0,0 +1,35 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "common/**/*", + "public/**/*", + "public/**/*.json", + "server/**/*", + "typings/**/*", + "../../../typings/**/*" + ], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + { "path": "../../../src/plugins/data/tsconfig.json" }, + { "path": "../../../src/plugins/home/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, + { "path": "../../../src/plugins/usage_collection/tsconfig.json" }, + { "path": "../alerting/tsconfig.json" }, + { "path": "../licensing/tsconfig.json" }, + { "path": "../cases/tsconfig.json" }, + { "path": "../lens/tsconfig.json" }, + { "path": "../rule_registry/tsconfig.json" }, + { "path": "../spaces/tsconfig.json" }, + { "path": "../timelines/tsconfig.json"}, + { "path": "../translations/tsconfig.json" }, + { "path": "../../../src/plugins/unified_search/tsconfig.json"}, + { "path": "../../../src/plugins/guided_onboarding/tsconfig.json"}, + ] +} From 5b2e999f2220801f5f4de92f7a8247257a52a9c5 Mon Sep 17 00:00:00 2001 From: Jason Rhodes Date: Tue, 22 Nov 2022 12:33:08 -0500 Subject: [PATCH 2/5] Asset inventory list --- .../asset_inventory/common/types_api.ts | 11 ++ .../asset_inventory/public/app/index.tsx | 17 ++- .../components/asset_filter_autocomplete.tsx | 84 +++++++++++ .../components/asset_filter_controls.tsx | 63 ++++++++ .../components/asset_filter_select_box.tsx | 48 ++++++ .../public/components/assets_table.tsx | 138 ++++++------------ .../{lib => components}/page_template.tsx | 0 .../public/hooks/asset_filters.tsx | 57 ++++++++ .../convert_asset_filters_to_query_string.ts | 14 ++ .../pages/asset_inventory_list_page.tsx | 15 +- .../asset_inventory/server/lib/get_assets.ts | 84 ++++++++++- .../lib/get_latest_collection_version.ts | 27 ++++ .../server/lib/get_values_for_field.ts | 59 ++++++++ .../plugins/asset_inventory/server/plugin.ts | 38 ++++- 14 files changed, 543 insertions(+), 112 deletions(-) create mode 100644 x-pack/plugins/asset_inventory/public/components/asset_filter_autocomplete.tsx create mode 100644 x-pack/plugins/asset_inventory/public/components/asset_filter_controls.tsx create mode 100644 x-pack/plugins/asset_inventory/public/components/asset_filter_select_box.tsx rename x-pack/plugins/asset_inventory/public/{lib => components}/page_template.tsx (100%) create mode 100644 x-pack/plugins/asset_inventory/public/hooks/asset_filters.tsx create mode 100644 x-pack/plugins/asset_inventory/public/lib/convert_asset_filters_to_query_string.ts create mode 100644 x-pack/plugins/asset_inventory/server/lib/get_latest_collection_version.ts create mode 100644 x-pack/plugins/asset_inventory/server/lib/get_values_for_field.ts diff --git a/x-pack/plugins/asset_inventory/common/types_api.ts b/x-pack/plugins/asset_inventory/common/types_api.ts index 8775496303ffb..937c89e740708 100644 --- a/x-pack/plugins/asset_inventory/common/types_api.ts +++ b/x-pack/plugins/asset_inventory/common/types_api.ts @@ -10,6 +10,7 @@ export type AssetType = 'k8s.pod' | 'k8s.cluster' | 'k8s.node'; export interface Asset { '@timestamp': string; + 'asset.collection_version'?: string; 'asset.ean': string; 'asset.id': string; 'asset.kind': AssetKind; @@ -57,3 +58,13 @@ export interface EcsOrchestratorFieldset { }; }; } + +export interface AssetFilters { + type?: AssetType; + kind?: AssetKind; + ean?: string; + id?: string; + typeLike?: string; + eanLike?: string; + collectionVersion?: number | 'latest' | 'all'; +} diff --git a/x-pack/plugins/asset_inventory/public/app/index.tsx b/x-pack/plugins/asset_inventory/public/app/index.tsx index f0e5a69d1b4d8..3377729a8b53d 100644 --- a/x-pack/plugins/asset_inventory/public/app/index.tsx +++ b/x-pack/plugins/asset_inventory/public/app/index.tsx @@ -18,6 +18,7 @@ import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common'; import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; import { AssetInventoryPublicPluginsStart } from '../plugin'; import { routes } from './routes'; +import { AssetFilterContextProvider } from '../hooks/asset_filters'; function App() { return ( @@ -69,13 +70,15 @@ export function renderApp({ - - - - - - - + + + + + + + + + , diff --git a/x-pack/plugins/asset_inventory/public/components/asset_filter_autocomplete.tsx b/x-pack/plugins/asset_inventory/public/components/asset_filter_autocomplete.tsx new file mode 100644 index 0000000000000..602e915f875d5 --- /dev/null +++ b/x-pack/plugins/asset_inventory/public/components/asset_filter_autocomplete.tsx @@ -0,0 +1,84 @@ +/* + * 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 { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui'; +import axios from 'axios'; +import React, { useEffect, useState } from 'react'; +import { AssetFilters } from '../../common/types_api'; +import { useAssetFilters } from '../hooks/asset_filters'; + +interface AutocompleteOptions { + field: string; + label: string; + filtersKey: keyof AssetFilters; + allowMultipleValues?: boolean; + placeholder?: string; + fullWidth?: boolean; +} + +interface FieldValueResult { + key: string; + doc_count: number; +} + +export function AssetFilterAutocomplete({ + field, + label, + filtersKey, + allowMultipleValues = false, + placeholder, + fullWidth = false, +}: AutocompleteOptions) { + const [options, setOptions] = useState([]); + const [selected, setSelected] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const { filters, setFilters } = useAssetFilters(); + + const { collectionVersion } = filters; + + useEffect(() => { + async function retrieve() { + setIsLoading(true); + const response = await axios.get<{ results: FieldValueResult[] }>( + `/local/api/asset-inventory/field-values?field=${field}&version=${ + collectionVersion || 'all' + }` + ); + + if (response.data.results) { + const newOptions: EuiComboBoxOptionOption[] = response.data.results.map((result) => ({ + label: `${result.key} (${result.doc_count})`, + value: result.key, + })); + setOptions(newOptions); + } + setIsLoading(false); + } + + retrieve(); + }, [field, collectionVersion]); + + useEffect(() => { + const selectedValues = selected.map((option) => option.value); + const values = allowMultipleValues ? selectedValues : selectedValues[0]; + setFilters((prevFilters) => ({ ...prevFilters, [filtersKey]: values })); + }, [selected, allowMultipleValues, filtersKey, setFilters]); + + return ( + + setSelected(newOptions)} + placeholder={placeholder} + fullWidth={fullWidth} + /> + + ); +} diff --git a/x-pack/plugins/asset_inventory/public/components/asset_filter_controls.tsx b/x-pack/plugins/asset_inventory/public/components/asset_filter_controls.tsx new file mode 100644 index 0000000000000..d02d7e07c6882 --- /dev/null +++ b/x-pack/plugins/asset_inventory/public/components/asset_filter_controls.tsx @@ -0,0 +1,63 @@ +/* + * 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 React from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { AssetFilterAutocomplete } from './asset_filter_autocomplete'; + +export function AssetFilterControls() { + return ( + <> + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/x-pack/plugins/asset_inventory/public/components/asset_filter_select_box.tsx b/x-pack/plugins/asset_inventory/public/components/asset_filter_select_box.tsx new file mode 100644 index 0000000000000..12b7c25f766af --- /dev/null +++ b/x-pack/plugins/asset_inventory/public/components/asset_filter_select_box.tsx @@ -0,0 +1,48 @@ +/* + * 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 { EuiFormRow, EuiSelect, EuiSelectOption, useGeneratedHtmlId } from '@elastic/eui'; +import React from 'react'; +import { AssetFilters } from '../../common/types_api'; +import { useAssetFilters } from '../hooks/asset_filters'; + +interface FilterSelectBoxOptions { + options: Array; + label?: string; + filterKey: keyof AssetFilters; + value: string | undefined; +} + +export function AssetFilterSelectBox({ + value = '', + options, + label, + filterKey, +}: FilterSelectBoxOptions) { + const basicSelectId = useGeneratedHtmlId({ prefix: 'asset-filter-select_' }); + const { setFilters } = useAssetFilters(); + + const euiSelectOptions = options.map((option) => { + if (typeof option === 'string') { + return { value: option, text: option }; + } else { + return option; + } + }); + + return ( + + { + setFilters((filters) => ({ ...filters, [filterKey]: e.target.value })); + }} + /> + + ); +} diff --git a/x-pack/plugins/asset_inventory/public/components/assets_table.tsx b/x-pack/plugins/asset_inventory/public/components/assets_table.tsx index 77555687039d3..d81c0f3cf3ee5 100644 --- a/x-pack/plugins/asset_inventory/public/components/assets_table.tsx +++ b/x-pack/plugins/asset_inventory/public/components/assets_table.tsx @@ -9,105 +9,57 @@ import React from 'react'; import { EuiBasicTable } from '@elastic/eui'; import { Asset } from '../../common/types_api'; -/* -Example user object: - -{ - id: '1', - firstName: 'john', - lastName: 'doe', - github: 'johndoe', - dateOfBirth: Date.now(), - nationality: 'NL', - online: true -} - -Example country object: - -{ - code: 'NL', - name: 'Netherlands', - flag: '🇳🇱' -} -*/ - interface AssetsTableProps { assets: Asset[]; } -export function AssetsTable({ assets }: AssetsTableProps) { - const columns = [ - { - field: '@timestamp', - name: 'Timestamp', - render: (date: Asset['@timestamp']) => { - const d = new Date(date); - return d.toLocaleString(); - }, - width: '200px', - }, - { - field: 'asset.ean', - name: 'EAN (Elastic Asset Name)', - sortable: true, - width: '300px', +const columns = [ + { + field: '@timestamp', + name: 'Timestamp', + render: (date: Asset['@timestamp']) => { + const d = new Date(date); + return d.toLocaleString(); }, - { - field: 'asset.kind', - name: 'Asset Kind', - sortable: true, - }, - { - field: 'asset.type', - name: 'Asset Type', - sortable: true, - }, - { - field: 'asset.id', - name: 'Asset Original ID', - sortable: true, - }, - { - field: 'asset.parents', - name: '# of parents', - render: (parents: Asset['asset.parents']) => parents?.length || 0, - width: '100px', - }, - { - field: 'asset.children', - name: '# of children', - render: (children: Asset['asset.children']) => children?.length || 0, - width: '100px', - }, - ]; - - // const getRowProps = (item) => { - // const { id } = item; - // return { - // 'data-test-subj': `row-${id}`, - // className: 'customRowClass', - // onClick: () => {}, - // }; - // }; - - // const getCellProps = (item, column) => { - // const { id } = item; - // const { field } = column; - // return { - // className: 'customCellClass', - // 'data-test-subj': `cell-${id}-${field}`, - // textOnly: true, - // }; - // }; + width: '200px', + }, + { + field: 'asset.ean', + name: 'EAN (Elastic Asset Name)', + sortable: true, + width: '300px', + }, + { + field: 'asset.kind', + name: 'Asset Kind', + sortable: true, + }, + { + field: 'asset.type', + name: 'Asset Type', + sortable: true, + }, + { + field: 'asset.id', + name: 'Asset Original ID', + sortable: true, + }, + { + field: 'asset.parents', + name: '# of parents', + render: (parents: Asset['asset.parents']) => parents?.length || 0, + width: '100px', + }, + { + field: 'asset.children', + name: '# of children', + render: (children: Asset['asset.children']) => children?.length || 0, + width: '100px', + }, +]; +export function AssetsTable({ assets }: AssetsTableProps) { return ( - - tableCaption="Asset Inventory Demo" - items={assets} - rowHeader="firstName" - columns={columns} - // rowProps={getRowProps} - // cellProps={getCellProps} - /> + tableCaption="Asset Inventory Demo" items={assets} columns={columns} /> ); } diff --git a/x-pack/plugins/asset_inventory/public/lib/page_template.tsx b/x-pack/plugins/asset_inventory/public/components/page_template.tsx similarity index 100% rename from x-pack/plugins/asset_inventory/public/lib/page_template.tsx rename to x-pack/plugins/asset_inventory/public/components/page_template.tsx diff --git a/x-pack/plugins/asset_inventory/public/hooks/asset_filters.tsx b/x-pack/plugins/asset_inventory/public/hooks/asset_filters.tsx new file mode 100644 index 0000000000000..ffb14368e5875 --- /dev/null +++ b/x-pack/plugins/asset_inventory/public/hooks/asset_filters.tsx @@ -0,0 +1,57 @@ +/* + * 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 React, { useContext, useState, useEffect } from 'react'; +import { AssetFilters } from '../../common/types_api'; +import { convertAssetFiltersToQueryString } from '../lib/convert_asset_filters_to_query_string'; + +export type FilterSetter = React.Dispatch>; + +export interface AssetFilterContextValue { + filters: AssetFilters; + setFilters: FilterSetter; + filtersQS: string; +} + +const AssetFilterContext = React.createContext({ + filters: {}, + setFilters: () => null, + filtersQS: '', +}); + +const AssetFilterContextConsumer = AssetFilterContext.Consumer; + +const AssetFilterContextProvider: React.FC<{}> = ({ children }) => { + const [filters, setFilters] = useState({}); + const [filtersQS, setFiltersQS] = useState(''); + + useEffect(() => { + setFiltersQS(convertAssetFiltersToQueryString(filters)); + }, [filters]); + + return ( + + {children} + + ); +}; + +const useAssetFilters = () => { + return useContext(AssetFilterContext); +}; + +export { + AssetFilterContext, + AssetFilterContextProvider, + AssetFilterContextConsumer, + useAssetFilters, +}; diff --git a/x-pack/plugins/asset_inventory/public/lib/convert_asset_filters_to_query_string.ts b/x-pack/plugins/asset_inventory/public/lib/convert_asset_filters_to_query_string.ts new file mode 100644 index 0000000000000..cb8089a8ad9eb --- /dev/null +++ b/x-pack/plugins/asset_inventory/public/lib/convert_asset_filters_to_query_string.ts @@ -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. + */ + +import { AssetFilters } from '../../common/types_api'; + +export function convertAssetFiltersToQueryString(filters: AssetFilters) { + const keys = Object.keys(filters) as unknown as Array; + const definedKeys = keys.filter((k) => typeof filters[k] !== 'undefined'); + return definedKeys.map((k) => `${k}=${filters[k]}`).join('&'); +} diff --git a/x-pack/plugins/asset_inventory/public/pages/asset_inventory_list_page.tsx b/x-pack/plugins/asset_inventory/public/pages/asset_inventory_list_page.tsx index 2f1a34cbc3dec..c6d0c394adcdf 100644 --- a/x-pack/plugins/asset_inventory/public/pages/asset_inventory_list_page.tsx +++ b/x-pack/plugins/asset_inventory/public/pages/asset_inventory_list_page.tsx @@ -6,29 +6,36 @@ */ import React, { useEffect, useState } from 'react'; -import { EuiPageTemplate } from '@elastic/eui'; +import { EuiPageTemplate, EuiSpacer } from '@elastic/eui'; import axios from 'axios'; -import { PageTemplate } from '../lib/page_template'; +import { PageTemplate } from '../components/page_template'; import { Asset } from '../../common/types_api'; import { AssetsTable } from '../components/assets_table'; +import { AssetFilterControls } from '../components/asset_filter_controls'; +import { useAssetFilters } from '../hooks/asset_filters'; export function AssetInventoryListPage() { const [assets, setAssets] = useState([]); + const { filtersQS } = useAssetFilters(); useEffect(() => { + // console.log('Filters changed, new qs:', filtersQS); + async function retrieve() { - const response = await axios.get('/local/api/asset-inventory'); + const response = await axios.get(`/local/api/asset-inventory?${filtersQS}`); if (response.data && response.data.assets) { setAssets(response.data.assets); } } retrieve(); - }, []); + }, [filtersQS]); return ( + + diff --git a/x-pack/plugins/asset_inventory/server/lib/get_assets.ts b/x-pack/plugins/asset_inventory/server/lib/get_assets.ts index bf6bf1eb43b16..9b8051e66efe6 100644 --- a/x-pack/plugins/asset_inventory/server/lib/get_assets.ts +++ b/x-pack/plugins/asset_inventory/server/lib/get_assets.ts @@ -5,15 +5,89 @@ * 2.0. */ -import { SearchRequest } from '@elastic/elasticsearch/lib/api/types'; -import { Asset } from '../../common/types_api'; +import { QueryDslQueryContainer, SearchRequest } from '@elastic/elasticsearch/lib/api/types'; +import { Asset, AssetFilters } from '../../common/types_api'; import { esClient } from './es_client'; -export async function getAssets(): Promise { - const query: SearchRequest = { +interface GetAssetsOptions { + filters?: AssetFilters; +} + +export async function getAssets({ filters = {} }: GetAssetsOptions = {}): Promise { + const dsl: SearchRequest = { index: 'assets', }; - const response = await esClient.search<{}>(query); + if (filters && Object.keys(filters).length > 0) { + const musts: QueryDslQueryContainer[] = []; + + if (typeof filters.collectionVersion === 'number') { + musts.push({ + term: { + ['asset.collection_version']: filters.collectionVersion, + }, + }); + } + + if (filters.type) { + musts.push({ + term: { + ['asset.type.keyword']: filters.type, + }, + }); + } + + if (filters.kind) { + musts.push({ + term: { + ['asset.kind.keyword']: filters.kind, + }, + }); + } + + if (filters.ean) { + musts.push({ + term: { + ['asset.ean.keyword']: filters.ean, + }, + }); + } + + if (filters.id) { + musts.push({ + term: { + ['asset.id.keyword']: filters.id, + }, + }); + } + + if (filters.typeLike) { + musts.push({ + wildcard: { + ['asset.type.keyword']: filters.typeLike, + }, + }); + } + + if (filters.eanLike) { + musts.push({ + wildcard: { + ['asset.ean.keyword']: filters.eanLike, + }, + }); + } + + if (musts.length > 0) { + dsl.query = { + bool: { + must: musts, + }, + }; + } + } + + // console.log('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); } diff --git a/x-pack/plugins/asset_inventory/server/lib/get_latest_collection_version.ts b/x-pack/plugins/asset_inventory/server/lib/get_latest_collection_version.ts new file mode 100644 index 0000000000000..52a3cf73371a8 --- /dev/null +++ b/x-pack/plugins/asset_inventory/server/lib/get_latest_collection_version.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 { SearchRequest } from '@elastic/elasticsearch/lib/api/types'; +import { esClient } from './es_client'; + +export async function getLatestCollectionVersion() { + const dsl: SearchRequest = { + index: 'assets', + size: 0, + aggregations: { + maxVersion: { + max: { + field: 'asset.collectionVersion', + }, + }, + }, + }; + + // console.log('Performing Latest Version Query', '\n\n', JSON.stringify(dsl, null, 2)); + + const response = await esClient.search(dsl); + return response.aggregations?.maxVersion.value || 0; +} diff --git a/x-pack/plugins/asset_inventory/server/lib/get_values_for_field.ts b/x-pack/plugins/asset_inventory/server/lib/get_values_for_field.ts new file mode 100644 index 0000000000000..26a693ef10202 --- /dev/null +++ b/x-pack/plugins/asset_inventory/server/lib/get_values_for_field.ts @@ -0,0 +1,59 @@ +/* + * 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 { SearchRequest } from '@elastic/elasticsearch/lib/api/types'; +import { Asset } from '../../common/types_api'; +import { esClient } from './es_client'; + +interface GetValuesForFieldOptions { + field: keyof Asset; + version?: number; + searchText?: string; +} + +interface AggBucket { + key: string; + doc_count: number; +} + +export async function getValuesForField({ + field, + version, + searchText, +}: GetValuesForFieldOptions): Promise { + const dsl: SearchRequest = { + index: 'assets', + size: 0, + aggs: { + field_values: { + terms: { + field, + include: searchText, + }, + }, + }, + }; + + if (version || version === 0) { + dsl.query = { + bool: { + must: [ + { + term: { + ['asset.collection_version']: version, + }, + }, + ], + }, + }; + } + + // console.log(`Performing Field Value Query for ${field}`, '\n\n', JSON.stringify(dsl, null, 2)); + + const response = await esClient.search<{}, { field_values: { buckets: AggBucket[] } }>(dsl); + return response.aggregations?.field_values.buckets || []; +} diff --git a/x-pack/plugins/asset_inventory/server/plugin.ts b/x-pack/plugins/asset_inventory/server/plugin.ts index dce68b6f21541..cdc20011c24c9 100644 --- a/x-pack/plugins/asset_inventory/server/plugin.ts +++ b/x-pack/plugins/asset_inventory/server/plugin.ts @@ -5,8 +5,11 @@ * 2.0. */ +import { schema } from '@kbn/config-schema'; import { Plugin, CoreSetup } from '@kbn/core/server'; +import { AssetFilters } from '../common/types_api'; import { getAssets } from './lib/get_assets'; +import { getValuesForField } from './lib/get_values_for_field'; export type AssetInventoryServerPluginSetup = ReturnType; @@ -27,16 +30,45 @@ export class AssetInventoryServerPlugin implements Plugin( { path: '/api/asset-inventory', - validate: false, + validate: { + query: schema.any({}), + }, }, async (context, req, res) => { + const filters = req.query || {}; + try { - const assets = await getAssets(); + const assets = await getAssets({ filters }); return res.ok({ body: { assets } }); } catch (error: unknown) { + // console.log('error looking up asset records', error); + return res.customError({ statusCode: 500 }); + } + } + ); + + router.get( + { + path: '/api/asset-inventory/field-values', + validate: { + query: schema.any({}), + }, + }, + async (context, req, res) => { + const { field, searchText, version } = req.query; + + try { + const results = await getValuesForField({ + field, + searchText, + version: Number(version), + }); + return res.ok({ body: { results } }); + } catch (error: unknown) { + // console.log('error looking up field values', error); return res.customError({ statusCode: 500 }); } } From a8aa3f3cf1d845328ac2990d521a965ded328a1d Mon Sep 17 00:00:00 2001 From: Jason Rhodes Date: Wed, 23 Nov 2022 11:58:15 -0500 Subject: [PATCH 3/5] K8s cluster info pages --- .../asset_inventory/common/types_api.ts | 22 ++++++ .../asset_inventory/public/app/routes.ts | 13 ++++ .../public/components/k8s_cluster_info.tsx | 47 +++++++++++++ .../public/components/k8s_clusters_table.tsx | 46 +++++++++++++ .../pages/asset_inventory_list_page.tsx | 18 ++++- .../public/pages/k8s/cluster_page.tsx | 63 +++++++++++++++++ .../public/pages/k8s/clusters_list_page.tsx | 52 ++++++++++++++ .../plugins/asset_inventory/public/plugin.ts | 4 +- .../asset_inventory/server/lib/get_assets.ts | 10 +++ .../server/lib/get_k8s_cluster.ts | 63 +++++++++++++++++ .../server/lib/get_k8s_clusters.ts | 61 +++++++++++++++++ .../server/lib/get_k8s_nodes.ts | 68 +++++++++++++++++++ .../plugins/asset_inventory/server/plugin.ts | 39 +++++++++++ 13 files changed, 502 insertions(+), 4 deletions(-) create mode 100644 x-pack/plugins/asset_inventory/public/components/k8s_cluster_info.tsx create mode 100644 x-pack/plugins/asset_inventory/public/components/k8s_clusters_table.tsx create mode 100644 x-pack/plugins/asset_inventory/public/pages/k8s/cluster_page.tsx create mode 100644 x-pack/plugins/asset_inventory/public/pages/k8s/clusters_list_page.tsx create mode 100644 x-pack/plugins/asset_inventory/server/lib/get_k8s_cluster.ts create mode 100644 x-pack/plugins/asset_inventory/server/lib/get_k8s_clusters.ts create mode 100644 x-pack/plugins/asset_inventory/server/lib/get_k8s_nodes.ts diff --git a/x-pack/plugins/asset_inventory/common/types_api.ts b/x-pack/plugins/asset_inventory/common/types_api.ts index 937c89e740708..3a31161a7df44 100644 --- a/x-pack/plugins/asset_inventory/common/types_api.ts +++ b/x-pack/plugins/asset_inventory/common/types_api.ts @@ -23,6 +23,28 @@ export interface Asset { orchestrator?: EcsOrchestratorFieldset; } +export interface K8sPod { + id: string; + name: string; + ean: string; + node?: string; +} + +export interface K8sNode { + id: string; + name: string; + ean: string; + pods?: K8sPod[]; + cluster?: string; +} + +export interface K8sCluster { + name: string; + nodes: K8sNode[]; + status: string; + version: string; +} + export interface EcsKubernetesFieldset { namespace?: string; pod?: { diff --git a/x-pack/plugins/asset_inventory/public/app/routes.ts b/x-pack/plugins/asset_inventory/public/app/routes.ts index 555a5e0d9b3fb..aad421fc2e4a4 100644 --- a/x-pack/plugins/asset_inventory/public/app/routes.ts +++ b/x-pack/plugins/asset_inventory/public/app/routes.ts @@ -7,10 +7,23 @@ import { RouteProps } from 'react-router-dom'; import { AssetInventoryListPage } from '../pages/asset_inventory_list_page'; +import { K8sClustersListPage } from '../pages/k8s/clusters_list_page'; +import { K8sClusterPage } from '../pages/k8s/cluster_page'; export const routes: RouteProps[] = [ { path: '/', + exact: true, component: AssetInventoryListPage, }, + { + path: '/k8s/clusters', + exact: true, + component: K8sClustersListPage, + }, + { + path: '/k8s/clusters/:name', + exact: true, + component: K8sClusterPage, + }, ]; diff --git a/x-pack/plugins/asset_inventory/public/components/k8s_cluster_info.tsx b/x-pack/plugins/asset_inventory/public/components/k8s_cluster_info.tsx new file mode 100644 index 0000000000000..b55eb37af62fb --- /dev/null +++ b/x-pack/plugins/asset_inventory/public/components/k8s_cluster_info.tsx @@ -0,0 +1,47 @@ +/* + * 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 { EuiDescriptionList, EuiPageTemplate } from '@elastic/eui'; +import React from 'react'; +import { Link } from 'react-router-dom'; +import { K8sCluster } from '../../common/types_api'; + +export function K8sClusterInfo({ cluster }: { cluster: K8sCluster }) { + const list = [ + { + title: 'Cluster name', + description: cluster.name, + }, + { + title: 'Status', + description: cluster.status, + }, + { + title: 'Version', + description: cluster.version, + }, + { + title: 'Nodes', + description: ( +
    + {cluster.nodes.map((node) => ( +
  • + {node.name} +
  • + ))} +
+ ), + }, + ]; + return ( + <> + + + + + ); +} diff --git a/x-pack/plugins/asset_inventory/public/components/k8s_clusters_table.tsx b/x-pack/plugins/asset_inventory/public/components/k8s_clusters_table.tsx new file mode 100644 index 0000000000000..92e30251ee2b3 --- /dev/null +++ b/x-pack/plugins/asset_inventory/public/components/k8s_clusters_table.tsx @@ -0,0 +1,46 @@ +/* + * 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 { EuiInMemoryTable } from '@elastic/eui'; +import React from 'react'; +import { Link } from 'react-router-dom'; +import { K8sCluster, K8sNode } from '../../common/types_api'; + +export function K8sClustersTable({ clusters }: { clusters: K8sCluster[] }) { + const columns = [ + { + field: 'name', + name: 'Cluster name', + sortable: true, + width: '400px', + render: (name: string) => { + return {name}; + }, + }, + { + field: 'status', + name: 'Status', + }, + { + field: 'name', + name: 'Provider', + render: () => <>GCP, + }, + { + field: 'version', + name: 'Version', + }, + { + field: 'nodes', + name: 'Nodes', + render: (nodes: K8sNode[]) => <>{nodes.length}, + }, + ]; + + // @ts-ignore + return ; +} diff --git a/x-pack/plugins/asset_inventory/public/pages/asset_inventory_list_page.tsx b/x-pack/plugins/asset_inventory/public/pages/asset_inventory_list_page.tsx index c6d0c394adcdf..fc32bc30f59e3 100644 --- a/x-pack/plugins/asset_inventory/public/pages/asset_inventory_list_page.tsx +++ b/x-pack/plugins/asset_inventory/public/pages/asset_inventory_list_page.tsx @@ -6,8 +6,9 @@ */ import React, { useEffect, useState } from 'react'; -import { EuiPageTemplate, EuiSpacer } from '@elastic/eui'; +import { EuiButton, EuiPageTemplate, EuiSpacer } from '@elastic/eui'; import axios from 'axios'; +import { useHistory } from 'react-router-dom'; import { PageTemplate } from '../components/page_template'; import { Asset } from '../../common/types_api'; import { AssetsTable } from '../components/assets_table'; @@ -17,6 +18,7 @@ import { useAssetFilters } from '../hooks/asset_filters'; export function AssetInventoryListPage() { const [assets, setAssets] = useState([]); const { filtersQS } = useAssetFilters(); + const history = useHistory(); useEffect(() => { // console.log('Filters changed, new qs:', filtersQS); @@ -32,7 +34,19 @@ export function AssetInventoryListPage() { return ( - + ) => { + e.preventDefault(); + history.push('/k8s/clusters'); + }} + > + K8s Clusters + , + ]} + /> diff --git a/x-pack/plugins/asset_inventory/public/pages/k8s/cluster_page.tsx b/x-pack/plugins/asset_inventory/public/pages/k8s/cluster_page.tsx new file mode 100644 index 0000000000000..72ac767b3fb35 --- /dev/null +++ b/x-pack/plugins/asset_inventory/public/pages/k8s/cluster_page.tsx @@ -0,0 +1,63 @@ +/* + * 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 React, { useEffect, useState } from 'react'; +import { EuiIcon, EuiPageTemplate } from '@elastic/eui'; +import axios from 'axios'; +import { useHistory, useParams } from 'react-router-dom'; +import { K8sCluster } from '../../../common/types_api'; +import { PageTemplate } from '../../components/page_template'; +import { K8sClusterInfo } from '../../components/k8s_cluster_info'; + +export function K8sClusterPage() { + const [cluster, setCluster] = useState(null); + const { name } = useParams<{ name: string }>(); + const history = useHistory(); + + useEffect(() => { + async function retrieve() { + const response = await axios.get( + `/local/api/asset-inventory/k8s/clusters/${name}` + ); + if (response.data && response.data?.result) { + setCluster(response.data.result); + } + } + retrieve(); + }, [name]); + + if (cluster === null) { + return null; + } + + return ( + + + Return to cluster list + + ), + color: 'primary', + 'aria-current': false, + href: '#', + onClick: (e) => { + e.preventDefault(); + history.push('/k8s/clusters'); + }, + }, + ]} + /> + + + + + ); +} diff --git a/x-pack/plugins/asset_inventory/public/pages/k8s/clusters_list_page.tsx b/x-pack/plugins/asset_inventory/public/pages/k8s/clusters_list_page.tsx new file mode 100644 index 0000000000000..0d8d2751cf7a9 --- /dev/null +++ b/x-pack/plugins/asset_inventory/public/pages/k8s/clusters_list_page.tsx @@ -0,0 +1,52 @@ +/* + * 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 React, { useEffect, useState } from 'react'; +import { EuiButton, EuiPageTemplate } from '@elastic/eui'; +import axios from 'axios'; +import { useHistory } from 'react-router-dom'; +import { K8sCluster } from '../../../common/types_api'; +import { PageTemplate } from '../../components/page_template'; +import { K8sClustersTable } from '../../components/k8s_clusters_table'; + +export function K8sClustersListPage() { + const [clusters, setClusters] = useState([]); + const history = useHistory(); + + useEffect(() => { + async function retrieve() { + const response = await axios.get( + `/local/api/asset-inventory/k8s/clusters` + ); + if (response.data && response.data?.results) { + setClusters(response.data.results); + } + } + retrieve(); + }, []); + + return ( + + ) => { + e.preventDefault(); + history.push('/'); + }} + > + Asset Inventory + , + ]} + /> + + + + + ); +} diff --git a/x-pack/plugins/asset_inventory/public/plugin.ts b/x-pack/plugins/asset_inventory/public/plugin.ts index e4e2df757289f..96d10585a176a 100644 --- a/x-pack/plugins/asset_inventory/public/plugin.ts +++ b/x-pack/plugins/asset_inventory/public/plugin.ts @@ -83,8 +83,8 @@ export class Plugin id: 'assetInventory', mount, order: 8000, - title: i18n.translate('xpack.observability.overviewLinkTitle', { - defaultMessage: 'Overview', + title: i18n.translate('xpack.assetInventory.overviewTitle', { + defaultMessage: 'Asset Inventory', }), updater$: appUpdater$, keywords: [ diff --git a/x-pack/plugins/asset_inventory/server/lib/get_assets.ts b/x-pack/plugins/asset_inventory/server/lib/get_assets.ts index 9b8051e66efe6..a0ea8499f988d 100644 --- a/x-pack/plugins/asset_inventory/server/lib/get_assets.ts +++ b/x-pack/plugins/asset_inventory/server/lib/get_assets.ts @@ -84,6 +84,16 @@ export async function getAssets({ filters = {} }: GetAssetsOptions = {}): Promis }, }; } + + dsl.collapse = { + field: 'asset.ean.keyword', + }; + + dsl.sort = { + '@timestamp': { + order: 'desc', + }, + }; } // console.log('Performing Asset Query', '\n\n', JSON.stringify(dsl, null, 2)); diff --git a/x-pack/plugins/asset_inventory/server/lib/get_k8s_cluster.ts b/x-pack/plugins/asset_inventory/server/lib/get_k8s_cluster.ts new file mode 100644 index 0000000000000..50d66371229db --- /dev/null +++ b/x-pack/plugins/asset_inventory/server/lib/get_k8s_cluster.ts @@ -0,0 +1,63 @@ +/* + * 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 { SearchRequest } from '@elastic/elasticsearch/lib/api/types'; +import { EcsOrchestratorFieldset, K8sCluster } from '../../common/types_api'; +import { esClient } from './es_client'; +import { getK8sNodes } from './get_k8s_nodes'; + +export async function getK8sCluster(name: string): Promise { + const dsl: SearchRequest = { + index: 'assets', + query: { + bool: { + must: [ + { + term: { + ['asset.name.keyword']: name, + }, + }, + { + term: { + ['asset.type.keyword']: 'k8s.cluster', + }, + }, + ], + }, + }, + collapse: { + field: 'asset.ean.keyword', + }, + sort: { + '@timestamp': { + order: 'desc', + }, + }, + }; + + // console.log('Performing K8s Clusters Query', '\n\n', JSON.stringify(dsl, null, 2)); + + const response = await esClient.search<{ + 'asset.name': string; + 'asset.ean': string; + 'asset.id': string; + orchestrator: EcsOrchestratorFieldset; + }>(dsl); + + const { _source: cluster } = response.hits.hits[0]; + if (!cluster) { + throw new Error('No cluster returned'); + } + const nodes = await getK8sNodes({ clusterEan: cluster['asset.ean'] }); + + return { + name: cluster['asset.name'], + nodes, + status: 'Healthy', + version: cluster.orchestrator?.cluster?.version || 'unspecified', + }; +} diff --git a/x-pack/plugins/asset_inventory/server/lib/get_k8s_clusters.ts b/x-pack/plugins/asset_inventory/server/lib/get_k8s_clusters.ts new file mode 100644 index 0000000000000..01370df0e02bd --- /dev/null +++ b/x-pack/plugins/asset_inventory/server/lib/get_k8s_clusters.ts @@ -0,0 +1,61 @@ +/* + * 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 { SearchRequest } from '@elastic/elasticsearch/lib/api/types'; +import { EcsOrchestratorFieldset, K8sCluster } from '../../common/types_api'; +import { esClient } from './es_client'; +import { getK8sNodes } from './get_k8s_nodes'; + +export async function getK8sClusters(): Promise { + const dsl: SearchRequest = { + index: 'assets', + query: { + bool: { + must: [ + { + term: { + ['asset.type.keyword']: 'k8s.cluster', + }, + }, + ], + }, + }, + collapse: { + field: 'asset.ean.keyword', + }, + sort: { + '@timestamp': { + order: 'desc', + }, + }, + }; + + // console.log('Performing K8s Clusters Query', '\n\n', JSON.stringify(dsl, null, 2)); + + const response = await esClient.search<{ + 'asset.name': string; + 'asset.ean': string; + 'asset.id': string; + orchestrator: EcsOrchestratorFieldset; + }>(dsl); + + const results = await Promise.all( + response.hits.hits.map(async (hit) => { + if (!hit._source) { + throw new Error('Missing _source in cluster result'); + } + return { + name: hit._source['asset.name'], + nodes: await getK8sNodes({ clusterEan: hit._source['asset.ean'] }), + status: 'Healthy', + version: hit._source?.orchestrator?.cluster?.version || 'Unspecified', + }; + }) + ); + + return results; +} diff --git a/x-pack/plugins/asset_inventory/server/lib/get_k8s_nodes.ts b/x-pack/plugins/asset_inventory/server/lib/get_k8s_nodes.ts new file mode 100644 index 0000000000000..c7832e31425db --- /dev/null +++ b/x-pack/plugins/asset_inventory/server/lib/get_k8s_nodes.ts @@ -0,0 +1,68 @@ +/* + * 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 { SearchRequest } from '@elastic/elasticsearch/lib/api/types'; +import { K8sNode } from '../../common/types_api'; +import { esClient } from './es_client'; + +interface GetK8sNodesOptions { + clusterEan?: string; +} + +export async function getK8sNodes({ clusterEan }: GetK8sNodesOptions = {}): Promise { + const dsl: SearchRequest = { + index: 'assets', + query: { + bool: { + must: [ + { + term: { + ['asset.type.keyword']: 'k8s.node', + }, + }, + { + term: { + 'asset.parents.keyword': clusterEan, + }, + }, + ], + }, + }, + collapse: { + field: 'asset.ean.keyword', + }, + sort: { + '@timestamp': { + order: 'desc', + }, + }, + }; + + // console.log('Performing K8s Nodes Query', '\n\n', JSON.stringify(dsl, null, 2)); + + const response = await esClient.search<{ + 'asset.id': string; + 'asset.name': string; + 'asset.ean': string; + }>(dsl); + + const results = await Promise.all( + response.hits.hits.map(async (hit) => { + if (!hit._source) { + throw new Error('Missing _source in node result'); + } + const s = hit._source; + return { + id: s['asset.id'], + name: s['asset.name'], + ean: s['asset.ean'], + }; + }) + ); + + return results; +} diff --git a/x-pack/plugins/asset_inventory/server/plugin.ts b/x-pack/plugins/asset_inventory/server/plugin.ts index cdc20011c24c9..589272d074eb1 100644 --- a/x-pack/plugins/asset_inventory/server/plugin.ts +++ b/x-pack/plugins/asset_inventory/server/plugin.ts @@ -9,6 +9,8 @@ import { schema } from '@kbn/config-schema'; import { Plugin, CoreSetup } from '@kbn/core/server'; import { AssetFilters } from '../common/types_api'; import { getAssets } from './lib/get_assets'; +import { getK8sCluster } from './lib/get_k8s_cluster'; +import { getK8sClusters } from './lib/get_k8s_clusters'; import { getValuesForField } from './lib/get_values_for_field'; export type AssetInventoryServerPluginSetup = ReturnType; @@ -74,6 +76,43 @@ export class AssetInventoryServerPlugin implements Plugin { + try { + const results = await getK8sClusters(); + return res.ok({ body: { results } }); + } catch (error: unknown) { + // console.log('error looking up field values', error); + return res.customError({ statusCode: 500 }); + } + } + ); + + router.get<{ name: string }, {}, {}>( + { + path: '/api/asset-inventory/k8s/clusters/{name}', + validate: { + params: schema.object({ + name: schema.string(), + }), + }, + }, + async (context, req, res) => { + const name = req.params.name; + try { + const result = await getK8sCluster(name); + return res.ok({ body: { result } }); + } catch (error: unknown) { + // console.log('error looking up field values', error); + return res.customError({ statusCode: 500 }); + } + } + ); + return {}; } From df19112336e6eb4a2b7a48ada68a83451c8100a4 Mon Sep 17 00:00:00 2001 From: Jason Rhodes Date: Thu, 1 Dec 2022 18:15:53 -0500 Subject: [PATCH 4/5] Clusters page works for AWS GKE discovery assets --- tsconfig.base.json | 2 + .../asset_inventory/common/debug_log.ts | 14 +++++++ .../asset_inventory/common/types_api.ts | 29 ++++++++++++--- .../public/components/k8s_clusters_table.tsx | 37 +++++++++++++++++-- .../asset_inventory/server/constants.ts | 8 ++++ .../asset_inventory/server/lib/get_assets.ts | 6 ++- .../server/lib/get_k8s_cluster.ts | 6 ++- .../server/lib/get_k8s_clusters.ts | 26 ++++++------- .../server/lib/get_k8s_nodes.ts | 6 ++- .../lib/get_latest_collection_version.ts | 6 ++- .../server/lib/get_values_for_field.ts | 6 ++- .../plugins/asset_inventory/server/plugin.ts | 9 +++-- 12 files changed, 118 insertions(+), 37 deletions(-) create mode 100644 x-pack/plugins/asset_inventory/common/debug_log.ts create mode 100644 x-pack/plugins/asset_inventory/server/constants.ts diff --git a/tsconfig.base.json b/tsconfig.base.json index 787992f6e2133..aaa195b4a304d 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1058,6 +1058,8 @@ "@kbn/alerting-plugin/*": ["x-pack/plugins/alerting/*"], "@kbn/apm-plugin": ["x-pack/plugins/apm"], "@kbn/apm-plugin/*": ["x-pack/plugins/apm/*"], + "@kbn/asset-inventory-plugin": ["x-pack/plugins/asset_inventory"], + "@kbn/asset-inventory-plugin/*": ["x-pack/plugins/asset_inventory/*"], "@kbn/banners-plugin": ["x-pack/plugins/banners"], "@kbn/banners-plugin/*": ["x-pack/plugins/banners/*"], "@kbn/canvas-plugin": ["x-pack/plugins/canvas"], diff --git a/x-pack/plugins/asset_inventory/common/debug_log.ts b/x-pack/plugins/asset_inventory/common/debug_log.ts new file mode 100644 index 0000000000000..fd819c0d1ecd5 --- /dev/null +++ b/x-pack/plugins/asset_inventory/common/debug_log.ts @@ -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 function debug(...args: any[]) { + if (process.env.NODE_ENV === 'production') { + return; + } + // eslint-disable-next-line no-console + console.log('[DEBUG LOG]', ...args); +} diff --git a/x-pack/plugins/asset_inventory/common/types_api.ts b/x-pack/plugins/asset_inventory/common/types_api.ts index 3a31161a7df44..b86a7ce0e85d4 100644 --- a/x-pack/plugins/asset_inventory/common/types_api.ts +++ b/x-pack/plugins/asset_inventory/common/types_api.ts @@ -8,29 +8,36 @@ export type AssetKind = 'unknown' | 'node'; export type AssetType = 'k8s.pod' | 'k8s.cluster' | 'k8s.node'; -export interface Asset { +export interface ECSDocument { '@timestamp': string; + kubernetes?: EcsKubernetesFieldset; + orchestrator?: EcsOrchestratorFieldset; + cloud?: EcsCloudFieldset; +} + +export type AssetStatus = 'CREATING' | 'ACTIVE' | 'DELETING' | 'FAILED' | 'UPDATING' | 'PENDING'; + +export interface Asset extends ECSDocument { 'asset.collection_version'?: string; 'asset.ean': string; 'asset.id': string; 'asset.kind': AssetKind; 'asset.name'?: string; 'asset.type': AssetType; + 'asset.status'?: AssetStatus; 'asset.parents'?: string | string[]; 'asset.children'?: string | string[]; 'asset.namespace'?: string; - kubernetes?: EcsKubernetesFieldset; - orchestrator?: EcsOrchestratorFieldset; } -export interface K8sPod { +export interface K8sPod extends ECSDocument { id: string; name: string; ean: string; node?: string; } -export interface K8sNode { +export interface K8sNode extends ECSDocument { id: string; name: string; ean: string; @@ -38,7 +45,7 @@ export interface K8sNode { cluster?: string; } -export interface K8sCluster { +export interface K8sCluster extends ECSDocument { name: string; nodes: K8sNode[]; status: string; @@ -81,6 +88,16 @@ export interface EcsOrchestratorFieldset { }; } +export type CloudProviderName = 'aws' | 'gcp' | 'azure' | 'other' | 'unknown' | 'none'; + +export interface EcsCloudFieldset { + provider: CloudProviderName; + region?: string; + service?: { + name?: string; + }; +} + export interface AssetFilters { type?: AssetType; kind?: AssetKind; diff --git a/x-pack/plugins/asset_inventory/public/components/k8s_clusters_table.tsx b/x-pack/plugins/asset_inventory/public/components/k8s_clusters_table.tsx index 92e30251ee2b3..b53dc920a2d0e 100644 --- a/x-pack/plugins/asset_inventory/public/components/k8s_clusters_table.tsx +++ b/x-pack/plugins/asset_inventory/public/components/k8s_clusters_table.tsx @@ -5,10 +5,29 @@ * 2.0. */ -import { EuiInMemoryTable } from '@elastic/eui'; +import { EuiHealth, EuiIcon, EuiInMemoryTable } from '@elastic/eui'; +import { capitalize } from 'lodash'; import React from 'react'; import { Link } from 'react-router-dom'; -import { K8sCluster, K8sNode } from '../../common/types_api'; +import { AssetStatus, CloudProviderName, K8sCluster, K8sNode } from '../../common/types_api'; + +const cloudIconMap: Record = { + gcp: 'logoGCP', + aws: 'logoAWS', + azure: 'logoAzure', + other: 'questionInCircle', + unknown: 'questionInCircle', + none: 'crossInACircleFilled', +}; + +const statusMap: Record = { + ACTIVE: 'success', + CREATING: 'subdued', + DELETING: 'subdued', + FAILED: 'danger', + UPDATING: 'subdued', + PENDING: 'warning', +}; export function K8sClustersTable({ clusters }: { clusters: K8sCluster[] }) { const columns = [ @@ -24,11 +43,21 @@ export function K8sClustersTable({ clusters }: { clusters: K8sCluster[] }) { { field: 'status', name: 'Status', + render: (status: AssetStatus) => ( + {capitalize(status)} + ), }, { - field: 'name', + field: 'cloud', name: 'Provider', - render: () => <>GCP, + render: (cloud: K8sCluster['cloud']) => ( + + ), + }, + { + field: 'cloud', + name: 'Region', + render: (cloud: K8sCluster['cloud']) => cloud?.region || 'unknown', }, { field: 'version', diff --git a/x-pack/plugins/asset_inventory/server/constants.ts b/x-pack/plugins/asset_inventory/server/constants.ts new file mode 100644 index 0000000000000..a16b78045a8d8 --- /dev/null +++ b/x-pack/plugins/asset_inventory/server/constants.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const ASSETS_INDEX = 'assets*'; diff --git a/x-pack/plugins/asset_inventory/server/lib/get_assets.ts b/x-pack/plugins/asset_inventory/server/lib/get_assets.ts index a0ea8499f988d..a538ebd4bbfbc 100644 --- a/x-pack/plugins/asset_inventory/server/lib/get_assets.ts +++ b/x-pack/plugins/asset_inventory/server/lib/get_assets.ts @@ -6,7 +6,9 @@ */ import { QueryDslQueryContainer, SearchRequest } from '@elastic/elasticsearch/lib/api/types'; +import { debug } from '../../common/debug_log'; import { Asset, AssetFilters } from '../../common/types_api'; +import { ASSETS_INDEX } from '../constants'; import { esClient } from './es_client'; interface GetAssetsOptions { @@ -15,7 +17,7 @@ interface GetAssetsOptions { export async function getAssets({ filters = {} }: GetAssetsOptions = {}): Promise { const dsl: SearchRequest = { - index: 'assets', + index: ASSETS_INDEX, }; if (filters && Object.keys(filters).length > 0) { @@ -96,7 +98,7 @@ export async function getAssets({ filters = {} }: GetAssetsOptions = {}): Promis }; } - // console.log('Performing Asset Query', '\n\n', JSON.stringify(dsl, null, 2)); + 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); diff --git a/x-pack/plugins/asset_inventory/server/lib/get_k8s_cluster.ts b/x-pack/plugins/asset_inventory/server/lib/get_k8s_cluster.ts index 50d66371229db..31d3ba3f28a9c 100644 --- a/x-pack/plugins/asset_inventory/server/lib/get_k8s_cluster.ts +++ b/x-pack/plugins/asset_inventory/server/lib/get_k8s_cluster.ts @@ -6,13 +6,15 @@ */ import { SearchRequest } from '@elastic/elasticsearch/lib/api/types'; +import { debug } from '../../common/debug_log'; import { EcsOrchestratorFieldset, K8sCluster } from '../../common/types_api'; +import { ASSETS_INDEX } from '../constants'; import { esClient } from './es_client'; import { getK8sNodes } from './get_k8s_nodes'; export async function getK8sCluster(name: string): Promise { const dsl: SearchRequest = { - index: 'assets', + index: ASSETS_INDEX, query: { bool: { must: [ @@ -39,7 +41,7 @@ export async function getK8sCluster(name: string): Promise { }, }; - // console.log('Performing K8s Clusters Query', '\n\n', JSON.stringify(dsl, null, 2)); + debug('Performing K8s Clusters Query', '\n\n', JSON.stringify(dsl, null, 2)); const response = await esClient.search<{ 'asset.name': string; diff --git a/x-pack/plugins/asset_inventory/server/lib/get_k8s_clusters.ts b/x-pack/plugins/asset_inventory/server/lib/get_k8s_clusters.ts index 01370df0e02bd..f907a185e7461 100644 --- a/x-pack/plugins/asset_inventory/server/lib/get_k8s_clusters.ts +++ b/x-pack/plugins/asset_inventory/server/lib/get_k8s_clusters.ts @@ -6,13 +6,15 @@ */ import { SearchRequest } from '@elastic/elasticsearch/lib/api/types'; -import { EcsOrchestratorFieldset, K8sCluster } from '../../common/types_api'; +import { debug } from '../../common/debug_log'; +import { Asset, K8sCluster } from '../../common/types_api'; +import { ASSETS_INDEX } from '../constants'; import { esClient } from './es_client'; import { getK8sNodes } from './get_k8s_nodes'; export async function getK8sClusters(): Promise { const dsl: SearchRequest = { - index: 'assets', + index: ASSETS_INDEX, query: { bool: { must: [ @@ -34,25 +36,23 @@ export async function getK8sClusters(): Promise { }, }; - // console.log('Performing K8s Clusters Query', '\n\n', JSON.stringify(dsl, null, 2)); + debug('Performing K8s Clusters Query', '\n\n', JSON.stringify(dsl, null, 2)); - const response = await esClient.search<{ - 'asset.name': string; - 'asset.ean': string; - 'asset.id': string; - orchestrator: EcsOrchestratorFieldset; - }>(dsl); + const response = await esClient.search(dsl); const results = await Promise.all( response.hits.hits.map(async (hit) => { if (!hit._source) { throw new Error('Missing _source in cluster result'); } + const doc = hit._source; return { - name: hit._source['asset.name'], - nodes: await getK8sNodes({ clusterEan: hit._source['asset.ean'] }), - status: 'Healthy', - version: hit._source?.orchestrator?.cluster?.version || 'Unspecified', + '@timestamp': doc['@timestamp'], + name: doc['asset.name'] || doc['asset.id'], + nodes: await getK8sNodes({ clusterEan: doc['asset.ean'] }), + status: doc['asset.status'] || 'UNKNOWN', + cloud: doc.cloud, + version: doc.orchestrator?.cluster?.version || 'Unspecified', }; }) ); diff --git a/x-pack/plugins/asset_inventory/server/lib/get_k8s_nodes.ts b/x-pack/plugins/asset_inventory/server/lib/get_k8s_nodes.ts index c7832e31425db..1c867a777c35a 100644 --- a/x-pack/plugins/asset_inventory/server/lib/get_k8s_nodes.ts +++ b/x-pack/plugins/asset_inventory/server/lib/get_k8s_nodes.ts @@ -6,7 +6,9 @@ */ import { SearchRequest } from '@elastic/elasticsearch/lib/api/types'; +import { debug } from '../../common/debug_log'; import { K8sNode } from '../../common/types_api'; +import { ASSETS_INDEX } from '../constants'; import { esClient } from './es_client'; interface GetK8sNodesOptions { @@ -15,7 +17,7 @@ interface GetK8sNodesOptions { export async function getK8sNodes({ clusterEan }: GetK8sNodesOptions = {}): Promise { const dsl: SearchRequest = { - index: 'assets', + index: ASSETS_INDEX, query: { bool: { must: [ @@ -42,7 +44,7 @@ export async function getK8sNodes({ clusterEan }: GetK8sNodesOptions = {}): Prom }, }; - // console.log('Performing K8s Nodes Query', '\n\n', JSON.stringify(dsl, null, 2)); + debug('Performing K8s Nodes Query', '\n\n', JSON.stringify(dsl, null, 2)); const response = await esClient.search<{ 'asset.id': string; diff --git a/x-pack/plugins/asset_inventory/server/lib/get_latest_collection_version.ts b/x-pack/plugins/asset_inventory/server/lib/get_latest_collection_version.ts index 52a3cf73371a8..60a778dad78dd 100644 --- a/x-pack/plugins/asset_inventory/server/lib/get_latest_collection_version.ts +++ b/x-pack/plugins/asset_inventory/server/lib/get_latest_collection_version.ts @@ -5,11 +5,13 @@ * 2.0. */ import { SearchRequest } from '@elastic/elasticsearch/lib/api/types'; +import { debug } from '../../common/debug_log'; +import { ASSETS_INDEX } from '../constants'; import { esClient } from './es_client'; export async function getLatestCollectionVersion() { const dsl: SearchRequest = { - index: 'assets', + index: ASSETS_INDEX, size: 0, aggregations: { maxVersion: { @@ -20,7 +22,7 @@ export async function getLatestCollectionVersion() { }, }; - // console.log('Performing Latest Version Query', '\n\n', JSON.stringify(dsl, null, 2)); + debug('Performing Latest Version Query', '\n\n', JSON.stringify(dsl, null, 2)); const response = await esClient.search(dsl); return response.aggregations?.maxVersion.value || 0; diff --git a/x-pack/plugins/asset_inventory/server/lib/get_values_for_field.ts b/x-pack/plugins/asset_inventory/server/lib/get_values_for_field.ts index 26a693ef10202..c8a487a1ee517 100644 --- a/x-pack/plugins/asset_inventory/server/lib/get_values_for_field.ts +++ b/x-pack/plugins/asset_inventory/server/lib/get_values_for_field.ts @@ -6,7 +6,9 @@ */ import { SearchRequest } from '@elastic/elasticsearch/lib/api/types'; +import { debug } from '../../common/debug_log'; import { Asset } from '../../common/types_api'; +import { ASSETS_INDEX } from '../constants'; import { esClient } from './es_client'; interface GetValuesForFieldOptions { @@ -26,7 +28,7 @@ export async function getValuesForField({ searchText, }: GetValuesForFieldOptions): Promise { const dsl: SearchRequest = { - index: 'assets', + index: ASSETS_INDEX, size: 0, aggs: { field_values: { @@ -52,7 +54,7 @@ export async function getValuesForField({ }; } - // console.log(`Performing Field Value Query for ${field}`, '\n\n', JSON.stringify(dsl, null, 2)); + debug(`Performing Field Value Query for ${field}`, '\n\n', JSON.stringify(dsl, null, 2)); const response = await esClient.search<{}, { field_values: { buckets: AggBucket[] } }>(dsl); return response.aggregations?.field_values.buckets || []; diff --git a/x-pack/plugins/asset_inventory/server/plugin.ts b/x-pack/plugins/asset_inventory/server/plugin.ts index 589272d074eb1..f393f4f99335a 100644 --- a/x-pack/plugins/asset_inventory/server/plugin.ts +++ b/x-pack/plugins/asset_inventory/server/plugin.ts @@ -7,6 +7,7 @@ import { schema } from '@kbn/config-schema'; import { Plugin, CoreSetup } from '@kbn/core/server'; +import { debug } from '../common/debug_log'; import { AssetFilters } from '../common/types_api'; import { getAssets } from './lib/get_assets'; import { getK8sCluster } from './lib/get_k8s_cluster'; @@ -46,7 +47,7 @@ export class AssetInventoryServerPlugin implements Plugin Date: Wed, 7 Dec 2022 18:16:33 -0500 Subject: [PATCH 5/5] Removed unnecessary .keyword distinctions in ES queries .keyword was introduced in the queries when the mappings were improperly configured. First attempt to change the mappings failed because it had subobjects: false in the top level of the dynamic assets mapping template. I moved subobjects: false into the properties -> asset block instead, so that ECS fields can be nested as usual, but asset fields will only work as dotted. --- .../asset_inventory/server/lib/get_assets.ts | 14 ++++++------- .../server/lib/get_k8s_cluster.ts | 8 ++++--- .../server/lib/get_k8s_clusters.ts | 4 ++-- .../server/lib/get_k8s_nodes.ts | 8 ++++--- x-pack/plugins/asset_inventory/tsconfig.json | 21 ++++++++++++------- 5 files changed, 33 insertions(+), 22 deletions(-) diff --git a/x-pack/plugins/asset_inventory/server/lib/get_assets.ts b/x-pack/plugins/asset_inventory/server/lib/get_assets.ts index a538ebd4bbfbc..8540865ea1afc 100644 --- a/x-pack/plugins/asset_inventory/server/lib/get_assets.ts +++ b/x-pack/plugins/asset_inventory/server/lib/get_assets.ts @@ -34,7 +34,7 @@ export async function getAssets({ filters = {} }: GetAssetsOptions = {}): Promis if (filters.type) { musts.push({ term: { - ['asset.type.keyword']: filters.type, + ['asset.type']: filters.type, }, }); } @@ -42,7 +42,7 @@ export async function getAssets({ filters = {} }: GetAssetsOptions = {}): Promis if (filters.kind) { musts.push({ term: { - ['asset.kind.keyword']: filters.kind, + ['asset.kind']: filters.kind, }, }); } @@ -50,7 +50,7 @@ export async function getAssets({ filters = {} }: GetAssetsOptions = {}): Promis if (filters.ean) { musts.push({ term: { - ['asset.ean.keyword']: filters.ean, + ['asset.ean']: filters.ean, }, }); } @@ -58,7 +58,7 @@ export async function getAssets({ filters = {} }: GetAssetsOptions = {}): Promis if (filters.id) { musts.push({ term: { - ['asset.id.keyword']: filters.id, + ['asset.id']: filters.id, }, }); } @@ -66,7 +66,7 @@ export async function getAssets({ filters = {} }: GetAssetsOptions = {}): Promis if (filters.typeLike) { musts.push({ wildcard: { - ['asset.type.keyword']: filters.typeLike, + ['asset.type']: filters.typeLike, }, }); } @@ -74,7 +74,7 @@ export async function getAssets({ filters = {} }: GetAssetsOptions = {}): Promis if (filters.eanLike) { musts.push({ wildcard: { - ['asset.ean.keyword']: filters.eanLike, + ['asset.ean']: filters.eanLike, }, }); } @@ -88,7 +88,7 @@ export async function getAssets({ filters = {} }: GetAssetsOptions = {}): Promis } dsl.collapse = { - field: 'asset.ean.keyword', + field: 'asset.ean', }; dsl.sort = { diff --git a/x-pack/plugins/asset_inventory/server/lib/get_k8s_cluster.ts b/x-pack/plugins/asset_inventory/server/lib/get_k8s_cluster.ts index 31d3ba3f28a9c..179c4b3fd3e63 100644 --- a/x-pack/plugins/asset_inventory/server/lib/get_k8s_cluster.ts +++ b/x-pack/plugins/asset_inventory/server/lib/get_k8s_cluster.ts @@ -20,19 +20,19 @@ export async function getK8sCluster(name: string): Promise { must: [ { term: { - ['asset.name.keyword']: name, + ['asset.name']: name, }, }, { term: { - ['asset.type.keyword']: 'k8s.cluster', + ['asset.type']: 'k8s.cluster', }, }, ], }, }, collapse: { - field: 'asset.ean.keyword', + field: 'asset.ean', }, sort: { '@timestamp': { @@ -44,6 +44,7 @@ export async function getK8sCluster(name: string): Promise { debug('Performing K8s Clusters Query', '\n\n', JSON.stringify(dsl, null, 2)); const response = await esClient.search<{ + '@timestamp': string; 'asset.name': string; 'asset.ean': string; 'asset.id': string; @@ -57,6 +58,7 @@ export async function getK8sCluster(name: string): Promise { const nodes = await getK8sNodes({ clusterEan: cluster['asset.ean'] }); return { + '@timestamp': cluster['@timestamp'], name: cluster['asset.name'], nodes, status: 'Healthy', diff --git a/x-pack/plugins/asset_inventory/server/lib/get_k8s_clusters.ts b/x-pack/plugins/asset_inventory/server/lib/get_k8s_clusters.ts index f907a185e7461..143cff526fe98 100644 --- a/x-pack/plugins/asset_inventory/server/lib/get_k8s_clusters.ts +++ b/x-pack/plugins/asset_inventory/server/lib/get_k8s_clusters.ts @@ -20,14 +20,14 @@ export async function getK8sClusters(): Promise { must: [ { term: { - ['asset.type.keyword']: 'k8s.cluster', + ['asset.type']: 'k8s.cluster', }, }, ], }, }, collapse: { - field: 'asset.ean.keyword', + field: 'asset.ean', }, sort: { '@timestamp': { diff --git a/x-pack/plugins/asset_inventory/server/lib/get_k8s_nodes.ts b/x-pack/plugins/asset_inventory/server/lib/get_k8s_nodes.ts index 1c867a777c35a..003cd1c6b3a38 100644 --- a/x-pack/plugins/asset_inventory/server/lib/get_k8s_nodes.ts +++ b/x-pack/plugins/asset_inventory/server/lib/get_k8s_nodes.ts @@ -23,19 +23,19 @@ export async function getK8sNodes({ clusterEan }: GetK8sNodesOptions = {}): Prom must: [ { term: { - ['asset.type.keyword']: 'k8s.node', + ['asset.type']: 'k8s.node', }, }, { term: { - 'asset.parents.keyword': clusterEan, + 'asset.parents': clusterEan, }, }, ], }, }, collapse: { - field: 'asset.ean.keyword', + field: 'asset.ean', }, sort: { '@timestamp': { @@ -47,6 +47,7 @@ export async function getK8sNodes({ clusterEan }: GetK8sNodesOptions = {}): Prom debug('Performing K8s Nodes Query', '\n\n', JSON.stringify(dsl, null, 2)); const response = await esClient.search<{ + '@timestamp': string; 'asset.id': string; 'asset.name': string; 'asset.ean': string; @@ -59,6 +60,7 @@ export async function getK8sNodes({ clusterEan }: GetK8sNodesOptions = {}): Prom } const s = hit._source; return { + '@timestamp': s['@timestamp'], id: s['asset.id'], name: s['asset.name'], ean: s['asset.ean'], diff --git a/x-pack/plugins/asset_inventory/tsconfig.json b/x-pack/plugins/asset_inventory/tsconfig.json index 163160f555669..4d38e0dfeaee4 100644 --- a/x-pack/plugins/asset_inventory/tsconfig.json +++ b/x-pack/plugins/asset_inventory/tsconfig.json @@ -17,19 +17,26 @@ "references": [ { "path": "../../../src/core/tsconfig.json" }, { "path": "../../../src/plugins/data/tsconfig.json" }, + { "path": "../../../src/plugins/embeddable/tsconfig.json" }, { "path": "../../../src/plugins/home/tsconfig.json" }, + { "path": "../../../src/plugins/inspector/tsconfig.json" }, { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, { "path": "../../../src/plugins/usage_collection/tsconfig.json" }, + { "path": "../actions/tsconfig.json" }, { "path": "../alerting/tsconfig.json" }, + { "path": "../cloud/tsconfig.json" }, + { "path": "../features/tsconfig.json" }, + { "path": "../infra/tsconfig.json" }, { "path": "../licensing/tsconfig.json" }, - { "path": "../cases/tsconfig.json" }, - { "path": "../lens/tsconfig.json" }, + { "path": "../maps/tsconfig.json" }, + { "path": "../ml/tsconfig.json" }, + { "path": "../observability/tsconfig.json" }, + { "path": "../reporting/tsconfig.json" }, { "path": "../rule_registry/tsconfig.json" }, - { "path": "../spaces/tsconfig.json" }, - { "path": "../timelines/tsconfig.json"}, - { "path": "../translations/tsconfig.json" }, - { "path": "../../../src/plugins/unified_search/tsconfig.json"}, - { "path": "../../../src/plugins/guided_onboarding/tsconfig.json"}, + { "path": "../security/tsconfig.json" }, + { "path": "../task_manager/tsconfig.json" }, + { "path": "../triggers_actions_ui/tsconfig.json" }, + { "path": "../fleet/tsconfig.json" } ] }