diff --git a/src/dev/typescript/projects.ts b/src/dev/typescript/projects.ts index cd74ffccf166c..34756912fc247 100644 --- a/src/dev/typescript/projects.ts +++ b/src/dev/typescript/projects.ts @@ -19,7 +19,6 @@ import glob from 'glob'; import { resolve } from 'path'; - import { REPO_ROOT } from '../constants'; import { Project } from './project'; diff --git a/src/plugins/es_ui_shared/public/index.ts b/src/plugins/es_ui_shared/public/index.ts index 67e3617a85115..2925e5e16458e 100644 --- a/src/plugins/es_ui_shared/public/index.ts +++ b/src/plugins/es_ui_shared/public/index.ts @@ -18,3 +18,12 @@ */ export { JsonEditor, OnJsonEditorUpdateHandler } from './components/json_editor'; + +export { + SendRequestConfig, + SendRequestResponse, + UseRequestConfig, + UseRequestResponse, + sendRequest, + useRequest, +} from './request/np_ready_request'; diff --git a/src/plugins/es_ui_shared/public/request/np_ready_request.ts b/src/plugins/es_ui_shared/public/request/np_ready_request.ts index 01144fa1da2e9..d221047ac37a7 100644 --- a/src/plugins/es_ui_shared/public/request/np_ready_request.ts +++ b/src/plugins/es_ui_shared/public/request/np_ready_request.ts @@ -28,8 +28,8 @@ export interface SendRequestConfig { body?: any; } -export interface SendRequestResponse { - data: any; +export interface SendRequestResponse { + data: D | null; error: Error | null; } @@ -39,18 +39,18 @@ export interface UseRequestConfig extends SendRequestConfig { deserializer?: (data: any) => any; } -export interface UseRequestResponse { +export interface UseRequestResponse { isInitialRequest: boolean; isLoading: boolean; - error: null | unknown; - data: any; - sendRequest: (...args: any[]) => Promise; + error: Error | null; + data: D | null; + sendRequest: (...args: any[]) => Promise>; } -export const sendRequest = async ( +export const sendRequest = async ( httpClient: HttpSetup, { path, method, body, query }: SendRequestConfig -): Promise => { +): Promise> => { try { const response = await httpClient[method](path, { body, query }); @@ -66,7 +66,7 @@ export const sendRequest = async ( } }; -export const useRequest = ( +export const useRequest = ( httpClient: HttpSetup, { path, @@ -77,8 +77,8 @@ export const useRequest = ( initialData, deserializer = (data: any): any => data, }: UseRequestConfig -): UseRequestResponse => { - const sendRequestRef = useRef<() => Promise>(); +): UseRequestResponse => { + const sendRequestRef = useRef<() => Promise>>(); // Main states for tracking request status and data const [error, setError] = useState(null); diff --git a/x-pack/index.js b/x-pack/index.js index 0188b3fc620b2..6b84c74690615 100644 --- a/x-pack/index.js +++ b/x-pack/index.js @@ -36,6 +36,7 @@ import { transform } from './legacy/plugins/transform'; import { actions } from './legacy/plugins/actions'; import { alerting } from './legacy/plugins/alerting'; import { lens } from './legacy/plugins/lens'; +import { ingestManager } from './legacy/plugins/ingest_manager'; import { triggersActionsUI } from './legacy/plugins/triggers_actions_ui'; module.exports = function(kibana) { @@ -72,6 +73,7 @@ module.exports = function(kibana) { snapshotRestore(kibana), actions(kibana), alerting(kibana), + ingestManager(kibana), triggersActionsUI(kibana), ]; }; diff --git a/x-pack/legacy/plugins/ingest_manager/index.ts b/x-pack/legacy/plugins/ingest_manager/index.ts new file mode 100644 index 0000000000000..c20cc7225d780 --- /dev/null +++ b/x-pack/legacy/plugins/ingest_manager/index.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { + savedObjectMappings, + OUTPUT_SAVED_OBJECT_TYPE, + AGENT_CONFIG_SAVED_OBJECT_TYPE, + DATASOURCE_SAVED_OBJECT_TYPE, +} from '../../../plugins/ingest_manager/server'; + +// TODO https://github.com/elastic/kibana/issues/46373 +// const INDEX_NAMES = { +// INGEST: '.kibana', +// }; + +export function ingestManager(kibana: any) { + return new kibana.Plugin({ + id: 'ingestManager', + uiExports: { + savedObjectSchemas: { + [AGENT_CONFIG_SAVED_OBJECT_TYPE]: { + isNamespaceAgnostic: true, + // indexPattern: INDEX_NAMES.INGEST, + }, + [OUTPUT_SAVED_OBJECT_TYPE]: { + isNamespaceAgnostic: true, + // indexPattern: INDEX_NAMES.INGEST, + }, + [DATASOURCE_SAVED_OBJECT_TYPE]: { + isNamespaceAgnostic: true, + // indexPattern: INDEX_NAMES.INGEST, + }, + }, + mappings: savedObjectMappings, + }, + }); +} diff --git a/x-pack/plugins/ingest_manager/README.md b/x-pack/plugins/ingest_manager/README.md new file mode 100644 index 0000000000000..60c2a457a2806 --- /dev/null +++ b/x-pack/plugins/ingest_manager/README.md @@ -0,0 +1,20 @@ +# Ingest Manager + +## Getting started +See the Kibana docs for [how to set up your dev environment](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#setting-up-your-development-environment), [run Elasticsearch](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#running-elasticsearch), and [start Kibana](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#running-kibana). + +One common workflow is: + + 1. `yarn es snapshot` + 1. In another shell: `yarn start --xpack.ingestManager.enabled=true` (or set in `config.yml`) + +## HTTP API + 1. Nothing by default. If `xpack.ingestManager.enabled=true`, it adds the `DATASOURCE_API_ROUTES` and `AGENT_CONFIG_API_ROUTES` values in [`common/constants/routes.ts`](./common/constants/routes.ts) + 1. [Integration tests](../../test/api_integration/apis/ingest_manager/endpoints.ts) + 1. In later versions the EPM and Fleet routes will be added when their flags are enabled. See the [currently disabled logic to add those routes](https://github.com/jfsiii/kibana/blob/feature-ingest-manager/x-pack/plugins/ingest_manager/server/plugin.ts#L86-L90). + +## Plugin architecture +Follows the `common`, `server`, `public` structure from the [Architecture Style Guide +](https://github.com/elastic/kibana/blob/master/style_guides/architecture_style_guide.md#file-and-folder-structure). + +We use New Platform approach (structure, APIs, etc) where possible. There's a `kibana.json` manifest, and the server uses the `server/{index,plugin}.ts` approach from [`MIGRATION.md`](https://github.com/elastic/kibana/blob/master/src/core/MIGRATION.md#architecture). \ No newline at end of file diff --git a/x-pack/plugins/ingest_manager/common/constants/agent_config.ts b/x-pack/plugins/ingest_manager/common/constants/agent_config.ts new file mode 100644 index 0000000000000..d0854d6ffeec7 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/constants/agent_config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { AgentConfigStatus } from '../types'; + +export const AGENT_CONFIG_SAVED_OBJECT_TYPE = 'agent_configs'; + +export const DEFAULT_AGENT_CONFIG_ID = 'default'; + +export const DEFAULT_AGENT_CONFIG = { + name: 'Default config', + namespace: 'default', + description: 'Default agent configuration created by Kibana', + status: AgentConfigStatus.Active, + datasources: [], +}; diff --git a/x-pack/plugins/ingest_manager/common/constants/datasource.ts b/x-pack/plugins/ingest_manager/common/constants/datasource.ts new file mode 100644 index 0000000000000..0ff472b2afeb0 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/constants/datasource.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const DATASOURCE_SAVED_OBJECT_TYPE = 'datasources'; diff --git a/x-pack/plugins/ingest_manager/common/constants/index.ts b/x-pack/plugins/ingest_manager/common/constants/index.ts new file mode 100644 index 0000000000000..aa3b204be4889 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/constants/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +export * from './plugin'; +export * from './routes'; + +export * from './agent_config'; +export * from './datasource'; +export * from './output'; diff --git a/x-pack/plugins/ingest_manager/common/constants/output.ts b/x-pack/plugins/ingest_manager/common/constants/output.ts new file mode 100644 index 0000000000000..e0262d0ca811c --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/constants/output.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { OutputType } from '../types'; + +export const OUTPUT_SAVED_OBJECT_TYPE = 'outputs'; + +export const DEFAULT_OUTPUT_ID = 'default'; + +export const DEFAULT_OUTPUT = { + name: DEFAULT_OUTPUT_ID, + type: OutputType.Elasticsearch, + hosts: [''], + ingest_pipeline: DEFAULT_OUTPUT_ID, + api_key: '', +}; diff --git a/x-pack/plugins/ingest_manager/common/constants/plugin.ts b/x-pack/plugins/ingest_manager/common/constants/plugin.ts new file mode 100644 index 0000000000000..7922e6cadfa28 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/constants/plugin.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const PLUGIN_ID = 'ingestManager'; diff --git a/x-pack/plugins/ingest_manager/common/constants/routes.ts b/x-pack/plugins/ingest_manager/common/constants/routes.ts new file mode 100644 index 0000000000000..efd6ef17ba05b --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/constants/routes.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +// Base API paths +export const API_ROOT = `/api/ingest_manager`; +export const DATASOURCE_API_ROOT = `${API_ROOT}/datasources`; +export const AGENT_CONFIG_API_ROOT = `${API_ROOT}/agent_configs`; +export const EPM_API_ROOT = `${API_ROOT}/epm`; +export const FLEET_API_ROOT = `${API_ROOT}/fleet`; + +// EPM API routes +export const EPM_API_ROUTES = { + LIST_PATTERN: `${EPM_API_ROOT}/list`, + INFO_PATTERN: `${EPM_API_ROOT}/package/{pkgkey}`, + INSTALL_PATTERN: `${EPM_API_ROOT}/install/{pkgkey}`, + DELETE_PATTERN: `${EPM_API_ROOT}/delete/{pkgkey}`, + CATEGORIES_PATTERN: `${EPM_API_ROOT}/categories`, +}; + +// Datasource API routes +export const DATASOURCE_API_ROUTES = { + LIST_PATTERN: `${DATASOURCE_API_ROOT}`, + INFO_PATTERN: `${DATASOURCE_API_ROOT}/{datasourceId}`, + CREATE_PATTERN: `${DATASOURCE_API_ROOT}`, + UPDATE_PATTERN: `${DATASOURCE_API_ROOT}/{datasourceId}`, + DELETE_PATTERN: `${DATASOURCE_API_ROOT}/delete`, +}; + +// Agent config API routes +export const AGENT_CONFIG_API_ROUTES = { + LIST_PATTERN: `${AGENT_CONFIG_API_ROOT}`, + INFO_PATTERN: `${AGENT_CONFIG_API_ROOT}/{agentConfigId}`, + CREATE_PATTERN: `${AGENT_CONFIG_API_ROOT}`, + UPDATE_PATTERN: `${AGENT_CONFIG_API_ROOT}/{agentConfigId}`, + DELETE_PATTERN: `${AGENT_CONFIG_API_ROOT}/delete`, +}; + +// Fleet setup API routes +export const FLEET_SETUP_API_ROUTES = { + INFO_PATTERN: `${FLEET_API_ROOT}/setup`, + CREATE_PATTERN: `${FLEET_API_ROOT}/setup`, +}; diff --git a/x-pack/plugins/ingest_manager/common/index.ts b/x-pack/plugins/ingest_manager/common/index.ts new file mode 100644 index 0000000000000..3d1c70ba2635e --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/index.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; + * you may not use this file except in compliance with the Elastic License. + */ +export * from './constants'; +export * from './services'; +export * from './types'; diff --git a/x-pack/plugins/ingest_manager/common/services/index.ts b/x-pack/plugins/ingest_manager/common/services/index.ts new file mode 100644 index 0000000000000..1b3ae4706e3a7 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/services/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +export * from './routes'; diff --git a/x-pack/plugins/ingest_manager/common/services/routes.ts b/x-pack/plugins/ingest_manager/common/services/routes.ts new file mode 100644 index 0000000000000..bcd1646fe1f0c --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/services/routes.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { + EPM_API_ROOT, + EPM_API_ROUTES, + DATASOURCE_API_ROUTES, + AGENT_CONFIG_API_ROUTES, +} from '../constants'; + +export const epmRouteService = { + getCategoriesPath: () => { + return EPM_API_ROUTES.CATEGORIES_PATTERN; + }, + + getListPath: () => { + return EPM_API_ROUTES.LIST_PATTERN; + }, + + getInfoPath: (pkgkey: string) => { + return EPM_API_ROUTES.INFO_PATTERN.replace('{pkgkey}', pkgkey); + }, + + getFilePath: (filePath: string) => { + return `${EPM_API_ROOT}${filePath}`; + }, + + getInstallPath: (pkgkey: string) => { + return EPM_API_ROUTES.INSTALL_PATTERN.replace('{pkgkey}', pkgkey).replace(/\/$/, ''); // trim trailing slash + }, + + getRemovePath: (pkgkey: string) => { + return EPM_API_ROUTES.DELETE_PATTERN.replace('{pkgkey}', pkgkey).replace(/\/$/, ''); // trim trailing slash + }, +}; + +export const datasourceRouteService = { + getListPath: () => { + return DATASOURCE_API_ROUTES.LIST_PATTERN; + }, + + getInfoPath: (datasourceId: string) => { + return DATASOURCE_API_ROUTES.INFO_PATTERN.replace('{datasourceId}', datasourceId); + }, + + getCreatePath: () => { + return DATASOURCE_API_ROUTES.CREATE_PATTERN; + }, + + getUpdatePath: (datasourceId: string) => { + return DATASOURCE_API_ROUTES.UPDATE_PATTERN.replace('{datasourceId}', datasourceId); + }, +}; + +export const agentConfigRouteService = { + getListPath: () => { + return AGENT_CONFIG_API_ROUTES.LIST_PATTERN; + }, + + getInfoPath: (agentConfigId: string) => { + return AGENT_CONFIG_API_ROUTES.INFO_PATTERN.replace('{agentConfigId}', agentConfigId); + }, + + getCreatePath: () => { + return AGENT_CONFIG_API_ROUTES.CREATE_PATTERN; + }, + + getUpdatePath: (agentConfigId: string) => { + return AGENT_CONFIG_API_ROUTES.UPDATE_PATTERN.replace('{agentConfigId}', agentConfigId); + }, + + getDeletePath: () => { + return AGENT_CONFIG_API_ROUTES.DELETE_PATTERN; + }, +}; diff --git a/x-pack/plugins/ingest_manager/common/types/index.ts b/x-pack/plugins/ingest_manager/common/types/index.ts new file mode 100644 index 0000000000000..4abb1b659f036 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/types/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; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './models'; +export * from './rest_spec'; + +export interface IngestManagerConfigType { + enabled: boolean; + epm: { + enabled: boolean; + registryUrl: string; + }; + fleet: { + enabled: boolean; + defaultOutputHost: string; + }; +} diff --git a/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts b/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts new file mode 100644 index 0000000000000..1cc8b32afe3c1 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/types/models/agent_config.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { DatasourceSchema } from './datasource'; + +export enum AgentConfigStatus { + Active = 'active', + Inactive = 'inactive', +} + +interface AgentConfigBaseSchema { + name: string; + namespace: string; + description?: string; +} + +export type NewAgentConfigSchema = AgentConfigBaseSchema; + +export type AgentConfigSchema = AgentConfigBaseSchema & { + id: string; + status: AgentConfigStatus; + datasources: Array; + updated_on: string; + updated_by: string; +}; + +export type NewAgentConfig = NewAgentConfigSchema; + +export type AgentConfig = AgentConfigSchema; diff --git a/x-pack/plugins/ingest_manager/common/types/models/datasource.ts b/x-pack/plugins/ingest_manager/common/types/models/datasource.ts new file mode 100644 index 0000000000000..f28037845c7f7 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/types/models/datasource.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +interface DatasourceBaseSchema { + name: string; + namespace: string; + read_alias: string; + agent_config_id: string; + package: { + assets: Array<{ + id: string; + type: string; + }>; + description: string; + name: string; + title: string; + version: string; + }; + streams: Array<{ + config: Record; + input: { + type: string; + config: Record; + fields: Array>; + ilm_policy: string; + index_template: string; + ingest_pipelines: string[]; + }; + output_id: string; + processors: string[]; + }>; +} + +export type NewDatasourceSchema = DatasourceBaseSchema; + +export type DatasourceSchema = DatasourceBaseSchema & { id: string }; + +export type NewDatasource = NewDatasourceSchema; + +export type Datasource = DatasourceSchema; diff --git a/x-pack/plugins/ingest_manager/common/types/models/index.ts b/x-pack/plugins/ingest_manager/common/types/models/index.ts new file mode 100644 index 0000000000000..959dfe1d937b9 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/types/models/index.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; + * you may not use this file except in compliance with the Elastic License. + */ +export * from './agent_config'; +export * from './datasource'; +export * from './output'; diff --git a/x-pack/plugins/ingest_manager/common/types/models/output.ts b/x-pack/plugins/ingest_manager/common/types/models/output.ts new file mode 100644 index 0000000000000..5f96fe33b5e16 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/types/models/output.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; + * you may not use this file except in compliance with the Elastic License. + */ + +export enum OutputType { + Elasticsearch = 'elasticsearch', +} + +interface OutputBaseSchema { + name: string; + type: OutputType; + username?: string; + password?: string; + index_name?: string; + ingest_pipeline?: string; + hosts?: string[]; + api_key?: string; + admin_username?: string; + admin_password?: string; + config?: Record; +} + +export type NewOutputSchema = OutputBaseSchema; + +export type OutputSchema = OutputBaseSchema & { + id: string; +}; + +export type NewOutput = NewOutputSchema; + +export type Output = OutputSchema; diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent_config.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent_config.ts new file mode 100644 index 0000000000000..5d281b03260db --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent_config.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; + * you may not use this file except in compliance with the Elastic License. + */ +import { AgentConfig, NewAgentConfigSchema } from '../models'; +import { ListWithKuerySchema } from './common'; + +export interface GetAgentConfigsRequestSchema { + query: ListWithKuerySchema; +} + +export interface GetAgentConfigsResponse { + items: AgentConfig[]; + total: number; + page: number; + perPage: number; + success: boolean; +} + +export interface GetOneAgentConfigRequestSchema { + params: { + agentConfigId: string; + }; +} + +export interface GetOneAgentConfigResponse { + item: AgentConfig; + success: boolean; +} + +export interface CreateAgentConfigRequestSchema { + body: NewAgentConfigSchema; +} + +export interface CreateAgentConfigResponse { + item: AgentConfig; + success: boolean; +} + +export type UpdateAgentConfigRequestSchema = GetOneAgentConfigRequestSchema & { + body: NewAgentConfigSchema; +}; + +export interface UpdateAgentConfigResponse { + item: AgentConfig; + success: boolean; +} + +export interface DeleteAgentConfigsRequestSchema { + body: { + agentConfigIds: string[]; + }; +} + +export type DeleteAgentConfigsResponse = Array<{ + id: string; + success: boolean; +}>; diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/common.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/common.ts new file mode 100644 index 0000000000000..d247933d4011f --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/common.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; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface ListWithKuerySchema { + page: number; + perPage: number; + kuery?: string; +} + +export type ListWithKuery = ListWithKuerySchema; diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/datasource.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/datasource.ts new file mode 100644 index 0000000000000..78859f2008005 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/datasource.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ +import { NewDatasourceSchema } from '../models'; +import { ListWithKuerySchema } from './common'; + +export interface GetDatasourcesRequestSchema { + query: ListWithKuerySchema; +} + +export interface GetOneDatasourceRequestSchema { + params: { + datasourceId: string; + }; +} + +export interface CreateDatasourceRequestSchema { + body: NewDatasourceSchema; +} + +export type UpdateDatasourceRequestSchema = GetOneDatasourceRequestSchema & { + body: NewDatasourceSchema; +}; + +export interface DeleteDatasourcesRequestSchema { + body: { + datasourceIds: string[]; + }; +} + +export type DeleteDatasourcesResponse = Array<{ + id: string; + success: boolean; +}>; diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/fleet_setup.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/fleet_setup.ts new file mode 100644 index 0000000000000..926021baab0ef --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/fleet_setup.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; + * you may not use this file except in compliance with the Elastic License. + */ + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface GetFleetSetupRequestSchema {} + +export interface CreateFleetSetupRequestSchema { + body: { + admin_username: string; + admin_password: string; + }; +} + +export interface CreateFleetSetupResponse { + isInitialized: boolean; +} diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/index.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/index.ts new file mode 100644 index 0000000000000..7d0d7e67f2db0 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +export * from './common'; +export * from './datasource'; +export * from './agent_config'; +export * from './fleet_setup'; diff --git a/x-pack/plugins/ingest_manager/kibana.json b/x-pack/plugins/ingest_manager/kibana.json new file mode 100644 index 0000000000000..cef1a293c104b --- /dev/null +++ b/x-pack/plugins/ingest_manager/kibana.json @@ -0,0 +1,9 @@ +{ + "id": "ingestManager", + "version": "kibana", + "server": true, + "ui": true, + "configPath": ["xpack", "ingestManager"], + "requiredPlugins": ["licensing", "data", "encryptedSavedObjects"], + "optionalPlugins": ["security", "features"] +} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/index.ts new file mode 100644 index 0000000000000..5133d82588494 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +export { Loading } from './loading'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/loading.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/loading.tsx new file mode 100644 index 0000000000000..c1fae19c5dab0 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/loading.tsx @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; + +export const Loading: React.FunctionComponent<{}> = () => ( + + + + + +); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/index.ts new file mode 100644 index 0000000000000..1af39a60455e0 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { + PLUGIN_ID, + EPM_API_ROUTES, + DEFAULT_AGENT_CONFIG_ID, + AGENT_CONFIG_SAVED_OBJECT_TYPE, +} from '../../../../common'; + +export const BASE_PATH = '/app/ingestManager'; +export const EPM_PATH = '/epm'; +export const AGENT_CONFIG_PATH = '/configs'; +export const AGENT_CONFIG_DETAILS_PATH = `${AGENT_CONFIG_PATH}/`; +export const FLEET_PATH = '/fleet'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/index.ts new file mode 100644 index 0000000000000..a224b599c13af --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/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; + * you may not use this file except in compliance with the Elastic License. + */ + +export { useCore, CoreContext } from './use_core'; +export { useConfig, ConfigContext } from './use_config'; +export { useDeps, DepsContext } from './use_deps'; +export { useLink } from './use_link'; +export { usePagination } from './use_pagination'; +export { useDebounce } from './use_debounce'; +export * from './use_request'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_config.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_config.ts new file mode 100644 index 0000000000000..d3f27a180cfd0 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_config.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useContext } from 'react'; +import { IngestManagerConfigType } from '../../../plugin'; + +export const ConfigContext = React.createContext(null); + +export function useConfig() { + const config = useContext(ConfigContext); + if (config === null) { + throw new Error('ConfigContext not initialized'); + } + return config; +} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_core.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_core.ts new file mode 100644 index 0000000000000..c6e91444d21f5 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_core.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useContext } from 'react'; +import { CoreStart } from 'kibana/public'; + +export const CoreContext = React.createContext(null); + +export function useCore() { + const core = useContext(CoreContext); + if (core === null) { + throw new Error('CoreContext not initialized'); + } + return core; +} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_debounce.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_debounce.tsx new file mode 100644 index 0000000000000..f701ebeaadbe5 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_debounce.tsx @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useState, useEffect } from 'react'; + +export function useDebounce(value: T, delay: number) { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return () => { + clearTimeout(handler); + }; + }, [value, delay]); + + return debouncedValue; +} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_deps.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_deps.ts new file mode 100644 index 0000000000000..a2e2f278930e3 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_deps.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useContext } from 'react'; +import { IngestManagerSetupDeps } from '../../../plugin'; + +export const DepsContext = React.createContext(null); + +export function useDeps() { + const deps = useContext(DepsContext); + if (deps === null) { + throw new Error('DepsContext not initialized'); + } + return deps; +} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_link.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_link.ts new file mode 100644 index 0000000000000..333606cec8028 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_link.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { BASE_PATH } from '../constants'; +import { useCore } from './'; + +export function useLink(path: string = '/') { + const core = useCore(); + return core.http.basePath.prepend(`${BASE_PATH}#${path}`); +} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_pagination.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_pagination.tsx new file mode 100644 index 0000000000000..ae0352a33b2ff --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_pagination.tsx @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useState } from 'react'; + +export interface Pagination { + currentPage: number; + pageSize: number; +} + +export function usePagination() { + const [pagination, setPagination] = useState({ + currentPage: 1, + pageSize: 20, + }); + + return { + pagination, + setPagination, + pageSizeOptions: 20, + }; +} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agent_config.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agent_config.ts new file mode 100644 index 0000000000000..389909e1d99ef --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agent_config.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { HttpFetchQuery } from 'kibana/public'; +import { useRequest, sendRequest } from './use_request'; +import { agentConfigRouteService } from '../../services'; +import { + GetAgentConfigsResponse, + GetOneAgentConfigResponse, + CreateAgentConfigRequestSchema, + CreateAgentConfigResponse, + UpdateAgentConfigRequestSchema, + UpdateAgentConfigResponse, + DeleteAgentConfigsRequestSchema, + DeleteAgentConfigsResponse, +} from '../../types'; + +export const useGetAgentConfigs = (query: HttpFetchQuery = {}) => { + return useRequest({ + path: agentConfigRouteService.getListPath(), + method: 'get', + query, + }); +}; + +export const useGetOneAgentConfig = (agentConfigId: string) => { + return useRequest({ + path: agentConfigRouteService.getInfoPath(agentConfigId), + method: 'get', + }); +}; + +export const sendCreateAgentConfig = (body: CreateAgentConfigRequestSchema['body']) => { + return sendRequest({ + path: agentConfigRouteService.getCreatePath(), + method: 'post', + body: JSON.stringify(body), + }); +}; + +export const sendUpdateAgentConfig = ( + agentConfigId: string, + body: UpdateAgentConfigRequestSchema['body'] +) => { + return sendRequest({ + path: agentConfigRouteService.getUpdatePath(agentConfigId), + method: 'put', + body: JSON.stringify(body), + }); +}; + +export const sendDeleteAgentConfigs = (body: DeleteAgentConfigsRequestSchema['body']) => { + return sendRequest({ + path: agentConfigRouteService.getDeletePath(), + method: 'post', + body: JSON.stringify(body), + }); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/index.ts new file mode 100644 index 0000000000000..68d67080d90ba --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +export { setHttpClient, sendRequest } from './use_request'; +export * from './agent_config'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/use_request.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/use_request.ts new file mode 100644 index 0000000000000..12b4d0bdf7df6 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/use_request.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { HttpSetup } from 'kibana/public'; +import { + SendRequestConfig, + SendRequestResponse, + UseRequestConfig, + sendRequest as _sendRequest, + useRequest as _useRequest, +} from '../../../../../../../../src/plugins/es_ui_shared/public'; + +let httpClient: HttpSetup; + +export const setHttpClient = (client: HttpSetup) => { + httpClient = client; +}; + +export const sendRequest = ( + config: SendRequestConfig +): Promise> => { + if (!httpClient) { + throw new Error('sendRequest has no http client set'); + } + return _sendRequest(httpClient, config); +}; + +export const useRequest = (config: UseRequestConfig) => { + if (!httpClient) { + throw new Error('sendRequest has no http client set'); + } + return _useRequest(httpClient, config); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx new file mode 100644 index 0000000000000..935eb42d0347e --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { useObservable } from 'react-use'; +import { HashRouter as Router, Redirect, Switch, Route, RouteProps } from 'react-router-dom'; +import { CoreStart, AppMountParameters } from 'kibana/public'; +import { EuiErrorBoundary } from '@elastic/eui'; +import { EuiThemeProvider } from '../../../../../legacy/common/eui_styled_components'; +import { IngestManagerSetupDeps, IngestManagerConfigType } from '../../plugin'; +import { EPM_PATH, FLEET_PATH, AGENT_CONFIG_PATH } from './constants'; +import { DefaultLayout } from './layouts'; +import { IngestManagerOverview, EPMApp, AgentConfigApp, FleetApp } from './sections'; +import { CoreContext, DepsContext, ConfigContext, setHttpClient, useConfig } from './hooks'; + +export interface ProtectedRouteProps extends RouteProps { + isAllowed?: boolean; + restrictedPath?: string; +} + +export const ProtectedRoute: React.FunctionComponent = ({ + isAllowed = false, + restrictedPath = '/', + ...routeProps +}: ProtectedRouteProps) => { + return isAllowed ? : ; +}; + +const IngestManagerRoutes = ({ ...rest }) => { + const { epm, fleet } = useConfig(); + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +const IngestManagerApp = ({ + basepath, + coreStart, + deps, + config, +}: { + basepath: string; + coreStart: CoreStart; + deps: IngestManagerSetupDeps; + config: IngestManagerConfigType; +}) => { + const isDarkMode = useObservable(coreStart.uiSettings.get$('theme:darkMode')); + return ( + + + + + + + + + + + + ); +}; + +export function renderApp( + coreStart: CoreStart, + { element, appBasePath }: AppMountParameters, + deps: IngestManagerSetupDeps, + config: IngestManagerConfigType +) { + setHttpClient(coreStart.http); + ReactDOM.render( + , + element + ); + + return () => { + ReactDOM.unmountComponentAtNode(element); + }; +} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx new file mode 100644 index 0000000000000..eaf49fed3d933 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { + EuiPage, + EuiPageBody, + EuiTabs, + EuiTab, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import euiStyled from '../../../../../../legacy/common/eui_styled_components'; +import { Section } from '../sections'; +import { useLink, useConfig } from '../hooks'; +import { EPM_PATH, FLEET_PATH, AGENT_CONFIG_PATH } from '../constants'; + +interface Props { + section: Section; + children?: React.ReactNode; +} + +const Nav = euiStyled.nav` + background: ${props => props.theme.eui.euiColorEmptyShade}; + border-bottom: ${props => props.theme.eui.euiBorderThin}; + padding: ${props => + `${props.theme.eui.euiSize} ${props.theme.eui.euiSizeL} ${props.theme.eui.euiSize} ${props.theme.eui.euiSizeL}`}; + .euiTabs { + padding-left: 3px; + margin-left: -3px; + }; +`; + +export const DefaultLayout: React.FunctionComponent = ({ section, children }) => { + const { epm, fleet } = useConfig(); + return ( +
+ + + {children} + +
+ ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/index.tsx new file mode 100644 index 0000000000000..858951bd0d38f --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/index.tsx @@ -0,0 +1,6 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +export { DefaultLayout } from './default'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_delete_provider.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_delete_provider.tsx new file mode 100644 index 0000000000000..6f51415a562a3 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_delete_provider.tsx @@ -0,0 +1,217 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment, useRef, useState } from 'react'; +import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { sendDeleteAgentConfigs, useCore, sendRequest } from '../../../hooks'; + +interface Props { + children: (deleteAgentConfigs: deleteAgentConfigs) => React.ReactElement; +} + +export type deleteAgentConfigs = (agentConfigs: string[], onSuccess?: OnSuccessCallback) => void; + +type OnSuccessCallback = (agentConfigsUnenrolled: string[]) => void; + +export const AgentConfigDeleteProvider: React.FunctionComponent = ({ children }) => { + const { notifications } = useCore(); + const [agentConfigs, setAgentConfigs] = useState([]); + const [isModalOpen, setIsModalOpen] = useState(false); + const [isLoadingAgentsCount, setIsLoadingAgentsCount] = useState(false); + const [agentsCount, setAgentsCount] = useState(0); + const [isLoading, setIsLoading] = useState(false); + const onSuccessCallback = useRef(null); + + const deleteAgentConfigsPrompt: deleteAgentConfigs = ( + agentConfigsToDelete, + onSuccess = () => undefined + ) => { + if ( + agentConfigsToDelete === undefined || + (Array.isArray(agentConfigsToDelete) && agentConfigsToDelete.length === 0) + ) { + throw new Error('No agent configs specified for deletion'); + } + setIsModalOpen(true); + setAgentConfigs(agentConfigsToDelete); + fetchAgentsCount(agentConfigsToDelete); + onSuccessCallback.current = onSuccess; + }; + + const closeModal = () => { + setAgentConfigs([]); + setIsLoading(false); + setIsLoadingAgentsCount(false); + setIsModalOpen(false); + }; + + const deleteAgentConfigs = async () => { + setIsLoading(true); + + try { + const { data } = await sendDeleteAgentConfigs({ + agentConfigIds: agentConfigs, + }); + const successfulResults = data?.filter(result => result.success) || []; + const failedResults = data?.filter(result => !result.success) || []; + + if (successfulResults.length) { + const hasMultipleSuccesses = successfulResults.length > 1; + const successMessage = hasMultipleSuccesses + ? i18n.translate( + 'xpack.ingestManager.deleteAgentConfigs.successMultipleNotificationTitle', + { + defaultMessage: 'Deleted {count} agent configs', + values: { count: successfulResults.length }, + } + ) + : i18n.translate( + 'xpack.ingestManager.deleteAgentConfigs.successSingleNotificationTitle', + { + defaultMessage: "Deleted agent config '{id}'", + values: { id: successfulResults[0].id }, + } + ); + notifications.toasts.addSuccess(successMessage); + } + + if (failedResults.length) { + const hasMultipleFailures = failedResults.length > 1; + const failureMessage = hasMultipleFailures + ? i18n.translate( + 'xpack.ingestManager.deleteAgentConfigs.failureMultipleNotificationTitle', + { + defaultMessage: 'Error deleting {count} agent configs', + values: { count: failedResults.length }, + } + ) + : i18n.translate( + 'xpack.ingestManager.deleteAgentConfigs.failureSingleNotificationTitle', + { + defaultMessage: "Error deleting agent config '{id}'", + values: { id: failedResults[0].id }, + } + ); + notifications.toasts.addDanger(failureMessage); + } + + if (onSuccessCallback.current) { + onSuccessCallback.current(successfulResults.map(result => result.id)); + } + } catch (e) { + notifications.toasts.addDanger( + i18n.translate('xpack.ingestManager.deleteAgentConfigs.fatalErrorNotificationTitle', { + defaultMessage: 'Error deleting agent configs', + }) + ); + } + closeModal(); + }; + + const fetchAgentsCount = async (agentConfigsToCheck: string[]) => { + if (isLoadingAgentsCount) { + return; + } + setIsLoadingAgentsCount(true); + const { data } = await sendRequest<{ total: number }>({ + path: `/api/fleet/agents`, + method: 'get', + query: { + kuery: `agents.policy_id : (${agentConfigsToCheck.join(' or ')})`, + }, + }); + setAgentsCount(data?.total || 0); + setIsLoadingAgentsCount(false); + }; + + const renderModal = () => { + if (!isModalOpen) { + return null; + } + + return ( + + + } + onCancel={closeModal} + onConfirm={deleteAgentConfigs} + cancelButtonText={ + + } + confirmButtonText={ + isLoading || isLoadingAgentsCount ? ( + + ) : agentsCount ? ( + + ) : ( + + ) + } + buttonColor="danger" + confirmButtonDisabled={isLoading || isLoadingAgentsCount} + > + {isLoadingAgentsCount ? ( + + ) : agentsCount ? ( + + ) : ( + + )} + + + ); + }; + + return ( + + {children(deleteAgentConfigsPrompt)} + {renderModal()} + + ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_form.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_form.tsx new file mode 100644 index 0000000000000..5a25dc8bc92b5 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_form.tsx @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState } from 'react'; +import { EuiFieldText, EuiForm, EuiFormRow } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { NewAgentConfig } from '../../../types'; + +interface ValidationResults { + [key: string]: JSX.Element[]; +} + +export const agentConfigFormValidation = ( + agentConfig: Partial +): ValidationResults => { + const errors: ValidationResults = {}; + + if (!agentConfig.name?.trim()) { + errors.name = [ + , + ]; + } + + return errors; +}; + +interface Props { + agentConfig: Partial; + updateAgentConfig: (u: Partial) => void; + validation: ValidationResults; +} + +export const AgentConfigForm: React.FunctionComponent = ({ + agentConfig, + updateAgentConfig, + validation, +}) => { + const [touchedFields, setTouchedFields] = useState<{ [key: string]: boolean }>({}); + const fields: Array<{ name: 'name' | 'description' | 'namespace'; label: JSX.Element }> = [ + { + name: 'name', + label: ( + + ), + }, + { + name: 'description', + label: ( + + ), + }, + { + name: 'namespace', + label: ( + + ), + }, + ]; + + return ( + + {fields.map(({ name, label }) => { + return ( + + updateAgentConfig({ [name]: e.target.value })} + isInvalid={Boolean(touchedFields[name] && validation[name])} + onBlur={() => setTouchedFields({ ...touchedFields, [name]: true })} + /> + + ); + })} + + ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/index.ts new file mode 100644 index 0000000000000..d838221cd844e --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/index.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; + * you may not use this file except in compliance with the Elastic License. + */ + +export { AgentConfigForm, agentConfigFormValidation } from './config_form'; +export { AgentConfigDeleteProvider } from './config_delete_provider'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/index.tsx new file mode 100644 index 0000000000000..c80c4496198be --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/index.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { HashRouter as Router, Switch, Route } from 'react-router-dom'; +import { AgentConfigListPage } from './list_page'; + +export const AgentConfigApp: React.FunctionComponent = () => ( + + + + + + + +); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/components/create_config.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/components/create_config.tsx new file mode 100644 index 0000000000000..c6fea7b22bcd1 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/components/create_config.tsx @@ -0,0 +1,143 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiFlyout, + EuiFlyoutHeader, + EuiTitle, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiButton, +} from '@elastic/eui'; +import { NewAgentConfig } from '../../../../types'; +import { useCore, sendCreateAgentConfig } from '../../../../hooks'; +import { AgentConfigForm, agentConfigFormValidation } from '../../components'; + +interface Props { + onClose: () => void; +} + +export const CreateAgentConfigFlyout: React.FunctionComponent = ({ onClose }) => { + const { notifications } = useCore(); + + const [agentConfig, setAgentConfig] = useState({ + name: '', + description: '', + namespace: '', + }); + const [isLoading, setIsLoading] = useState(false); + const validation = agentConfigFormValidation(agentConfig); + + const updateAgentConfig = (updatedFields: Partial) => { + setAgentConfig({ + ...agentConfig, + ...updatedFields, + }); + }; + + const createAgentConfig = async () => { + return await sendCreateAgentConfig(agentConfig); + }; + + const header = ( + + +

+ +

+
+
+ ); + + const body = ( + + + + ); + + const footer = ( + + + + + + + + + 0} + onClick={async () => { + setIsLoading(true); + try { + const { data, error } = await createAgentConfig(); + if (data?.success) { + notifications.toasts.addSuccess( + i18n.translate( + 'xpack.ingestManager.createAgentConfig.successNotificationTitle', + { + defaultMessage: "Agent config '{name}' created", + values: { name: agentConfig.name }, + } + ) + ); + } else { + notifications.toasts.addDanger( + error + ? error.message + : i18n.translate( + 'xpack.ingestManager.createAgentConfig.errorNotificationTitle', + { + defaultMessage: 'Unable to create agent config', + } + ) + ); + } + } catch (e) { + notifications.toasts.addDanger( + i18n.translate('xpack.ingestManager.createAgentConfig.errorNotificationTitle', { + defaultMessage: 'Unable to create agent config', + }) + ); + } + setIsLoading(false); + onClose(); + }} + > + + + + + + ); + + return ( + + {header} + {body} + {footer} + + ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/components/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/components/index.ts new file mode 100644 index 0000000000000..43668b4ffb804 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/components/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +export { CreateAgentConfigFlyout } from './create_config'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx new file mode 100644 index 0000000000000..ca9fb195166f6 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx @@ -0,0 +1,280 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { useState } from 'react'; +import { + EuiPageBody, + EuiPageContent, + EuiTitle, + EuiSpacer, + EuiText, + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiEmptyPrompt, + // @ts-ignore + EuiSearchBar, + EuiBasicTable, + EuiLink, + EuiBadge, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { AgentConfig } from '../../../types'; +import { DEFAULT_AGENT_CONFIG_ID, AGENT_CONFIG_DETAILS_PATH } from '../../../constants'; +// import { SearchBar } from '../../../components'; +import { useGetAgentConfigs, usePagination, useLink } from '../../../hooks'; +import { AgentConfigDeleteProvider } from '../components'; +import { CreateAgentConfigFlyout } from './components'; + +export const AgentConfigListPage: React.FunctionComponent<{}> = () => { + // Create agent config flyout state + const [isCreateAgentConfigFlyoutOpen, setIsCreateAgentConfigFlyoutOpen] = useState( + false + ); + + // Table and search states + const [search, setSearch] = useState(''); + const { pagination, setPagination } = usePagination(); + const [selectedAgentConfigs, setSelectedAgentConfigs] = useState([]); + + // Fetch agent configs + const { isLoading, data: agentConfigData, sendRequest } = useGetAgentConfigs(); + + // Base path for config details + const DETAILS_URI = useLink(AGENT_CONFIG_DETAILS_PATH); + + // Some configs retrieved, set up table props + const columns = [ + { + field: 'name', + name: i18n.translate('xpack.ingestManager.agentConfigList.nameColumnTitle', { + defaultMessage: 'Name', + }), + render: (name: string, agentConfig: AgentConfig) => name || agentConfig.id, + }, + { + field: 'namespace', + name: i18n.translate('xpack.ingestManager.agentConfigList.namespaceColumnTitle', { + defaultMessage: 'Namespace', + }), + render: (namespace: string) => (namespace ? {namespace} : null), + }, + { + field: 'description', + name: i18n.translate('xpack.ingestManager.agentConfigList.descriptionColumnTitle', { + defaultMessage: 'Description', + }), + }, + { + field: 'datasources', + name: i18n.translate('xpack.ingestManager.agentConfigList.datasourcesCountColumnTitle', { + defaultMessage: 'Datasources', + }), + render: (datasources: AgentConfig['datasources']) => (datasources ? datasources.length : 0), + }, + { + name: i18n.translate('xpack.ingestManager.agentConfigList.actionsColumnTitle', { + defaultMessage: 'Actions', + }), + actions: [ + { + render: ({ id }: AgentConfig) => { + return ( + + + + ); + }, + }, + ], + width: '100px', + }, + ]; + + const emptyPrompt = ( + + + + } + actions={ + setIsCreateAgentConfigFlyoutOpen(true)} + > + + + } + /> + ); + + return ( + + + {isCreateAgentConfigFlyoutOpen ? ( + { + setIsCreateAgentConfigFlyoutOpen(false); + sendRequest(); + }} + /> + ) : null} + + +

+ +

+
+ + + + + + + + + + + + + + {selectedAgentConfigs.length ? ( + + + {deleteAgentConfigsPrompt => ( + { + deleteAgentConfigsPrompt( + selectedAgentConfigs.map(agentConfig => agentConfig.id), + () => { + sendRequest(); + setSelectedAgentConfigs([]); + } + ); + }} + > + + + )} + + + ) : null} + + {/* { + setPagination({ + ...pagination, + currentPage: 1, + }); + setSearch(newSearch); + }} + fieldPrefix={AGENT_CONFIG_SAVED_OBJECT_TYPE} + /> */} + + + sendRequest()}> + + + + + setIsCreateAgentConfigFlyoutOpen(true)} + > + + + + + + + + ) : !search.trim() && agentConfigData?.total === 0 ? ( + emptyPrompt + ) : ( + setSearch('')}> + + + ), + }} + /> + ) + } + items={agentConfigData ? agentConfigData.items : []} + itemId="id" + columns={columns} + isSelectable={true} + selection={{ + selectable: (agentConfig: AgentConfig) => agentConfig.id !== DEFAULT_AGENT_CONFIG_ID, + onSelectionChange: (newSelectedAgentConfigs: AgentConfig[]) => { + setSelectedAgentConfigs(newSelectedAgentConfigs); + }, + }} + pagination={{ + pageIndex: pagination.currentPage - 1, + pageSize: pagination.pageSize, + totalItemCount: agentConfigData ? agentConfigData.total : 0, + }} + onChange={({ page }: { page: { index: number; size: number } }) => { + const newPagination = { + ...pagination, + currentPage: page.index + 1, + pageSize: page.size, + }; + setPagination(newPagination); + sendRequest(); // todo: fix this to send pagination options + }} + /> +
+
+ ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/index.tsx new file mode 100644 index 0000000000000..ca8c22be9c34c --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/index.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { useConfig } from '../../hooks'; + +export const EPMApp: React.FunctionComponent = () => { + const { epm } = useConfig(); + return epm.enabled ?
hello world - epm app
: null; +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/index.tsx new file mode 100644 index 0000000000000..978414769004d --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/index.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { useConfig } from '../../hooks'; + +export const FleetApp: React.FunctionComponent = () => { + const { fleet } = useConfig(); + return fleet.enabled ?
hello world - fleet app
: null; +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/index.tsx new file mode 100644 index 0000000000000..c691bb609d435 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/index.tsx @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +export { IngestManagerOverview } from './overview'; +export { EPMApp } from './epm'; +export { AgentConfigApp } from './agent_config'; +export { FleetApp } from './fleet'; + +export type Section = 'overview' | 'epm' | 'agent_config' | 'fleet'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx new file mode 100644 index 0000000000000..da4a78a39e2fe --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; + +export const IngestManagerOverview: React.FunctionComponent = () => { + return
Ingest manager overview page
; +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts new file mode 100644 index 0000000000000..6502b0fff7123 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { agentConfigRouteService } from '../../../../common'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts new file mode 100644 index 0000000000000..8597d6fd59323 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/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; + * you may not use this file except in compliance with the Elastic License. + */ +export { + // Object types + AgentConfig, + NewAgentConfig, + // API schemas + GetAgentConfigsResponse, + GetOneAgentConfigResponse, + CreateAgentConfigRequestSchema, + CreateAgentConfigResponse, + UpdateAgentConfigRequestSchema, + UpdateAgentConfigResponse, + DeleteAgentConfigsRequestSchema, + DeleteAgentConfigsResponse, +} from '../../../../common'; diff --git a/x-pack/plugins/ingest_manager/public/index.ts b/x-pack/plugins/ingest_manager/public/index.ts new file mode 100644 index 0000000000000..a9e40a2a42302 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { PluginInitializerContext } from 'kibana/public'; +import { IngestManagerPlugin } from './plugin'; + +export const plugin = (initializerContext: PluginInitializerContext) => { + return new IngestManagerPlugin(initializerContext); +}; diff --git a/x-pack/plugins/ingest_manager/public/plugin.ts b/x-pack/plugins/ingest_manager/public/plugin.ts new file mode 100644 index 0000000000000..ae244e7ebec3d --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/plugin.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ +import { + AppMountParameters, + CoreSetup, + CoreStart, + Plugin, + PluginInitializerContext, +} from 'kibana/public'; +import { i18n } from '@kbn/i18n'; +import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils'; +import { DataPublicPluginSetup } from '../../../../src/plugins/data/public'; +import { LicensingPluginSetup } from '../../licensing/public'; +import { PLUGIN_ID } from '../common/constants'; +import { IngestManagerConfigType } from '../common/types'; + +export { IngestManagerConfigType } from '../common/types'; + +export type IngestManagerSetup = void; +export type IngestManagerStart = void; + +export interface IngestManagerSetupDeps { + licensing: LicensingPluginSetup; + data: DataPublicPluginSetup; +} + +export class IngestManagerPlugin implements Plugin { + private config: IngestManagerConfigType; + + constructor(private readonly initializerContext: PluginInitializerContext) { + this.config = this.initializerContext.config.get(); + } + + public setup(core: CoreSetup, deps: IngestManagerSetupDeps) { + const config = this.config; + + // Register main Ingest Manager app + core.application.register({ + id: PLUGIN_ID, + category: DEFAULT_APP_CATEGORIES.management, + title: i18n.translate('xpack.ingestManager.appTitle', { defaultMessage: 'Ingest Manager' }), + euiIconType: 'savedObjectsApp', + async mount(params: AppMountParameters) { + const [coreStart] = await core.getStartServices(); + const { renderApp } = await import('./applications/ingest_manager'); + return renderApp(coreStart, params, deps, config); + }, + }); + } + + public start(core: CoreStart) {} + + public stop() {} +} diff --git a/x-pack/plugins/ingest_manager/server/constants/index.ts b/x-pack/plugins/ingest_manager/server/constants/index.ts new file mode 100644 index 0000000000000..6b54afa1d81cb --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/constants/index.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +export { + // Routes + PLUGIN_ID, + EPM_API_ROUTES, + DATASOURCE_API_ROUTES, + AGENT_CONFIG_API_ROUTES, + FLEET_SETUP_API_ROUTES, + // Saved object types + AGENT_CONFIG_SAVED_OBJECT_TYPE, + DATASOURCE_SAVED_OBJECT_TYPE, + OUTPUT_SAVED_OBJECT_TYPE, + // Defaults + DEFAULT_AGENT_CONFIG_ID, + DEFAULT_AGENT_CONFIG, + DEFAULT_OUTPUT_ID, + DEFAULT_OUTPUT, +} from '../../common'; diff --git a/x-pack/plugins/ingest_manager/server/index.ts b/x-pack/plugins/ingest_manager/server/index.ts new file mode 100644 index 0000000000000..5228f1e0e3469 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/index.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { PluginInitializerContext } from 'kibana/server'; +import { IngestManagerPlugin } from './plugin'; + +export const config = { + exposeToBrowser: { + epm: true, + fleet: true, + }, + schema: schema.object({ + enabled: schema.boolean({ defaultValue: false }), + epm: schema.object({ + enabled: schema.boolean({ defaultValue: false }), + registryUrl: schema.uri({ defaultValue: 'https://epr-staging.elastic.co' }), + }), + fleet: schema.object({ + enabled: schema.boolean({ defaultValue: false }), + defaultOutputHost: schema.string({ defaultValue: 'http://localhost:9200' }), + }), + }), +}; + +export const plugin = (initializerContext: PluginInitializerContext) => { + return new IngestManagerPlugin(initializerContext); +}; + +// Saved object information bootstrapped by legacy `ingest_manager` plugin +// TODO: Remove once saved object mappings can be done from NP +export { savedObjectMappings } from './saved_objects'; +export { + OUTPUT_SAVED_OBJECT_TYPE, + AGENT_CONFIG_SAVED_OBJECT_TYPE, + DATASOURCE_SAVED_OBJECT_TYPE, +} from './constants'; + +// TODO: Temporary exports for Fleet dependencies, remove once Fleet moved into this plugin +export { agentConfigService, outputService } from './services'; diff --git a/x-pack/plugins/ingest_manager/server/integration_tests/router.test.ts b/x-pack/plugins/ingest_manager/server/integration_tests/router.test.ts new file mode 100644 index 0000000000000..38976957173f4 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/integration_tests/router.test.ts @@ -0,0 +1,190 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { resolve } from 'path'; +import * as kbnTestServer from '../../../../../src/test_utils/kbn_server'; + +function createXPackRoot(config: {} = {}) { + return kbnTestServer.createRoot({ + plugins: { + scanDirs: [], + paths: [ + resolve(__dirname, '../../../../../x-pack/plugins/encrypted_saved_objects'), + resolve(__dirname, '../../../../../x-pack/plugins/ingest_manager'), + resolve(__dirname, '../../../../../x-pack/plugins/licensing'), + ], + }, + migrations: { skip: true }, + xpack: config, + }); +} + +describe('ingestManager', () => { + describe('default. manager, EPM, and Fleet all disabled', () => { + let root: ReturnType; + beforeAll(async () => { + root = createXPackRoot(); + await root.setup(); + await root.start(); + }, 30000); + + afterAll(async () => await root.shutdown()); + + it('does not have agent config api', async () => { + await kbnTestServer.request.get(root, '/api/ingest_manager/agent_configs').expect(404); + }); + + it('does not have datasources api', async () => { + await kbnTestServer.request.get(root, '/api/ingest_manager/datasources').expect(404); + }); + + it('does not have EPM api', async () => { + await kbnTestServer.request.get(root, '/api/ingest_manager/epm').expect(404); + }); + + it('does not have Fleet api', async () => { + await kbnTestServer.request.get(root, '/api/ingest_manager/fleet').expect(404); + }); + }); + + describe('manager only (no EPM, no Fleet)', () => { + let root: ReturnType; + beforeAll(async () => { + const ingestManagerConfig = { + enabled: true, + }; + root = createXPackRoot({ + ingestManager: ingestManagerConfig, + }); + await root.setup(); + await root.start(); + }, 30000); + + afterAll(async () => await root.shutdown()); + + it('has agent config api', async () => { + await kbnTestServer.request.get(root, '/api/ingest_manager/agent_configs').expect(200); + }); + + it('has datasources api', async () => { + await kbnTestServer.request.get(root, '/api/ingest_manager/datasources').expect(200); + }); + + it('does not have EPM api', async () => { + await kbnTestServer.request.get(root, '/api/ingest_manager/epm/list').expect(404); + }); + + it('does not have Fleet api', async () => { + await kbnTestServer.request.get(root, '/api/ingest_manager/fleet/setup').expect(404); + }); + }); + + // For now, only the manager routes (/agent_configs & /datasources) are added + // EPM and ingest will be conditionally added when we enable these lines + // https://github.com/jfsiii/kibana/blob/f73b54ebb7e0f6fc00efd8a6800a01eb2d9fb772/x-pack/plugins/ingest_manager/server/plugin.ts#L84 + // adding tests to confirm the Fleet & EPM routes are never added + + describe('manager and EPM; no Fleet', () => { + let root: ReturnType; + beforeAll(async () => { + const ingestManagerConfig = { + enabled: true, + epm: { enabled: true }, + }; + root = createXPackRoot({ + ingestManager: ingestManagerConfig, + }); + await root.setup(); + await root.start(); + }, 30000); + + afterAll(async () => await root.shutdown()); + + it('has agent config api', async () => { + await kbnTestServer.request.get(root, '/api/ingest_manager/agent_configs').expect(200); + }); + + it('has datasources api', async () => { + await kbnTestServer.request.get(root, '/api/ingest_manager/datasources').expect(200); + }); + + it('does not have EPM api', async () => { + await kbnTestServer.request.get(root, '/api/ingest_manager/epm/list').expect(404); + }); + + it('does not have Fleet api', async () => { + await kbnTestServer.request.get(root, '/api/ingest_manager/fleet/setup').expect(404); + }); + }); + + describe('manager and Fleet; no EPM)', () => { + let root: ReturnType; + beforeAll(async () => { + const ingestManagerConfig = { + enabled: true, + epm: { enabled: true }, + fleet: { enabled: true }, + }; + root = createXPackRoot({ + ingestManager: ingestManagerConfig, + }); + await root.setup(); + await root.start(); + }, 30000); + + afterAll(async () => await root.shutdown()); + + it('has agent config api', async () => { + await kbnTestServer.request.get(root, '/api/ingest_manager/agent_configs').expect(200); + }); + + it('has datasources api', async () => { + await kbnTestServer.request.get(root, '/api/ingest_manager/datasources').expect(200); + }); + + it('does not have EPM api', async () => { + await kbnTestServer.request.get(root, '/api/ingest_manager/epm/list').expect(404); + }); + + it('does not have Fleet api', async () => { + await kbnTestServer.request.get(root, '/api/ingest_manager/fleet/setup').expect(404); + }); + }); + + describe('all flags enabled: manager, EPM, and Fleet)', () => { + let root: ReturnType; + beforeAll(async () => { + const ingestManagerConfig = { + enabled: true, + epm: { enabled: true }, + fleet: { enabled: true }, + }; + root = createXPackRoot({ + ingestManager: ingestManagerConfig, + }); + await root.setup(); + await root.start(); + }, 30000); + + afterAll(async () => await root.shutdown()); + + it('has agent config api', async () => { + await kbnTestServer.request.get(root, '/api/ingest_manager/agent_configs').expect(200); + }); + + it('has datasources api', async () => { + await kbnTestServer.request.get(root, '/api/ingest_manager/datasources').expect(200); + }); + + it('does not have EPM api', async () => { + await kbnTestServer.request.get(root, '/api/ingest_manager/epm/list').expect(404); + }); + + it('does not have Fleet api', async () => { + await kbnTestServer.request.get(root, '/api/ingest_manager/fleet/setup').expect(404); + }); + }); +}); diff --git a/x-pack/plugins/ingest_manager/server/plugin.ts b/x-pack/plugins/ingest_manager/server/plugin.ts new file mode 100644 index 0000000000000..4f30a171ab0c0 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/plugin.ts @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { Observable } from 'rxjs'; +import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'kibana/server'; +import { LicensingPluginSetup } from '../../licensing/server'; +import { EncryptedSavedObjectsPluginStart } from '../../encrypted_saved_objects/server'; +import { SecurityPluginSetup } from '../../security/server'; +import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; +import { PLUGIN_ID } from './constants'; +import { appContextService } from './services'; +import { registerDatasourceRoutes, registerAgentConfigRoutes } from './routes'; +import { IngestManagerConfigType } from '../common'; + +export interface IngestManagerSetupDeps { + licensing: LicensingPluginSetup; + security?: SecurityPluginSetup; + features?: FeaturesPluginSetup; +} + +export interface IngestManagerAppContext { + encryptedSavedObjects: EncryptedSavedObjectsPluginStart; + security?: SecurityPluginSetup; + config$?: Observable; +} + +export class IngestManagerPlugin implements Plugin { + private config$: Observable; + private security: SecurityPluginSetup | undefined; + + constructor(private readonly initializerContext: PluginInitializerContext) { + this.config$ = this.initializerContext.config.create(); + } + + public async setup(core: CoreSetup, deps: IngestManagerSetupDeps) { + if (deps.security) { + this.security = deps.security; + } + + // Register feature + // TODO: Flesh out privileges + if (deps.features) { + deps.features.registerFeature({ + id: PLUGIN_ID, + name: 'Ingest Manager', + icon: 'savedObjectsApp', + navLinkId: PLUGIN_ID, + app: [PLUGIN_ID, 'kibana'], + privileges: { + all: { + api: [PLUGIN_ID], + savedObject: { + all: [], + read: [], + }, + ui: ['show'], + }, + read: { + api: [PLUGIN_ID], + savedObject: { + all: [], + read: [], + }, + ui: ['show'], + }, + }, + }); + } + + // Create router + const router = core.http.createRouter(); + + // Register routes + registerAgentConfigRoutes(router); + registerDatasourceRoutes(router); + + // Optional route registration depending on Kibana config + // restore when EPM & Fleet features are added + // const config = await this.config$.pipe(first()).toPromise(); + // if (config.epm.enabled) registerEPMRoutes(router); + // if (config.fleet.enabled) registerFleetSetupRoutes(router); + } + + public async start( + core: CoreStart, + plugins: { + encryptedSavedObjects: EncryptedSavedObjectsPluginStart; + } + ) { + appContextService.start({ + encryptedSavedObjects: plugins.encryptedSavedObjects, + security: this.security, + config$: this.config$, + }); + } + + public async stop() { + appContextService.stop(); + } +} diff --git a/x-pack/plugins/ingest_manager/server/routes/agent_config/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent_config/handlers.ts new file mode 100644 index 0000000000000..67da6a4cf2f1d --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/routes/agent_config/handlers.ts @@ -0,0 +1,144 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { TypeOf } from '@kbn/config-schema'; +import { RequestHandler } from 'kibana/server'; +import { appContextService, agentConfigService } from '../../services'; +import { + GetAgentConfigsRequestSchema, + GetAgentConfigsResponse, + GetOneAgentConfigRequestSchema, + GetOneAgentConfigResponse, + CreateAgentConfigRequestSchema, + CreateAgentConfigResponse, + UpdateAgentConfigRequestSchema, + UpdateAgentConfigResponse, + DeleteAgentConfigsRequestSchema, + DeleteAgentConfigsResponse, +} from '../../types'; + +export const getAgentConfigsHandler: RequestHandler< + undefined, + TypeOf +> = async (context, request, response) => { + const soClient = context.core.savedObjects.client; + try { + const { items, total, page, perPage } = await agentConfigService.list(soClient, request.query); + const body: GetAgentConfigsResponse = { + items, + total, + page, + perPage, + success: true, + }; + return response.ok({ body }); + } catch (e) { + return response.customError({ + statusCode: 500, + body: { message: e.message }, + }); + } +}; + +export const getOneAgentConfigHandler: RequestHandler> = async (context, request, response) => { + const soClient = context.core.savedObjects.client; + try { + const agentConfig = await agentConfigService.get(soClient, request.params.agentConfigId); + if (agentConfig) { + const body: GetOneAgentConfigResponse = { + item: agentConfig, + success: true, + }; + return response.ok({ + body, + }); + } else { + return response.customError({ + statusCode: 404, + body: { message: 'Agent config not found' }, + }); + } + } catch (e) { + return response.customError({ + statusCode: 500, + body: { message: e.message }, + }); + } +}; + +export const createAgentConfigHandler: RequestHandler< + undefined, + undefined, + TypeOf +> = async (context, request, response) => { + const soClient = context.core.savedObjects.client; + const user = await appContextService.getSecurity()?.authc.getCurrentUser(request); + try { + const agentConfig = await agentConfigService.create(soClient, request.body, { + user: user || undefined, + }); + const body: CreateAgentConfigResponse = { item: agentConfig, success: true }; + return response.ok({ + body, + }); + } catch (e) { + return response.customError({ + statusCode: 500, + body: { message: e.message }, + }); + } +}; + +export const updateAgentConfigHandler: RequestHandler< + TypeOf, + unknown, + TypeOf +> = async (context, request, response) => { + const soClient = context.core.savedObjects.client; + const user = await appContextService.getSecurity()?.authc.getCurrentUser(request); + try { + const agentConfig = await agentConfigService.update( + soClient, + request.params.agentConfigId, + request.body, + { + user: user || undefined, + } + ); + const body: UpdateAgentConfigResponse = { item: agentConfig, success: true }; + return response.ok({ + body, + }); + } catch (e) { + return response.customError({ + statusCode: 500, + body: { message: e.message }, + }); + } +}; + +export const deleteAgentConfigsHandler: RequestHandler< + unknown, + unknown, + TypeOf +> = async (context, request, response) => { + const soClient = context.core.savedObjects.client; + try { + const body: DeleteAgentConfigsResponse = await agentConfigService.delete( + soClient, + request.body.agentConfigIds + ); + return response.ok({ + body, + }); + } catch (e) { + return response.customError({ + statusCode: 500, + body: { message: e.message }, + }); + } +}; diff --git a/x-pack/plugins/ingest_manager/server/routes/agent_config/index.ts b/x-pack/plugins/ingest_manager/server/routes/agent_config/index.ts new file mode 100644 index 0000000000000..67ad915b71e45 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/routes/agent_config/index.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { IRouter } from 'kibana/server'; +import { PLUGIN_ID, AGENT_CONFIG_API_ROUTES } from '../../constants'; +import { + GetAgentConfigsRequestSchema, + GetOneAgentConfigRequestSchema, + CreateAgentConfigRequestSchema, + UpdateAgentConfigRequestSchema, + DeleteAgentConfigsRequestSchema, +} from '../../types'; +import { + getAgentConfigsHandler, + getOneAgentConfigHandler, + createAgentConfigHandler, + updateAgentConfigHandler, + deleteAgentConfigsHandler, +} from './handlers'; + +export const registerRoutes = (router: IRouter) => { + // List + router.get( + { + path: AGENT_CONFIG_API_ROUTES.LIST_PATTERN, + validate: GetAgentConfigsRequestSchema, + options: { tags: [`access:${PLUGIN_ID}`] }, + }, + getAgentConfigsHandler + ); + + // Get one + router.get( + { + path: AGENT_CONFIG_API_ROUTES.INFO_PATTERN, + validate: GetOneAgentConfigRequestSchema, + options: { tags: [`access:${PLUGIN_ID}`] }, + }, + getOneAgentConfigHandler + ); + + // Create + router.post( + { + path: AGENT_CONFIG_API_ROUTES.CREATE_PATTERN, + validate: CreateAgentConfigRequestSchema, + options: { tags: [`access:${PLUGIN_ID}`] }, + }, + createAgentConfigHandler + ); + + // Update + router.put( + { + path: AGENT_CONFIG_API_ROUTES.UPDATE_PATTERN, + validate: UpdateAgentConfigRequestSchema, + options: { tags: [`access:${PLUGIN_ID}`] }, + }, + updateAgentConfigHandler + ); + + // Delete + router.post( + { + path: AGENT_CONFIG_API_ROUTES.DELETE_PATTERN, + validate: DeleteAgentConfigsRequestSchema, + options: { tags: [`access:${PLUGIN_ID}`] }, + }, + deleteAgentConfigsHandler + ); +}; diff --git a/x-pack/plugins/ingest_manager/server/routes/datasource/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/datasource/handlers.ts new file mode 100644 index 0000000000000..78cad2e21c5fa --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/routes/datasource/handlers.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; + * you may not use this file except in compliance with the Elastic License. + */ +import { TypeOf } from '@kbn/config-schema'; +import { RequestHandler } from 'kibana/server'; +import { datasourceService } from '../../services'; +import { + GetDatasourcesRequestSchema, + GetOneDatasourceRequestSchema, + CreateDatasourceRequestSchema, + UpdateDatasourceRequestSchema, + DeleteDatasourcesRequestSchema, + DeleteDatasourcesResponse, +} from '../../types'; + +export const getDatasourcesHandler: RequestHandler< + undefined, + TypeOf +> = async (context, request, response) => { + const soClient = context.core.savedObjects.client; + try { + const { items, total, page, perPage } = await datasourceService.list(soClient, request.query); + return response.ok({ + body: { + items, + total, + page, + perPage, + success: true, + }, + }); + } catch (e) { + return response.customError({ + statusCode: 500, + body: { message: e.message }, + }); + } +}; + +export const getOneDatasourceHandler: RequestHandler> = async (context, request, response) => { + const soClient = context.core.savedObjects.client; + try { + const datasource = await datasourceService.get(soClient, request.params.datasourceId); + if (datasource) { + return response.ok({ + body: { + item: datasource, + success: true, + }, + }); + } else { + return response.customError({ + statusCode: 404, + body: { message: 'Datasource not found' }, + }); + } + } catch (e) { + return response.customError({ + statusCode: 500, + body: { message: e.message }, + }); + } +}; + +export const createDatasourceHandler: RequestHandler< + undefined, + undefined, + TypeOf +> = async (context, request, response) => { + const soClient = context.core.savedObjects.client; + try { + const datasource = await datasourceService.create(soClient, request.body); + return response.ok({ + body: { item: datasource, success: true }, + }); + } catch (e) { + return response.customError({ + statusCode: 500, + body: { message: e.message }, + }); + } +}; + +export const updateDatasourceHandler: RequestHandler< + TypeOf, + unknown, + TypeOf +> = async (context, request, response) => { + const soClient = context.core.savedObjects.client; + try { + const datasource = await datasourceService.update( + soClient, + request.params.datasourceId, + request.body + ); + return response.ok({ + body: { item: datasource, success: true }, + }); + } catch (e) { + return response.customError({ + statusCode: 500, + body: { message: e.message }, + }); + } +}; + +export const deleteDatasourcesHandler: RequestHandler< + unknown, + unknown, + TypeOf +> = async (context, request, response) => { + const soClient = context.core.savedObjects.client; + try { + const body: DeleteDatasourcesResponse = await datasourceService.delete( + soClient, + request.body.datasourceIds + ); + return response.ok({ + body, + }); + } catch (e) { + return response.customError({ + statusCode: 500, + body: { message: e.message }, + }); + } +}; diff --git a/x-pack/plugins/ingest_manager/server/routes/datasource/index.ts b/x-pack/plugins/ingest_manager/server/routes/datasource/index.ts new file mode 100644 index 0000000000000..d9e3ba9de8838 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/routes/datasource/index.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { IRouter } from 'kibana/server'; +import { PLUGIN_ID, DATASOURCE_API_ROUTES } from '../../constants'; +import { + GetDatasourcesRequestSchema, + GetOneDatasourceRequestSchema, + CreateDatasourceRequestSchema, + UpdateDatasourceRequestSchema, + DeleteDatasourcesRequestSchema, +} from '../../types'; +import { + getDatasourcesHandler, + getOneDatasourceHandler, + createDatasourceHandler, + updateDatasourceHandler, + deleteDatasourcesHandler, +} from './handlers'; + +export const registerRoutes = (router: IRouter) => { + // List + router.get( + { + path: DATASOURCE_API_ROUTES.LIST_PATTERN, + validate: GetDatasourcesRequestSchema, + options: { tags: [`access:${PLUGIN_ID}`] }, + }, + getDatasourcesHandler + ); + + // Get one + router.get( + { + path: DATASOURCE_API_ROUTES.INFO_PATTERN, + validate: GetOneDatasourceRequestSchema, + options: { tags: [`access:${PLUGIN_ID}`] }, + }, + getOneDatasourceHandler + ); + + // Create + router.post( + { + path: DATASOURCE_API_ROUTES.CREATE_PATTERN, + validate: CreateDatasourceRequestSchema, + options: { tags: [`access:${PLUGIN_ID}`] }, + }, + createDatasourceHandler + ); + + // Update + router.put( + { + path: DATASOURCE_API_ROUTES.UPDATE_PATTERN, + validate: UpdateDatasourceRequestSchema, + options: { tags: [`access:${PLUGIN_ID}`] }, + }, + updateDatasourceHandler + ); + + // Delete + router.post( + { + path: DATASOURCE_API_ROUTES.DELETE_PATTERN, + validate: DeleteDatasourcesRequestSchema, + options: { tags: [`access:${PLUGIN_ID}`] }, + }, + deleteDatasourcesHandler + ); +}; diff --git a/x-pack/plugins/ingest_manager/server/routes/epm/index.ts b/x-pack/plugins/ingest_manager/server/routes/epm/index.ts new file mode 100644 index 0000000000000..7bdcafe633843 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/routes/epm/index.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { IRouter } from 'kibana/server'; +import { PLUGIN_ID, EPM_API_ROUTES } from '../../constants'; + +export const registerRoutes = (router: IRouter) => { + router.get( + { + path: EPM_API_ROUTES.CATEGORIES_PATTERN, + validate: false, + options: { tags: [`access:${PLUGIN_ID}`] }, + }, + async (context, req, res) => { + return res.ok({ body: { hello: 'world' } }); + } + ); + + router.get( + { + path: EPM_API_ROUTES.LIST_PATTERN, + validate: false, + options: { tags: [`access:${PLUGIN_ID}`] }, + }, + async (context, req, res) => { + return res.ok({ body: { hello: 'world' } }); + } + ); + + router.get( + { + path: `${EPM_API_ROUTES.INFO_PATTERN}/{filePath*}`, + validate: false, + options: { tags: [`access:${PLUGIN_ID}`] }, + }, + async (context, req, res) => { + return res.ok({ body: { hello: 'world' } }); + } + ); + + router.get( + { + path: EPM_API_ROUTES.INFO_PATTERN, + validate: false, + options: { tags: [`access:${PLUGIN_ID}`] }, + }, + async (context, req, res) => { + return res.ok({ body: { hello: 'world' } }); + } + ); + + router.get( + { + path: EPM_API_ROUTES.INSTALL_PATTERN, + validate: false, + options: { tags: [`access:${PLUGIN_ID}`] }, + }, + async (context, req, res) => { + return res.ok({ body: { hello: 'world' } }); + } + ); + + router.get( + { + path: EPM_API_ROUTES.DELETE_PATTERN, + validate: false, + options: { tags: [`access:${PLUGIN_ID}`] }, + }, + async (context, req, res) => { + return res.ok({ body: { hello: 'world' } }); + } + ); +}; diff --git a/x-pack/plugins/ingest_manager/server/routes/fleet_setup/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/fleet_setup/handlers.ts new file mode 100644 index 0000000000000..72fe34eb23c5f --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/routes/fleet_setup/handlers.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { TypeOf } from '@kbn/config-schema'; +import { RequestHandler } from 'kibana/server'; +import { DEFAULT_OUTPUT_ID } from '../../constants'; +import { outputService, agentConfigService } from '../../services'; +import { CreateFleetSetupRequestSchema, CreateFleetSetupResponse } from '../../types'; + +export const getFleetSetupHandler: RequestHandler = async (context, request, response) => { + const soClient = context.core.savedObjects.client; + const successBody: CreateFleetSetupResponse = { isInitialized: true }; + const failureBody: CreateFleetSetupResponse = { isInitialized: false }; + try { + const output = await outputService.get(soClient, DEFAULT_OUTPUT_ID); + if (output) { + return response.ok({ + body: successBody, + }); + } else { + return response.ok({ + body: failureBody, + }); + } + } catch (e) { + return response.ok({ + body: failureBody, + }); + } +}; + +export const createFleetSetupHandler: RequestHandler< + undefined, + undefined, + TypeOf +> = async (context, request, response) => { + const soClient = context.core.savedObjects.client; + try { + await outputService.createDefaultOutput(soClient, { + username: request.body.admin_username, + password: request.body.admin_password, + }); + await agentConfigService.ensureDefaultAgentConfig(soClient); + return response.ok({ + body: { isInitialized: true }, + }); + } catch (e) { + return response.customError({ + statusCode: 500, + body: { message: e.message }, + }); + } +}; diff --git a/x-pack/plugins/ingest_manager/server/routes/fleet_setup/index.ts b/x-pack/plugins/ingest_manager/server/routes/fleet_setup/index.ts new file mode 100644 index 0000000000000..c23164db6b6eb --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/routes/fleet_setup/index.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { IRouter } from 'kibana/server'; +import { PLUGIN_ID, FLEET_SETUP_API_ROUTES } from '../../constants'; +import { GetFleetSetupRequestSchema, CreateFleetSetupRequestSchema } from '../../types'; +import { getFleetSetupHandler, createFleetSetupHandler } from './handlers'; + +export const registerRoutes = (router: IRouter) => { + // Get + router.get( + { + path: FLEET_SETUP_API_ROUTES.INFO_PATTERN, + validate: GetFleetSetupRequestSchema, + options: { tags: [`access:${PLUGIN_ID}`] }, + }, + getFleetSetupHandler + ); + + // Create + router.post( + { + path: FLEET_SETUP_API_ROUTES.CREATE_PATTERN, + validate: CreateFleetSetupRequestSchema, + options: { tags: [`access:${PLUGIN_ID}`] }, + }, + createFleetSetupHandler + ); +}; diff --git a/x-pack/plugins/ingest_manager/server/routes/index.ts b/x-pack/plugins/ingest_manager/server/routes/index.ts new file mode 100644 index 0000000000000..b458ef31dee45 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/routes/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +export { registerRoutes as registerAgentConfigRoutes } from './agent_config'; +export { registerRoutes as registerDatasourceRoutes } from './datasource'; +export { registerRoutes as registerEPMRoutes } from './epm'; +export { registerRoutes as registerFleetSetupRoutes } from './fleet_setup'; diff --git a/x-pack/plugins/ingest_manager/server/saved_objects.ts b/x-pack/plugins/ingest_manager/server/saved_objects.ts new file mode 100644 index 0000000000000..976556f388acf --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/saved_objects.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { + OUTPUT_SAVED_OBJECT_TYPE, + AGENT_CONFIG_SAVED_OBJECT_TYPE, + DATASOURCE_SAVED_OBJECT_TYPE, +} from './constants'; + +/* + * Saved object mappings + * + * Please update typings in `/common/types` if mappings are updated. + */ +export const savedObjectMappings = { + [AGENT_CONFIG_SAVED_OBJECT_TYPE]: { + properties: { + id: { type: 'keyword' }, + name: { type: 'text' }, + namespace: { type: 'keyword' }, + description: { type: 'text' }, + status: { type: 'keyword' }, + datasources: { type: 'keyword' }, + updated_on: { type: 'keyword' }, + updated_by: { type: 'keyword' }, + }, + }, + [OUTPUT_SAVED_OBJECT_TYPE]: { + properties: { + id: { type: 'keyword' }, + name: { type: 'keyword' }, + type: { type: 'keyword' }, + username: { type: 'keyword' }, + password: { type: 'keyword' }, + index_name: { type: 'keyword' }, + ingest_pipeline: { type: 'keyword' }, + hosts: { type: 'keyword' }, + api_key: { type: 'keyword' }, + admin_username: { type: 'binary' }, + admin_password: { type: 'binary' }, + config: { type: 'flattened' }, + }, + }, + [DATASOURCE_SAVED_OBJECT_TYPE]: { + properties: { + id: { type: 'keyword' }, + name: { type: 'keyword' }, + namespace: { type: 'keyword' }, + read_alias: { type: 'keyword' }, + agent_config_id: { type: 'keyword' }, + package: { + properties: { + assets: { + properties: { + id: { type: 'keyword' }, + type: { type: 'keyword' }, + }, + }, + description: { type: 'keyword' }, + name: { type: 'keyword' }, + title: { type: 'keyword' }, + version: { type: 'keyword' }, + }, + }, + streams: { + properties: { + config: { type: 'flattened' }, + id: { type: 'keyword' }, + input: { type: 'flattened' }, + output_id: { type: 'keyword' }, + processors: { type: 'keyword' }, + }, + }, + }, + }, +}; diff --git a/x-pack/plugins/ingest_manager/server/services/agent_config.ts b/x-pack/plugins/ingest_manager/server/services/agent_config.ts new file mode 100644 index 0000000000000..0690e115ca862 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/agent_config.ts @@ -0,0 +1,258 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { SavedObjectsClientContract } from 'kibana/server'; +import { AuthenticatedUser } from '../../../security/server'; +import { + DEFAULT_AGENT_CONFIG_ID, + DEFAULT_AGENT_CONFIG, + AGENT_CONFIG_SAVED_OBJECT_TYPE, +} from '../constants'; +import { + NewAgentConfig, + AgentConfig, + AgentConfigStatus, + AgentConfigUpdateHandler, + ListWithKuery, + DeleteAgentConfigsResponse, +} from '../types'; +import { datasourceService } from './datasource'; + +const SAVED_OBJECT_TYPE = AGENT_CONFIG_SAVED_OBJECT_TYPE; + +class AgentConfigService { + private eventsHandler: AgentConfigUpdateHandler[] = []; + + public registerAgentConfigUpdateHandler(handler: AgentConfigUpdateHandler) { + this.eventsHandler.push(handler); + } + + public triggerAgentConfigUpdatedEvent: AgentConfigUpdateHandler = async ( + action, + agentConfigId + ) => { + for (const handler of this.eventsHandler) { + await handler(action, agentConfigId); + } + }; + + private async _update( + soClient: SavedObjectsClientContract, + id: string, + agentConfig: Partial, + user?: AuthenticatedUser + ): Promise { + await soClient.update(SAVED_OBJECT_TYPE, id, { + ...agentConfig, + updated_on: new Date().toString(), + updated_by: user ? user.username : 'system', + }); + + await this.triggerAgentConfigUpdatedEvent('updated', id); + + return (await this.get(soClient, id)) as AgentConfig; + } + + public async ensureDefaultAgentConfig(soClient: SavedObjectsClientContract) { + let defaultAgentConfig; + + try { + defaultAgentConfig = await this.get(soClient, DEFAULT_AGENT_CONFIG_ID); + } catch (err) { + if (!err.isBoom || err.output.statusCode !== 404) { + throw err; + } + } + + if (!defaultAgentConfig) { + const newDefaultAgentConfig: NewAgentConfig = { + ...DEFAULT_AGENT_CONFIG, + }; + + await this.create(soClient, newDefaultAgentConfig, { + id: DEFAULT_AGENT_CONFIG_ID, + }); + } + } + + public async create( + soClient: SavedObjectsClientContract, + agentConfig: NewAgentConfig, + options?: { id?: string; user?: AuthenticatedUser } + ): Promise { + const newSo = await soClient.create( + SAVED_OBJECT_TYPE, + { + ...agentConfig, + updated_on: new Date().toISOString(), + updated_by: options?.user?.username || 'system', + } as AgentConfig, + options + ); + + await this.triggerAgentConfigUpdatedEvent('created', newSo.id); + + return { + id: newSo.id, + ...newSo.attributes, + }; + } + + public async get(soClient: SavedObjectsClientContract, id: string): Promise { + const agentConfigSO = await soClient.get(SAVED_OBJECT_TYPE, id); + if (!agentConfigSO) { + return null; + } + + if (agentConfigSO.error) { + throw new Error(agentConfigSO.error.message); + } + + return { + id: agentConfigSO.id, + ...agentConfigSO.attributes, + datasources: + (await datasourceService.getByIDs( + soClient, + (agentConfigSO.attributes.datasources as string[]) || [] + )) || [], + }; + } + + public async list( + soClient: SavedObjectsClientContract, + options: ListWithKuery + ): Promise<{ items: AgentConfig[]; total: number; page: number; perPage: number }> { + const { page = 1, perPage = 20, kuery } = options; + + const agentConfigs = await soClient.find({ + type: SAVED_OBJECT_TYPE, + page, + perPage, + // To ensure users don't need to know about SO data structure... + filter: kuery + ? kuery.replace( + new RegExp(`${SAVED_OBJECT_TYPE}\.`, 'g'), + `${SAVED_OBJECT_TYPE}.attributes.` + ) + : undefined, + }); + + return { + items: agentConfigs.saved_objects.map(agentConfigSO => { + return { + id: agentConfigSO.id, + ...agentConfigSO.attributes, + }; + }), + total: agentConfigs.total, + page, + perPage, + }; + } + + public async update( + soClient: SavedObjectsClientContract, + id: string, + agentConfig: Partial, + options?: { user?: AuthenticatedUser } + ): Promise { + const oldAgentConfig = await this.get(soClient, id); + + if (!oldAgentConfig) { + throw new Error('Agent config not found'); + } + + if ( + oldAgentConfig.status === AgentConfigStatus.Inactive && + agentConfig.status !== AgentConfigStatus.Active + ) { + throw new Error( + `Agent config ${id} cannot be updated because it is ${oldAgentConfig.status}` + ); + } + + return this._update(soClient, id, agentConfig, options?.user); + } + + public async assignDatasources( + soClient: SavedObjectsClientContract, + id: string, + datasourceIds: string[], + options?: { user?: AuthenticatedUser } + ): Promise { + const oldAgentConfig = await this.get(soClient, id); + + if (!oldAgentConfig) { + throw new Error('Agent config not found'); + } + + return await this._update( + soClient, + id, + { + ...oldAgentConfig, + datasources: [...((oldAgentConfig.datasources || []) as string[])].concat(datasourceIds), + }, + options?.user + ); + } + + public async unassignDatasources( + soClient: SavedObjectsClientContract, + id: string, + datasourceIds: string[], + options?: { user?: AuthenticatedUser } + ): Promise { + const oldAgentConfig = await this.get(soClient, id); + + if (!oldAgentConfig) { + throw new Error('Agent config not found'); + } + + return await this._update( + soClient, + id, + { + ...oldAgentConfig, + datasources: [...((oldAgentConfig.datasources || []) as string[])].filter( + dsId => !datasourceIds.includes(dsId) + ), + }, + options?.user + ); + } + + public async delete( + soClient: SavedObjectsClientContract, + ids: string[] + ): Promise { + const result: DeleteAgentConfigsResponse = []; + + if (ids.includes(DEFAULT_AGENT_CONFIG_ID)) { + throw new Error('The default agent configuration cannot be deleted'); + } + + for (const id of ids) { + try { + await soClient.delete(SAVED_OBJECT_TYPE, id); + await this.triggerAgentConfigUpdatedEvent('deleted', id); + result.push({ + id, + success: true, + }); + } catch (e) { + result.push({ + id, + success: false, + }); + } + } + + return result; + } +} + +export const agentConfigService = new AgentConfigService(); diff --git a/x-pack/plugins/ingest_manager/server/services/app_context.ts b/x-pack/plugins/ingest_manager/server/services/app_context.ts new file mode 100644 index 0000000000000..69a014fca37fb --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/app_context.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { BehaviorSubject, Observable } from 'rxjs'; +import { first } from 'rxjs/operators'; +import { EncryptedSavedObjectsPluginStart } from '../../../encrypted_saved_objects/server'; +import { SecurityPluginSetup } from '../../../security/server'; +import { IngestManagerConfigType } from '../../common'; +import { IngestManagerAppContext } from '../plugin'; + +class AppContextService { + private encryptedSavedObjects: EncryptedSavedObjectsPluginStart | undefined; + private security: SecurityPluginSetup | undefined; + private config$?: Observable; + private configSubject$?: BehaviorSubject; + + public async start(appContext: IngestManagerAppContext) { + this.encryptedSavedObjects = appContext.encryptedSavedObjects; + this.security = appContext.security; + + if (appContext.config$) { + this.config$ = appContext.config$; + const initialValue = await this.config$.pipe(first()).toPromise(); + this.configSubject$ = new BehaviorSubject(initialValue); + this.config$.subscribe(this.configSubject$); + } + } + + public stop() {} + + public getEncryptedSavedObjects() { + return this.encryptedSavedObjects; + } + + public getSecurity() { + return this.security; + } + + public getConfig() { + return this.configSubject$?.value; + } + + public getConfig$() { + return this.config$; + } +} + +export const appContextService = new AppContextService(); diff --git a/x-pack/plugins/ingest_manager/server/services/datasource.ts b/x-pack/plugins/ingest_manager/server/services/datasource.ts new file mode 100644 index 0000000000000..b305ccaab777b --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/datasource.ts @@ -0,0 +1,132 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { SavedObjectsClientContract } from 'kibana/server'; +import { DATASOURCE_SAVED_OBJECT_TYPE } from '../constants'; +import { NewDatasource, Datasource, DeleteDatasourcesResponse, ListWithKuery } from '../types'; + +const SAVED_OBJECT_TYPE = DATASOURCE_SAVED_OBJECT_TYPE; + +class DatasourceService { + public async create( + soClient: SavedObjectsClientContract, + datasource: NewDatasource, + options?: { id?: string } + ): Promise { + const newSo = await soClient.create( + SAVED_OBJECT_TYPE, + datasource as Datasource, + options + ); + + return { + id: newSo.id, + ...newSo.attributes, + }; + } + + public async get(soClient: SavedObjectsClientContract, id: string): Promise { + const datasourceSO = await soClient.get(SAVED_OBJECT_TYPE, id); + if (!datasourceSO) { + return null; + } + + if (datasourceSO.error) { + throw new Error(datasourceSO.error.message); + } + + return { + id: datasourceSO.id, + ...datasourceSO.attributes, + }; + } + + public async getByIDs( + soClient: SavedObjectsClientContract, + ids: string[] + ): Promise { + const datasourceSO = await soClient.bulkGet( + ids.map(id => ({ + id, + type: SAVED_OBJECT_TYPE, + })) + ); + if (!datasourceSO) { + return null; + } + + return datasourceSO.saved_objects.map(so => ({ + id: so.id, + ...so.attributes, + })); + } + + public async list( + soClient: SavedObjectsClientContract, + options: ListWithKuery + ): Promise<{ items: Datasource[]; total: number; page: number; perPage: number }> { + const { page = 1, perPage = 20, kuery } = options; + + const datasources = await soClient.find({ + type: SAVED_OBJECT_TYPE, + page, + perPage, + // To ensure users don't need to know about SO data structure... + filter: kuery + ? kuery.replace( + new RegExp(`${SAVED_OBJECT_TYPE}\.`, 'g'), + `${SAVED_OBJECT_TYPE}.attributes.` + ) + : undefined, + }); + + return { + items: datasources.saved_objects.map(datasourceSO => { + return { + id: datasourceSO.id, + ...datasourceSO.attributes, + }; + }), + total: datasources.total, + page, + perPage, + }; + } + + public async update( + soClient: SavedObjectsClientContract, + id: string, + datasource: NewDatasource + ): Promise { + await soClient.update(SAVED_OBJECT_TYPE, id, datasource); + return (await this.get(soClient, id)) as Datasource; + } + + public async delete( + soClient: SavedObjectsClientContract, + ids: string[] + ): Promise { + const result: DeleteDatasourcesResponse = []; + + for (const id of ids) { + try { + await soClient.delete(SAVED_OBJECT_TYPE, id); + result.push({ + id, + success: true, + }); + } catch (e) { + result.push({ + id, + success: false, + }); + } + } + + return result; + } +} + +export const datasourceService = new DatasourceService(); diff --git a/x-pack/plugins/ingest_manager/server/services/index.ts b/x-pack/plugins/ingest_manager/server/services/index.ts new file mode 100644 index 0000000000000..dd0c898afa425 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +export { appContextService } from './app_context'; + +// Saved object services +export { datasourceService } from './datasource'; +export { agentConfigService } from './agent_config'; +export { outputService } from './output'; diff --git a/x-pack/plugins/ingest_manager/server/services/output.ts b/x-pack/plugins/ingest_manager/server/services/output.ts new file mode 100644 index 0000000000000..8f60ed295205d --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/output.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ +import { SavedObjectsClientContract, KibanaRequest } from 'kibana/server'; +import { NewOutput, Output } from '../types'; +import { DEFAULT_OUTPUT, DEFAULT_OUTPUT_ID, OUTPUT_SAVED_OBJECT_TYPE } from '../constants'; +import { appContextService } from './app_context'; + +const SAVED_OBJECT_TYPE = OUTPUT_SAVED_OBJECT_TYPE; + +class OutputService { + public async createDefaultOutput( + soClient: SavedObjectsClientContract, + adminUser: { username: string; password: string } + ) { + let defaultOutput; + + try { + defaultOutput = await this.get(soClient, DEFAULT_OUTPUT_ID); + } catch (err) { + if (!err.isBoom || err.output.statusCode !== 404) { + throw err; + } + } + + if (!defaultOutput) { + const newDefaultOutput = { + ...DEFAULT_OUTPUT, + hosts: [appContextService.getConfig()!.fleet.defaultOutputHost], + api_key: await this.createDefaultOutputApiKey(adminUser.username, adminUser.password), + admin_username: adminUser.username, + admin_password: adminUser.password, + } as NewOutput; + + await this.create(soClient, newDefaultOutput, { + id: DEFAULT_OUTPUT_ID, + }); + } + } + + public async getAdminUser() { + const so = await appContextService + .getEncryptedSavedObjects() + ?.getDecryptedAsInternalUser(OUTPUT_SAVED_OBJECT_TYPE, DEFAULT_OUTPUT_ID); + + return { + username: so!.attributes.admin_username, + password: so!.attributes.admin_password, + }; + } + + // TODO: TEMPORARY this is going to be per agent + private async createDefaultOutputApiKey(username: string, password: string): Promise { + const key = await appContextService.getSecurity()?.authc.createAPIKey( + { + headers: { + authorization: `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`, + }, + } as KibanaRequest, + { + name: 'fleet-default-output', + role_descriptors: { + 'fleet-output': { + cluster: ['monitor'], + index: [ + { + names: ['logs-*', 'metrics-*'], + privileges: ['write'], + }, + ], + }, + }, + } + ); + if (!key) { + throw new Error('An error occured while creating default API Key'); + } + return `${key.id}:${key.api_key}`; + } + + public async create( + soClient: SavedObjectsClientContract, + output: NewOutput, + options?: { id?: string } + ): Promise { + const newSo = await soClient.create(SAVED_OBJECT_TYPE, output as Output, options); + + return { + id: newSo.id, + ...newSo.attributes, + }; + } + + public async get(soClient: SavedObjectsClientContract, id: string): Promise { + const outputSO = await soClient.get(SAVED_OBJECT_TYPE, id); + if (!outputSO) { + return null; + } + + if (outputSO.error) { + throw new Error(outputSO.error.message); + } + + return { + id: outputSO.id, + ...outputSO.attributes, + }; + } +} + +export const outputService = new OutputService(); diff --git a/x-pack/plugins/ingest_manager/server/types/index.tsx b/x-pack/plugins/ingest_manager/server/types/index.tsx new file mode 100644 index 0000000000000..f44d03923d424 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/types/index.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ +export * from './models'; +export * from './rest_spec'; + +export type AgentConfigUpdateHandler = ( + action: 'created' | 'updated' | 'deleted', + agentConfigId: string +) => Promise; diff --git a/x-pack/plugins/ingest_manager/server/types/models/agent_config.ts b/x-pack/plugins/ingest_manager/server/types/models/agent_config.ts new file mode 100644 index 0000000000000..d3acb0c167837 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/types/models/agent_config.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; + * you may not use this file except in compliance with the Elastic License. + */ +import { schema, TypeOf } from '@kbn/config-schema'; +import { DatasourceSchema } from './datasource'; + +export enum AgentConfigStatus { + Active = 'active', + Inactive = 'inactive', +} + +const AgentConfigBaseSchema = { + name: schema.string(), + namespace: schema.string(), + description: schema.maybe(schema.string()), +}; + +export const NewAgentConfigSchema = schema.object({ + ...AgentConfigBaseSchema, +}); + +export const AgentConfigSchema = schema.object({ + ...AgentConfigBaseSchema, + id: schema.string(), + status: schema.oneOf([ + schema.literal(AgentConfigStatus.Active), + schema.literal(AgentConfigStatus.Inactive), + ]), + datasources: schema.oneOf([schema.arrayOf(schema.string()), schema.arrayOf(DatasourceSchema)]), + updated_on: schema.string(), + updated_by: schema.string(), +}); + +export type NewAgentConfig = TypeOf; + +export type AgentConfig = TypeOf; diff --git a/x-pack/plugins/ingest_manager/server/types/models/datasource.ts b/x-pack/plugins/ingest_manager/server/types/models/datasource.ts new file mode 100644 index 0000000000000..4179d4c3a511a --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/types/models/datasource.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { schema, TypeOf } from '@kbn/config-schema'; + +const DatasourceBaseSchema = { + name: schema.string(), + namespace: schema.maybe(schema.string()), + read_alias: schema.maybe(schema.string()), + agent_config_id: schema.string(), + package: schema.maybe( + schema.object({ + assets: schema.arrayOf( + schema.object({ + id: schema.string(), + type: schema.string(), + }) + ), + description: schema.string(), + name: schema.string(), + title: schema.string(), + version: schema.string(), + }) + ), + streams: schema.arrayOf( + schema.object({ + config: schema.recordOf(schema.string(), schema.any()), + input: schema.object({ + type: schema.string(), + config: schema.recordOf(schema.string(), schema.any()), + fields: schema.maybe(schema.arrayOf(schema.recordOf(schema.string(), schema.any()))), + ilm_policy: schema.maybe(schema.string()), + index_template: schema.maybe(schema.string()), + ingest_pipelines: schema.maybe(schema.arrayOf(schema.string())), + }), + output_id: schema.string(), + processors: schema.maybe(schema.arrayOf(schema.string())), + }) + ), +}; + +export const NewDatasourceSchema = schema.object({ + ...DatasourceBaseSchema, +}); + +export const DatasourceSchema = schema.object({ + ...DatasourceBaseSchema, + id: schema.string(), +}); + +export type NewDatasource = TypeOf; + +export type Datasource = TypeOf; diff --git a/x-pack/plugins/ingest_manager/server/types/models/index.ts b/x-pack/plugins/ingest_manager/server/types/models/index.ts new file mode 100644 index 0000000000000..959dfe1d937b9 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/types/models/index.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; + * you may not use this file except in compliance with the Elastic License. + */ +export * from './agent_config'; +export * from './datasource'; +export * from './output'; diff --git a/x-pack/plugins/ingest_manager/server/types/models/output.ts b/x-pack/plugins/ingest_manager/server/types/models/output.ts new file mode 100644 index 0000000000000..610fa6796cc2d --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/types/models/output.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { schema, TypeOf } from '@kbn/config-schema'; + +export enum OutputType { + Elasticsearch = 'elasticsearch', +} + +const OutputBaseSchema = { + name: schema.string(), + type: schema.oneOf([schema.literal(OutputType.Elasticsearch)]), + username: schema.maybe(schema.string()), + password: schema.maybe(schema.string()), + index_name: schema.maybe(schema.string()), + ingest_pipeline: schema.maybe(schema.string()), + hosts: schema.maybe(schema.arrayOf(schema.string())), + api_key: schema.maybe(schema.string()), + admin_username: schema.maybe(schema.string()), + admin_password: schema.maybe(schema.string()), + config: schema.maybe(schema.recordOf(schema.string(), schema.any())), +}; + +export const NewOutputSchema = schema.object({ + ...OutputBaseSchema, +}); + +export const OutputSchema = schema.object({ + ...OutputBaseSchema, + id: schema.string(), +}); + +export type NewOutput = TypeOf; + +export type Output = TypeOf; diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/agent_config.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/agent_config.ts new file mode 100644 index 0000000000000..cb4680a4eed04 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/agent_config.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; + * you may not use this file except in compliance with the Elastic License. + */ +import { schema } from '@kbn/config-schema'; +import { AgentConfig, NewAgentConfigSchema } from '../models'; +import { ListWithKuerySchema } from './common'; + +export const GetAgentConfigsRequestSchema = { + query: ListWithKuerySchema, +}; + +export interface GetAgentConfigsResponse { + items: AgentConfig[]; + total: number; + page: number; + perPage: number; + success: boolean; +} + +export const GetOneAgentConfigRequestSchema = { + params: schema.object({ + agentConfigId: schema.string(), + }), +}; + +export interface GetOneAgentConfigResponse { + item: AgentConfig; + success: boolean; +} + +export const CreateAgentConfigRequestSchema = { + body: NewAgentConfigSchema, +}; + +export interface CreateAgentConfigResponse { + item: AgentConfig; + success: boolean; +} + +export const UpdateAgentConfigRequestSchema = { + ...GetOneAgentConfigRequestSchema, + body: NewAgentConfigSchema, +}; + +export interface UpdateAgentConfigResponse { + item: AgentConfig; + success: boolean; +} + +export const DeleteAgentConfigsRequestSchema = { + body: schema.object({ + agentConfigIds: schema.arrayOf(schema.string()), + }), +}; + +export type DeleteAgentConfigsResponse = Array<{ + id: string; + success: boolean; +}>; diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/common.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/common.ts new file mode 100644 index 0000000000000..2c8134d2e8f92 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/common.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; + * you may not use this file except in compliance with the Elastic License. + */ +import { schema, TypeOf } from '@kbn/config-schema'; + +export const ListWithKuerySchema = schema.object({ + page: schema.number({ defaultValue: 1 }), + perPage: schema.number({ defaultValue: 20 }), + kuery: schema.maybe(schema.string()), +}); + +export type ListWithKuery = TypeOf; diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/datasource.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/datasource.ts new file mode 100644 index 0000000000000..5c165517e9c9d --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/datasource.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; + * you may not use this file except in compliance with the Elastic License. + */ +import { schema } from '@kbn/config-schema'; +import { NewDatasourceSchema } from '../models'; +import { ListWithKuerySchema } from './common'; + +export const GetDatasourcesRequestSchema = { + query: ListWithKuerySchema, +}; + +export const GetOneDatasourceRequestSchema = { + params: schema.object({ + datasourceId: schema.string(), + }), +}; + +export const CreateDatasourceRequestSchema = { + body: NewDatasourceSchema, +}; + +export const UpdateDatasourceRequestSchema = { + ...GetOneDatasourceRequestSchema, + body: NewDatasourceSchema, +}; + +export const DeleteDatasourcesRequestSchema = { + body: schema.object({ + datasourceIds: schema.arrayOf(schema.string()), + }), +}; + +export type DeleteDatasourcesResponse = Array<{ + id: string; + success: boolean; +}>; diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/fleet_setup.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/fleet_setup.ts new file mode 100644 index 0000000000000..3772e6c24c56e --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/fleet_setup.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; + * you may not use this file except in compliance with the Elastic License. + */ +import { schema } from '@kbn/config-schema'; + +export const GetFleetSetupRequestSchema = {}; + +export const CreateFleetSetupRequestSchema = { + body: schema.object({ + admin_username: schema.string(), + admin_password: schema.string(), + }), +}; + +export interface CreateFleetSetupResponse { + isInitialized: boolean; +} diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/index.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/index.ts new file mode 100644 index 0000000000000..7d0d7e67f2db0 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +export * from './common'; +export * from './datasource'; +export * from './agent_config'; +export * from './fleet_setup'; diff --git a/x-pack/plugins/watcher/public/application/sections/watch_edit/components/threshold_watch_edit/watch_visualization.tsx b/x-pack/plugins/watcher/public/application/sections/watch_edit/components/threshold_watch_edit/watch_visualization.tsx index b2627e1c58aff..96f1669d8f0e3 100644 --- a/x-pack/plugins/watcher/public/application/sections/watch_edit/components/threshold_watch_edit/watch_visualization.tsx +++ b/x-pack/plugins/watcher/public/application/sections/watch_edit/components/threshold_watch_edit/watch_visualization.tsx @@ -180,7 +180,7 @@ export const WatchVisualization = () => { defaultMessage="Cannot load watch visualization" /> } - error={error as Error} + error={(error as unknown) as Error} /> diff --git a/x-pack/plugins/watcher/public/application/sections/watch_list/components/watch_list.tsx b/x-pack/plugins/watcher/public/application/sections/watch_list/components/watch_list.tsx index 2d552d7fbdda6..54f4209a137b9 100644 --- a/x-pack/plugins/watcher/public/application/sections/watch_list/components/watch_list.tsx +++ b/x-pack/plugins/watcher/public/application/sections/watch_list/components/watch_list.tsx @@ -226,7 +226,7 @@ export const WatchList = () => { defaultMessage="Error loading watches" /> } - error={error as Error} + error={(error as unknown) as Error} /> ); } else if (availableWatches) { diff --git a/x-pack/plugins/watcher/public/application/sections/watch_status/components/watch_history.tsx b/x-pack/plugins/watcher/public/application/sections/watch_status/components/watch_history.tsx index b78a2e4230171..541df89d042cb 100644 --- a/x-pack/plugins/watcher/public/application/sections/watch_status/components/watch_history.tsx +++ b/x-pack/plugins/watcher/public/application/sections/watch_status/components/watch_history.tsx @@ -107,7 +107,7 @@ export const WatchHistory = () => { defaultMessage="Error loading execution history" /> } - error={historyError as Error} + error={(historyError as unknown) as Error} /> ); @@ -186,7 +186,7 @@ export const WatchHistory = () => { defaultMessage="Error loading execution details" /> } - error={watchHistoryDetailsError as Error} + error={(watchHistoryDetailsError as unknown) as Error} data-test-subj="errorMessage" />