From 1626490c3668cb8abd2490a5d3aad8e78031adc1 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Fri, 14 Aug 2020 13:09:47 -0500 Subject: [PATCH 01/15] [Security solution] Sourcerer: Kibana index pattern selector for security views (#74706) --- .../security_solution/common/constants.ts | 1 + .../security_solution/public/app/app.tsx | 14 +- .../components/sourcerer/index.test.tsx | 112 +++++ .../common/components/sourcerer/index.tsx | 161 +++++++ .../components/sourcerer/translations.ts | 35 ++ .../common/containers/sourcerer/constants.ts | 29 ++ .../containers/sourcerer/format.test.tsx | 23 + .../common/containers/sourcerer/format.ts | 96 ++++ .../containers/sourcerer/index.test.tsx | 181 ++++++++ .../common/containers/sourcerer/index.tsx | 415 ++++++++++++++++++ .../common/containers/sourcerer/mocks.ts | 114 +++++ .../components/overview_host/index.tsx | 6 +- .../containers/overview_host/index.tsx | 13 +- 13 files changed, 1191 insertions(+), 9 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/sourcerer/translations.ts create mode 100644 x-pack/plugins/security_solution/public/common/containers/sourcerer/constants.ts create mode 100644 x-pack/plugins/security_solution/public/common/containers/sourcerer/format.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/containers/sourcerer/format.ts create mode 100644 x-pack/plugins/security_solution/public/common/containers/sourcerer/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx create mode 100644 x-pack/plugins/security_solution/public/common/containers/sourcerer/mocks.ts diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 0fc42895050a5..e5ccbb71c2b76 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -10,6 +10,7 @@ export const APP_NAME = 'Security'; export const APP_ICON = 'securityAnalyticsApp'; export const APP_PATH = `/app/security`; export const ADD_DATA_PATH = `/app/home#/tutorial_directory/security`; +export const ADD_INDEX_PATH = `/app/management/kibana/indexPatterns/create`; export const DEFAULT_BYTES_FORMAT = 'format:bytes:defaultPattern'; export const DEFAULT_DATE_FORMAT = 'dateFormat'; export const DEFAULT_DATE_FORMAT_TZ = 'dateFormat:tz'; diff --git a/x-pack/plugins/security_solution/public/app/app.tsx b/x-pack/plugins/security_solution/public/app/app.tsx index 147efb9e0d2e2..b5e952b0ffa8e 100644 --- a/x-pack/plugins/security_solution/public/app/app.tsx +++ b/x-pack/plugins/security_solution/public/app/app.tsx @@ -27,7 +27,7 @@ import { ApolloClientContext } from '../common/utils/apollo_context'; import { ManageGlobalTimeline } from '../timelines/components/manage_timeline'; import { StartServices } from '../types'; import { PageRouter } from './routes'; - +import { ManageSource } from '../common/containers/sourcerer'; interface StartAppComponent extends AppFrontendLibs { children: React.ReactNode; history: History; @@ -54,11 +54,13 @@ const StartAppComponent: FC = ({ children, apolloClient, hist - - - {children} - - + + + + {children} + + + diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx new file mode 100644 index 0000000000000..c330bb073b146 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx @@ -0,0 +1,112 @@ +/* + * 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 { mount } from 'enzyme'; +import { SecurityPageName } from '../../containers/sourcerer/constants'; +import { mockPatterns, mockSourceGroup } from '../../containers/sourcerer/mocks'; +import { MaybeSourcerer } from './index'; +import * as i18n from './translations'; +import { ADD_INDEX_PATH } from '../../../../common/constants'; + +const updateSourceGroupIndicies = jest.fn(); +const mockManageSource = { + activeSourceGroupId: SecurityPageName.default, + availableIndexPatterns: mockPatterns, + availableSourceGroupIds: [SecurityPageName.default], + getManageSourceGroupById: jest.fn().mockReturnValue(mockSourceGroup(SecurityPageName.default)), + initializeSourceGroup: jest.fn(), + isIndexPatternsLoading: false, + setActiveSourceGroupId: jest.fn(), + updateSourceGroupIndicies, +}; +jest.mock('../../containers/sourcerer', () => { + const original = jest.requireActual('../../containers/sourcerer'); + + return { + ...original, + useManageSource: () => mockManageSource, + }; +}); + +const mockOptions = [ + { label: 'auditbeat-*', key: 'auditbeat-*-0', value: 'auditbeat-*', checked: 'on' }, + { label: 'endgame-*', key: 'endgame-*-1', value: 'endgame-*', checked: 'on' }, + { label: 'filebeat-*', key: 'filebeat-*-2', value: 'filebeat-*', checked: 'on' }, + { label: 'logs-*', key: 'logs-*-3', value: 'logs-*', checked: 'on' }, + { label: 'packetbeat-*', key: 'packetbeat-*-4', value: 'packetbeat-*', checked: undefined }, + { label: 'winlogbeat-*', key: 'winlogbeat-*-5', value: 'winlogbeat-*', checked: 'on' }, + { + label: 'apm-*-transaction*', + key: 'apm-*-transaction*-0', + value: 'apm-*-transaction*', + disabled: true, + checked: undefined, + }, + { + label: 'blobbeat-*', + key: 'blobbeat-*-1', + value: 'blobbeat-*', + disabled: true, + checked: undefined, + }, +]; + +describe('Sourcerer component', () => { + // Using props callback instead of simulating clicks, + // because EuiSelectable uses a virtualized list, which isn't easily testable via test subjects + it('Mounts with correct options selected and disabled', () => { + const wrapper = mount(); + wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click'); + + expect( + wrapper.find(`[data-test-subj="indexPattern-switcher"]`).first().prop('options') + ).toEqual(mockOptions); + }); + it('onChange calls updateSourceGroupIndicies', () => { + const wrapper = mount(); + wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click'); + + const switcherOnChange = wrapper + .find(`[data-test-subj="indexPattern-switcher"]`) + .first() + .prop('onChange'); + // @ts-ignore + switcherOnChange([mockOptions[0], mockOptions[1]]); + expect(updateSourceGroupIndicies).toHaveBeenCalledWith(SecurityPageName.default, [ + mockOptions[0].value, + mockOptions[1].value, + ]); + }); + it('Disabled options have icon tooltip', () => { + const wrapper = mount(); + wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click'); + // @ts-ignore + const Rendered = wrapper + .find(`[data-test-subj="indexPattern-switcher"]`) + .first() + .prop('renderOption')( + { + label: 'blobbeat-*', + key: 'blobbeat-*-1', + value: 'blobbeat-*', + disabled: true, + checked: undefined, + }, + '' + ); + expect(Rendered.props.children[1].props.content).toEqual(i18n.DISABLED_INDEX_PATTERNS); + }); + + it('Button links to index path', () => { + const wrapper = mount(); + wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click'); + + expect(wrapper.find(`[data-test-subj="add-index"]`).first().prop('href')).toEqual( + ADD_INDEX_PATH + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx new file mode 100644 index 0000000000000..6275ce19c3608 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx @@ -0,0 +1,161 @@ +/* + * 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, { useCallback, useMemo, useState } from 'react'; +import { + EuiButton, + EuiButtonEmpty, + EuiHighlight, + EuiIconTip, + EuiPopover, + EuiPopoverFooter, + EuiPopoverTitle, + EuiSelectable, +} from '@elastic/eui'; +import { EuiSelectableOption } from '@elastic/eui/src/components/selectable/selectable_option'; +import { useManageSource } from '../../containers/sourcerer'; +import * as i18n from './translations'; +import { SOURCERER_FEATURE_FLAG_ON } from '../../containers/sourcerer/constants'; +import { ADD_INDEX_PATH } from '../../../../common/constants'; + +export const MaybeSourcerer = React.memo(() => { + const { + activeSourceGroupId, + availableIndexPatterns, + getManageSourceGroupById, + isIndexPatternsLoading, + updateSourceGroupIndicies, + } = useManageSource(); + const { defaultPatterns, indexPatterns: selectedOptions, loading: loadingIndices } = useMemo( + () => getManageSourceGroupById(activeSourceGroupId), + [getManageSourceGroupById, activeSourceGroupId] + ); + + const loading = useMemo(() => loadingIndices || isIndexPatternsLoading, [ + isIndexPatternsLoading, + loadingIndices, + ]); + + const onChangeIndexPattern = useCallback( + (newIndexPatterns: string[]) => { + updateSourceGroupIndicies(activeSourceGroupId, newIndexPatterns); + }, + [activeSourceGroupId, updateSourceGroupIndicies] + ); + + const [isPopoverOpen, setPopoverIsOpen] = useState(false); + const setPopoverIsOpenCb = useCallback(() => setPopoverIsOpen((prevState) => !prevState), []); + const trigger = useMemo( + () => ( + + {i18n.SOURCERER} + + ), + [setPopoverIsOpenCb] + ); + const options: EuiSelectableOption[] = useMemo( + () => + availableIndexPatterns.map((title, id) => ({ + label: title, + key: `${title}-${id}`, + value: title, + checked: selectedOptions.includes(title) ? 'on' : undefined, + })), + [availableIndexPatterns, selectedOptions] + ); + const unSelectableOptions: EuiSelectableOption[] = useMemo( + () => + defaultPatterns + .filter((title) => !availableIndexPatterns.includes(title)) + .map((title, id) => ({ + label: title, + key: `${title}-${id}`, + value: title, + disabled: true, + checked: undefined, + })), + [availableIndexPatterns, defaultPatterns] + ); + const renderOption = useCallback( + (option, searchValue) => ( + <> + {option.label} + {option.disabled ? ( + + ) : null} + + ), + [] + ); + const onChange = useCallback( + (choices: EuiSelectableOption[]) => { + const choice = choices.reduce( + (acc, { checked, label }) => (checked === 'on' ? [...acc, label] : acc), + [] + ); + onChangeIndexPattern(choice); + }, + [onChangeIndexPattern] + ); + const allOptions = useMemo(() => [...options, ...unSelectableOptions], [ + options, + unSelectableOptions, + ]); + return ( + setPopoverIsOpen(false)} + display="block" + panelPaddingSize="s" + ownFocus + > +
+ + <> + {i18n.CHANGE_INDEX_PATTERNS} + + + + + {(list, search) => ( + <> + {search} + {list} + + )} + + + + {i18n.ADD_INDEX_PATTERNS} + + +
+
+ ); +}); +MaybeSourcerer.displayName = 'Sourcerer'; + +export const Sourcerer = SOURCERER_FEATURE_FLAG_ON ? MaybeSourcerer : () => null; diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/translations.ts b/x-pack/plugins/security_solution/public/common/components/sourcerer/translations.ts new file mode 100644 index 0000000000000..71b1734dad6a6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/translations.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 { i18n } from '@kbn/i18n'; + +export const SOURCERER = i18n.translate('xpack.securitySolution.indexPatterns.sourcerer', { + defaultMessage: 'Sourcerer', +}); + +export const CHANGE_INDEX_PATTERNS = i18n.translate('xpack.securitySolution.indexPatterns.help', { + defaultMessage: 'Change index patterns', +}); + +export const ADD_INDEX_PATTERNS = i18n.translate('xpack.securitySolution.indexPatterns.add', { + defaultMessage: 'Configure Kibana index patterns', +}); + +export const CONFIGURE_INDEX_PATTERNS = i18n.translate( + 'xpack.securitySolution.indexPatterns.configure', + { + defaultMessage: + 'Configure additional Kibana index patterns to see them become available in the Security Solution', + } +); + +export const DISABLED_INDEX_PATTERNS = i18n.translate( + 'xpack.securitySolution.indexPatterns.disabled', + { + defaultMessage: + 'Disabled index patterns are recommended on this page, but first need to be configured in your Kibana index pattern settings', + } +); diff --git a/x-pack/plugins/security_solution/public/common/containers/sourcerer/constants.ts b/x-pack/plugins/security_solution/public/common/containers/sourcerer/constants.ts new file mode 100644 index 0000000000000..106294ba54f5a --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/containers/sourcerer/constants.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const SOURCERER_FEATURE_FLAG_ON = false; + +export enum SecurityPageName { + default = 'default', + host = 'host', + detections = 'detections', + timeline = 'timeline', + network = 'network', +} + +export type SourceGroupsType = keyof typeof SecurityPageName; + +export const sourceGroups = { + [SecurityPageName.default]: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'winlogbeat-*', + 'blobbeat-*', + ], +}; diff --git a/x-pack/plugins/security_solution/public/common/containers/sourcerer/format.test.tsx b/x-pack/plugins/security_solution/public/common/containers/sourcerer/format.test.tsx new file mode 100644 index 0000000000000..b8017df09b738 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/containers/sourcerer/format.test.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 { indicesExistOrDataTemporarilyUnavailable } from './format'; + +describe('indicesExistOrDataTemporarilyUnavailable', () => { + it('it returns true when undefined', () => { + let undefVar; + const result = indicesExistOrDataTemporarilyUnavailable(undefVar); + expect(result).toBeTruthy(); + }); + it('it returns true when true', () => { + const result = indicesExistOrDataTemporarilyUnavailable(true); + expect(result).toBeTruthy(); + }); + it('it returns false when false', () => { + const result = indicesExistOrDataTemporarilyUnavailable(false); + expect(result).toBeFalsy(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/containers/sourcerer/format.ts b/x-pack/plugins/security_solution/public/common/containers/sourcerer/format.ts new file mode 100644 index 0000000000000..8c9a16ed705ef --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/containers/sourcerer/format.ts @@ -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 { isEmpty, pick } from 'lodash/fp'; +import memoizeOne from 'memoize-one'; +import { set } from '@elastic/safer-lodash-set/fp'; +import { isUndefined } from 'lodash'; +import { IndexField } from '../../../graphql/types'; +import { IIndexPattern } from '../../../../../../../src/plugins/data/common/index_patterns'; + +export interface BrowserField { + aggregatable: boolean; + category: string; + description: string | null; + example: string | number | null; + fields: Readonly>>; + format: string; + indexes: string[]; + name: string; + searchable: boolean; + type: string; +} + +export interface DocValueFields { + field: string; + format: string; +} + +export type BrowserFields = Readonly>>; + +export const getAllBrowserFields = (browserFields: BrowserFields): Array> => + Object.values(browserFields).reduce>>( + (acc, namespace) => [ + ...acc, + ...Object.values(namespace.fields != null ? namespace.fields : {}), + ], + [] + ); + +export const getIndexFields = memoizeOne( + (title: string, fields: IndexField[]): IIndexPattern => + fields && fields.length > 0 + ? { + fields: fields.map((field) => + pick(['name', 'searchable', 'type', 'aggregatable', 'esTypes', 'subType'], field) + ), + title, + } + : { fields: [], title }, + (newArgs, lastArgs) => newArgs[0] === lastArgs[0] && newArgs[1].length === lastArgs[1].length +); + +export const getBrowserFields = memoizeOne( + (_title: string, fields: IndexField[]): BrowserFields => + fields && fields.length > 0 + ? fields.reduce( + (accumulator: BrowserFields, field: IndexField) => + set([field.category, 'fields', field.name], field, accumulator), + {} + ) + : {}, + // Update the value only if _title has changed + (newArgs, lastArgs) => newArgs[0] === lastArgs[0] +); + +export const getDocValueFields = memoizeOne( + (_title: string, fields: IndexField[]): DocValueFields[] => + fields && fields.length > 0 + ? fields.reduce((accumulator: DocValueFields[], field: IndexField) => { + if (field.type === 'date' && accumulator.length < 100) { + const format: string = + field.format != null && !isEmpty(field.format) ? field.format : 'date_time'; + return [ + ...accumulator, + { + field: field.name, + format, + }, + ]; + } + return accumulator; + }, []) + : [], + // Update the value only if _title has changed + (newArgs, lastArgs) => newArgs[0] === lastArgs[0] +); + +export const indicesExistOrDataTemporarilyUnavailable = ( + indicesExist: boolean | null | undefined +) => indicesExist || isUndefined(indicesExist); + +export const EMPTY_BROWSER_FIELDS = {}; +export const EMPTY_DOCVALUE_FIELD: DocValueFields[] = []; diff --git a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.test.tsx b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.test.tsx new file mode 100644 index 0000000000000..38af84e0968f8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.test.tsx @@ -0,0 +1,181 @@ +/* + * 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 { act, renderHook } from '@testing-library/react-hooks'; + +import { getSourceDefaults, useSourceManager, UseSourceManager } from '.'; +import { + mockSourceSelections, + mockSourceGroup, + mockSourceGroups, + mockPatterns, + mockSource, +} from './mocks'; +import { SecurityPageName } from './constants'; +const mockSourceDefaults = mockSource(SecurityPageName.default); +jest.mock('../../lib/kibana', () => ({ + useKibana: jest.fn().mockReturnValue({ + services: { + data: { + indexPatterns: { + getTitles: jest.fn().mockImplementation(() => Promise.resolve(mockPatterns)), + }, + }, + }, + }), +})); +jest.mock('../../utils/apollo_context', () => ({ + useApolloClient: jest.fn().mockReturnValue({ + query: jest.fn().mockImplementation(() => Promise.resolve(mockSourceDefaults)), + }), +})); + +describe('Sourcerer Hooks', () => { + const testId = SecurityPageName.default; + const uninitializedId = SecurityPageName.host; + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + describe('Initialization', () => { + it('initializes loading default index patterns', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useSourceManager() + ); + await waitForNextUpdate(); + expect(result.current).toEqual({ + activeSourceGroupId: 'default', + availableIndexPatterns: [], + availableSourceGroupIds: [], + isIndexPatternsLoading: true, + sourceGroups: {}, + getManageSourceGroupById: result.current.getManageSourceGroupById, + initializeSourceGroup: result.current.initializeSourceGroup, + setActiveSourceGroupId: result.current.setActiveSourceGroupId, + updateSourceGroupIndicies: result.current.updateSourceGroupIndicies, + }); + }); + }); + it('initializes loading default source group', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useSourceManager() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual({ + activeSourceGroupId: 'default', + availableIndexPatterns: mockPatterns, + availableSourceGroupIds: [], + isIndexPatternsLoading: false, + sourceGroups: {}, + getManageSourceGroupById: result.current.getManageSourceGroupById, + initializeSourceGroup: result.current.initializeSourceGroup, + setActiveSourceGroupId: result.current.setActiveSourceGroupId, + updateSourceGroupIndicies: result.current.updateSourceGroupIndicies, + }); + }); + }); + it('initialize completes with formatted source group data', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useSourceManager() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual({ + activeSourceGroupId: testId, + availableIndexPatterns: mockPatterns, + availableSourceGroupIds: [testId], + isIndexPatternsLoading: false, + sourceGroups: { + default: mockSourceGroup(testId), + }, + getManageSourceGroupById: result.current.getManageSourceGroupById, + initializeSourceGroup: result.current.initializeSourceGroup, + setActiveSourceGroupId: result.current.setActiveSourceGroupId, + updateSourceGroupIndicies: result.current.updateSourceGroupIndicies, + }); + }); + }); + }); + describe('Methods', () => { + it('getManageSourceGroupById: initialized source group returns defaults', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useSourceManager() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + await waitForNextUpdate(); + const initializedSourceGroup = result.current.getManageSourceGroupById(testId); + expect(initializedSourceGroup).toEqual(mockSourceGroup(testId)); + }); + }); + it('getManageSourceGroupById: uninitialized source group returns defaults', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useSourceManager() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + await waitForNextUpdate(); + const uninitializedSourceGroup = result.current.getManageSourceGroupById(uninitializedId); + expect(uninitializedSourceGroup).toEqual(getSourceDefaults(uninitializedId, mockPatterns)); + }); + }); + it('initializeSourceGroup: initializes source group', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useSourceManager() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.initializeSourceGroup( + uninitializedId, + mockSourceGroups[uninitializedId], + true + ); + await waitForNextUpdate(); + const initializedSourceGroup = result.current.getManageSourceGroupById(uninitializedId); + expect(initializedSourceGroup.indexPatterns).toEqual(mockSourceSelections[uninitializedId]); + }); + }); + it('setActiveSourceGroupId: active source group id gets set only if it gets initialized first', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useSourceManager() + ); + await waitForNextUpdate(); + expect(result.current.activeSourceGroupId).toEqual(testId); + result.current.setActiveSourceGroupId(uninitializedId); + expect(result.current.activeSourceGroupId).toEqual(testId); + result.current.initializeSourceGroup(uninitializedId); + result.current.setActiveSourceGroupId(uninitializedId); + expect(result.current.activeSourceGroupId).toEqual(uninitializedId); + }); + }); + it('updateSourceGroupIndicies: updates source group indicies', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useSourceManager() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + await waitForNextUpdate(); + let sourceGroup = result.current.getManageSourceGroupById(testId); + expect(sourceGroup.indexPatterns).toEqual(mockSourceSelections[testId]); + result.current.updateSourceGroupIndicies(testId, ['endgame-*', 'filebeat-*']); + await waitForNextUpdate(); + sourceGroup = result.current.getManageSourceGroupById(testId); + expect(sourceGroup.indexPatterns).toEqual(['endgame-*', 'filebeat-*']); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx new file mode 100644 index 0000000000000..91907b45aa449 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx @@ -0,0 +1,415 @@ +/* + * 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 { get, noop, isEmpty } from 'lodash/fp'; +import React, { createContext, useCallback, useContext, useEffect, useReducer } from 'react'; +import { IIndexPattern } from 'src/plugins/data/public'; + +import { NO_ALERT_INDEX } from '../../../../common/constants'; +import { useKibana } from '../../lib/kibana'; + +import { SourceQuery } from '../../../graphql/types'; + +import { sourceQuery } from '../source/index.gql_query'; +import { useApolloClient } from '../../utils/apollo_context'; +import { + sourceGroups, + SecurityPageName, + SourceGroupsType, + SOURCERER_FEATURE_FLAG_ON, +} from './constants'; +import { + BrowserFields, + DocValueFields, + EMPTY_BROWSER_FIELDS, + EMPTY_DOCVALUE_FIELD, + getBrowserFields, + getDocValueFields, + getIndexFields, + indicesExistOrDataTemporarilyUnavailable, +} from './format'; + +// TYPES +interface ManageSource { + browserFields: BrowserFields; + defaultPatterns: string[]; + docValueFields: DocValueFields[]; + errorMessage: string | null; + id: SourceGroupsType; + indexPattern: IIndexPattern; + indexPatterns: string[]; + indicesExist: boolean | undefined | null; + loading: boolean; +} + +interface ManageSourceInit extends Partial { + id: SourceGroupsType; +} + +type ManageSourceGroupById = { + [id in SourceGroupsType]?: ManageSource; +}; + +type ActionManageSource = + | { + type: 'SET_SOURCE'; + id: SourceGroupsType; + defaultIndex: string[]; + payload: ManageSourceInit; + } + | { + type: 'SET_IS_SOURCE_LOADING'; + id: SourceGroupsType; + payload: boolean; + } + | { + type: 'SET_ACTIVE_SOURCE_GROUP_ID'; + payload: SourceGroupsType; + } + | { + type: 'SET_AVAILABLE_INDEX_PATTERNS'; + payload: string[]; + } + | { + type: 'SET_IS_INDEX_PATTERNS_LOADING'; + payload: boolean; + }; + +interface ManageSourcerer { + activeSourceGroupId: SourceGroupsType; + availableIndexPatterns: string[]; + availableSourceGroupIds: SourceGroupsType[]; + isIndexPatternsLoading: boolean; + sourceGroups: ManageSourceGroupById; +} + +export interface UseSourceManager extends ManageSourcerer { + getManageSourceGroupById: (id: SourceGroupsType) => ManageSource; + initializeSourceGroup: ( + id: SourceGroupsType, + indexToAdd?: string[] | null, + onlyCheckIndexToAdd?: boolean + ) => void; + setActiveSourceGroupId: (id: SourceGroupsType) => void; + updateSourceGroupIndicies: (id: SourceGroupsType, updatedIndicies: string[]) => void; +} + +// DEFAULTS/INIT +export const getSourceDefaults = (id: SourceGroupsType, defaultIndex: string[]) => ({ + browserFields: EMPTY_BROWSER_FIELDS, + defaultPatterns: defaultIndex, + docValueFields: EMPTY_DOCVALUE_FIELD, + errorMessage: null, + id, + indexPattern: getIndexFields(defaultIndex.join(), []), + indexPatterns: defaultIndex, + indicesExist: indicesExistOrDataTemporarilyUnavailable(undefined), + loading: true, +}); + +const initManageSource: ManageSourcerer = { + activeSourceGroupId: SecurityPageName.default, + availableIndexPatterns: [], + availableSourceGroupIds: [], + isIndexPatternsLoading: true, + sourceGroups: {}, +}; +const init: UseSourceManager = { + ...initManageSource, + getManageSourceGroupById: (id: SourceGroupsType) => getSourceDefaults(id, []), + initializeSourceGroup: () => noop, + setActiveSourceGroupId: () => noop, + updateSourceGroupIndicies: () => noop, +}; + +const reducerManageSource = (state: ManageSourcerer, action: ActionManageSource) => { + switch (action.type) { + case 'SET_SOURCE': + return { + ...state, + sourceGroups: { + ...state.sourceGroups, + [action.id]: { + ...getSourceDefaults(action.id, action.defaultIndex), + ...state.sourceGroups[action.id], + ...action.payload, + }, + }, + availableSourceGroupIds: state.availableSourceGroupIds.includes(action.id) + ? state.availableSourceGroupIds + : [...state.availableSourceGroupIds, action.id], + }; + case 'SET_IS_SOURCE_LOADING': + return { + ...state, + sourceGroups: { + ...state.sourceGroups, + [action.id]: { + ...state.sourceGroups[action.id], + id: action.id, + loading: action.payload, + }, + }, + }; + case 'SET_ACTIVE_SOURCE_GROUP_ID': + return { + ...state, + activeSourceGroupId: action.payload, + }; + case 'SET_AVAILABLE_INDEX_PATTERNS': + return { + ...state, + availableIndexPatterns: action.payload, + }; + case 'SET_IS_INDEX_PATTERNS_LOADING': + return { + ...state, + isIndexPatternsLoading: action.payload, + }; + default: + return state; + } +}; + +// HOOKS +export const useSourceManager = (): UseSourceManager => { + const { + services: { + data: { indexPatterns }, + }, + } = useKibana(); + const apolloClient = useApolloClient(); + const [state, dispatch] = useReducer(reducerManageSource, initManageSource); + + // Kibana Index Patterns + const setIsIndexPatternsLoading = useCallback((loading: boolean) => { + dispatch({ + type: 'SET_IS_INDEX_PATTERNS_LOADING', + payload: loading, + }); + }, []); + const getDefaultIndex = useCallback( + (indexToAdd?: string[] | null, onlyCheckIndexToAdd?: boolean) => { + const filterIndexAdd = (indexToAdd ?? []).filter((item) => item !== NO_ALERT_INDEX); + if (!isEmpty(filterIndexAdd)) { + return onlyCheckIndexToAdd + ? filterIndexAdd.sort() + : [ + ...state.availableIndexPatterns, + ...filterIndexAdd.filter((index) => !state.availableIndexPatterns.includes(index)), + ].sort(); + } + return state.availableIndexPatterns.sort(); + }, + [state.availableIndexPatterns] + ); + const setAvailableIndexPatterns = useCallback((availableIndexPatterns: string[]) => { + dispatch({ + type: 'SET_AVAILABLE_INDEX_PATTERNS', + payload: availableIndexPatterns, + }); + }, []); + const fetchKibanaIndexPatterns = useCallback(() => { + setIsIndexPatternsLoading(true); + const abortCtrl = new AbortController(); + + async function fetchTitles() { + try { + const result = await indexPatterns.getTitles(); + setAvailableIndexPatterns(result); + setIsIndexPatternsLoading(false); + } catch (error) { + setIsIndexPatternsLoading(false); + } + } + + fetchTitles(); + + return () => { + return abortCtrl.abort(); + }; + }, [indexPatterns, setAvailableIndexPatterns, setIsIndexPatternsLoading]); + + // Security Solution Source Groups + const setActiveSourceGroupId = useCallback( + (sourceGroupId: SourceGroupsType) => { + if (state.availableSourceGroupIds.includes(sourceGroupId)) { + dispatch({ + type: 'SET_ACTIVE_SOURCE_GROUP_ID', + payload: sourceGroupId, + }); + } + }, + [state.availableSourceGroupIds] + ); + const setIsSourceLoading = useCallback( + ({ id, loading }: { id: SourceGroupsType; loading: boolean }) => { + dispatch({ + type: 'SET_IS_SOURCE_LOADING', + id, + payload: loading, + }); + }, + [] + ); + const enrichSource = useCallback( + (id: SourceGroupsType, indexToAdd?: string[] | null, onlyCheckIndexToAdd?: boolean) => { + let isSubscribed = true; + const abortCtrl = new AbortController(); + const defaultIndex = getDefaultIndex(indexToAdd, onlyCheckIndexToAdd); + const selectedPatterns = defaultIndex.filter((pattern) => + state.availableIndexPatterns.includes(pattern) + ); + if (state.sourceGroups[id] == null) { + dispatch({ + type: 'SET_SOURCE', + id, + defaultIndex: selectedPatterns, + payload: { defaultPatterns: defaultIndex, id }, + }); + } + + async function fetchSource() { + if (!apolloClient) return; + setIsSourceLoading({ id, loading: true }); + try { + const result = await apolloClient.query({ + query: sourceQuery, + fetchPolicy: 'network-only', + variables: { + sourceId: 'default', // always + defaultIndex: selectedPatterns, + }, + context: { + fetchOptions: { + signal: abortCtrl.signal, + }, + }, + }); + if (isSubscribed) { + dispatch({ + type: 'SET_SOURCE', + id, + defaultIndex: selectedPatterns, + payload: { + browserFields: getBrowserFields( + selectedPatterns.join(), + get('data.source.status.indexFields', result) + ), + docValueFields: getDocValueFields( + selectedPatterns.join(), + get('data.source.status.indexFields', result) + ), + errorMessage: null, + id, + indexPattern: getIndexFields( + selectedPatterns.join(), + get('data.source.status.indexFields', result) + ), + indexPatterns: selectedPatterns, + indicesExist: indicesExistOrDataTemporarilyUnavailable( + get('data.source.status.indicesExist', result) + ), + loading: false, + }, + }); + } + } catch (error) { + if (isSubscribed) { + dispatch({ + type: 'SET_SOURCE', + id, + defaultIndex: selectedPatterns, + payload: { + errorMessage: error.message, + id, + loading: false, + }, + }); + } + } + } + + fetchSource(); + + return () => { + isSubscribed = false; + return abortCtrl.abort(); + }; + }, + [ + apolloClient, + getDefaultIndex, + setIsSourceLoading, + state.availableIndexPatterns, + state.sourceGroups, + ] + ); + + const initializeSourceGroup = useCallback( + (id: SourceGroupsType, indexToAdd?: string[] | null, onlyCheckIndexToAdd?: boolean) => + enrichSource(id, indexToAdd, onlyCheckIndexToAdd), + [enrichSource] + ); + + const updateSourceGroupIndicies = useCallback( + (id: SourceGroupsType, updatedIndicies: string[]) => enrichSource(id, updatedIndicies, true), + [enrichSource] + ); + const getManageSourceGroupById = useCallback( + (id: SourceGroupsType) => { + const sourceById = state.sourceGroups[id]; + if (sourceById != null) { + return sourceById; + } + return getSourceDefaults(id, getDefaultIndex()); + }, + [getDefaultIndex, state.sourceGroups] + ); + + // load initial default index + useEffect(() => { + fetchKibanaIndexPatterns(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + if (!state.isIndexPatternsLoading) { + Object.entries(sourceGroups).forEach(([key, value]) => + initializeSourceGroup(key as SourceGroupsType, value, true) + ); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [state.isIndexPatternsLoading]); + + return { + ...state, + getManageSourceGroupById, + initializeSourceGroup, + setActiveSourceGroupId, + updateSourceGroupIndicies, + }; +}; + +const ManageSourceContext = createContext(init); + +export const useManageSource = () => useContext(ManageSourceContext); + +interface ManageSourceProps { + children: React.ReactNode; +} + +export const MaybeManageSource = ({ children }: ManageSourceProps) => { + const indexPatternManager = useSourceManager(); + return ( + + {children} + + ); +}; +export const ManageSource = SOURCERER_FEATURE_FLAG_ON + ? MaybeManageSource + : ({ children }: ManageSourceProps) => <>{children}; diff --git a/x-pack/plugins/security_solution/public/common/containers/sourcerer/mocks.ts b/x-pack/plugins/security_solution/public/common/containers/sourcerer/mocks.ts new file mode 100644 index 0000000000000..cde14e54694f0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/containers/sourcerer/mocks.ts @@ -0,0 +1,114 @@ +/* + * 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 { SecurityPageName } from './constants'; +import { getSourceDefaults } from './index'; + +export const mockPatterns = [ + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', +]; + +export const mockSourceGroups = { + [SecurityPageName.default]: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'blobbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'winlogbeat-*', + ], + [SecurityPageName.host]: [ + 'apm-*-transaction*', + 'endgame-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ], +}; + +export const mockSourceSelections = { + [SecurityPageName.default]: ['auditbeat-*', 'endgame-*', 'filebeat-*', 'logs-*', 'winlogbeat-*'], + [SecurityPageName.host]: ['endgame-*', 'logs-*', 'packetbeat-*', 'winlogbeat-*'], +}; +export const mockSource = (testId: SecurityPageName.default | SecurityPageName.host) => ({ + data: { + source: { + id: 'default', + status: { + indicesExist: true, + indexFields: [ + { + category: '_id', + description: 'Each document has an _id that uniquely identifies it', + example: 'Y-6TfmcB0WOhS6qyMv3s', + indexes: mockSourceSelections[testId], + name: '_id', + searchable: true, + type: 'string', + aggregatable: false, + format: null, + esTypes: null, + subType: null, + __typename: 'IndexField', + }, + ], + }, + }, + }, + loading: false, + networkStatus: 7, + stale: false, +}); + +export const mockSourceGroup = (testId: SecurityPageName.default | SecurityPageName.host) => { + const indexes = mockSourceSelections[testId]; + return { + ...getSourceDefaults(testId, mockPatterns), + defaultPatterns: mockSourceGroups[testId], + browserFields: { + _id: { + fields: { + _id: { + __typename: 'IndexField', + aggregatable: false, + category: '_id', + description: 'Each document has an _id that uniquely identifies it', + esTypes: null, + example: 'Y-6TfmcB0WOhS6qyMv3s', + format: null, + indexes, + name: '_id', + searchable: true, + subType: null, + type: 'string', + }, + }, + }, + }, + indexPattern: { + fields: [ + { + aggregatable: false, + esTypes: null, + name: '_id', + searchable: true, + subType: null, + type: 'string', + }, + ], + title: indexes.join(), + }, + indexPatterns: indexes, + indicesExist: true, + loading: false, + }; +}; diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_host/index.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_host/index.tsx index 583c76d1464a8..783e433dfba26 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_host/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_host/index.tsx @@ -22,6 +22,7 @@ import { InspectButtonContainer } from '../../../common/components/inspect'; import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; import { SecurityPageName } from '../../../app/types'; import { LinkButton } from '../../../common/components/links'; +import { Sourcerer } from '../../../common/components/sourcerer'; export interface OwnProps { startDate: GlobalTimeArgs['from']; @@ -107,7 +108,10 @@ const OverviewHostComponent: React.FC = ({ /> } > - {hostPageButton} + <> + + {hostPageButton} + ( ({ id = ID, children, filterQuery, isInspected, sourceId, startDate, endDate }) => { + const { activeSourceGroupId, getManageSourceGroupById } = useManageSource(); + const { indexPatterns } = useMemo(() => getManageSourceGroupById(activeSourceGroupId), [ + getManageSourceGroupById, + activeSourceGroupId, + ]); + const uiDefaultIndexPatterns = useUiSetting(DEFAULT_INDEX_KEY); + const defaultIndex = SOURCERER_FEATURE_FLAG_ON ? indexPatterns : uiDefaultIndexPatterns; return ( query={overviewHostQuery} @@ -50,7 +59,7 @@ const OverviewHostComponentQuery = React.memo(DEFAULT_INDEX_KEY), + defaultIndex, inspect: isInspected, }} > From ac04e0546a4839b35af9b892f14c793c363c41b2 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Fri, 14 Aug 2020 15:28:34 -0500 Subject: [PATCH 02/15] [Enterprise Search] Add Workplace Search side navigation (#74894) * Add routes * Add version for use in doc link * Set up basic router layout + WorkplaceSearchNav * Update views to account for Layout * Move version to common folder * Fix version path * Remove product button No longer needed since we have all top-level app links in Kibana as a part of this PR * You know, for search * Remove comment * Remove unused i18n properties from JSON Tests were failing after removing component: https://kibana-ci.elastic.co/job/elastic+kibana+pipeline-pull-request/67797/execution/node/382/log/ * Revert button and i18n copy removal This reverts commit ba0535187e732b1a5560f9cdd31a87b76fa9c4f1. * Move Overview out of layout to hide nav For now, the route for groups was added to avoid having comment out the code. Will add the groups component in a future PR * Revert layout changes to Overview Since there is no nav, the padding was missing and the view looked off. Reverting to the 7.9, centered column view * Remove extra Overview component Was causing tests to fail * Revert error state to use EuiPage Co-authored-by: Elastic Machine --- .../enterprise_search/common/version.ts | 11 ++ .../components/error_state/error_state.tsx | 2 + .../components/layout/index.ts | 7 ++ .../components/layout/nav.test.tsx | 22 ++++ .../components/layout/nav.tsx | 74 +++++++++++++ .../components/overview/overview.tsx | 2 + .../workplace_search/index.test.tsx | 2 +- .../applications/workplace_search/index.tsx | 41 +++++-- .../applications/workplace_search/routes.ts | 104 +++++++++++++++++- 9 files changed, 254 insertions(+), 11 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/common/version.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx diff --git a/x-pack/plugins/enterprise_search/common/version.ts b/x-pack/plugins/enterprise_search/common/version.ts new file mode 100644 index 0000000000000..e29ad8a9f866b --- /dev/null +++ b/x-pack/plugins/enterprise_search/common/version.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 { SemVer } from 'semver'; +import pkg from '../../../../package.json'; + +export const CURRENT_VERSION = new SemVer(pkg.version as string); +export const CURRENT_MAJOR_VERSION = CURRENT_VERSION.major; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/error_state/error_state.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/error_state/error_state.tsx index e1114986d2244..53f3a7a274429 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/error_state/error_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/error_state/error_state.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +// TODO: Remove EuiPage & EuiPageBody before exposing full app + import React from 'react'; import { EuiPage, EuiPageBody, EuiPageContent } from '@elastic/eui'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/index.ts new file mode 100644 index 0000000000000..41861a8ee2dc5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/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 { WorkplaceSearchNav } from './nav'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx new file mode 100644 index 0000000000000..0e85d8467cff0 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx @@ -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. + */ + +import '../../../__mocks__/shallow_usecontext.mock'; +import React from 'react'; +import { shallow } from 'enzyme'; + +import { SideNav, SideNavLink } from '../../../shared/layout'; +import { WorkplaceSearchNav } from './'; + +describe('WorkplaceSearchNav', () => { + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(SideNav)).toHaveLength(1); + expect(wrapper.find(SideNavLink).first().prop('to')).toEqual('/'); + expect(wrapper.find(SideNavLink).last().prop('to')).toEqual('http://localhost:3002/ws/search'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx new file mode 100644 index 0000000000000..8f8edc61620ab --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx @@ -0,0 +1,74 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +import { EuiSpacer } from '@elastic/eui'; + +import { WORKPLACE_SEARCH_PLUGIN } from '../../../../../common/constants'; +import { KibanaContext, IKibanaContext } from '../../../index'; +import { SideNav, SideNavLink } from '../../../shared/layout'; + +import { + ORG_SOURCES_PATH, + SOURCES_PATH, + SECURITY_PATH, + ROLE_MAPPINGS_PATH, + GROUPS_PATH, + ORG_SETTINGS_PATH, +} from '../../routes'; + +export const WorkplaceSearchNav: React.FC = () => { + const { enterpriseSearchUrl } = useContext(KibanaContext) as IKibanaContext; + const legacyUrl = (path: string) => `${enterpriseSearchUrl}/ws#${path}`; + + // TODO: icons + return ( + + + {i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.overview', { + defaultMessage: 'Overview', + })} + + + {i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.sources', { + defaultMessage: 'Sources', + })} + + + {i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.groups', { + defaultMessage: 'Groups', + })} + + + {i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.roleMappings', { + defaultMessage: 'Role Mappings', + })} + + + {i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.security', { + defaultMessage: 'Security', + })} + + + {i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.settings', { + defaultMessage: 'Settings', + })} + + + + {i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.personalDashboard', { + defaultMessage: 'View my personal dashboard', + })} + + + {i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.search', { + defaultMessage: 'Go to search application', + })} + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview.tsx index 2c3e78b404d42..b816eb2973207 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +// TODO: Remove EuiPage & EuiPageBody before exposing full app + import React, { useContext, useEffect } from 'react'; import { EuiPage, EuiPageBody, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx index 743080d965c36..a4af405247f83 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx @@ -15,7 +15,7 @@ import { Overview } from './components/overview'; import { WorkplaceSearch } from './'; -describe('Workplace Search Routes', () => { +describe('Workplace Search', () => { describe('/', () => { it('redirects to Setup Guide when enterpriseSearchUrl is not set', () => { (useContext as jest.Mock).mockImplementationOnce(() => ({ enterpriseSearchUrl: '' })); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx index cfa70ea29eca8..6470a3b78c5f1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx @@ -5,7 +5,7 @@ */ import React, { useContext } from 'react'; -import { Route, Redirect } from 'react-router-dom'; +import { Route, Redirect, Switch } from 'react-router-dom'; import { Provider } from 'react-redux'; import { Store } from 'redux'; import { getContext, resetContext } from 'kea'; @@ -15,6 +15,8 @@ resetContext({ createStore: true }); const store = getContext().store as Store; import { KibanaContext, IKibanaContext } from '../index'; +import { Layout } from '../shared/layout'; +import { WorkplaceSearchNav } from './components/layout/nav'; import { SETUP_GUIDE_PATH } from './routes'; @@ -23,14 +25,39 @@ import { Overview } from './components/overview'; export const WorkplaceSearch: React.FC = () => { const { enterpriseSearchUrl } = useContext(KibanaContext) as IKibanaContext; + if (!enterpriseSearchUrl) + return ( + + + + + + + {/* Kibana displays a blank page on redirect if this isn't included */} + + + ); + return ( - - {!enterpriseSearchUrl ? : } - - - - + + + + + + + + + }> + + + {/* Will replace with groups component subsequent PR */} +
+ + + + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts index d9798d1f30cfc..993a1a378e738 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts @@ -4,9 +4,107 @@ * you may not use this file except in compliance with the Elastic License. */ -export const ORG_SOURCES_PATH = '/org/sources'; -export const USERS_PATH = '/org/users'; -export const ORG_SETTINGS_PATH = '/org/settings'; +import { CURRENT_MAJOR_VERSION } from '../../../common/version'; + export const SETUP_GUIDE_PATH = '/setup_guide'; +export const LEAVE_FEEDBACK_EMAIL = 'support@elastic.co'; +export const LEAVE_FEEDBACK_URL = `mailto:${LEAVE_FEEDBACK_EMAIL}?Subject=Elastic%20Workplace%20Search%20Feedback`; + +export const DOCS_PREFIX = `https://www.elastic.co/guide/en/workplace-search/${CURRENT_MAJOR_VERSION}`; +export const ENT_SEARCH_DOCS_PREFIX = `https://www.elastic.co/guide/en/enterprise-search/${CURRENT_MAJOR_VERSION}`; +export const DOCUMENT_PERMISSIONS_DOCS_URL = `${DOCS_PREFIX}/workplace-search-sources-document-permissions.html`; +export const DOCUMENT_PERMISSIONS_SYNC_DOCS_URL = `${DOCUMENT_PERMISSIONS_DOCS_URL}#sources-permissions-synchronizing`; +export const PRIVATE_SOURCES_DOCS_URL = `${DOCUMENT_PERMISSIONS_DOCS_URL}#sources-permissions-org-private`; +export const EXTERNAL_IDENTITIES_DOCS_URL = `${DOCS_PREFIX}/workplace-search-external-identities-api.html`; +export const SECURITY_DOCS_URL = `${DOCS_PREFIX}/workplace-search-security.html`; +export const SMTP_DOCS_URL = `${DOCS_PREFIX}/workplace-search-smtp-mailer.html`; +export const CONFLUENCE_DOCS_URL = `${DOCS_PREFIX}/workplace-search-confluence-cloud-connector.html`; +export const CONFLUENCE_SERVER_DOCS_URL = `${DOCS_PREFIX}/workplace-search-confluence-server-connector.html`; +export const DROPBOX_DOCS_URL = `${DOCS_PREFIX}/workplace-search-dropbox-connector.html`; +export const GITHUB_DOCS_URL = `${DOCS_PREFIX}/workplace-search-github-connector.html`; +export const GITHUB_ENTERPRISE_DOCS_URL = `${DOCS_PREFIX}/workplace-search-github-connector.html`; +export const GMAIL_DOCS_URL = `${DOCS_PREFIX}/workplace-search-gmail-connector.html`; +export const GOOGLE_DRIVE_DOCS_URL = `${DOCS_PREFIX}/workplace-search-google-drive-connector.html`; +export const JIRA_DOCS_URL = `${DOCS_PREFIX}/workplace-search-jira-cloud-connector.html`; +export const JIRA_SERVER_DOCS_URL = `${DOCS_PREFIX}/workplace-search-jira-server-connector.html`; +export const ONE_DRIVE_DOCS_URL = `${DOCS_PREFIX}/workplace-search-onedrive-connector.html`; +export const SALESFORCE_DOCS_URL = `${DOCS_PREFIX}/workplace-search-salesforce-connector.html`; +export const SERVICE_NOW_DOCS_URL = `${DOCS_PREFIX}/workplace-search-servicenow-connector.html`; +export const SHARE_POINT_DOCS_URL = `${DOCS_PREFIX}/workplace-search-sharepoint-online-connector.html`; +export const SLACK_DOCS_URL = `${DOCS_PREFIX}/workplace-search-slack-connector.html`; +export const ZENDESK_DOCS_URL = `${DOCS_PREFIX}/workplace-search-zendesk-connector.html`; +export const CUSTOM_SOURCE_DOCS_URL = `${DOCS_PREFIX}/workplace-search-custom-api-sources.html`; +export const CUSTOM_API_DOCS_URL = `${DOCS_PREFIX}/workplace-search-custom-sources-api.html`; +export const CUSTOM_API_DOCUMENT_PERMISSIONS_DOCS_URL = `${CUSTOM_SOURCE_DOCS_URL}#custom-api-source-document-level-access-control`; +export const ENT_SEARCH_LICENSE_MANAGEMENT = `${ENT_SEARCH_DOCS_PREFIX}/license-management.html`; + +export const ORG_PATH = '/org'; + +export const ROLE_MAPPINGS_PATH = `${ORG_PATH}/role-mappings`; +export const ROLE_MAPPING_PATH = `${ROLE_MAPPINGS_PATH}/:roleId`; +export const ROLE_MAPPING_NEW_PATH = `${ROLE_MAPPINGS_PATH}/new`; + +export const USERS_PATH = `${ORG_PATH}/users`; +export const SECURITY_PATH = `${ORG_PATH}/security`; + +export const GROUPS_PATH = `${ORG_PATH}/groups`; +export const GROUP_PATH = `${GROUPS_PATH}/:groupId`; +export const GROUP_SOURCE_PRIORITIZATION_PATH = `${GROUPS_PATH}/:groupId/source-prioritization`; + +export const SOURCES_PATH = '/sources'; +export const ORG_SOURCES_PATH = `${ORG_PATH}${SOURCES_PATH}`; + +export const SOURCE_ADDED_PATH = `${SOURCES_PATH}/added`; +export const ADD_SOURCE_PATH = `${SOURCES_PATH}/add`; +export const ADD_CONFLUENCE_PATH = `${SOURCES_PATH}/add/confluence-cloud`; +export const ADD_CONFLUENCE_SERVER_PATH = `${SOURCES_PATH}/add/confluence-server`; +export const ADD_DROPBOX_PATH = `${SOURCES_PATH}/add/dropbox`; +export const ADD_GITHUB_ENTERPRISE_PATH = `${SOURCES_PATH}/add/github-enterprise-server`; +export const ADD_GITHUB_PATH = `${SOURCES_PATH}/add/github`; +export const ADD_GMAIL_PATH = `${SOURCES_PATH}/add/gmail`; +export const ADD_GOOGLE_DRIVE_PATH = `${SOURCES_PATH}/add/google-drive`; +export const ADD_JIRA_PATH = `${SOURCES_PATH}/add/jira-cloud`; +export const ADD_JIRA_SERVER_PATH = `${SOURCES_PATH}/add/jira-server`; +export const ADD_ONE_DRIVE_PATH = `${SOURCES_PATH}/add/one-drive`; +export const ADD_SALESFORCE_PATH = `${SOURCES_PATH}/add/salesforce`; +export const ADD_SERVICE_NOW_PATH = `${SOURCES_PATH}/add/service-now`; +export const ADD_SHARE_POINT_PATH = `${SOURCES_PATH}/add/share-point`; +export const ADD_SLACK_PATH = `${SOURCES_PATH}/add/slack`; +export const ADD_ZENDESK_PATH = `${SOURCES_PATH}/add/zendesk`; +export const ADD_CUSTOM_PATH = `${SOURCES_PATH}/add/custom`; + +export const PERSONAL_SETTINGS_PATH = '/settings'; + +export const SOURCE_DETAILS_PATH = `${SOURCES_PATH}/:sourceId`; +export const SOURCE_CONTENT_PATH = `${SOURCES_PATH}/:sourceId/content`; +export const SOURCE_SCHEMAS_PATH = `${SOURCES_PATH}/:sourceId/schemas`; +export const SOURCE_DISPLAY_SETTINGS_PATH = `${SOURCES_PATH}/:sourceId/display-settings`; +export const SOURCE_SETTINGS_PATH = `${SOURCES_PATH}/:sourceId/settings`; +export const REINDEX_JOB_PATH = `${SOURCES_PATH}/:sourceId/schema-errors/:activeReindexJobId`; + +export const DISPLAY_SETTINGS_SEARCH_RESULT_PATH = `${SOURCE_DISPLAY_SETTINGS_PATH}/`; +export const DISPLAY_SETTINGS_RESULT_DETAIL_PATH = `${SOURCE_DISPLAY_SETTINGS_PATH}/result-detail`; + +export const ORG_SETTINGS_PATH = `${ORG_PATH}/settings`; +export const ORG_SETTINGS_CUSTOMIZE_PATH = `${ORG_SETTINGS_PATH}/customize`; +export const ORG_SETTINGS_CONNECTORS_PATH = `${ORG_SETTINGS_PATH}/connectors`; +export const ORG_SETTINGS_OAUTH_APPLICATION_PATH = `${ORG_SETTINGS_PATH}/oauth`; +export const EDIT_CONFLUENCE_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/confluence-cloud/edit`; +export const EDIT_CONFLUENCE_SERVER_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/confluence-server/edit`; +export const EDIT_DROPBOX_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/dropbox/edit`; +export const EDIT_GITHUB_ENTERPRISE_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/github-enterprise-server/edit`; +export const EDIT_GITHUB_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/github/edit`; +export const EDIT_GMAIL_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/gmail/edit`; +export const EDIT_GOOGLE_DRIVE_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/google-drive/edit`; +export const EDIT_JIRA_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/jira-cloud/edit`; +export const EDIT_JIRA_SERVER_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/jira-server/edit`; +export const EDIT_ONE_DRIVE_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/one-drive/edit`; +export const EDIT_SALESFORCE_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/salesforce/edit`; +export const EDIT_SERVICE_NOW_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/service-now/edit`; +export const EDIT_SHARE_POINT_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/share-point/edit`; +export const EDIT_SLACK_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/slack/edit`; +export const EDIT_ZENDESK_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/zendesk/edit`; +export const EDIT_CUSTOM_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/custom/edit`; + export const getSourcePath = (sourceId: string): string => `${ORG_SOURCES_PATH}/${sourceId}`; From 459e9d603d04ae66c72188091107322bf4b65bab Mon Sep 17 00:00:00 2001 From: Robert Austin Date: Fri, 14 Aug 2020 16:31:28 -0400 Subject: [PATCH 03/15] [Resolver] simulator tests select elements directly instead of using descendant selectors. (#75058) Our tests shouldn't rely on the DOM structure of Resolver (when its arbitrary) because that will make them brittle. If the user doesn't care about the DOM structure, then neither should our tests. Note: sometimes the user does care about the DOM structure, and in those cases the tests should as well. --- .../test_utilities/simulator/index.tsx | 22 +++++++++---- .../resolver/view/clickthrough.test.tsx | 32 +++++++++---------- .../resolver/view/process_event_dot.tsx | 3 ++ .../public/resolver/view/query_params.test.ts | 16 +++++----- .../public/resolver/view/submenu.tsx | 3 ++ 5 files changed, 45 insertions(+), 31 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx index 14cdc26c80f53..43a03bb771501 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx @@ -175,14 +175,24 @@ export class Simulator { } /** - * Return an Enzyme ReactWrapper for any child elements of a specific processNodeElement - * - * @param entityID The entity ID of the proocess node to select in - * @param selector The selector for the child element of the process node + * The button that opens a node's submenu. */ - public processNodeChildElements(entityID: string, selector: string): ReactWrapper { + public processNodeSubmenuButton( + /** nodeID for the related node */ entityID: string + ): ReactWrapper { return this.domNodes( - `${processNodeElementSelector({ entityID })} [data-test-subj="${selector}"]` + `[data-test-subj="resolver:submenu:button"][data-test-resolver-node-id="${entityID}"]` + ); + } + + /** + * The primary button (used to select a node) which contains a label for the node as its content. + */ + public processNodePrimaryButton( + /** nodeID for the related node */ entityID: string + ): ReactWrapper { + return this.domNodes( + `[data-test-subj="resolver:node:primary-button"][data-test-resolver-node-id="${entityID}"]` ); } diff --git a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx index 09fcd273a9c9b..3265ee8bcfca0 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx @@ -62,13 +62,13 @@ describe('Resolver, when analyzing a tree that has no ancestors and 2 children', selectedOriginCount: simulator.selectedProcessNode(entityIDs.origin).length, unselectedFirstChildCount: simulator.unselectedProcessNode(entityIDs.firstChild).length, unselectedSecondChildCount: simulator.unselectedProcessNode(entityIDs.secondChild).length, - processNodeCount: simulator.processNodeElements().length, + nodePrimaryButtonCount: simulator.testSubject('resolver:node:primary-button').length, })) ).toYieldEqualTo({ selectedOriginCount: 1, unselectedFirstChildCount: 1, unselectedSecondChildCount: 1, - processNodeCount: 3, + nodePrimaryButtonCount: 3, }); }); @@ -82,13 +82,14 @@ describe('Resolver, when analyzing a tree that has no ancestors and 2 children', }); describe("when the second child node's first button has been clicked", () => { - beforeEach(() => { - // Click the first button under the second child element. - simulator - .processNodeElements({ entityID: entityIDs.secondChild }) - .find('button') - .first() - .simulate('click'); + beforeEach(async () => { + const button = await simulator.resolveWrapper(() => + simulator.processNodePrimaryButton(entityIDs.secondChild) + ); + // Click the second child node's primary button + if (button) { + button.simulate('click'); + } }); it('should render the second child node as selected, and the origin as not selected, and the query string should indicate that the second child is selected', async () => { await expect( @@ -141,23 +142,20 @@ describe('Resolver, when analyzing a tree that has two related events for the or graphElements: simulator.testSubject('resolver:graph').length, graphLoadingElements: simulator.testSubject('resolver:graph:loading').length, graphErrorElements: simulator.testSubject('resolver:graph:error').length, - originNode: simulator.processNodeElements({ entityID: entityIDs.origin }).length, + originNodeButton: simulator.processNodePrimaryButton(entityIDs.origin).length, })) ).toYieldEqualTo({ graphElements: 1, graphLoadingElements: 0, graphErrorElements: 0, - originNode: 1, + originNodeButton: 1, }); }); it('should render a related events button', async () => { await expect( simulator.map(() => ({ - relatedEventButtons: simulator.processNodeChildElements( - entityIDs.origin, - 'resolver:submenu:button' - ).length, + relatedEventButtons: simulator.processNodeSubmenuButton(entityIDs.origin).length, })) ).toYieldEqualTo({ relatedEventButtons: 1, @@ -166,7 +164,7 @@ describe('Resolver, when analyzing a tree that has two related events for the or describe('when the related events button is clicked', () => { beforeEach(async () => { const button = await simulator.resolveWrapper(() => - simulator.processNodeChildElements(entityIDs.origin, 'resolver:submenu:button') + simulator.processNodeSubmenuButton(entityIDs.origin) ); if (button) { button.simulate('click'); @@ -183,7 +181,7 @@ describe('Resolver, when analyzing a tree that has two related events for the or describe('and when the related events button is clicked again', () => { beforeEach(async () => { const button = await simulator.resolveWrapper(() => - simulator.processNodeChildElements(entityIDs.origin, 'resolver:submenu:button') + simulator.processNodeSubmenuButton(entityIDs.origin) ); if (button) { button.simulate('click'); diff --git a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx index 2a5d91028d9f5..2bb104801866f 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx @@ -404,6 +404,8 @@ const UnstyledProcessEventDot = React.memo( }} tabIndex={-1} title={eventModel.processNameSafeVersion(event)} + data-test-subj="resolver:node:primary-button" + data-test-resolver-node-id={nodeID} > @@ -433,6 +435,7 @@ const UnstyledProcessEventDot = React.memo( menuTitle={subMenuAssets.relatedEvents.title} projectionMatrix={projectionMatrix} optionsWithActions={relatedEventStatusOrOptions} + nodeID={nodeID} /> )} diff --git a/x-pack/plugins/security_solution/public/resolver/view/query_params.test.ts b/x-pack/plugins/security_solution/public/resolver/view/query_params.test.ts index 26c25cfab2c21..a86237e0e2b45 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/query_params.test.ts +++ b/x-pack/plugins/security_solution/public/resolver/view/query_params.test.ts @@ -34,12 +34,12 @@ describe('Resolver, when analyzing a tree that has no ancestors and 2 children', describe("when the second child node's first button has been clicked", () => { beforeEach(async () => { - const node = await simulator.resolveWrapper(() => - simulator.processNodeElements({ entityID: entityIDs.secondChild }).find('button') + const button = await simulator.resolveWrapper(() => + simulator.processNodePrimaryButton(entityIDs.secondChild) ); - if (node) { + if (button) { // Click the first button under the second child element. - node.first().simulate('click'); + button.simulate('click'); } }); const expectedSearch = urlSearch(resolverComponentInstanceID, { @@ -68,12 +68,12 @@ describe('Resolver, when analyzing a tree that has no ancestors and 2 children', }); describe("when the user clicks the second child node's button again", () => { beforeEach(async () => { - const node = await simulator.resolveWrapper(() => - simulator.processNodeElements({ entityID: entityIDs.secondChild }).find('button') + const button = await simulator.resolveWrapper(() => + simulator.processNodePrimaryButton(entityIDs.secondChild) ); - if (node) { + if (button) { // Click the first button under the second child element. - node.first().simulate('click'); + button.simulate('click'); } }); it(`should have a url search of ${urlSearch(newInstanceID, { diff --git a/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx b/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx index 359a4e2dafd2e..14d6470c95207 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx @@ -137,6 +137,7 @@ const NodeSubMenuComponents = React.memo( optionsWithActions, className, projectionMatrix, + nodeID, }: { menuTitle: string; className?: string; @@ -148,6 +149,7 @@ const NodeSubMenuComponents = React.memo( * Receive the projection matrix, so we can see when the camera position changed, so we can force the submenu to reposition itself. */ projectionMatrix: Matrix3; + nodeID: string; } & { optionsWithActions?: ResolverSubmenuOptionList | string | undefined; }) => { @@ -236,6 +238,7 @@ const NodeSubMenuComponents = React.memo( iconSide="right" tabIndex={-1} data-test-subj="resolver:submenu:button" + data-test-resolver-node-id={nodeID} > {count ? : ''} {menuTitle} From 64b8b88c649506a465d775b56cb2e16b7e217c0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yulia=20=C4=8Cech?= <6585477+yuliacech@users.noreply.github.com> Date: Fri, 14 Aug 2020 23:12:01 +0200 Subject: [PATCH 04/15] [ILM] TS conversion of Edit policy components (#74747) * [ILM] Convert node allocation component to TS and use hooks * [ILM] Fix jest tests * [ILM] Fix i18n check * [ILM] Implement code review suggestions * [ILM] Fix type check, docs link and button maxWidth in NodeAllocation component * Fix internaliation error * [ILM] Convert node details flyout to TS * [ILM] Fix useState declaration * [ILM] Fix useState declaration * [ILM] Fix jest test * [ILM] Change error message when unable to load node attributes * [ILM] Change error message when unable to load node details * [ILM] Delete a period in error callout * [ILM] Delete a period in error callout * [ILM] Convert node details flyout to TS * [ILM] Fix i18n check * [ILM] Fix useState declaration * [ILM] Fix useState declaration * [ILM] Fix jest test * [ILM] Change error message when unable to load node details * [ILM] Delete a period in error callout * [ILM] edit components * [ILM] Fix review suggestions Co-authored-by: Elastic Machine --- .../components/active_badge.tsx} | 0 .../cold_phase/cold_phase.container.js | 22 ----- .../components/cold_phase/index.js | 7 -- .../delete_phase/delete_phase.container.js | 21 ---- .../components/delete_phase/index.js | 7 -- .../{ => components}/form_errors.tsx | 0 .../hot_phase/hot_phase.container.js | 22 ----- .../edit_policy/components/hot_phase/index.js | 7 -- .../components/index.ts} | 9 +- .../components/learn_more_link.tsx | 2 +- .../{min_age_input.js => min_age_input.tsx} | 27 +++-- .../{node_allocation => }/node_allocation.tsx | 15 +-- .../node_attrs_details.tsx | 2 +- .../components/node_attrs_details/index.ts | 7 -- .../components/optional_label.tsx} | 0 .../components/phase_error_message.tsx} | 2 +- .../components/policy_json_flyout.js | 95 ------------------ .../components/policy_json_flyout.tsx | 98 +++++++++++++++++++ ...iority_input.js => set_priority_input.tsx} | 22 ++++- .../snapshot_policies.tsx | 2 +- .../components/snapshot_policies/index.ts | 7 -- .../components/warm_phase/index.js | 7 -- .../warm_phase/warm_phase.container.js | 22 ----- .../edit_policy/edit_policy.container.js | 4 + .../sections/edit_policy/edit_policy.js | 29 ++++-- .../cold_phase.js => phases/cold_phase.tsx} | 32 +++--- .../delete_phase.tsx} | 38 ++++--- .../hot_phase.js => phases/hot_phase.tsx} | 27 ++--- .../node_allocation => phases}/index.ts | 5 +- .../warm_phase.js => phases/warm_phase.tsx} | 34 ++++--- .../add_policy_to_template_confirm_modal.js | 2 +- .../public/application/store/actions/nodes.js | 3 - 32 files changed, 267 insertions(+), 310 deletions(-) rename x-pack/plugins/index_lifecycle_management/public/application/sections/{components/active_badge.js => edit_policy/components/active_badge.tsx} (100%) delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/cold_phase/cold_phase.container.js delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/cold_phase/index.js delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/delete_phase/delete_phase.container.js delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/delete_phase/index.js rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/{ => components}/form_errors.tsx (100%) delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/hot_phase/hot_phase.container.js delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/hot_phase/index.js rename x-pack/plugins/index_lifecycle_management/public/application/sections/{components/index.js => edit_policy/components/index.ts} (54%) rename x-pack/plugins/index_lifecycle_management/public/application/sections/{ => edit_policy}/components/learn_more_link.tsx (92%) rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/{min_age_input.js => min_age_input.tsx} (91%) rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/{node_allocation => }/node_allocation.tsx (93%) rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/{node_attrs_details => }/node_attrs_details.tsx (97%) delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_attrs_details/index.ts rename x-pack/plugins/index_lifecycle_management/public/application/sections/{components/optional_label.js => edit_policy/components/optional_label.tsx} (100%) rename x-pack/plugins/index_lifecycle_management/public/application/sections/{components/phase_error_message.js => edit_policy/components/phase_error_message.tsx} (87%) delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.js create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.tsx rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/{set_priority_input.js => set_priority_input.tsx} (80%) rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/{snapshot_policies => }/snapshot_policies.tsx (98%) delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/snapshot_policies/index.ts delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/warm_phase/index.js delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/warm_phase/warm_phase.container.js rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/{components/cold_phase/cold_phase.js => phases/cold_phase.tsx} (92%) rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/{components/delete_phase/delete_phase.js => phases/delete_phase.tsx} (88%) rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/{components/hot_phase/hot_phase.js => phases/hot_phase.tsx} (96%) rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/{components/node_allocation => phases}/index.ts (58%) rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/{components/warm_phase/warm_phase.js => phases/warm_phase.tsx} (95%) diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/components/active_badge.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/active_badge.tsx similarity index 100% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/components/active_badge.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/active_badge.tsx diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/cold_phase/cold_phase.container.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/cold_phase/cold_phase.container.js deleted file mode 100644 index d4605ceb43499..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/cold_phase/cold_phase.container.js +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { connect } from 'react-redux'; - -import { getPhase } from '../../../../store/selectors'; -import { setPhaseData } from '../../../../store/actions'; -import { PHASE_COLD, PHASE_HOT, PHASE_ROLLOVER_ENABLED } from '../../../../constants'; -import { ColdPhase as PresentationComponent } from './cold_phase'; - -export const ColdPhase = connect( - (state) => ({ - phaseData: getPhase(state, PHASE_COLD), - hotPhaseRolloverEnabled: getPhase(state, PHASE_HOT)[PHASE_ROLLOVER_ENABLED], - }), - { - setPhaseData: (key, value) => setPhaseData(PHASE_COLD, key, value), - } -)(PresentationComponent); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/cold_phase/index.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/cold_phase/index.js deleted file mode 100644 index e0d70ceb57726..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/cold_phase/index.js +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { ColdPhase } from './cold_phase.container'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/delete_phase/delete_phase.container.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/delete_phase/delete_phase.container.js deleted file mode 100644 index 84bd17e3637e8..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/delete_phase/delete_phase.container.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { connect } from 'react-redux'; -import { getPhase } from '../../../../store/selectors'; -import { setPhaseData } from '../../../../store/actions'; -import { PHASE_DELETE, PHASE_HOT, PHASE_ROLLOVER_ENABLED } from '../../../../constants'; -import { DeletePhase as PresentationComponent } from './delete_phase'; - -export const DeletePhase = connect( - (state) => ({ - phaseData: getPhase(state, PHASE_DELETE), - hotPhaseRolloverEnabled: getPhase(state, PHASE_HOT)[PHASE_ROLLOVER_ENABLED], - }), - { - setPhaseData: (key, value) => setPhaseData(PHASE_DELETE, key, value), - } -)(PresentationComponent); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/delete_phase/index.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/delete_phase/index.js deleted file mode 100644 index 5f909ab2c0f79..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/delete_phase/index.js +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { DeletePhase } from './delete_phase.container'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_errors.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/form_errors.tsx similarity index 100% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_errors.tsx rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/form_errors.tsx diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/hot_phase/hot_phase.container.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/hot_phase/hot_phase.container.js deleted file mode 100644 index 5f1451afdcc31..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/hot_phase/hot_phase.container.js +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { connect } from 'react-redux'; - -import { getPhase } from '../../../../store/selectors'; -import { setPhaseData } from '../../../../store/actions'; -import { PHASE_HOT, PHASE_WARM, WARM_PHASE_ON_ROLLOVER } from '../../../../constants'; -import { HotPhase as PresentationComponent } from './hot_phase'; - -export const HotPhase = connect( - (state) => ({ - phaseData: getPhase(state, PHASE_HOT), - }), - { - setPhaseData: (key, value) => setPhaseData(PHASE_HOT, key, value), - setWarmPhaseOnRollover: (value) => setPhaseData(PHASE_WARM, WARM_PHASE_ON_ROLLOVER, value), - } -)(PresentationComponent); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/hot_phase/index.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/hot_phase/index.js deleted file mode 100644 index 114e34c3ef4d0..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/hot_phase/index.js +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { HotPhase } from './hot_phase.container'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/components/index.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts similarity index 54% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/components/index.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts index a2ae37780b9f9..e933c46e98491 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/components/index.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts @@ -5,6 +5,13 @@ */ export { ActiveBadge } from './active_badge'; +export { ErrableFormRow } from './form_errors'; export { LearnMoreLink } from './learn_more_link'; -export { PhaseErrorMessage } from './phase_error_message'; +export { MinAgeInput } from './min_age_input'; +export { NodeAllocation } from './node_allocation'; +export { NodeAttrsDetails } from './node_attrs_details'; export { OptionalLabel } from './optional_label'; +export { PhaseErrorMessage } from './phase_error_message'; +export { PolicyJsonFlyout } from './policy_json_flyout'; +export { SetPriorityInput } from './set_priority_input'; +export { SnapshotPolicies } from './snapshot_policies'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/components/learn_more_link.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/learn_more_link.tsx similarity index 92% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/components/learn_more_link.tsx rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/learn_more_link.tsx index 623ff982438d7..5ada49b318018 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/components/learn_more_link.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/learn_more_link.tsx @@ -8,7 +8,7 @@ import React, { ReactNode } from 'react'; import { EuiLink } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { createDocLink } from '../../services/documentation'; +import { createDocLink } from '../../../services/documentation'; interface Props { docPath: string; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.tsx similarity index 91% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.tsx index d90ad9378efd4..c9732f2311758 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.tsx @@ -16,10 +16,10 @@ import { PHASE_COLD, PHASE_DELETE, } from '../../../constants'; -import { LearnMoreLink } from '../../components'; -import { ErrableFormRow } from '../form_errors'; +import { LearnMoreLink } from './learn_more_link'; +import { ErrableFormRow } from './form_errors'; -function getTimingLabelForPhase(phase) { +function getTimingLabelForPhase(phase: string) { // NOTE: Hot phase isn't necessary, because indices begin in the hot phase. switch (phase) { case PHASE_WARM: @@ -39,7 +39,7 @@ function getTimingLabelForPhase(phase) { } } -function getUnitsAriaLabelForPhase(phase) { +function getUnitsAriaLabelForPhase(phase: string) { // NOTE: Hot phase isn't necessary, because indices begin in the hot phase. switch (phase) { case PHASE_WARM: @@ -68,9 +68,24 @@ function getUnitsAriaLabelForPhase(phase) { } } -export const MinAgeInput = (props) => { - const { rolloverEnabled, errors, phaseData, phase, setPhaseData, isShowingErrors } = props; +interface Props { + rolloverEnabled: boolean; + errors: Record; + phase: string; + // TODO add types for phaseData and setPhaseData after policy is typed + phaseData: any; + setPhaseData: (dataKey: string, value: any) => void; + isShowingErrors: boolean; +} +export const MinAgeInput: React.FunctionComponent = ({ + rolloverEnabled, + errors, + phaseData, + phase, + setPhaseData, + isShowingErrors, +}) => { let daysOptionLabel; let hoursOptionLabel; let minutesOptionLabel; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/node_allocation.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation.tsx similarity index 93% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/node_allocation.tsx rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation.tsx index 31261de45c743..576483a5ab9c2 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/node_allocation.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation.tsx @@ -16,17 +16,18 @@ import { EuiButton, } from '@elastic/eui'; -import { PHASE_NODE_ATTRS } from '../../../../constants'; -import { LearnMoreLink } from '../../../components/learn_more_link'; -import { ErrableFormRow } from '../../form_errors'; -import { useLoadNodes } from '../../../../services/api'; -import { NodeAttrsDetails } from '../node_attrs_details'; +import { PHASE_NODE_ATTRS } from '../../../constants'; +import { LearnMoreLink } from './learn_more_link'; +import { ErrableFormRow } from './form_errors'; +import { useLoadNodes } from '../../../services/api'; +import { NodeAttrsDetails } from './node_attrs_details'; interface Props { phase: string; - setPhaseData: (dataKey: string, value: any) => void; - errors: any; + errors: Record; + // TODO add types for phaseData and setPhaseData after policy is typed phaseData: any; + setPhaseData: (dataKey: string, value: any) => void; isShowingErrors: boolean; } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_attrs_details/node_attrs_details.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_attrs_details.tsx similarity index 97% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_attrs_details/node_attrs_details.tsx rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_attrs_details.tsx index 6fcbd94dc5e9a..cd87cc324a414 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_attrs_details/node_attrs_details.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_attrs_details.tsx @@ -20,7 +20,7 @@ import { EuiButton, } from '@elastic/eui'; -import { useLoadNodeDetails } from '../../../../services/api'; +import { useLoadNodeDetails } from '../../../services/api'; interface Props { close: () => void; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_attrs_details/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_attrs_details/index.ts deleted file mode 100644 index 056d2f2f600f3..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_attrs_details/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { NodeAttrsDetails } from './node_attrs_details'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/components/optional_label.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/optional_label.tsx similarity index 100% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/components/optional_label.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/optional_label.tsx diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/components/phase_error_message.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_error_message.tsx similarity index 87% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/components/phase_error_message.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_error_message.tsx index 904ac7c25f2f9..750f68543f221 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/components/phase_error_message.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_error_message.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { EuiBadge } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -export const PhaseErrorMessage = ({ isShowingErrors }) => { +export const PhaseErrorMessage = ({ isShowingErrors }: { isShowingErrors: boolean }) => { return isShowingErrors ? ( '}`; - const request = `${endpoint}\n${this.getEsJson(lifecycle)}`; - - return ( - - - -

- {policyName ? ( - - ) : ( - - )} -

-
-
- - - -

- -

-
- - - - - {request} - -
- - - - - - -
- ); - } -} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.tsx new file mode 100644 index 0000000000000..aaf4aa6e6222d --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.tsx @@ -0,0 +1,98 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n/react'; + +import { + EuiButtonEmpty, + EuiCodeBlock, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; + +interface Props { + close: () => void; + // TODO add types for lifecycle after policy is typed + lifecycle: any; + policyName: string; +} + +export const PolicyJsonFlyout: React.FunctionComponent = ({ + close, + lifecycle, + policyName, +}) => { + // @ts-ignore until store is typed + const getEsJson = ({ phases }) => { + return JSON.stringify( + { + policy: { + phases, + }, + }, + null, + 2 + ); + }; + + const endpoint = `PUT _ilm/policy/${policyName || ''}`; + const request = `${endpoint}\n${getEsJson(lifecycle)}`; + + return ( + + + +

+ {policyName ? ( + + ) : ( + + )} +

+
+
+ + + +

+ +

+
+ + + + + {request} + +
+ + + + + + +
+ ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.tsx similarity index 80% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.tsx index bdcc1e23b4230..0034de85fce17 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.tsx @@ -8,12 +8,26 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFieldNumber, EuiTextColor, EuiDescribedFormGroup } from '@elastic/eui'; import { PHASE_INDEX_PRIORITY } from '../../../constants'; -import { LearnMoreLink, OptionalLabel } from '../../components'; -import { ErrableFormRow } from '../form_errors'; -export const SetPriorityInput = (props) => { - const { errors, phaseData, phase, setPhaseData, isShowingErrors } = props; +import { LearnMoreLink } from './'; +import { OptionalLabel } from './'; +import { ErrableFormRow } from './'; +interface Props { + errors: Record; + // TODO add types for phaseData and setPhaseData after policy is typed + phase: string; + phaseData: any; + setPhaseData: (dataKey: string, value: any) => void; + isShowingErrors: boolean; +} +export const SetPriorityInput: React.FunctionComponent = ({ + errors, + phaseData, + phase, + setPhaseData, + isShowingErrors, +}) => { return ( ({ - phaseData: getPhase(state, PHASE_WARM), - hotPhaseRolloverEnabled: getPhase(state, PHASE_HOT)[PHASE_ROLLOVER_ENABLED], - }), - { - setPhaseData: (key, value) => setPhaseData(PHASE_WARM, key, value), - } -)(PresentationComponent); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.js index 1c6ced8953211..e7f20a66d09f0 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.js @@ -15,6 +15,7 @@ import { isPolicyListLoaded, getIsNewPolicy, getSelectedOriginalPolicyName, + getPhases, } from '../../store/selectors'; import { @@ -23,6 +24,7 @@ import { setSaveAsNewPolicy, saveLifecyclePolicy, fetchPolicies, + setPhaseData, } from '../../store/actions'; import { findFirstError } from '../../services/find_errors'; @@ -42,6 +44,7 @@ export const EditPolicy = connect( isPolicyListLoaded: isPolicyListLoaded(state), isNewPolicy: getIsNewPolicy(state), originalPolicyName: getSelectedOriginalPolicyName(state), + phases: getPhases(state), }; }, { @@ -50,5 +53,6 @@ export const EditPolicy = connect( setSaveAsNewPolicy, saveLifecyclePolicy, fetchPolicies, + setPhaseData, } )(PresentationComponent); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.js index d9d8866a2e2cc..a29ecd07c5e45 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.js @@ -33,17 +33,15 @@ import { PHASE_DELETE, PHASE_WARM, STRUCTURE_POLICY_NAME, + WARM_PHASE_ON_ROLLOVER, + PHASE_ROLLOVER_ENABLED, } from '../../constants'; import { toasts } from '../../services/notification'; import { findFirstError } from '../../services/find_errors'; -import { LearnMoreLink } from '../components'; -import { PolicyJsonFlyout } from './components/policy_json_flyout'; -import { ErrableFormRow } from './form_errors'; -import { HotPhase } from './components/hot_phase'; -import { WarmPhase } from './components/warm_phase'; -import { DeletePhase } from './components/delete_phase'; -import { ColdPhase } from './components/cold_phase'; +import { LearnMoreLink, PolicyJsonFlyout, ErrableFormRow } from './components'; + +import { HotPhase, WarmPhase, ColdPhase, DeletePhase } from './phases'; export class EditPolicy extends Component { static propTypes = { @@ -137,6 +135,8 @@ export class EditPolicy extends Component { isNewPolicy, lifecycle, originalPolicyName, + phases, + setPhaseData, } = this.props; const selectedPolicyName = selectedPolicy.name; const { isShowingErrors, isShowingPolicyJsonFlyout } = this.state; @@ -275,9 +275,13 @@ export class EditPolicy extends Component { setPhaseData(PHASE_HOT, key, value)} + phaseData={phases[PHASE_HOT]} + setWarmPhaseOnRollover={(value) => + setPhaseData(PHASE_WARM, WARM_PHASE_ON_ROLLOVER, value) + } /> @@ -285,6 +289,9 @@ export class EditPolicy extends Component { setPhaseData(PHASE_WARM, key, value)} + phaseData={phases[PHASE_WARM]} + hotPhaseRolloverEnabled={phases[PHASE_HOT][PHASE_ROLLOVER_ENABLED]} /> @@ -292,6 +299,9 @@ export class EditPolicy extends Component { setPhaseData(PHASE_COLD, key, value)} + phaseData={phases[PHASE_COLD]} + hotPhaseRolloverEnabled={phases[PHASE_HOT][PHASE_ROLLOVER_ENABLED]} /> @@ -300,6 +310,9 @@ export class EditPolicy extends Component { errors={errors[PHASE_DELETE]} isShowingErrors={isShowingErrors && !!findFirstError(errors[PHASE_DELETE], false)} getUrlForApp={this.props.getUrlForApp} + setPhaseData={(key, value) => setPhaseData(PHASE_DELETE, key, value)} + phaseData={phases[PHASE_DELETE]} + hotPhaseRolloverEnabled={phases[PHASE_HOT][PHASE_ROLLOVER_ENABLED]} /> diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/cold_phase/cold_phase.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/cold_phase.tsx similarity index 92% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/cold_phase/cold_phase.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/cold_phase.tsx index 200bf0e767d9d..babbbf7638ebe 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/cold_phase/cold_phase.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/cold_phase.tsx @@ -5,7 +5,6 @@ */ import React, { PureComponent, Fragment } from 'react'; -import PropTypes from 'prop-types'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; @@ -24,20 +23,27 @@ import { PHASE_ENABLED, PHASE_REPLICA_COUNT, PHASE_FREEZE_ENABLED, -} from '../../../../constants'; -import { LearnMoreLink, ActiveBadge, PhaseErrorMessage, OptionalLabel } from '../../../components'; -import { ErrableFormRow } from '../../form_errors'; -import { MinAgeInput } from '../min_age_input'; -import { NodeAllocation } from '../node_allocation'; -import { SetPriorityInput } from '../set_priority_input'; +} from '../../../constants'; +import { + LearnMoreLink, + ActiveBadge, + PhaseErrorMessage, + OptionalLabel, + ErrableFormRow, + MinAgeInput, + NodeAllocation, + SetPriorityInput, +} from '../components'; -export class ColdPhase extends PureComponent { - static propTypes = { - setPhaseData: PropTypes.func.isRequired, +interface Props { + setPhaseData: (key: string, value: any) => void; + phaseData: any; + isShowingErrors: boolean; + errors: Record; + hotPhaseRolloverEnabled: boolean; +} - isShowingErrors: PropTypes.bool.isRequired, - errors: PropTypes.object.isRequired, - }; +export class ColdPhase extends PureComponent { render() { const { setPhaseData, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/delete_phase/delete_phase.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/delete_phase.tsx similarity index 88% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/delete_phase/delete_phase.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/delete_phase.tsx index 2b12eec953e11..0143cc4af24e3 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/delete_phase/delete_phase.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/delete_phase.tsx @@ -5,22 +5,35 @@ */ import React, { PureComponent, Fragment } from 'react'; -import PropTypes from 'prop-types'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiDescribedFormGroup, EuiSwitch, EuiTextColor, EuiFormRow } from '@elastic/eui'; -import { PHASE_DELETE, PHASE_ENABLED, PHASE_WAIT_FOR_SNAPSHOT_POLICY } from '../../../../constants'; -import { ActiveBadge, LearnMoreLink, OptionalLabel, PhaseErrorMessage } from '../../../components'; -import { MinAgeInput } from '../min_age_input'; -import { SnapshotPolicies } from '../snapshot_policies'; +import { PHASE_DELETE, PHASE_ENABLED, PHASE_WAIT_FOR_SNAPSHOT_POLICY } from '../../../constants'; +import { + ActiveBadge, + LearnMoreLink, + OptionalLabel, + PhaseErrorMessage, + MinAgeInput, + SnapshotPolicies, +} from '../components'; -export class DeletePhase extends PureComponent { - static propTypes = { - setPhaseData: PropTypes.func.isRequired, - isShowingErrors: PropTypes.bool.isRequired, - errors: PropTypes.object.isRequired, - }; +interface Props { + setPhaseData: (key: string, value: any) => void; + phaseData: any; + isShowingErrors: boolean; + errors: Record; + hotPhaseRolloverEnabled: boolean; + getUrlForApp: ( + appId: string, + options?: { + path?: string; + absolute?: boolean; + } + ) => string; +} +export class DeletePhase extends PureComponent { render() { const { setPhaseData, @@ -28,6 +41,7 @@ export class DeletePhase extends PureComponent { errors, isShowingErrors, hotPhaseRolloverEnabled, + getUrlForApp, } = this.props; return ( @@ -123,7 +137,7 @@ export class DeletePhase extends PureComponent { setPhaseData(PHASE_WAIT_FOR_SNAPSHOT_POLICY, value)} - getUrlForApp={this.props.getUrlForApp} + getUrlForApp={getUrlForApp} /> diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/hot_phase/hot_phase.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/hot_phase.tsx similarity index 96% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/hot_phase/hot_phase.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/hot_phase.tsx index b420442198712..dbd48f3a85634 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/hot_phase/hot_phase.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/hot_phase.tsx @@ -5,7 +5,6 @@ */ import React, { Fragment, PureComponent } from 'react'; -import PropTypes from 'prop-types'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; @@ -28,18 +27,24 @@ import { PHASE_ROLLOVER_MAX_SIZE_STORED, PHASE_ROLLOVER_MAX_SIZE_STORED_UNITS, PHASE_ROLLOVER_ENABLED, -} from '../../../../constants'; -import { LearnMoreLink, ActiveBadge, PhaseErrorMessage } from '../../../components'; -import { ErrableFormRow } from '../../form_errors'; -import { SetPriorityInput } from '../set_priority_input'; +} from '../../../constants'; +import { + LearnMoreLink, + ActiveBadge, + PhaseErrorMessage, + ErrableFormRow, + SetPriorityInput, +} from '../components'; -export class HotPhase extends PureComponent { - static propTypes = { - setPhaseData: PropTypes.func.isRequired, - isShowingErrors: PropTypes.bool.isRequired, - errors: PropTypes.object.isRequired, - }; +interface Props { + errors: Record; + isShowingErrors: boolean; + phaseData: any; + setPhaseData: (key: string, value: any) => void; + setWarmPhaseOnRollover: (value: boolean) => void; +} +export class HotPhase extends PureComponent { render() { const { setPhaseData, phaseData, isShowingErrors, errors, setWarmPhaseOnRollover } = this.props; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/index.ts similarity index 58% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/index.ts rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/index.ts index 4675ab46ee501..8d1ace5950497 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/index.ts @@ -4,4 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export { NodeAllocation } from './node_allocation'; +export { HotPhase } from './hot_phase'; +export { WarmPhase } from './warm_phase'; +export { ColdPhase } from './cold_phase'; +export { DeletePhase } from './delete_phase'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/warm_phase/warm_phase.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/warm_phase.tsx similarity index 95% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/warm_phase/warm_phase.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/warm_phase.tsx index 60b5ab4781b6d..6ed81bf8f45d5 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/warm_phase/warm_phase.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/warm_phase.tsx @@ -5,7 +5,6 @@ */ import React, { Fragment, PureComponent } from 'react'; -import PropTypes from 'prop-types'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { @@ -28,21 +27,26 @@ import { PHASE_PRIMARY_SHARD_COUNT, PHASE_REPLICA_COUNT, PHASE_SHRINK_ENABLED, -} from '../../../../constants'; -import { LearnMoreLink, ActiveBadge, PhaseErrorMessage, OptionalLabel } from '../../../components'; -import { ErrableFormRow } from '../../form_errors'; -import { SetPriorityInput } from '../set_priority_input'; -import { NodeAllocation } from '../node_allocation'; -import { MinAgeInput } from '../min_age_input'; - -export class WarmPhase extends PureComponent { - static propTypes = { - setPhaseData: PropTypes.func.isRequired, - - isShowingErrors: PropTypes.bool.isRequired, - errors: PropTypes.object.isRequired, - }; +} from '../../../constants'; +import { + LearnMoreLink, + ActiveBadge, + PhaseErrorMessage, + OptionalLabel, + ErrableFormRow, + SetPriorityInput, + NodeAllocation, + MinAgeInput, +} from '../components'; +interface Props { + setPhaseData: (key: string, value: any) => void; + phaseData: any; + isShowingErrors: boolean; + errors: Record; + hotPhaseRolloverEnabled: boolean; +} +export class WarmPhase extends PureComponent { render() { const { setPhaseData, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/add_policy_to_template_confirm_modal.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/add_policy_to_template_confirm_modal.js index 8e53569047d8f..47134ad097720 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/add_policy_to_template_confirm_modal.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/add_policy_to_template_confirm_modal.js @@ -23,7 +23,7 @@ import { import { toasts } from '../../../../services/notification'; import { addLifecyclePolicyToTemplate, loadIndexTemplates } from '../../../../services/api'; import { showApiError } from '../../../../services/api_errors'; -import { LearnMoreLink } from '../../../components/learn_more_link'; +import { LearnMoreLink } from '../../../edit_policy/components'; export class AddPolicyToTemplateConfirmModal extends Component { state = { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/actions/nodes.js b/x-pack/plugins/index_lifecycle_management/public/application/store/actions/nodes.js index 3f1c00db621a7..45a8e63f70e83 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/actions/nodes.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/store/actions/nodes.js @@ -4,8 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ import { createAction } from 'redux-actions'; -import { SET_SELECTED_NODE_ATTRS } from '../../constants'; - -export const setSelectedNodeAttrs = createAction(SET_SELECTED_NODE_ATTRS); export const setSelectedPrimaryShardCount = createAction('SET_SELECTED_PRIMARY_SHARED_COUNT'); export const setSelectedReplicaCount = createAction('SET_SELECTED_REPLICA_COUNT'); From 52bd6d98ea567fd266429e77412722888b5c5ef9 Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Fri, 14 Aug 2020 14:20:12 -0700 Subject: [PATCH 05/15] Actions add proxy support (#74289) * Added proxy support for action types * Fixed tests * added rejectUnauthorizedCertificates config setting * removed slack not used code * Fixed Slack proxy * fixed typecheck errors * Cleanup code * Fixed slack * Added unit tests * added proxy server for test * Fixed build * Added functional tests * fixed due to comments * Fixed tests and some changes due to comments * Fixed functional tests * fixed circular deps * Added proxy unit test to action type --- x-pack/package.json | 12 ++- .../server/builtin_action_types/case/types.ts | 8 +- .../server/builtin_action_types/case/utils.ts | 16 ++- .../server/builtin_action_types/email.test.ts | 2 + .../server/builtin_action_types/email.ts | 1 + .../server/builtin_action_types/index.ts | 6 +- .../server/builtin_action_types/jira/index.ts | 32 ++++-- .../builtin_action_types/jira/service.test.ts | 62 +++++++---- .../builtin_action_types/jira/service.ts | 19 +++- .../lib/axios_utils.test.ts | 101 ++++++++++++------ .../builtin_action_types/lib/axios_utils.ts | 46 +++++--- .../lib/get_proxy_agent.test.ts | 30 ++++++ .../lib/get_proxy_agent.ts | 31 ++++++ .../lib/post_pagerduty.ts | 26 +++-- .../lib/send_email.test.ts | 63 ++++++++++- .../builtin_action_types/lib/send_email.ts | 14 ++- .../server/builtin_action_types/pagerduty.ts | 3 +- .../builtin_action_types/resilient/index.ts | 32 ++++-- .../resilient/service.test.ts | 63 +++++++---- .../builtin_action_types/resilient/service.ts | 19 +++- .../builtin_action_types/servicenow/index.ts | 12 ++- .../servicenow/service.test.ts | 50 ++++++--- .../servicenow/service.ts | 21 +++- .../server/builtin_action_types/slack.test.ts | 45 +++++++- .../server/builtin_action_types/slack.ts | 25 ++++- .../builtin_action_types/webhook.test.ts | 81 ++++++++++++-- .../server/builtin_action_types/webhook.ts | 12 ++- x-pack/plugins/actions/server/config.test.ts | 3 + x-pack/plugins/actions/server/config.ts | 3 + .../actions/server/lib/action_executor.ts | 4 + x-pack/plugins/actions/server/plugin.test.ts | 8 +- x-pack/plugins/actions/server/plugin.ts | 17 ++- x-pack/plugins/actions/server/types.ts | 7 ++ .../alerting_api_integration/basic/config.ts | 1 + .../alerting_api_integration/common/config.ts | 9 ++ .../common/lib/get_proxy_server.ts | 30 ++++++ .../security_and_spaces/config.ts | 1 + .../actions/builtin_action_types/jira.ts | 15 +++ .../actions/builtin_action_types/pagerduty.ts | 16 +++ .../actions/builtin_action_types/resilient.ts | 15 +++ .../builtin_action_types/servicenow.ts | 15 +++ .../actions/builtin_action_types/slack.ts | 12 +++ .../actions/builtin_action_types/webhook.ts | 14 ++- .../spaces_only/config.ts | 6 +- yarn.lock | 14 +++ 45 files changed, 827 insertions(+), 195 deletions(-) create mode 100644 x-pack/plugins/actions/server/builtin_action_types/lib/get_proxy_agent.test.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/lib/get_proxy_agent.ts create mode 100644 x-pack/test/alerting_api_integration/common/lib/get_proxy_server.ts diff --git a/x-pack/package.json b/x-pack/package.json index 42fa74a3bc84e..57a0b88f8c2a5 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -44,9 +44,9 @@ "@storybook/addon-storyshots": "^5.3.19", "@storybook/react": "^5.3.19", "@storybook/theming": "^5.3.19", + "@testing-library/jest-dom": "^5.8.0", "@testing-library/react": "^9.3.2", "@testing-library/react-hooks": "^3.2.1", - "@testing-library/jest-dom": "^5.8.0", "@types/angular": "^1.6.56", "@types/archiver": "^3.1.0", "@types/base64-js": "^1.2.5", @@ -72,8 +72,9 @@ "@types/gulp": "^4.0.6", "@types/hapi__wreck": "^15.0.1", "@types/he": "^1.1.1", - "@types/hoist-non-react-statics": "^3.3.1", "@types/history": "^4.7.3", + "@types/hoist-non-react-statics": "^3.3.1", + "@types/http-proxy": "^1.17.4", "@types/jest": "^25.2.3", "@types/jest-specific-snapshot": "^0.5.4", "@types/joi": "^13.4.2", @@ -94,6 +95,7 @@ "@types/object-hash": "^1.3.0", "@types/papaparse": "^5.0.3", "@types/pngjs": "^3.3.2", + "@types/pretty-ms": "^5.0.0", "@types/prop-types": "^15.5.3", "@types/proper-lockfile": "^3.0.1", "@types/puppeteer": "^1.20.1", @@ -109,6 +111,7 @@ "@types/redux-actions": "^2.6.1", "@types/set-value": "^2.0.0", "@types/sinon": "^7.0.13", + "@types/stats-lite": "^2.2.0", "@types/styled-components": "^5.1.0", "@types/supertest": "^2.0.5", "@types/tar-fs": "^1.16.1", @@ -116,11 +119,9 @@ "@types/tinycolor2": "^1.4.1", "@types/use-resize-observer": "^6.0.0", "@types/uuid": "^3.4.4", + "@types/webpack-env": "^1.15.2", "@types/xml-crypto": "^1.4.0", "@types/xml2js": "^0.4.5", - "@types/stats-lite": "^2.2.0", - "@types/pretty-ms": "^5.0.0", - "@types/webpack-env": "^1.15.2", "@welldone-software/why-did-you-render": "^4.0.0", "abab": "^1.0.4", "autoprefixer": "^9.7.4", @@ -227,6 +228,7 @@ "@turf/circle": "6.0.1", "@turf/distance": "6.0.1", "@turf/helpers": "6.0.1", + "@types/http-proxy-agent": "^2.0.2", "angular": "^1.8.0", "angular-resource": "1.8.0", "angular-sanitize": "1.8.0", diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/types.ts b/x-pack/plugins/actions/server/builtin_action_types/case/types.ts index de96864d0b295..1030e3d9c5d8e 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/case/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/case/types.ts @@ -9,6 +9,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { TypeOf } from '@kbn/config-schema'; +import { Logger } from '../../../../../../src/core/server'; import { ExternalIncidentServiceConfigurationSchema, @@ -122,7 +123,12 @@ export interface ExternalServiceApi { export interface CreateExternalServiceBasicArgs { api: ExternalServiceApi; - createExternalService: (credentials: ExternalServiceCredentials) => ExternalService; + createExternalService: ( + credentials: ExternalServiceCredentials, + logger: Logger, + proxySettings?: any + ) => ExternalService; + logger: Logger; } export interface CreateExternalServiceArgs extends CreateExternalServiceBasicArgs { diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts b/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts index 82dedb09c429e..d895bf386a367 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts @@ -67,6 +67,7 @@ export const mapParams = ( export const createConnectorExecutor = ({ api, createExternalService, + logger, }: CreateExternalServiceBasicArgs) => async ( execOptions: ActionTypeExecutorOptions< ExternalIncidentServiceConfiguration, @@ -83,10 +84,14 @@ export const createConnectorExecutor = ({ actionId, }; - const externalService = createExternalService({ - config, - secrets, - }); + const externalService = createExternalService( + { + config, + secrets, + }, + logger, + execOptions.proxySettings + ); if (!api[subAction]) { throw new Error('[Action][ExternalService] Unsupported subAction type.'); @@ -122,10 +127,11 @@ export const createConnector = ({ validate, createExternalService, validationSchema, + logger, }: CreateExternalServiceArgs) => { return ({ configurationUtilities, - executor = createConnectorExecutor({ api, createExternalService }), + executor = createConnectorExecutor({ api, createExternalService, logger }), }: CreateActionTypeArgs): ActionType => ({ ...config, validate: { diff --git a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts index 195f6db538ae5..62f369816d714 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts @@ -269,6 +269,7 @@ describe('execute()', () => { "message": "a message to you", "subject": "the subject", }, + "proxySettings": undefined, "routing": Object { "bcc": Array [ "jimmy@example.com", @@ -326,6 +327,7 @@ describe('execute()', () => { "message": "a message to you", "subject": "the subject", }, + "proxySettings": undefined, "routing": Object { "bcc": Array [ "jimmy@example.com", diff --git a/x-pack/plugins/actions/server/builtin_action_types/email.ts b/x-pack/plugins/actions/server/builtin_action_types/email.ts index a51a0432a01e0..e9dc4eea5dcfc 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/email.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/email.ts @@ -184,6 +184,7 @@ async function executor( subject: params.subject, message: params.message, }, + proxySettings: execOptions.proxySettings, }; let result; diff --git a/x-pack/plugins/actions/server/builtin_action_types/index.ts b/x-pack/plugins/actions/server/builtin_action_types/index.ts index 80a171cbe624d..3591e05fb3acf 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/index.ts @@ -31,9 +31,9 @@ export function registerBuiltInActionTypes({ actionTypeRegistry.register(getIndexActionType({ logger })); actionTypeRegistry.register(getPagerDutyActionType({ logger, configurationUtilities })); actionTypeRegistry.register(getServerLogActionType({ logger })); - actionTypeRegistry.register(getSlackActionType({ configurationUtilities })); + actionTypeRegistry.register(getSlackActionType({ logger, configurationUtilities })); actionTypeRegistry.register(getWebhookActionType({ logger, configurationUtilities })); actionTypeRegistry.register(getServiceNowActionType({ logger, configurationUtilities })); - actionTypeRegistry.register(getJiraActionType({ configurationUtilities })); - actionTypeRegistry.register(getResilientActionType({ configurationUtilities })); + actionTypeRegistry.register(getJiraActionType({ logger, configurationUtilities })); + actionTypeRegistry.register(getResilientActionType({ logger, configurationUtilities })); } diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts index a2d7bb5930a75..66be0bad02d7b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts @@ -4,21 +4,33 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Logger } from '../../../../../../src/core/server'; import { createConnector } from '../case/utils'; +import { ActionType } from '../../types'; import { api } from './api'; import { config } from './config'; import { validate } from './validators'; import { createExternalService } from './service'; import { JiraSecretConfiguration, JiraPublicConfiguration } from './schema'; +import { ActionsConfigurationUtilities } from '../../actions_config'; -export const getActionType = createConnector({ - api, - config, - validate, - createExternalService, - validationSchema: { - config: JiraPublicConfiguration, - secrets: JiraSecretConfiguration, - }, -}); +export function getActionType({ + logger, + configurationUtilities, +}: { + logger: Logger; + configurationUtilities: ActionsConfigurationUtilities; +}): ActionType { + return createConnector({ + api, + config, + validate, + createExternalService, + validationSchema: { + config: JiraPublicConfiguration, + secrets: JiraSecretConfiguration, + }, + logger, + })({ configurationUtilities }); +} diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts index 3de3926b7d821..547595b4c183f 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts @@ -9,6 +9,9 @@ import axios from 'axios'; import { createExternalService } from './service'; import * as utils from '../lib/axios_utils'; import { ExternalService } from '../case/types'; +import { Logger } from '../../../../../../src/core/server'; +import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; +const logger = loggingSystemMock.create().get() as jest.Mocked; jest.mock('axios'); jest.mock('../lib/axios_utils', () => { @@ -26,10 +29,13 @@ describe('Jira service', () => { let service: ExternalService; beforeAll(() => { - service = createExternalService({ - config: { apiUrl: 'https://siem-kibana.atlassian.net', projectKey: 'CK' }, - secrets: { apiToken: 'token', email: 'elastic@elastic.com' }, - }); + service = createExternalService( + { + config: { apiUrl: 'https://siem-kibana.atlassian.net', projectKey: 'CK' }, + secrets: { apiToken: 'token', email: 'elastic@elastic.com' }, + }, + logger + ); }); beforeEach(() => { @@ -39,37 +45,49 @@ describe('Jira service', () => { describe('createExternalService', () => { test('throws without url', () => { expect(() => - createExternalService({ - config: { apiUrl: null, projectKey: 'CK' }, - secrets: { apiToken: 'token', email: 'elastic@elastic.com' }, - }) + createExternalService( + { + config: { apiUrl: null, projectKey: 'CK' }, + secrets: { apiToken: 'token', email: 'elastic@elastic.com' }, + }, + logger + ) ).toThrow(); }); test('throws without projectKey', () => { expect(() => - createExternalService({ - config: { apiUrl: 'test.com', projectKey: null }, - secrets: { apiToken: 'token', email: 'elastic@elastic.com' }, - }) + createExternalService( + { + config: { apiUrl: 'test.com', projectKey: null }, + secrets: { apiToken: 'token', email: 'elastic@elastic.com' }, + }, + logger + ) ).toThrow(); }); test('throws without username', () => { expect(() => - createExternalService({ - config: { apiUrl: 'test.com' }, - secrets: { apiToken: '', email: 'elastic@elastic.com' }, - }) + createExternalService( + { + config: { apiUrl: 'test.com' }, + secrets: { apiToken: '', email: 'elastic@elastic.com' }, + }, + logger + ) ).toThrow(); }); test('throws without password', () => { expect(() => - createExternalService({ - config: { apiUrl: 'test.com' }, - secrets: { apiToken: '', email: undefined }, - }) + createExternalService( + { + config: { apiUrl: 'test.com' }, + secrets: { apiToken: '', email: undefined }, + }, + logger + ) ).toThrow(); }); }); @@ -92,6 +110,7 @@ describe('Jira service', () => { expect(requestMock).toHaveBeenCalledWith({ axios, url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/1', + logger, }); }); @@ -146,6 +165,7 @@ describe('Jira service', () => { expect(requestMock).toHaveBeenCalledWith({ axios, url: 'https://siem-kibana.atlassian.net/rest/api/2/issue', + logger, method: 'post', data: { fields: { @@ -210,6 +230,7 @@ describe('Jira service', () => { expect(requestMock).toHaveBeenCalledWith({ axios, + logger, method: 'put', url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/1', data: { fields: { summary: 'title', description: 'desc' } }, @@ -272,6 +293,7 @@ describe('Jira service', () => { expect(requestMock).toHaveBeenCalledWith({ axios, + logger, method: 'post', url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/1/comment', data: { body: 'comment' }, diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts index 240b645c3a7dc..aec73cfb375ed 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts @@ -7,6 +7,7 @@ import axios from 'axios'; import { ExternalServiceCredentials, ExternalService, ExternalServiceParams } from '../case/types'; +import { Logger } from '../../../../../../src/core/server'; import { JiraPublicConfigurationType, JiraSecretConfigurationType, @@ -17,6 +18,7 @@ import { import * as i18n from './translations'; import { request, getErrorMessage } from '../lib/axios_utils'; +import { ProxySettings } from '../../types'; const VERSION = '2'; const BASE_URL = `rest/api/${VERSION}`; @@ -25,10 +27,11 @@ const COMMENT_URL = `comment`; const VIEW_INCIDENT_URL = `browse`; -export const createExternalService = ({ - config, - secrets, -}: ExternalServiceCredentials): ExternalService => { +export const createExternalService = ( + { config, secrets }: ExternalServiceCredentials, + logger: Logger, + proxySettings?: ProxySettings +): ExternalService => { const { apiUrl: url, projectKey } = config as JiraPublicConfigurationType; const { apiToken, email } = secrets as JiraSecretConfigurationType; @@ -55,6 +58,8 @@ export const createExternalService = ({ const res = await request({ axios: axiosInstance, url: `${incidentUrl}/${id}`, + logger, + proxySettings, }); const { fields, ...rest } = res.data; @@ -75,10 +80,12 @@ export const createExternalService = ({ const res = await request({ axios: axiosInstance, url: `${incidentUrl}`, + logger, method: 'post', data: { fields: { ...incident, project: { key: projectKey }, issuetype: { name: 'Task' } }, }, + proxySettings, }); const updatedIncident = await getIncident(res.data.id); @@ -102,7 +109,9 @@ export const createExternalService = ({ axios: axiosInstance, method: 'put', url: `${incidentUrl}/${incidentId}`, + logger, data: { fields: { ...incident } }, + proxySettings, }); const updatedIncident = await getIncident(incidentId); @@ -129,7 +138,9 @@ export const createExternalService = ({ axios: axiosInstance, method: 'post', url: getCommentsURL(incidentId), + logger, data: { body: comment.comment }, + proxySettings, }); return { diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts index 4a52ae60bcdda..844aa6d2de7ed 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts @@ -5,7 +5,11 @@ */ import axios from 'axios'; -import { addTimeZoneToDate, throwIfNotAlive, request, patch, getErrorMessage } from './axios_utils'; +import HttpProxyAgent from 'http-proxy-agent'; +import { Logger } from '../../../../../../src/core/server'; +import { addTimeZoneToDate, request, patch, getErrorMessage } from './axios_utils'; +import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; +const logger = loggingSystemMock.create().get() as jest.Mocked; jest.mock('axios'); const axiosMock = (axios as unknown) as jest.Mock; @@ -21,26 +25,6 @@ describe('addTimeZoneToDate', () => { }); }); -describe('throwIfNotAlive ', () => { - test('throws correctly when status is invalid', async () => { - expect(() => { - throwIfNotAlive(404, 'application/json'); - }).toThrow('Instance is not alive.'); - }); - - test('throws correctly when content is invalid', () => { - expect(() => { - throwIfNotAlive(200, 'application/html'); - }).toThrow('Instance is not alive.'); - }); - - test('do NOT throws with custom validStatusCodes', async () => { - expect(() => { - throwIfNotAlive(404, 'application/json', [404]); - }).not.toThrow('Instance is not alive.'); - }); -}); - describe('request', () => { beforeEach(() => { axiosMock.mockImplementation(() => ({ @@ -51,9 +35,22 @@ describe('request', () => { }); test('it fetch correctly with defaults', async () => { - const res = await request({ axios, url: '/test' }); + const res = await request({ + axios, + url: '/test', + logger, + }); - expect(axiosMock).toHaveBeenCalledWith('/test', { method: 'get', data: {} }); + expect(axiosMock).toHaveBeenCalledWith('/test', { + method: 'get', + data: {}, + headers: undefined, + httpAgent: undefined, + httpsAgent: undefined, + params: undefined, + proxy: false, + validateStatus: undefined, + }); expect(res).toEqual({ status: 200, headers: { 'content-type': 'application/json' }, @@ -61,10 +58,27 @@ describe('request', () => { }); }); - test('it fetch correctly', async () => { - const res = await request({ axios, url: '/test', method: 'post', data: { id: '123' } }); + test('it have been called with proper proxy agent', async () => { + const res = await request({ + axios, + url: '/testProxy', + logger, + proxySettings: { + proxyUrl: 'http://localhost:1212', + rejectUnauthorizedCertificates: false, + }, + }); - expect(axiosMock).toHaveBeenCalledWith('/test', { method: 'post', data: { id: '123' } }); + expect(axiosMock).toHaveBeenCalledWith('/testProxy', { + method: 'get', + data: {}, + headers: undefined, + httpAgent: new HttpProxyAgent('http://localhost:1212'), + httpsAgent: new HttpProxyAgent('http://localhost:1212'), + params: undefined, + proxy: false, + validateStatus: undefined, + }); expect(res).toEqual({ status: 200, headers: { 'content-type': 'application/json' }, @@ -72,14 +86,24 @@ describe('request', () => { }); }); - test('it throws correctly', async () => { - axiosMock.mockImplementation(() => ({ - status: 404, + test('it fetch correctly', async () => { + const res = await request({ axios, url: '/test', method: 'post', logger, data: { id: '123' } }); + + expect(axiosMock).toHaveBeenCalledWith('/test', { + method: 'post', + data: { id: '123' }, + headers: undefined, + httpAgent: undefined, + httpsAgent: undefined, + params: undefined, + proxy: false, + validateStatus: undefined, + }); + expect(res).toEqual({ + status: 200, headers: { 'content-type': 'application/json' }, data: { incidentId: '123' }, - })); - - await expect(request({ axios, url: '/test' })).rejects.toThrow(); + }); }); }); @@ -92,8 +116,17 @@ describe('patch', () => { }); test('it fetch correctly', async () => { - await patch({ axios, url: '/test', data: { id: '123' } }); - expect(axiosMock).toHaveBeenCalledWith('/test', { method: 'patch', data: { id: '123' } }); + await patch({ axios, url: '/test', data: { id: '123' }, logger }); + expect(axiosMock).toHaveBeenCalledWith('/test', { + method: 'patch', + data: { id: '123' }, + headers: undefined, + httpAgent: undefined, + httpsAgent: undefined, + params: undefined, + proxy: false, + validateStatus: undefined, + }); }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts index d527cf632bace..e26a3b686179c 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts @@ -4,50 +4,68 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AxiosInstance, Method, AxiosResponse } from 'axios'; - -export const throwIfNotAlive = ( - status: number, - contentType: string, - validStatusCodes: number[] = [200, 201, 204] -) => { - if (!validStatusCodes.includes(status) || !contentType.includes('application/json')) { - throw new Error('Instance is not alive.'); - } -}; +import { AxiosInstance, Method, AxiosResponse, AxiosBasicCredentials } from 'axios'; +import { Logger } from '../../../../../../src/core/server'; +import { ProxySettings } from '../../types'; +import { getProxyAgent } from './get_proxy_agent'; export const request = async ({ axios, url, + logger, method = 'get', data, params, + proxySettings, + headers, + validateStatus, + auth, }: { axios: AxiosInstance; url: string; + logger: Logger; method?: Method; data?: T; params?: unknown; + proxySettings?: ProxySettings; + headers?: Record | null; + validateStatus?: (status: number) => boolean; + auth?: AxiosBasicCredentials; }): Promise => { - const res = await axios(url, { method, data: data ?? {}, params }); - throwIfNotAlive(res.status, res.headers['content-type']); - return res; + return await axios(url, { + method, + data: data ?? {}, + params, + auth, + // use httpsAgent and embedded proxy: false, to be able to handle fail on invalid certs + httpsAgent: proxySettings ? getProxyAgent(proxySettings, logger) : undefined, + httpAgent: proxySettings ? getProxyAgent(proxySettings, logger) : undefined, + proxy: false, // the same way as it done for IncomingWebhook in + headers, + validateStatus, + }); }; export const patch = async ({ axios, url, data, + logger, + proxySettings, }: { axios: AxiosInstance; url: string; data: T; + logger: Logger; + proxySettings?: ProxySettings; }): Promise => { return request({ axios, url, + logger, method: 'patch', data, + proxySettings, }); }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_proxy_agent.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_proxy_agent.test.ts new file mode 100644 index 0000000000000..2468fab8c6ac5 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_proxy_agent.test.ts @@ -0,0 +1,30 @@ +/* + * 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 HttpProxyAgent from 'http-proxy-agent'; +import { HttpsProxyAgent } from 'https-proxy-agent'; +import { Logger } from '../../../../../../src/core/server'; +import { getProxyAgent } from './get_proxy_agent'; +import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; +const logger = loggingSystemMock.create().get() as jest.Mocked; + +describe('getProxyAgent', () => { + test('return HttpsProxyAgent for https proxy url', () => { + const agent = getProxyAgent( + { proxyUrl: 'https://someproxyhost', rejectUnauthorizedCertificates: false }, + logger + ); + expect(agent instanceof HttpsProxyAgent).toBeTruthy(); + }); + + test('return HttpProxyAgent for http proxy url', () => { + const agent = getProxyAgent( + { proxyUrl: 'http://someproxyhost', rejectUnauthorizedCertificates: false }, + logger + ); + expect(agent instanceof HttpProxyAgent).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_proxy_agent.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_proxy_agent.ts new file mode 100644 index 0000000000000..bb4dadd3a4698 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_proxy_agent.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 HttpProxyAgent from 'http-proxy-agent'; +import { HttpsProxyAgent } from 'https-proxy-agent'; +import { Logger } from '../../../../../../src/core/server'; +import { ProxySettings } from '../../types'; + +export function getProxyAgent( + proxySettings: ProxySettings, + logger: Logger +): HttpsProxyAgent | HttpProxyAgent { + logger.debug(`Create proxy agent for ${proxySettings.proxyUrl}.`); + + if (/^https/i.test(proxySettings.proxyUrl)) { + const proxyUrl = new URL(proxySettings.proxyUrl); + return new HttpsProxyAgent({ + host: proxyUrl.hostname, + port: Number(proxyUrl.port), + protocol: proxyUrl.protocol, + headers: proxySettings.proxyHeaders, + // do not fail on invalid certs if value is false + rejectUnauthorized: proxySettings.rejectUnauthorizedCertificates, + }); + } else { + return new HttpProxyAgent(proxySettings.proxyUrl); + } +} diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/post_pagerduty.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/post_pagerduty.ts index 92f88ebe0be22..d78237beb98a1 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/post_pagerduty.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/post_pagerduty.ts @@ -5,22 +5,34 @@ */ import axios, { AxiosResponse } from 'axios'; -import { Services } from '../../types'; +import { Logger } from '../../../../../../src/core/server'; +import { Services, ProxySettings } from '../../types'; +import { request } from './axios_utils'; interface PostPagerdutyOptions { apiUrl: string; data: unknown; headers: Record; services: Services; + proxySettings?: ProxySettings; } // post an event to pagerduty -export async function postPagerduty(options: PostPagerdutyOptions): Promise { - const { apiUrl, data, headers } = options; - const axiosOptions = { +export async function postPagerduty( + options: PostPagerdutyOptions, + logger: Logger +): Promise { + const { apiUrl, data, headers, proxySettings } = options; + const axiosInstance = axios.create(); + + return await request({ + axios: axiosInstance, + url: apiUrl, + method: 'post', + logger, + data, + proxySettings, headers, validateStatus: () => true, - }; - - return axios.post(apiUrl, data, axiosOptions); + }); } diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts index 3514bd4257b0f..8287ee944bca9 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts @@ -12,6 +12,7 @@ import { Logger } from '../../../../../../src/core/server'; import { sendEmail } from './send_email'; import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; import nodemailer from 'nodemailer'; +import { ProxySettings } from '../../types'; const createTransportMock = nodemailer.createTransport as jest.Mock; const sendMailMockResult = { result: 'does not matter' }; @@ -63,6 +64,59 @@ describe('send_email module', () => { }); test('handles unauthenticated email using not secure host/port', async () => { + const sendEmailOptions = getSendEmailOptions( + { + transport: { + host: 'example.com', + port: 1025, + }, + }, + { + proxyUrl: 'https://example.com', + rejectUnauthorizedCertificates: false, + } + ); + delete sendEmailOptions.transport.service; + delete sendEmailOptions.transport.user; + delete sendEmailOptions.transport.password; + const result = await sendEmail(mockLogger, sendEmailOptions); + expect(result).toBe(sendMailMockResult); + expect(createTransportMock.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "headers": undefined, + "host": "example.com", + "port": 1025, + "proxy": "https://example.com", + "secure": false, + "tls": Object { + "rejectUnauthorized": false, + }, + }, + ] + `); + expect(sendMailMock.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "bcc": Array [], + "cc": Array [ + "bob@example.com", + "robert@example.com", + ], + "from": "fred@example.com", + "html": "

a message

+ ", + "subject": "a subject", + "text": "a message", + "to": Array [ + "jim@example.com", + ], + }, + ] + `); + }); + + test('rejectUnauthorized default setting email using not secure host/port', async () => { const sendEmailOptions = getSendEmailOptions({ transport: { host: 'example.com', @@ -80,9 +134,6 @@ describe('send_email module', () => { "host": "example.com", "port": 1025, "secure": false, - "tls": Object { - "rejectUnauthorized": false, - }, }, ] `); @@ -161,7 +212,10 @@ describe('send_email module', () => { }); }); -function getSendEmailOptions({ content = {}, routing = {}, transport = {} } = {}) { +function getSendEmailOptions( + { content = {}, routing = {}, transport = {} } = {}, + proxySettings?: ProxySettings +) { return { content: { ...content, @@ -181,5 +235,6 @@ function getSendEmailOptions({ content = {}, routing = {}, transport = {} } = {} user: 'elastic', password: 'changeme', }, + proxySettings, }; } diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts index 869db34f034ae..a4f32f1880cb5 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts @@ -6,10 +6,10 @@ // info on nodemailer: https://nodemailer.com/about/ import nodemailer from 'nodemailer'; - import { default as MarkdownIt } from 'markdown-it'; import { Logger } from '../../../../../../src/core/server'; +import { ProxySettings } from '../../types'; // an email "service" which doesn't actually send, just returns what it would send export const JSON_TRANSPORT_SERVICE = '__json'; @@ -18,6 +18,7 @@ export interface SendEmailOptions { transport: Transport; routing: Routing; content: Content; + proxySettings?: ProxySettings; } // config validation ensures either service is set or host/port are set @@ -44,7 +45,7 @@ export interface Content { // send an email export async function sendEmail(logger: Logger, options: SendEmailOptions): Promise { - const { transport, routing, content } = options; + const { transport, routing, content, proxySettings } = options; const { service, host, port, secure, user, password } = transport; const { from, to, cc, bcc } = routing; const { subject, message } = content; @@ -67,11 +68,16 @@ export async function sendEmail(logger: Logger, options: SendEmailOptions): Prom transportConfig.host = host; transportConfig.port = port; transportConfig.secure = !!secure; - if (!transportConfig.secure) { + if (proxySettings && !transportConfig.secure) { transportConfig.tls = { - rejectUnauthorized: false, + // do not fail on invalid certs if value is false + rejectUnauthorized: proxySettings?.rejectUnauthorizedCertificates, }; } + if (proxySettings) { + transportConfig.proxy = proxySettings.proxyUrl; + transportConfig.headers = proxySettings.proxyHeaders; + } } const nodemailerTransport = nodemailer.createTransport(transportConfig); diff --git a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts index b76e57419bc56..c0edfc530e738 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts @@ -161,6 +161,7 @@ async function executor( const secrets = execOptions.secrets; const params = execOptions.params; const services = execOptions.services; + const proxySettings = execOptions.proxySettings; const apiUrl = getPagerDutyApiUrl(config); const headers = { @@ -171,7 +172,7 @@ async function executor( let response; try { - response = await postPagerduty({ apiUrl, data, headers, services }); + response = await postPagerduty({ apiUrl, data, headers, services, proxySettings }, logger); } catch (err) { const message = i18n.translate('xpack.actions.builtin.pagerduty.postingErrorMessage', { defaultMessage: 'error posting pagerduty event', diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/index.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/index.ts index e98bc71559d3f..1e9cb15589702 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Logger } from '../../../../../../src/core/server'; import { createConnector } from '../case/utils'; import { api } from './api'; @@ -11,14 +12,25 @@ import { config } from './config'; import { validate } from './validators'; import { createExternalService } from './service'; import { ResilientSecretConfiguration, ResilientPublicConfiguration } from './schema'; +import { ActionsConfigurationUtilities } from '../../actions_config'; +import { ActionType } from '../../types'; -export const getActionType = createConnector({ - api, - config, - validate, - createExternalService, - validationSchema: { - config: ResilientPublicConfiguration, - secrets: ResilientSecretConfiguration, - }, -}); +export function getActionType({ + logger, + configurationUtilities, +}: { + logger: Logger; + configurationUtilities: ActionsConfigurationUtilities; +}): ActionType { + return createConnector({ + api, + config, + validate, + createExternalService, + validationSchema: { + config: ResilientPublicConfiguration, + secrets: ResilientSecretConfiguration, + }, + logger, + })({ configurationUtilities }); +} diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/service.test.ts index 573885698014e..a9271671f68b9 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/service.test.ts @@ -9,6 +9,9 @@ import axios from 'axios'; import { createExternalService, getValueTextContent, formatUpdateRequest } from './service'; import * as utils from '../lib/axios_utils'; import { ExternalService } from '../case/types'; +import { Logger } from '../../../../../../src/core/server'; +import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; +const logger = loggingSystemMock.create().get() as jest.Mocked; jest.mock('axios'); jest.mock('../lib/axios_utils', () => { @@ -72,10 +75,13 @@ describe('IBM Resilient service', () => { let service: ExternalService; beforeAll(() => { - service = createExternalService({ - config: { apiUrl: 'https://resilient.elastic.co', orgId: '201' }, - secrets: { apiKeyId: 'keyId', apiKeySecret: 'secret' }, - }); + service = createExternalService( + { + config: { apiUrl: 'https://resilient.elastic.co', orgId: '201' }, + secrets: { apiKeyId: 'keyId', apiKeySecret: 'secret' }, + }, + logger + ); }); afterAll(() => { @@ -138,37 +144,49 @@ describe('IBM Resilient service', () => { describe('createExternalService', () => { test('throws without url', () => { expect(() => - createExternalService({ - config: { apiUrl: null, orgId: '201' }, - secrets: { apiKeyId: 'token', apiKeySecret: 'secret' }, - }) + createExternalService( + { + config: { apiUrl: null, orgId: '201' }, + secrets: { apiKeyId: 'token', apiKeySecret: 'secret' }, + }, + logger + ) ).toThrow(); }); test('throws without orgId', () => { expect(() => - createExternalService({ - config: { apiUrl: 'test.com', orgId: null }, - secrets: { apiKeyId: 'token', apiKeySecret: 'secret' }, - }) + createExternalService( + { + config: { apiUrl: 'test.com', orgId: null }, + secrets: { apiKeyId: 'token', apiKeySecret: 'secret' }, + }, + logger + ) ).toThrow(); }); test('throws without username', () => { expect(() => - createExternalService({ - config: { apiUrl: 'test.com', orgId: '201' }, - secrets: { apiKeyId: '', apiKeySecret: 'secret' }, - }) + createExternalService( + { + config: { apiUrl: 'test.com', orgId: '201' }, + secrets: { apiKeyId: '', apiKeySecret: 'secret' }, + }, + logger + ) ).toThrow(); }); test('throws without password', () => { expect(() => - createExternalService({ - config: { apiUrl: 'test.com', orgId: '201' }, - secrets: { apiKeyId: '', apiKeySecret: undefined }, - }) + createExternalService( + { + config: { apiUrl: 'test.com', orgId: '201' }, + secrets: { apiKeyId: '', apiKeySecret: undefined }, + }, + logger + ) ).toThrow(); }); }); @@ -197,6 +215,7 @@ describe('IBM Resilient service', () => { await service.getIncident('1'); expect(requestMock).toHaveBeenCalledWith({ axios, + logger, url: 'https://resilient.elastic.co/rest/orgs/201/incidents/1', params: { text_content_output_format: 'objects_convert', @@ -256,6 +275,7 @@ describe('IBM Resilient service', () => { expect(requestMock).toHaveBeenCalledWith({ axios, url: 'https://resilient.elastic.co/rest/orgs/201/incidents', + logger, method: 'post', data: { name: 'title', @@ -311,6 +331,7 @@ describe('IBM Resilient service', () => { // The second call to the API is the update call. expect(requestMock.mock.calls[1][0]).toEqual({ axios, + logger, method: 'patch', url: 'https://resilient.elastic.co/rest/orgs/201/incidents/1', data: { @@ -392,7 +413,9 @@ describe('IBM Resilient service', () => { expect(requestMock).toHaveBeenCalledWith({ axios, + logger, method: 'post', + proxySettings: undefined, url: 'https://resilient.elastic.co/rest/orgs/201/incidents/1/comments', data: { text: { diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/service.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/service.ts index 8d0526ca3b571..b2150081f2c89 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/service.ts @@ -6,6 +6,7 @@ import axios from 'axios'; +import { Logger } from '../../../../../../src/core/server'; import { ExternalServiceCredentials, ExternalService, ExternalServiceParams } from '../case/types'; import { ResilientPublicConfigurationType, @@ -19,6 +20,7 @@ import { import * as i18n from './translations'; import { getErrorMessage, request } from '../lib/axios_utils'; +import { ProxySettings } from '../../types'; const BASE_URL = `rest`; const INCIDENT_URL = `incidents`; @@ -57,10 +59,11 @@ export const formatUpdateRequest = ({ }; }; -export const createExternalService = ({ - config, - secrets, -}: ExternalServiceCredentials): ExternalService => { +export const createExternalService = ( + { config, secrets }: ExternalServiceCredentials, + logger: Logger, + proxySettings?: ProxySettings +): ExternalService => { const { apiUrl: url, orgId } = config as ResilientPublicConfigurationType; const { apiKeyId, apiKeySecret } = secrets as ResilientSecretConfigurationType; @@ -88,9 +91,11 @@ export const createExternalService = ({ const res = await request({ axios: axiosInstance, url: `${incidentUrl}/${id}`, + logger, params: { text_content_output_format: 'objects_convert', }, + proxySettings, }); return { ...res.data, description: res.data.description?.content ?? '' }; @@ -107,6 +112,7 @@ export const createExternalService = ({ axios: axiosInstance, url: `${incidentUrl}`, method: 'post', + logger, data: { ...incident, description: { @@ -115,6 +121,7 @@ export const createExternalService = ({ }, discovered_date: Date.now(), }, + proxySettings, }); return { @@ -139,7 +146,9 @@ export const createExternalService = ({ axios: axiosInstance, method: 'patch', url: `${incidentUrl}/${incidentId}`, + logger, data, + proxySettings, }); if (!res.data.success) { @@ -170,7 +179,9 @@ export const createExternalService = ({ axios: axiosInstance, method: 'post', url: getCommentsURL(incidentId), + logger, data: { text: { format: 'text', content: comment.comment } }, + proxySettings, }); return { diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts index 109008b8fc9fb..3addbe7c54dac 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts @@ -76,10 +76,14 @@ async function executor( const { subAction, subActionParams } = params; let data: PushToServiceResponse | null = null; - const externalService = createExternalService({ - config, - secrets, - }); + const externalService = createExternalService( + { + config, + secrets, + }, + logger, + execOptions.proxySettings + ); if (!api[subAction]) { const errorMessage = `[Action][ExternalService] Unsupported subAction type ${subAction}.`; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.test.ts index 07d60ec9f7a05..2adcdf561ce17 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.test.ts @@ -9,6 +9,9 @@ import axios from 'axios'; import { createExternalService } from './service'; import * as utils from '../lib/axios_utils'; import { ExternalService } from './types'; +import { Logger } from '../../../../../../src/core/server'; +import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; +const logger = loggingSystemMock.create().get() as jest.Mocked; jest.mock('axios'); jest.mock('../lib/axios_utils', () => { @@ -28,10 +31,13 @@ describe('ServiceNow service', () => { let service: ExternalService; beforeAll(() => { - service = createExternalService({ - config: { apiUrl: 'https://dev102283.service-now.com' }, - secrets: { username: 'admin', password: 'admin' }, - }); + service = createExternalService( + { + config: { apiUrl: 'https://dev102283.service-now.com' }, + secrets: { username: 'admin', password: 'admin' }, + }, + logger + ); }); beforeEach(() => { @@ -41,28 +47,37 @@ describe('ServiceNow service', () => { describe('createExternalService', () => { test('throws without url', () => { expect(() => - createExternalService({ - config: { apiUrl: null }, - secrets: { username: 'admin', password: 'admin' }, - }) + createExternalService( + { + config: { apiUrl: null }, + secrets: { username: 'admin', password: 'admin' }, + }, + logger + ) ).toThrow(); }); test('throws without username', () => { expect(() => - createExternalService({ - config: { apiUrl: 'test.com' }, - secrets: { username: '', password: 'admin' }, - }) + createExternalService( + { + config: { apiUrl: 'test.com' }, + secrets: { username: '', password: 'admin' }, + }, + logger + ) ).toThrow(); }); test('throws without password', () => { expect(() => - createExternalService({ - config: { apiUrl: 'test.com' }, - secrets: { username: '', password: undefined }, - }) + createExternalService( + { + config: { apiUrl: 'test.com' }, + secrets: { username: '', password: undefined }, + }, + logger + ) ).toThrow(); }); }); @@ -84,6 +99,7 @@ describe('ServiceNow service', () => { await service.getIncident('1'); expect(requestMock).toHaveBeenCalledWith({ axios, + logger, url: 'https://dev102283.service-now.com/api/now/v2/table/incident/1', }); }); @@ -127,6 +143,7 @@ describe('ServiceNow service', () => { expect(requestMock).toHaveBeenCalledWith({ axios, + logger, url: 'https://dev102283.service-now.com/api/now/v2/table/incident', method: 'post', data: { short_description: 'title', description: 'desc' }, @@ -179,6 +196,7 @@ describe('ServiceNow service', () => { expect(patchMock).toHaveBeenCalledWith({ axios, + logger, url: 'https://dev102283.service-now.com/api/now/v2/table/incident/1', data: { short_description: 'title', description: 'desc' }, }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts index 2b5204af2eb7d..cf1c26e6462a2 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts @@ -9,8 +9,10 @@ import axios from 'axios'; import { ExternalServiceCredentials, ExternalService, ExternalServiceParams } from './types'; import * as i18n from './translations'; +import { Logger } from '../../../../../../src/core/server'; import { ServiceNowPublicConfigurationType, ServiceNowSecretConfigurationType } from './types'; import { request, getErrorMessage, addTimeZoneToDate, patch } from '../lib/axios_utils'; +import { ProxySettings } from '../../types'; const API_VERSION = 'v2'; const INCIDENT_URL = `api/now/${API_VERSION}/table/incident`; @@ -18,10 +20,11 @@ const INCIDENT_URL = `api/now/${API_VERSION}/table/incident`; // Based on: https://docs.servicenow.com/bundle/orlando-platform-user-interface/page/use/navigation/reference/r_NavigatingByURLExamples.html const VIEW_INCIDENT_URL = `nav_to.do?uri=incident.do?sys_id=`; -export const createExternalService = ({ - config, - secrets, -}: ExternalServiceCredentials): ExternalService => { +export const createExternalService = ( + { config, secrets }: ExternalServiceCredentials, + logger: Logger, + proxySettings?: ProxySettings +): ExternalService => { const { apiUrl: url } = config as ServiceNowPublicConfigurationType; const { username, password } = secrets as ServiceNowSecretConfigurationType; @@ -43,6 +46,8 @@ export const createExternalService = ({ const res = await request({ axios: axiosInstance, url: `${incidentUrl}/${id}`, + logger, + proxySettings, }); return { ...res.data.result }; @@ -58,6 +63,8 @@ export const createExternalService = ({ const res = await request({ axios: axiosInstance, url: incidentUrl, + logger, + proxySettings, params, }); @@ -71,9 +78,13 @@ export const createExternalService = ({ const createIncident = async ({ incident }: ExternalServiceParams) => { try { + logger.warn(`incident error : ${JSON.stringify(proxySettings)}`); + logger.warn(`incident error : ${url}`); const res = await request({ axios: axiosInstance, url: `${incidentUrl}`, + logger, + proxySettings, method: 'post', data: { ...(incident as Record) }, }); @@ -96,7 +107,9 @@ export const createExternalService = ({ const res = await patch({ axios: axiosInstance, url: `${incidentUrl}/${incidentId}`, + logger, data: { ...(incident as Record) }, + proxySettings, }); return { diff --git a/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts b/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts index 6d4176067c3ba..812657138152c 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts @@ -4,25 +4,40 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Logger } from '../../../../../src/core/server'; import { Services, ActionTypeExecutorResult } from '../types'; import { validateParams, validateSecrets } from '../lib'; import { getActionType, SlackActionType, SlackActionTypeExecutorOptions } from './slack'; import { actionsConfigMock } from '../actions_config.mock'; import { actionsMock } from '../mocks'; +import { createActionTypeRegistry } from './index.test'; + +jest.mock('@slack/webhook', () => { + return { + IncomingWebhook: jest.fn().mockImplementation(() => { + return { send: (message: string) => {} }; + }), + }; +}); const ACTION_TYPE_ID = '.slack'; const services: Services = actionsMock.createServices(); let actionType: SlackActionType; +let mockedLogger: jest.Mocked; beforeAll(() => { + const { logger } = createActionTypeRegistry(); actionType = getActionType({ async executor(options) { return { status: 'ok', actionId: options.actionId }; }, configurationUtilities: actionsConfigMock.create(), + logger, }); + mockedLogger = logger; + expect(actionType).toBeTruthy(); }); describe('action registeration', () => { @@ -83,6 +98,7 @@ describe('validateActionTypeSecrets()', () => { test('should validate and pass when the slack webhookUrl is whitelisted', () => { actionType = getActionType({ + logger: mockedLogger, configurationUtilities: { ...actionsConfigMock.create(), ensureWhitelistedUri: (url) => { @@ -98,9 +114,10 @@ describe('validateActionTypeSecrets()', () => { test('config validation returns an error if the specified URL isnt whitelisted', () => { actionType = getActionType({ + logger: mockedLogger, configurationUtilities: { ...actionsConfigMock.create(), - ensureWhitelistedHostname: (url) => { + ensureWhitelistedHostname: () => { throw new Error(`target hostname is not whitelisted`); }, }, @@ -136,6 +153,7 @@ describe('execute()', () => { actionType = getActionType({ executor: mockSlackExecutor, + logger: mockedLogger, configurationUtilities: actionsConfigMock.create(), }); }); @@ -147,6 +165,10 @@ describe('execute()', () => { config: {}, secrets: { webhookUrl: 'http://example.com' }, params: { message: 'this invocation should succeed' }, + proxySettings: { + proxyUrl: 'https://someproxyhost', + rejectUnauthorizedCertificates: false, + }, }); expect(response).toMatchInlineSnapshot(` Object { @@ -170,4 +192,25 @@ describe('execute()', () => { `"slack mockExecutor failure: this invocation should fail"` ); }); + + test('calls the mock executor with success proxy', async () => { + const actionTypeProxy = getActionType({ + logger: mockedLogger, + configurationUtilities: actionsConfigMock.create(), + }); + await actionTypeProxy.executor({ + actionId: 'some-id', + services, + config: {}, + secrets: { webhookUrl: 'http://example.com' }, + params: { message: 'this invocation should succeed' }, + proxySettings: { + proxyUrl: 'https://someproxyhost', + rejectUnauthorizedCertificates: false, + }, + }); + expect(mockedLogger.info).toHaveBeenCalledWith( + 'IncomingWebhook was called with proxyUrl https://someproxyhost' + ); + }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/slack.ts b/x-pack/plugins/actions/server/builtin_action_types/slack.ts index 209582585256b..293328c809435 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/slack.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/slack.ts @@ -6,11 +6,14 @@ import { URL } from 'url'; import { curry } from 'lodash'; +import { HttpsProxyAgent } from 'https-proxy-agent'; +import HttpProxyAgent from 'http-proxy-agent'; import { i18n } from '@kbn/i18n'; import { schema, TypeOf } from '@kbn/config-schema'; import { IncomingWebhook, IncomingWebhookResult } from '@slack/webhook'; import { pipe } from 'fp-ts/lib/pipeable'; import { map, getOrElse } from 'fp-ts/lib/Option'; +import { Logger } from '../../../../../src/core/server'; import { getRetryAfterIntervalFromHeaders } from './lib/http_rersponse_retry_header'; import { @@ -20,6 +23,7 @@ import { ExecutorType, } from '../types'; import { ActionsConfigurationUtilities } from '../actions_config'; +import { getProxyAgent } from './lib/get_proxy_agent'; export type SlackActionType = ActionType<{}, ActionTypeSecretsType, ActionParamsType, unknown>; export type SlackActionTypeExecutorOptions = ActionTypeExecutorOptions< @@ -49,9 +53,11 @@ const ParamsSchema = schema.object({ // customizing executor is only used for tests export function getActionType({ + logger, configurationUtilities, - executor = slackExecutor, + executor = curry(slackExecutor)({ logger }), }: { + logger: Logger; configurationUtilities: ActionsConfigurationUtilities; executor?: ExecutorType<{}, ActionTypeSecretsType, ActionParamsType, unknown>; }): SlackActionType { @@ -99,6 +105,7 @@ function valdiateActionTypeConfig( // action executor async function slackExecutor( + { logger }: { logger: Logger }, execOptions: SlackActionTypeExecutorOptions ): Promise> { const actionId = execOptions.actionId; @@ -109,10 +116,22 @@ async function slackExecutor( const { webhookUrl } = secrets; const { message } = params; + let proxyAgent: HttpsProxyAgent | HttpProxyAgent | undefined; + if (execOptions.proxySettings) { + proxyAgent = getProxyAgent(execOptions.proxySettings, logger); + logger.info(`IncomingWebhook was called with proxyUrl ${execOptions.proxySettings.proxyUrl}`); + } + try { - const webhook = new IncomingWebhook(webhookUrl); + // https://slack.dev/node-slack-sdk/webhook + // node-slack-sdk use Axios inside :) + const webhook = new IncomingWebhook(webhookUrl, { + agent: proxyAgent, + }); result = await webhook.send(message); } catch (err) { + logger.error(`error on ${actionId} slack event: ${err.message}`); + if (err.original == null || err.original.response == null) { return serviceErrorResult(actionId, err.message); } @@ -143,6 +162,8 @@ async function slackExecutor( }, } ); + logger.error(`error on ${actionId} slack action: ${errMessage}`); + return errorResult(actionId, errMessage); } diff --git a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts index 26dd8a1a1402a..ea9f30452918c 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts @@ -4,10 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -jest.mock('axios', () => ({ - request: jest.fn(), -})); - import { Services } from '../types'; import { validateConfig, validateSecrets, validateParams } from '../lib'; import { actionsConfigMock } from '../actions_config.mock'; @@ -24,7 +20,22 @@ import { WebhookMethods, } from './webhook'; -const axiosRequestMock = axios.request as jest.Mock; +import * as utils from './lib/axios_utils'; + +jest.mock('axios'); +jest.mock('./lib/axios_utils', () => { + const originalUtils = jest.requireActual('./lib/axios_utils'); + return { + ...originalUtils, + request: jest.fn(), + patch: jest.fn(), + }; +}); + +axios.create = jest.fn(() => axios); +const requestMock = utils.request as jest.Mock; + +axios.create = jest.fn(() => axios); const ACTION_TYPE_ID = '.webhook'; @@ -227,7 +238,7 @@ describe('params validation', () => { describe('execute()', () => { beforeAll(() => { - axiosRequestMock.mockReset(); + requestMock.mockReset(); actionType = getActionType({ logger: mockedLogger, configurationUtilities: actionsConfigMock.create(), @@ -235,8 +246,8 @@ describe('execute()', () => { }); beforeEach(() => { - axiosRequestMock.mockReset(); - axiosRequestMock.mockResolvedValue({ + requestMock.mockReset(); + requestMock.mockResolvedValue({ status: 200, statusText: '', data: '', @@ -261,17 +272,42 @@ describe('execute()', () => { params: { body: 'some data' }, }); - expect(axiosRequestMock.mock.calls[0][0]).toMatchInlineSnapshot(` + expect(requestMock.mock.calls[0][0]).toMatchInlineSnapshot(` Object { "auth": Object { "password": "123", "username": "abc", }, + "axios": undefined, "data": "some data", "headers": Object { "aheader": "a value", }, + "logger": Object { + "context": Array [], + "debug": [MockFunction] { + "calls": Array [ + Array [ + "response from webhook action \\"some-id\\": [HTTP 200] ", + ], + ], + "results": Array [ + Object { + "type": "return", + "value": undefined, + }, + ], + }, + "error": [MockFunction], + "fatal": [MockFunction], + "get": [MockFunction], + "info": [MockFunction], + "log": [MockFunction], + "trace": [MockFunction], + "warn": [MockFunction], + }, "method": "post", + "proxySettings": undefined, "url": "https://abc.def/my-webhook", } `); @@ -294,13 +330,38 @@ describe('execute()', () => { params: { body: 'some data' }, }); - expect(axiosRequestMock.mock.calls[0][0]).toMatchInlineSnapshot(` + expect(requestMock.mock.calls[0][0]).toMatchInlineSnapshot(` Object { + "axios": undefined, "data": "some data", "headers": Object { "aheader": "a value", }, + "logger": Object { + "context": Array [], + "debug": [MockFunction] { + "calls": Array [ + Array [ + "response from webhook action \\"some-id\\": [HTTP 200] ", + ], + ], + "results": Array [ + Object { + "type": "return", + "value": undefined, + }, + ], + }, + "error": [MockFunction], + "fatal": [MockFunction], + "get": [MockFunction], + "info": [MockFunction], + "log": [MockFunction], + "trace": [MockFunction], + "warn": [MockFunction], + }, "method": "post", + "proxySettings": undefined, "url": "https://abc.def/my-webhook", } `); diff --git a/x-pack/plugins/actions/server/builtin_action_types/webhook.ts b/x-pack/plugins/actions/server/builtin_action_types/webhook.ts index be75742fa882e..d9a005565498d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/webhook.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/webhook.ts @@ -15,6 +15,7 @@ import { isOk, promiseResult, Result } from './lib/result_type'; import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../types'; import { ActionsConfigurationUtilities } from '../actions_config'; import { Logger } from '../../../../../src/core/server'; +import { request } from './lib/axios_utils'; // config definition export enum WebhookMethods { @@ -136,13 +137,18 @@ export async function executor( ? { auth: { username: secrets.user, password: secrets.password } } : {}; + const axiosInstance = axios.create(); + const result: Result = await promiseResult( - axios.request({ + request({ + axios: axiosInstance, method, url, + logger, ...basicAuth, headers, data, + proxySettings: execOptions.proxySettings, }) ); @@ -159,7 +165,7 @@ export async function executor( if (error.response) { const { status, statusText, headers: responseHeaders } = error.response; const message = `[${status}] ${statusText}`; - logger.warn(`error on ${actionId} webhook event: ${message}`); + logger.error(`error on ${actionId} webhook event: ${message}`); // The request was made and the server responded with a status code // that falls out of the range of 2xx // special handling for 5xx @@ -178,7 +184,7 @@ export async function executor( return errorResultInvalid(actionId, message); } - logger.warn(`error on ${actionId} webhook action: unexpected error`); + logger.error(`error on ${actionId} webhook action: unexpected error`); return errorResultUnexpectedError(actionId); } } diff --git a/x-pack/plugins/actions/server/config.test.ts b/x-pack/plugins/actions/server/config.test.ts index e86f2d7832828..795fbbf84145b 100644 --- a/x-pack/plugins/actions/server/config.test.ts +++ b/x-pack/plugins/actions/server/config.test.ts @@ -15,6 +15,7 @@ describe('config validation', () => { "*", ], "preconfigured": Object {}, + "rejectUnauthorizedCertificates": true, "whitelistedHosts": Array [ "*", ], @@ -33,6 +34,7 @@ describe('config validation', () => { }, }, }, + rejectUnauthorizedCertificates: false, }; expect(configSchema.validate(config)).toMatchInlineSnapshot(` Object { @@ -50,6 +52,7 @@ describe('config validation', () => { "secrets": Object {}, }, }, + "rejectUnauthorizedCertificates": false, "whitelistedHosts": Array [ "*", ], diff --git a/x-pack/plugins/actions/server/config.ts b/x-pack/plugins/actions/server/config.ts index b2f3fa2680a9c..ba80915ebe243 100644 --- a/x-pack/plugins/actions/server/config.ts +++ b/x-pack/plugins/actions/server/config.ts @@ -32,6 +32,9 @@ export const configSchema = schema.object({ defaultValue: {}, validate: validatePreconfigured, }), + proxyUrl: schema.maybe(schema.string()), + proxyHeaders: schema.maybe(schema.recordOf(schema.string(), schema.string())), + rejectUnauthorizedCertificates: schema.boolean({ defaultValue: true }), }); export type ActionsConfig = TypeOf; diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts index bce06c829b1bc..97c08124f5546 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.ts @@ -12,6 +12,7 @@ import { GetServicesFunction, RawAction, PreConfiguredAction, + ProxySettings, } from '../types'; import { EncryptedSavedObjectsClient } from '../../../encrypted_saved_objects/server'; import { SpacesServiceSetup } from '../../../spaces/server'; @@ -28,6 +29,7 @@ export interface ActionExecutorContext { actionTypeRegistry: ActionTypeRegistryContract; eventLogger: IEventLogger; preconfiguredActions: PreConfiguredAction[]; + proxySettings?: ProxySettings; } export interface ExecuteOptions { @@ -78,6 +80,7 @@ export class ActionExecutor { eventLogger, preconfiguredActions, getActionsClientWithRequest, + proxySettings, } = this.actionExecutorContext!; const services = getServices(request); @@ -133,6 +136,7 @@ export class ActionExecutor { params: validatedParams, config: validatedConfig, secrets: validatedSecrets, + proxySettings, }); } catch (err) { rawResult = { diff --git a/x-pack/plugins/actions/server/plugin.test.ts b/x-pack/plugins/actions/server/plugin.test.ts index ca93e88d01203..341a17889923f 100644 --- a/x-pack/plugins/actions/server/plugin.test.ts +++ b/x-pack/plugins/actions/server/plugin.test.ts @@ -34,6 +34,7 @@ describe('Actions Plugin', () => { enabledActionTypes: ['*'], whitelistedHosts: ['*'], preconfigured: {}, + rejectUnauthorizedCertificates: true, }); plugin = new ActionsPlugin(context); coreSetup = coreMock.createSetup(); @@ -194,6 +195,7 @@ describe('Actions Plugin', () => { secrets: {}, }, }, + rejectUnauthorizedCertificates: true, }); plugin = new ActionsPlugin(context); coreSetup = coreMock.createSetup(); @@ -217,7 +219,7 @@ describe('Actions Plugin', () => { // coreMock.createSetup doesn't support Plugin generics // eslint-disable-next-line @typescript-eslint/no-explicit-any await plugin.setup(coreSetup as any, pluginsSetup); - const pluginStart = plugin.start(coreStart, pluginsStart); + const pluginStart = await plugin.start(coreStart, pluginsStart); expect(pluginStart.isActionExecutable('preconfiguredServerLog', '.server-log')).toBe(true); }); @@ -232,7 +234,7 @@ describe('Actions Plugin', () => { usingEphemeralEncryptionKey: false, }, }); - const pluginStart = plugin.start(coreStart, pluginsStart); + const pluginStart = await plugin.start(coreStart, pluginsStart); await pluginStart.getActionsClientWithRequest(httpServerMock.createKibanaRequest()); }); @@ -241,7 +243,7 @@ describe('Actions Plugin', () => { // coreMock.createSetup doesn't support Plugin generics // eslint-disable-next-line @typescript-eslint/no-explicit-any await plugin.setup(coreSetup as any, pluginsSetup); - const pluginStart = plugin.start(coreStart, pluginsStart); + const pluginStart = await plugin.start(coreStart, pluginsStart); expect(pluginsSetup.encryptedSavedObjects.usingEphemeralEncryptionKey).toEqual(true); await expect( diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index ee50ee81d507c..413e6663105b8 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -116,6 +116,7 @@ export class ActionsPlugin implements Plugin, Plugi private readonly config: Promise; private readonly logger: Logger; + private actionsConfig?: ActionsConfig; private serverBasePath?: string; private taskRunnerFactory?: TaskRunnerFactory; private actionTypeRegistry?: ActionTypeRegistry; @@ -173,12 +174,12 @@ export class ActionsPlugin implements Plugin, Plugi // get executions count const taskRunnerFactory = new TaskRunnerFactory(actionExecutor); - const actionsConfig = (await this.config) as ActionsConfig; - const actionsConfigUtils = getActionsConfigurationUtilities(actionsConfig); + this.actionsConfig = (await this.config) as ActionsConfig; + const actionsConfigUtils = getActionsConfigurationUtilities(this.actionsConfig); - for (const preconfiguredId of Object.keys(actionsConfig.preconfigured)) { + for (const preconfiguredId of Object.keys(this.actionsConfig.preconfigured)) { this.preconfiguredActions.push({ - ...actionsConfig.preconfigured[preconfiguredId], + ...this.actionsConfig.preconfigured[preconfiguredId], id: preconfiguredId, isPreconfigured: true, }); @@ -317,6 +318,14 @@ export class ActionsPlugin implements Plugin, Plugi encryptedSavedObjectsClient, actionTypeRegistry: actionTypeRegistry!, preconfiguredActions, + proxySettings: + this.actionsConfig && this.actionsConfig.proxyUrl + ? { + proxyUrl: this.actionsConfig.proxyUrl, + proxyHeaders: this.actionsConfig.proxyHeaders, + rejectUnauthorizedCertificates: this.actionsConfig.rejectUnauthorizedCertificates, + } + : undefined, }); taskRunnerFactory!.initialize({ diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts index ecec45ade0460..bf7bd709a4a88 100644 --- a/x-pack/plugins/actions/server/types.ts +++ b/x-pack/plugins/actions/server/types.ts @@ -58,6 +58,7 @@ export interface ActionTypeExecutorOptions { config: Config; secrets: Secrets; params: Params; + proxySettings?: ProxySettings; } export interface ActionResult { @@ -140,3 +141,9 @@ export interface ActionTaskExecutorParams { spaceId: string; actionTaskParamsId: string; } + +export interface ProxySettings { + proxyUrl: string; + proxyHeaders?: Record; + rejectUnauthorizedCertificates: boolean; +} diff --git a/x-pack/test/alerting_api_integration/basic/config.ts b/x-pack/test/alerting_api_integration/basic/config.ts index f9c248ec3d56f..f58b7753b74f7 100644 --- a/x-pack/test/alerting_api_integration/basic/config.ts +++ b/x-pack/test/alerting_api_integration/basic/config.ts @@ -11,4 +11,5 @@ export default createTestConfig('basic', { disabledPlugins: [], license: 'basic', ssl: true, + enableActionsProxy: false, }); diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index 4947cdbf55484..34e23a2dba0b2 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -5,6 +5,7 @@ */ import path from 'path'; +import getPort from 'get-port'; import fs from 'fs'; import { CA_CERT_PATH } from '@kbn/dev-utils'; import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; @@ -15,6 +16,7 @@ interface CreateTestConfigOptions { license: string; disabledPlugins?: string[]; ssl?: boolean; + enableActionsProxy: boolean; } // test.not-enabled is specifically not enabled @@ -56,6 +58,10 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) fs.statSync(path.resolve(__dirname, 'fixtures', 'plugins', file)).isDirectory() ); + const actionsProxyUrl = options.enableActionsProxy + ? [`--xpack.actions.proxyUrl=http://localhost:${await getPort()}`] + : []; + return { testFiles: [require.resolve(`../${name}/tests/`)], servers, @@ -85,6 +91,9 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) ])}`, '--xpack.encryptedSavedObjects.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"', `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, + ...actionsProxyUrl, + '--xpack.actions.rejectUnauthorizedCertificates=false', + '--xpack.eventLog.logEntries=true', `--xpack.actions.preconfigured=${JSON.stringify({ 'my-slack1': { diff --git a/x-pack/test/alerting_api_integration/common/lib/get_proxy_server.ts b/x-pack/test/alerting_api_integration/common/lib/get_proxy_server.ts new file mode 100644 index 0000000000000..4540556e73c5f --- /dev/null +++ b/x-pack/test/alerting_api_integration/common/lib/get_proxy_server.ts @@ -0,0 +1,30 @@ +/* + * 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 httpProxy from 'http-proxy'; + +export const getHttpProxyServer = ( + targetUrl: string, + onProxyResHandler: (proxyRes?: unknown, req?: unknown, res?: unknown) => void +): httpProxy => { + const proxyServer = httpProxy.createProxyServer({ + target: targetUrl, + secure: false, + selfHandleResponse: false, + }); + proxyServer.on('proxyRes', (proxyRes: unknown, req: unknown, res: unknown) => { + onProxyResHandler(proxyRes, req, res); + }); + return proxyServer; +}; + +export const getProxyUrl = (kbnTestServerConfig: any) => { + const proxyUrl = kbnTestServerConfig + .find((val: string) => val.startsWith('--xpack.actions.proxyUrl=')) + .replace('--xpack.actions.proxyUrl=', ''); + + return new URL(proxyUrl); +}; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/config.ts b/x-pack/test/alerting_api_integration/security_and_spaces/config.ts index 081b901c47fc3..97f53ae2c3664 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/config.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/config.ts @@ -11,4 +11,5 @@ export default createTestConfig('security_and_spaces', { disabledPlugins: [], license: 'trial', ssl: true, + enableActionsProxy: true, }); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts index 24931f11d4999..a0ba5331105bc 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts @@ -6,6 +6,7 @@ import expect from '@kbn/expect'; +import { getHttpProxyServer, getProxyUrl } from '../../../../common/lib/get_proxy_server'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { @@ -35,6 +36,7 @@ const mapping = [ export default function jiraTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const kibanaServer = getService('kibanaServer'); + const config = getService('config'); const mockJira = { config: { @@ -73,12 +75,19 @@ export default function jiraTest({ getService }: FtrProviderContext) { }; let jiraSimulatorURL: string = ''; + let proxyServer: any; + let proxyHaveBeenCalled = false; describe('Jira', () => { before(() => { jiraSimulatorURL = kibanaServer.resolveUrl( getExternalServiceSimulatorPath(ExternalServiceSimulator.JIRA) ); + proxyServer = getHttpProxyServer(kibanaServer.resolveUrl('/'), () => { + proxyHaveBeenCalled = true; + }); + const proxyUrl = getProxyUrl(config.get('kbnTestServer.serverArgs')); + proxyServer.listen(Number(proxyUrl.port)); }); describe('Jira - Action Creation', () => { @@ -529,6 +538,8 @@ export default function jiraTest({ getService }: FtrProviderContext) { }) .expect(200); + expect(proxyHaveBeenCalled).to.equal(true); + expect(body).to.eql({ status: 'ok', actionId: simulatedActionId, @@ -542,5 +553,9 @@ export default function jiraTest({ getService }: FtrProviderContext) { }); }); }); + + after(() => { + proxyServer.close(); + }); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts index f4fcbb65ab5a3..c697cf69bb4d5 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts @@ -6,6 +6,7 @@ import expect from '@kbn/expect'; +import { getHttpProxyServer, getProxyUrl } from '../../../../common/lib/get_proxy_server'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { @@ -17,16 +18,25 @@ import { export default function pagerdutyTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const kibanaServer = getService('kibanaServer'); + const config = getService('config'); describe('pagerduty action', () => { let simulatedActionId = ''; let pagerdutySimulatorURL: string = ''; + let proxyServer: any; + let proxyHaveBeenCalled = false; // need to wait for kibanaServer to settle ... before(() => { pagerdutySimulatorURL = kibanaServer.resolveUrl( getExternalServiceSimulatorPath(ExternalServiceSimulator.PAGERDUTY) ); + + proxyServer = getHttpProxyServer(kibanaServer.resolveUrl('/'), () => { + proxyHaveBeenCalled = true; + }); + const proxyUrl = getProxyUrl(config.get('kbnTestServer.serverArgs')); + proxyServer.listen(Number(proxyUrl.port)); }); it('should return successfully when passed valid create parameters', async () => { @@ -144,6 +154,8 @@ export default function pagerdutyTest({ getService }: FtrProviderContext) { }, }) .expect(200); + expect(proxyHaveBeenCalled).to.equal(true); + expect(result).to.eql({ status: 'ok', actionId: simulatedActionId, @@ -202,5 +214,9 @@ export default function pagerdutyTest({ getService }: FtrProviderContext) { expect(result.message).to.match(/error posting pagerduty event: http status 502/); expect(result.retry).to.equal(true); }); + + after(() => { + proxyServer.close(); + }); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts index 94feabb556a51..5085c87550d01 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts @@ -6,6 +6,7 @@ import expect from '@kbn/expect'; +import { getHttpProxyServer, getProxyUrl } from '../../../../common/lib/get_proxy_server'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { @@ -35,6 +36,7 @@ const mapping = [ export default function resilientTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const kibanaServer = getService('kibanaServer'); + const config = getService('config'); const mockResilient = { config: { @@ -73,12 +75,19 @@ export default function resilientTest({ getService }: FtrProviderContext) { }; let resilientSimulatorURL: string = ''; + let proxyServer: any; + let proxyHaveBeenCalled = false; describe('IBM Resilient', () => { before(() => { resilientSimulatorURL = kibanaServer.resolveUrl( getExternalServiceSimulatorPath(ExternalServiceSimulator.RESILIENT) ); + proxyServer = getHttpProxyServer(kibanaServer.resolveUrl('/'), () => { + proxyHaveBeenCalled = true; + }); + const proxyUrl = getProxyUrl(config.get('kbnTestServer.serverArgs')); + proxyServer.listen(Number(proxyUrl.port)); }); describe('IBM Resilient - Action Creation', () => { @@ -529,6 +538,8 @@ export default function resilientTest({ getService }: FtrProviderContext) { }) .expect(200); + expect(proxyHaveBeenCalled).to.equal(true); + expect(body).to.eql({ status: 'ok', actionId: simulatedActionId, @@ -542,5 +553,9 @@ export default function resilientTest({ getService }: FtrProviderContext) { }); }); }); + + after(() => { + proxyServer.close(); + }); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts index d3b72d01216d0..70b6a8fe512e1 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts @@ -6,6 +6,7 @@ import expect from '@kbn/expect'; +import { getHttpProxyServer, getProxyUrl } from '../../../../common/lib/get_proxy_server'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { @@ -35,6 +36,7 @@ const mapping = [ export default function servicenowTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const kibanaServer = getService('kibanaServer'); + const config = getService('config'); const mockServiceNow = { config: { @@ -72,12 +74,20 @@ export default function servicenowTest({ getService }: FtrProviderContext) { }; let servicenowSimulatorURL: string = ''; + let proxyServer: any; + let proxyHaveBeenCalled = false; describe('ServiceNow', () => { before(() => { servicenowSimulatorURL = kibanaServer.resolveUrl( getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW) ); + + proxyServer = getHttpProxyServer(kibanaServer.resolveUrl('/'), () => { + proxyHaveBeenCalled = true; + }); + const proxyUrl = getProxyUrl(config.get('kbnTestServer.serverArgs')); + proxyServer.listen(Number(proxyUrl.port)); }); describe('ServiceNow - Action Creation', () => { @@ -448,6 +458,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { }, }) .expect(200); + expect(proxyHaveBeenCalled).to.equal(true); expect(result).to.eql({ status: 'ok', @@ -462,5 +473,9 @@ export default function servicenowTest({ getService }: FtrProviderContext) { }); }); }); + + after(() => { + proxyServer.close(); + }); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts index c68bcaa0ad4e8..45f9ba369dc23 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts @@ -7,6 +7,7 @@ import expect from '@kbn/expect'; import http from 'http'; import getPort from 'get-port'; +import { getHttpProxyServer, getProxyUrl } from '../../../../common/lib/get_proxy_server'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { getSlackServer } from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; @@ -14,18 +15,27 @@ import { getSlackServer } from '../../../../common/fixtures/plugins/actions_simu // eslint-disable-next-line import/no-default-export export default function slackTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); + const config = getService('config'); describe('slack action', () => { let simulatedActionId = ''; let slackSimulatorURL: string = ''; let slackServer: http.Server; + let proxyServer: any; + let proxyHaveBeenCalled = false; // need to wait for kibanaServer to settle ... before(async () => { slackServer = await getSlackServer(); const availablePort = await getPort({ port: 9000 }); slackServer.listen(availablePort); slackSimulatorURL = `http://localhost:${availablePort}`; + + proxyServer = getHttpProxyServer(slackSimulatorURL, () => { + proxyHaveBeenCalled = true; + }); + const proxyUrl = getProxyUrl(config.get('kbnTestServer.serverArgs')); + proxyServer.listen(Number(proxyUrl.port)); }); it('should return 200 when creating a slack action successfully', async () => { @@ -155,6 +165,7 @@ export default function slackTest({ getService }: FtrProviderContext) { }) .expect(200); expect(result.status).to.eql('ok'); + expect(proxyHaveBeenCalled).to.equal(true); }); it('should handle an empty message error', async () => { @@ -222,6 +233,7 @@ export default function slackTest({ getService }: FtrProviderContext) { after(() => { slackServer.close(); + proxyServer.close(); }); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts index 8f17ab54184b5..896026611043f 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts @@ -8,6 +8,7 @@ import http from 'http'; import getPort from 'get-port'; import expect from '@kbn/expect'; import { URL, format as formatUrl } from 'url'; +import { getHttpProxyServer, getProxyUrl } from '../../../../common/lib/get_proxy_server'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { getExternalServiceSimulatorPath, @@ -31,6 +32,7 @@ function parsePort(url: Record): Record { webhookServer = await getWebhookServer(); @@ -76,6 +80,12 @@ export default function webhookTest({ getService }: FtrProviderContext) { webhookServer.listen(availablePort); webhookSimulatorURL = `http://localhost:${availablePort}`; + proxyServer = getHttpProxyServer(webhookSimulatorURL, () => { + proxyHaveBeenCalled = true; + }); + const proxyUrl = getProxyUrl(configService.get('kbnTestServer.serverArgs')); + proxyServer.listen(Number(proxyUrl.port)); + kibanaURL = kibanaServer.resolveUrl( getExternalServiceSimulatorPath(ExternalServiceSimulator.WEBHOOK) ); @@ -140,6 +150,7 @@ export default function webhookTest({ getService }: FtrProviderContext) { .expect(200); expect(result.status).to.eql('ok'); + expect(proxyHaveBeenCalled).to.equal(true); }); it('should support the POST method against webhook target', async () => { @@ -218,7 +229,7 @@ export default function webhookTest({ getService }: FtrProviderContext) { .expect(200); expect(result.status).to.eql('error'); - expect(result.message).to.match(/error calling webhook, unexpected error/); + expect(result.message).to.match(/error calling webhook, retry later/); }); it('should handle failing webhook targets', async () => { @@ -240,6 +251,7 @@ export default function webhookTest({ getService }: FtrProviderContext) { after(() => { webhookServer.close(); + proxyServer.close(); }); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/config.ts b/x-pack/test/alerting_api_integration/spaces_only/config.ts index c79c26ef68752..f9860b642f13a 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/config.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/config.ts @@ -7,4 +7,8 @@ import { createTestConfig } from '../common/config'; // eslint-disable-next-line import/no-default-export -export default createTestConfig('spaces_only', { disabledPlugins: ['security'], license: 'trial' }); +export default createTestConfig('spaces_only', { + disabledPlugins: ['security'], + license: 'trial', + enableActionsProxy: false, +}); diff --git a/yarn.lock b/yarn.lock index 41323c5bb2761..81bb7338e615f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3913,6 +3913,20 @@ resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.0.tgz#9140779736aa2655635ee756e2467d787cfe8a2a" integrity sha512-c3Xy026kOF7QOTn00hbIllV1dLR9hG9NkSrLQgCVs8NF6sBU+VGWjD3wLPhmh1TYAc7ugCFsvHYMN4VcBN1U1A== +"@types/http-proxy-agent@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@types/http-proxy-agent/-/http-proxy-agent-2.0.2.tgz#942c1f35c7e1f0edd1b6ffae5d0f9051cfb32be1" + integrity sha512-2S6IuBRhqUnH1/AUx9k8KWtY3Esg4eqri946MnxTG5HwehF1S5mqLln8fcyMiuQkY72p2gH3W+rIPqp5li0LyQ== + dependencies: + "@types/node" "*" + +"@types/http-proxy@^1.17.4": + version "1.17.4" + resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.4.tgz#e7c92e3dbe3e13aa799440ff42e6d3a17a9d045b" + integrity sha512-IrSHl2u6AWXduUaDLqYpt45tLVCtYv7o4Z0s1KghBCDgIIS9oW5K1H8mZG/A2CfeLdEa7rTd1ACOiHBc1EMT2Q== + dependencies: + "@types/node" "*" + "@types/inert@^5.1.2": version "5.1.2" resolved "https://registry.yarnpkg.com/@types/inert/-/inert-5.1.2.tgz#2bb8bef3b2462f904c960654c9edfa39285a85c6" From df7221245d58840ceb768001864fbf2442b71d2a Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Fri, 14 Aug 2020 17:37:23 -0400 Subject: [PATCH 06/15] skip flaky suite (#74814) --- .../open_timeline/open_timeline_modal/index.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/index.test.tsx index d3139601bfa17..365444032b402 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/index.test.tsx @@ -55,7 +55,8 @@ jest.mock('react-virtualized-auto-sizer', () => { }) => children({ width: 100, height: 500 }); }); -describe('OpenTimelineModal', () => { +// Failing: See https://github.com/elastic/kibana/issues/74814 +describe.skip('OpenTimelineModal', () => { const theme = () => ({ eui: euiDarkVars, darkMode: true }); const mockInstallPrepackagedTimelines = jest.fn(); beforeEach(() => { From 2129b13155592b7936c1b143d618bbce53616788 Mon Sep 17 00:00:00 2001 From: Spencer Date: Fri, 14 Aug 2020 15:30:24 -0700 Subject: [PATCH 07/15] remove .kbn-optimizer-cache upload (#75086) Co-authored-by: spalger --- vars/kibanaPipeline.groovy | 1 - 1 file changed, 1 deletion(-) diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index 173c5b7e11764..5f3e8d1a0660d 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -170,7 +170,6 @@ def uploadCoverageArtifacts(prefix, pattern) { def withGcsArtifactUpload(workerName, closure) { def uploadPrefix = "kibana-ci-artifacts/jobs/${env.JOB_NAME}/${BUILD_NUMBER}/${workerName}" def ARTIFACT_PATTERNS = [ - '**/target/public/.kbn-optimizer-cache', 'target/kibana-*', 'target/test-metrics/*', 'target/kibana-security-solution/**/*.png', From ef5c95ea37c0d804df0533dec97ad7199e43d0d5 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Fri, 14 Aug 2020 13:44:44 -0700 Subject: [PATCH 08/15] [Reporting/Flaky Test] Skip test for paging list of reports (#75075) --- .../test/functional/apps/reporting_management/report_listing.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/reporting_management/report_listing.ts b/x-pack/test/functional/apps/reporting_management/report_listing.ts index ca5fb888e67e1..3a6a60f55db1f 100644 --- a/x-pack/test/functional/apps/reporting_management/report_listing.ts +++ b/x-pack/test/functional/apps/reporting_management/report_listing.ts @@ -67,7 +67,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); }); - it('Paginates historical reports', async () => { + it.skip('Paginates historical reports', async () => { // wait for first row of page 1 await testSubjects.find('checkboxSelectRow-k9a9xlwl0gpe1457b10rraq3'); From 1439b11fd249013f8b3d0390890489b0f3642616 Mon Sep 17 00:00:00 2001 From: Spencer Date: Fri, 14 Aug 2020 16:32:45 -0700 Subject: [PATCH 09/15] [jenkins] add pipeline for hourly security solution cypress tests (#75087) * [jenkins] add pipeline for hourly security solution cypress tests * support customizing email for status emails * apply review feedback Co-authored-by: spalger Co-authored-by: Elastic Machine --- .ci/Jenkinsfile_security_cypress | 21 +++++++++++++++++++++ vars/kibanaPipeline.groovy | 10 ++++++---- 2 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 .ci/Jenkinsfile_security_cypress diff --git a/.ci/Jenkinsfile_security_cypress b/.ci/Jenkinsfile_security_cypress new file mode 100644 index 0000000000000..bdfef18024b78 --- /dev/null +++ b/.ci/Jenkinsfile_security_cypress @@ -0,0 +1,21 @@ +#!/bin/groovy + +library 'kibana-pipeline-library' +kibanaLibrary.load() + +kibanaPipeline(timeoutMinutes: 180) { + slackNotifications.onFailure( + disabled: !params.NOTIFY_ON_FAILURE, + channel: '#security-solution-slack-testing' + ) { + catchError { + workers.base(size: 's', ramDisk: false) { + kibanaPipeline.bash('test/scripts/jenkins_security_solution_cypress.sh', 'Execute Security Solution Cypress Tests') + } + } + } + + if (params.NOTIFY_ON_FAILURE) { + kibanaPipeline.sendMail(to: 'gloria.delatorre@elastic.co') + } +} diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index 5f3e8d1a0660d..00668f2ccdaa7 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -220,7 +220,7 @@ def publishJunit() { } } -def sendMail() { +def sendMail(Map params = [:]) { // If the build doesn't have a result set by this point, there haven't been any errors and it can be marked as a success // The e-mail plugin for the infra e-mail depends upon this being set currentBuild.result = currentBuild.result ?: 'SUCCESS' @@ -229,7 +229,7 @@ def sendMail() { if (buildStatus != 'SUCCESS' && buildStatus != 'ABORTED') { node('flyweight') { sendInfraMail() - sendKibanaMail() + sendKibanaMail(params) } } } @@ -245,12 +245,14 @@ def sendInfraMail() { } } -def sendKibanaMail() { +def sendKibanaMail(Map params = [:]) { + def config = [to: 'build-kibana@elastic.co'] + params + catchErrors { def buildStatus = buildUtils.getBuildStatus() if(params.NOTIFY_ON_FAILURE && buildStatus != 'SUCCESS' && buildStatus != 'ABORTED') { emailext( - to: 'build-kibana@elastic.co', + config.to, subject: "${env.JOB_NAME} - Build # ${env.BUILD_NUMBER} - ${buildStatus}", body: '${SCRIPT,template="groovy-html.template"}', mimeType: 'text/html', From 9760498ebf278a2d24b951482f49888bad5094a2 Mon Sep 17 00:00:00 2001 From: Aaron Levy Date: Fri, 14 Aug 2020 17:27:07 -0700 Subject: [PATCH 10/15] =?UTF-8?q?Adding=20/etc/rc.d/init.d/functions=20to?= =?UTF-8?q?=20the=20init=20script=20when=20present=20to=20=E2=80=A6=20(#22?= =?UTF-8?q?985)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Adding /etc/rc.d/init.d/functions to the init script when present to improve integration with systemd on systemd-based distros. See https://github.com/elastic/kibana/issues/22255 * Adding SysV Init Functions for Debian and SUSE distros * Adding /etc/rc.d/init.d/functions to the init script when present to improve integration with systemd on systemd-based distros. See https://github.com/elastic/kibana/issues/22255 * Adding SysV Init Functions for Debian and SUSE distros * docs(NA): include a comment to explain the change Co-authored-by: Elastic Machine Co-authored-by: Tiago Costa --- .../service_templates/sysv/etc/init.d/kibana | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/dev/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana b/src/dev/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana index 449fc4e75fce8..c13676ef031b0 100755 --- a/src/dev/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana +++ b/src/dev/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana @@ -15,6 +15,24 @@ # Description: Kibana ### END INIT INFO +# +# Source function libraries if present. +# (It improves integration with systemd) +# +# Red Hat +if [ -f /etc/rc.d/init.d/functions ]; then + . /etc/rc.d/init.d/functions + +# Debian +elif [ -f /lib/lsb/init-functions ]; then + . /lib/lsb/init-functions + +# SUSE +elif [ -f /etc/rc.status ]; then + . /etc/rc.status + rc_reset +fi + name=kibana program=/usr/share/kibana/bin/kibana pidfile="/var/run/kibana/$name.pid" From d4f52471bfa916602dfed92d12e2aa12b3435d1c Mon Sep 17 00:00:00 2001 From: spalger Date: Fri, 14 Aug 2020 17:29:07 -0700 Subject: [PATCH 11/15] skip flaky suite (#75044) --- .../functional/apps/reporting_management/report_listing.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/x-pack/test/functional/apps/reporting_management/report_listing.ts b/x-pack/test/functional/apps/reporting_management/report_listing.ts index 3a6a60f55db1f..6662bfb17949c 100644 --- a/x-pack/test/functional/apps/reporting_management/report_listing.ts +++ b/x-pack/test/functional/apps/reporting_management/report_listing.ts @@ -28,7 +28,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const testSubjects = getService('testSubjects'); const esArchiver = getService('esArchiver'); - describe('Listing of Reports', function () { + // FLAKY: https://github.com/elastic/kibana/issues/75044 + describe.skip('Listing of Reports', function () { before(async () => { await security.testUser.setRoles(['kibana_admin', 'reporting_user']); await esArchiver.load('empty_kibana'); @@ -67,7 +68,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); }); - it.skip('Paginates historical reports', async () => { + it('Paginates historical reports', async () => { // wait for first row of page 1 await testSubjects.find('checkboxSelectRow-k9a9xlwl0gpe1457b10rraq3'); From 41262d1b64eac54f32b939447df70e3ab16b886d Mon Sep 17 00:00:00 2001 From: Spencer Date: Fri, 14 Aug 2020 22:11:17 -0700 Subject: [PATCH 12/15] [cli] remove reference to removed --optimize flag (#75083) Co-authored-by: spalger Co-authored-by: Elastic Machine --- src/cli/serve/serve.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index 3bc710e44f7bc..345156b2491a1 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -136,11 +136,6 @@ function applyConfigOverrides(rawConfig, opts, extraCliOptions) { if (opts.verbose) set('logging.verbose', true); if (opts.logFile) set('logging.dest', opts.logFile); - if (opts.optimize) { - set('server.autoListen', false); - set('plugins.initialize', false); - } - set('plugins.scanDirs', _.compact([].concat(get('plugins.scanDirs'), opts.pluginDir))); set( 'plugins.paths', From cf93604e43f10d6243002dc0f358d3482cce9725 Mon Sep 17 00:00:00 2001 From: Spencer Date: Sat, 15 Aug 2020 13:31:37 -0700 Subject: [PATCH 13/15] [jest] temporarily extend default test timeout (#75118) * [jest] temporarily extend default test timeout * fix, 30 seconds Co-authored-by: spalger --- src/dev/jest/config.js | 1 + src/dev/jest/setup/default_timeout.js | 25 +++++++++++++++++++++ x-pack/dev-tools/jest/create_jest_config.js | 1 + 3 files changed, 27 insertions(+) create mode 100644 src/dev/jest/setup/default_timeout.js diff --git a/src/dev/jest/config.js b/src/dev/jest/config.js index 74e1ec5e2b4ed..d46b955f6668d 100644 --- a/src/dev/jest/config.js +++ b/src/dev/jest/config.js @@ -78,6 +78,7 @@ export default { setupFilesAfterEnv: [ '/src/dev/jest/setup/mocks.js', '/src/dev/jest/setup/react_testing_library.js', + '/src/dev/jest/setup/default_timeout.js', ], coverageDirectory: '/target/kibana-coverage/jest', coverageReporters: !!process.env.CODE_COVERAGE ? ['json'] : ['html', 'text'], diff --git a/src/dev/jest/setup/default_timeout.js b/src/dev/jest/setup/default_timeout.js new file mode 100644 index 0000000000000..eea38e745b960 --- /dev/null +++ b/src/dev/jest/setup/default_timeout.js @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/* eslint-env jest */ + +/** + * Set the default timeout for the unit tests to 30 seconds, temporarily + */ +jest.setTimeout(30 * 1000); diff --git a/x-pack/dev-tools/jest/create_jest_config.js b/x-pack/dev-tools/jest/create_jest_config.js index a0574dbdf36da..0e3f82792a2ba 100644 --- a/x-pack/dev-tools/jest/create_jest_config.js +++ b/x-pack/dev-tools/jest/create_jest_config.js @@ -58,6 +58,7 @@ export function createJestConfig({ kibanaDirectory, rootDir, xPackKibanaDirector `${xPackKibanaDirectory}/dev-tools/jest/setup/setup_test.js`, `${kibanaDirectory}/src/dev/jest/setup/mocks.js`, `${kibanaDirectory}/src/dev/jest/setup/react_testing_library.js`, + `${kibanaDirectory}/src/dev/jest/setup/default_timeout.js`, ], testEnvironment: 'jest-environment-jsdom-thirteen', testMatch: ['**/*.test.{js,mjs,ts,tsx}'], From 48deb7b0a129650ccba548bd05a099679b175677 Mon Sep 17 00:00:00 2001 From: Sandra Gonzales Date: Sat, 15 Aug 2020 16:16:08 -0500 Subject: [PATCH 14/15] move tests for placeholder indices to setup (#75096) Co-authored-by: Elastic Machine --- .../apis/epm/install_remove_assets.ts | 12 ------- .../apis/index.js | 3 +- .../apis/setup.ts | 32 +++++++++++++++++++ 3 files changed, 34 insertions(+), 13 deletions(-) create mode 100644 x-pack/test/ingest_manager_api_integration/apis/setup.ts diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts index 7fb8b0a2b1708..fc0589c2e90e9 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts @@ -106,18 +106,6 @@ export default function (providerContext: FtrProviderContext) { }); expect(resSearch.id).equal('sample_search'); }); - it('should have installed placeholder indices', async function () { - const resLogsIndexPatternPlaceholder = await es.transport.request({ - method: 'GET', - path: `/logs-index_pattern_placeholder`, - }); - expect(resLogsIndexPatternPlaceholder.statusCode).equal(200); - const resMetricsIndexPatternPlaceholder = await es.transport.request({ - method: 'GET', - path: `/metrics-index_pattern_placeholder`, - }); - expect(resMetricsIndexPatternPlaceholder.statusCode).equal(200); - }); it('should have created the correct saved object', async function () { const res = await kibanaServer.savedObjects.get({ type: 'epm-packages', diff --git a/x-pack/test/ingest_manager_api_integration/apis/index.js b/x-pack/test/ingest_manager_api_integration/apis/index.js index 72121b2164bfd..948f953ebe3f5 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/index.js +++ b/x-pack/test/ingest_manager_api_integration/apis/index.js @@ -7,7 +7,8 @@ export default function ({ loadTestFile }) { describe('Ingest Manager Endpoints', function () { this.tags('ciGroup7'); - + // Ingest Manager setup + loadTestFile(require.resolve('./setup')); // Fleet loadTestFile(require.resolve('./fleet/index')); diff --git a/x-pack/test/ingest_manager_api_integration/apis/setup.ts b/x-pack/test/ingest_manager_api_integration/apis/setup.ts new file mode 100644 index 0000000000000..daf0c8937715f --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/setup.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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../api_integration/ftr_provider_context'; + +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; + const supertest = getService('supertest'); + const es = getService('es'); + describe('ingest manager setup', async () => { + before(async () => { + await supertest.post(`/api/ingest_manager/setup`).set('kbn-xsrf', 'xxx').send(); + }); + + it('should have installed placeholder indices', async function () { + const resLogsIndexPatternPlaceholder = await es.transport.request({ + method: 'GET', + path: `/logs-index_pattern_placeholder`, + }); + expect(resLogsIndexPatternPlaceholder.statusCode).equal(200); + const resMetricsIndexPatternPlaceholder = await es.transport.request({ + method: 'GET', + path: `/metrics-index_pattern_placeholder`, + }); + expect(resMetricsIndexPatternPlaceholder.statusCode).equal(200); + }); + }); +} From 46a268fb99606d29f7bd3b27c454cc210f49f9b9 Mon Sep 17 00:00:00 2001 From: Sandra Gonzales Date: Sat, 15 Aug 2020 16:18:24 -0500 Subject: [PATCH 15/15] [Ingest Manager] fix removing ingest pipelines from elasticsearch (#75092) * fix removing ingest pipelines bug * undo unneeded changes to default.yml entry pipeline --- .../elasticsearch/ingest_pipeline/index.ts | 2 +- .../elasticsearch/ingest_pipeline/remove.ts | 26 ++++++----- .../server/services/epm/packages/install.ts | 4 +- .../apis/epm/install_remove_assets.ts | 38 ++++++++++++++++ .../apis/epm/update_assets.ts | 45 +++++++++++++++---- .../ingest_pipeline/pipeline1.yml | 9 ++++ .../ingest_pipeline/pipeline2.yml | 9 ++++ .../ingest_pipeline/pipeline1.yml | 9 ++++ 8 files changed, 121 insertions(+), 21 deletions(-) create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/dataset/test_logs/elasticsearch/ingest_pipeline/pipeline1.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/dataset/test_logs/elasticsearch/ingest_pipeline/pipeline2.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/dataset/test_logs/elasticsearch/ingest_pipeline/pipeline1.yml diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/index.ts index 6450f7303dd88..4f2c7c4c339f1 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/index.ts @@ -6,4 +6,4 @@ export { installPipelines } from './install'; -export { deletePipelines, deletePipeline } from './remove'; +export { deletePreviousPipelines, deletePipeline } from './remove'; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/remove.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/remove.ts index 8be3a1beab392..836b53b5a9225 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/remove.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/remove.ts @@ -8,24 +8,32 @@ import { SavedObjectsClientContract } from 'src/core/server'; import { appContextService } from '../../../'; import { CallESAsCurrentUser, ElasticsearchAssetType } from '../../../../types'; import { getInstallation } from '../../packages/get'; -import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../../../common'; +import { PACKAGES_SAVED_OBJECT_TYPE, EsAssetReference } from '../../../../../common'; -export const deletePipelines = async ( +export const deletePreviousPipelines = async ( callCluster: CallESAsCurrentUser, savedObjectsClient: SavedObjectsClientContract, pkgName: string, - pkgVersion: string + previousPkgVersion: string ) => { const logger = appContextService.getLogger(); - const previousPipelinesPattern = `*-${pkgName}.*-${pkgVersion}`; - + const installation = await getInstallation({ savedObjectsClient, pkgName }); + if (!installation) return; + const installedEsAssets = installation.installed_es; + const installedPipelines = installedEsAssets.filter( + ({ type, id }) => + type === ElasticsearchAssetType.ingestPipeline && id.includes(previousPkgVersion) + ); + const deletePipelinePromises = installedPipelines.map(({ type, id }) => { + return deletePipeline(callCluster, id); + }); try { - await deletePipeline(callCluster, previousPipelinesPattern); + await Promise.all(deletePipelinePromises); } catch (e) { logger.error(e); } try { - await deletePipelineRefs(savedObjectsClient, pkgName, pkgVersion); + await deletePipelineRefs(savedObjectsClient, installedEsAssets, pkgName, previousPkgVersion); } catch (e) { logger.error(e); } @@ -33,12 +41,10 @@ export const deletePipelines = async ( export const deletePipelineRefs = async ( savedObjectsClient: SavedObjectsClientContract, + installedEsAssets: EsAssetReference[], pkgName: string, pkgVersion: string ) => { - const installation = await getInstallation({ savedObjectsClient, pkgName }); - if (!installation) return; - const installedEsAssets = installation.installed_es; const filteredAssets = installedEsAssets.filter(({ type, id }) => { if (type !== ElasticsearchAssetType.ingestPipeline) return true; if (!id.includes(pkgVersion)) return true; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts index 0911aaf248e7a..6bc461845f124 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts @@ -22,7 +22,7 @@ import * as Registry from '../registry'; import { getInstallation, getInstallationObject, isRequiredPackage } from './index'; import { installTemplates } from '../elasticsearch/template/install'; import { generateESIndexPatterns } from '../elasticsearch/template/template'; -import { installPipelines, deletePipelines } from '../elasticsearch/ingest_pipeline/'; +import { installPipelines, deletePreviousPipelines } from '../elasticsearch/ingest_pipeline/'; import { installILMPolicy } from '../elasticsearch/ilm/install'; import { installKibanaAssets, @@ -183,7 +183,7 @@ export async function installPackage({ // if this is an update, delete the previous version's pipelines if (installedPkg && !reinstall) { - await deletePipelines( + await deletePreviousPipelines( callCluster, savedObjectsClient, pkgName, diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts index fc0589c2e90e9..e575d7b680301 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts @@ -61,6 +61,16 @@ export default function (providerContext: FtrProviderContext) { path: `/_ingest/pipeline/${logsTemplateName}-${pkgVersion}`, }); expect(res.statusCode).equal(200); + const resPipeline1 = await es.transport.request({ + method: 'GET', + path: `/_ingest/pipeline/${logsTemplateName}-${pkgVersion}-pipeline1`, + }); + expect(resPipeline1.statusCode).equal(200); + const resPipeline2 = await es.transport.request({ + method: 'GET', + path: `/_ingest/pipeline/${logsTemplateName}-${pkgVersion}-pipeline2`, + }); + expect(resPipeline2.statusCode).equal(200); }); it('should have installed the template components', async function () { const res = await es.transport.request({ @@ -135,6 +145,14 @@ export default function (providerContext: FtrProviderContext) { id: 'logs-all_assets.test_logs-0.1.0', type: 'ingest_pipeline', }, + { + id: 'logs-all_assets.test_logs-0.1.0-pipeline1', + type: 'ingest_pipeline', + }, + { + id: 'logs-all_assets.test_logs-0.1.0-pipeline2', + type: 'ingest_pipeline', + }, { id: 'logs-all_assets.test_logs', type: 'index_template', @@ -195,6 +213,26 @@ export default function (providerContext: FtrProviderContext) { } ); expect(res.statusCode).equal(404); + const resPipeline1 = await es.transport.request( + { + method: 'GET', + path: `/_ingest/pipeline/${logsTemplateName}-${pkgVersion}-pipeline1`, + }, + { + ignore: [404], + } + ); + expect(resPipeline1.statusCode).equal(404); + const resPipeline2 = await es.transport.request( + { + method: 'GET', + path: `/_ingest/pipeline/${logsTemplateName}-${pkgVersion}-pipeline2`, + }, + { + ignore: [404], + } + ); + expect(resPipeline2.statusCode).equal(404); }); it('should have uninstalled the kibana assets', async function () { let resDashboard; diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/update_assets.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/update_assets.ts index 59ad7a9744ae1..8ad6fe12dcd43 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/update_assets.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/update_assets.ts @@ -154,24 +154,49 @@ export default function (providerContext: FtrProviderContext) { }, }); }); - it('should have installed the new versionized pipeline', async function () { + it('should have installed the new versionized pipelines', async function () { const res = await es.transport.request({ method: 'GET', path: `/_ingest/pipeline/${logsTemplateName}-${pkgUpdateVersion}`, }); expect(res.statusCode).equal(200); + const resPipeline1 = await es.transport.request({ + method: 'GET', + path: `/_ingest/pipeline/${logsTemplateName}-${pkgUpdateVersion}-pipeline1`, + }); + expect(resPipeline1.statusCode).equal(200); }); it('should have removed the old versionized pipelines', async function () { - let res; - try { - res = await es.transport.request({ + const res = await es.transport.request( + { method: 'GET', path: `/_ingest/pipeline/${logsTemplateName}-${pkgVersion}`, - }); - } catch (err) { - res = err; - } + }, + { + ignore: [404], + } + ); expect(res.statusCode).equal(404); + const resPipeline1 = await es.transport.request( + { + method: 'GET', + path: `/_ingest/pipeline/${logsTemplateName}-${pkgVersion}-pipeline1`, + }, + { + ignore: [404], + } + ); + expect(resPipeline1.statusCode).equal(404); + const resPipeline2 = await es.transport.request( + { + method: 'GET', + path: `/_ingest/pipeline/${logsTemplateName}-${pkgVersion}-pipeline2`, + }, + { + ignore: [404], + } + ); + expect(resPipeline2.statusCode).equal(404); }); it('should have updated the template components', async function () { const res = await es.transport.request({ @@ -272,6 +297,10 @@ export default function (providerContext: FtrProviderContext) { id: 'logs-all_assets.test_logs-0.2.0', type: 'ingest_pipeline', }, + { + id: 'logs-all_assets.test_logs-0.2.0-pipeline1', + type: 'ingest_pipeline', + }, { id: 'logs-all_assets.test_logs', type: 'index_template', diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/dataset/test_logs/elasticsearch/ingest_pipeline/pipeline1.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/dataset/test_logs/elasticsearch/ingest_pipeline/pipeline1.yml new file mode 100644 index 0000000000000..c2471c56ee22a --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/dataset/test_logs/elasticsearch/ingest_pipeline/pipeline1.yml @@ -0,0 +1,9 @@ +--- +description: Pipeline test +processors: +- remove: + field: messag +on_failure: + - set: + field: error.message + value: "{{ _ingest.on_failure_message }}" \ No newline at end of file diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/dataset/test_logs/elasticsearch/ingest_pipeline/pipeline2.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/dataset/test_logs/elasticsearch/ingest_pipeline/pipeline2.yml new file mode 100644 index 0000000000000..c2471c56ee22a --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/dataset/test_logs/elasticsearch/ingest_pipeline/pipeline2.yml @@ -0,0 +1,9 @@ +--- +description: Pipeline test +processors: +- remove: + field: messag +on_failure: + - set: + field: error.message + value: "{{ _ingest.on_failure_message }}" \ No newline at end of file diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/dataset/test_logs/elasticsearch/ingest_pipeline/pipeline1.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/dataset/test_logs/elasticsearch/ingest_pipeline/pipeline1.yml new file mode 100644 index 0000000000000..c2471c56ee22a --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/dataset/test_logs/elasticsearch/ingest_pipeline/pipeline1.yml @@ -0,0 +1,9 @@ +--- +description: Pipeline test +processors: +- remove: + field: messag +on_failure: + - set: + field: error.message + value: "{{ _ingest.on_failure_message }}" \ No newline at end of file