diff --git a/.buildkite/ftr_oblt_stateful_configs.yml b/.buildkite/ftr_oblt_stateful_configs.yml index 6f0cb38be3a62..7655ce6de38cf 100644 --- a/.buildkite/ftr_oblt_stateful_configs.yml +++ b/.buildkite/ftr_oblt_stateful_configs.yml @@ -30,7 +30,6 @@ enabled: - x-pack/test/api_integration/apis/metrics_ui/config.ts - x-pack/test/api_integration/apis/osquery/config.ts - x-pack/test/api_integration/apis/synthetics/config.ts - - x-pack/test/api_integration/apis/slos/config.ts - x-pack/test/api_integration/apis/uptime/config.ts - x-pack/test/api_integration/apis/entity_manager/config.ts - x-pack/test/apm_api_integration/basic/config.ts diff --git a/.gitignore b/.gitignore index 7e06f1e23f863..9bda927a92b6a 100644 --- a/.gitignore +++ b/.gitignore @@ -157,4 +157,5 @@ x-pack/test/security_solution_playwright/playwright-report/ x-pack/test/security_solution_playwright/blob-report/ x-pack/test/security_solution_playwright/playwright/.cache/ x-pack/test/security_solution_playwright/.auth/ -x-pack/test/security_solution_playwright/.env \ No newline at end of file +x-pack/test/security_solution_playwright/.env +.codeql diff --git a/scripts/codeql/codeql.dockerfile b/scripts/codeql/codeql.dockerfile new file mode 100644 index 0000000000000..fce6b9c3fdd63 --- /dev/null +++ b/scripts/codeql/codeql.dockerfile @@ -0,0 +1,39 @@ +FROM ubuntu:latest + +ENV DEBIAN_FRONTEND=noninteractive + +ARG USERNAME=codeql +ARG CODEQL_VERSION="v2.19.0" +ENV CODEQL_HOME /usr/local/codeql-home + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + passwd \ + adduser \ + bash \ + curl \ + git \ + unzip \ + nodejs \ + jq + +RUN adduser --home ${CODEQL_HOME} ${USERNAME} + +RUN curl -Lk "https://github.com/github/codeql-action/releases/download/codeql-bundle-${CODEQL_VERSION}/codeql-bundle-linux64.tar.gz" -o codeql.tar.gz \ + && mkdir -p ${CODEQL_HOME} \ + && tar -xvzf codeql.tar.gz -C ${CODEQL_HOME} \ + && rm codeql.tar.gz + +RUN chmod +x ${CODEQL_HOME}/codeql/codeql + +RUN chown -R ${USERNAME}:${USERNAME} ${CODEQL_HOME} + +USER ${USERNAME} + +ENV PATH="${CODEQL_HOME}/codeql:${PATH}" + +RUN echo $PATH && codeql --version + +WORKDIR /workspace + +ENTRYPOINT ["/bin/bash", "-c"] diff --git a/scripts/codeql/quick_check.sh b/scripts/codeql/quick_check.sh new file mode 100644 index 0000000000000..15023bfb13bfa --- /dev/null +++ b/scripts/codeql/quick_check.sh @@ -0,0 +1,126 @@ +#!/bin/bash + +LANGUAGE="javascript" +CODEQL_DIR=".codeql" +DATABASE_PATH="$CODEQL_DIR/database" +QUERY_OUTPUT="$DATABASE_PATH/results.sarif" +OUTPUT_FORMAT="sarif-latest" +DOCKER_IMAGE="codeql-env" +BASE_DIR="$(cd "$(dirname "$0")"; pwd)" + +# Colors +bold=$(tput bold) +reset=$(tput sgr0) +red=$(tput setaf 1) +green=$(tput setaf 2) +blue=$(tput setaf 4) +yellow=$(tput setaf 3) + +while getopts ":s:r:" opt; do + case $opt in + s) SRC_DIR="$OPTARG" ;; + r) CODEQL_DIR="$OPTARG"; DATABASE_PATH="$CODEQL_DIR/database"; QUERY_OUTPUT="$DATABASE_PATH/results.sarif" ;; + \?) echo "Invalid option -$OPTARG" >&2; exit 1 ;; + :) echo "Option -$OPTARG requires an argument." >&2; exit 1 ;; + esac +done + +if [ -z "$SRC_DIR" ]; then + echo "Usage: $0 -s [-r ]" + exit 1 +fi + +mkdir -p "$CODEQL_DIR" + +# Check the architecture +ARCH=$(uname -m) +PLATFORM_FLAG="" + +# CodeQL CLI binary does not support arm64 architecture, setting the platform to linux/amd64 +if [[ "$ARCH" == "arm64" ]]; then + PLATFORM_FLAG="--platform linux/amd64" +fi + +if [[ "$(docker images -q $DOCKER_IMAGE 2> /dev/null)" == "" ]]; then + echo "Docker image $DOCKER_IMAGE not found. Building locally..." + docker build $PLATFORM_FLAG -t "$DOCKER_IMAGE" -f "$BASE_DIR/codeql.dockerfile" "$BASE_DIR" + if [ $? -ne 0 ]; then + echo "${red}Docker image build failed.${reset}" + exit 1 + fi +fi + +cleanup_database() { + echo "Deleting contents of $CODEQL_DIR." + rm -rf "$CODEQL_DIR"/* +} + +SRC_DIR="$(cd "$(dirname "$SRC_DIR")"; pwd)/$(basename "$SRC_DIR")" +CODEQL_DIR="$(cd "$(dirname "$CODEQL_DIR")"; pwd)/$(basename "$CODEQL_DIR")" +DATABASE_PATH="$(cd "$(dirname "$DATABASE_PATH")"; pwd)/$(basename "$DATABASE_PATH")" + +# Step 1: Run the Docker container to create a CodeQL database from the source code. +echo "Creating a CodeQL database from the source code: $SRC_DIR" +docker run $PLATFORM_FLAG --rm -v "$SRC_DIR":/workspace/source-code \ + -v "${DATABASE_PATH}":/workspace/shared $DOCKER_IMAGE \ + "codeql database create /workspace/shared/codeql-db --language=javascript --source-root=/workspace/source-code --overwrite" + +if [ $? -ne 0 ]; then + echo "CodeQL database creation failed." + cleanup_database + exit 1 +fi + +echo "Analyzing a CodeQL database: $DATABASE_PATH" +# Step 2: Run the Docker container to analyze the CodeQL database. +docker run $PLATFORM_FLAG --rm -v "${DATABASE_PATH}":/workspace/shared $DOCKER_IMAGE \ + "codeql database analyze --format=${OUTPUT_FORMAT} --output=/workspace/shared/results.sarif /workspace/shared/codeql-db javascript-security-and-quality.qls" + +if [ $? -ne 0 ]; then + echo "CodeQL database analysis failed." + cleanup_database + exit 1 +fi + +# Step 3: Print summary of SARIF results +echo "Analysis complete. Results saved to $QUERY_OUTPUT" +if command -v jq &> /dev/null; then + vulnerabilities=$(jq -r '.runs[] | select(.results | length > 0)' "$QUERY_OUTPUT") + + if [[ -z "$vulnerabilities" ]]; then + echo "${blue}${bold}No vulnerabilities found in the SARIF results.${reset}" + else + echo "${yellow}${bold}Summary of SARIF results:${reset}" + jq -r ' + .runs[] | + .results[] as $result | + .tool.driver.rules[] as $rule | + select($rule.id == $result.ruleId) | + "Rule: \($result.ruleId)\nMessage: \($result.message.text)\nFile: \($result.locations[].physicalLocation.artifactLocation.uri)\nLine: \($result.locations[].physicalLocation.region.startLine)\nSecurity Severity: \($rule.properties."security-severity" // "N/A")\n"' "$QUERY_OUTPUT" | + while IFS= read -r line; do + case "$line" in + Rule:*) + echo "${red}${bold}$line${reset}" + ;; + Message:*) + echo "${green}$line${reset}" + ;; + File:*) + echo "${blue}$line${reset}" + ;; + Line:*) + echo "${yellow}$line${reset}" + ;; + Security\ Severity:*) + echo "${yellow}$line${reset}" + ;; + *) + echo "$line" + ;; + esac + done + fi +else + echo "${red}${bold}Please install jq to display a summary of the SARIF results.${reset}" + echo "${bold}You can view the full results in the SARIF file using a SARIF viewer.${reset}" +fi diff --git a/src/plugins/discover/public/application/main/components/total_documents/total_documents.tsx b/src/plugins/discover/public/application/main/components/total_documents/total_documents.tsx index f9a3f04cc1c56..5644efc8a9bbd 100644 --- a/src/plugins/discover/public/application/main/components/total_documents/total_documents.tsx +++ b/src/plugins/discover/public/application/main/components/total_documents/total_documents.tsx @@ -11,7 +11,19 @@ import React from 'react'; import { FormattedMessage, FormattedNumber } from '@kbn/i18n-react'; import { EuiText } from '@elastic/eui'; -export const TotalDocuments = ({ totalHitCount }: { totalHitCount: number }) => { +export const TotalDocuments = ({ + totalHitCount, + isEsqlMode, +}: { + totalHitCount: number; + isEsqlMode?: boolean; +}) => { + const totalDocuments = ( + + + + ); + return ( style={{ paddingRight: 2 }} data-test-subj="savedSearchTotalDocuments" > - - - - ), - }} - /> + {isEsqlMode ? ( + + ) : ( + + )} ); }; diff --git a/src/plugins/discover/public/components/view_mode_toggle/view_mode_toggle.test.tsx b/src/plugins/discover/public/components/view_mode_toggle/view_mode_toggle.test.tsx index f045e78de5ddf..7d88f9ad1fef4 100644 --- a/src/plugins/discover/public/components/view_mode_toggle/view_mode_toggle.test.tsx +++ b/src/plugins/discover/public/components/view_mode_toggle/view_mode_toggle.test.tsx @@ -94,6 +94,7 @@ describe('Document view mode toggle component', () => { expect(findTestSubject(component, 'dscViewModeDocumentButton').exists()).toBe(true); expect(findTestSubject(component, 'dscViewModePatternAnalysisButton').exists()).toBe(true); expect(findTestSubject(component, 'dscViewModeFieldStatsButton').exists()).toBe(true); + expect(findTestSubject(component, 'dscViewModeDocumentButton').text()).toBe('Documents (10)'); }); it('should not render if SHOW_FIELD_STATISTICS is false', async () => { @@ -114,6 +115,7 @@ describe('Document view mode toggle component', () => { expect(findTestSubject(component, 'dscViewModeDocumentButton').exists()).toBe(true); expect(findTestSubject(component, 'dscViewModePatternAnalysisButton').exists()).toBe(false); expect(findTestSubject(component, 'dscViewModeFieldStatsButton').exists()).toBe(true); + expect(findTestSubject(component, 'dscViewModeDocumentButton').text()).toBe('Results (10)'); }); it('should set the view mode to VIEW_MODE.DOCUMENT_LEVEL when dscViewModeDocumentButton is clicked', async () => { diff --git a/src/plugins/discover/public/components/view_mode_toggle/view_mode_toggle.tsx b/src/plugins/discover/public/components/view_mode_toggle/view_mode_toggle.tsx index 10a92dc8fefa9..22c4aaa11b43a 100644 --- a/src/plugins/discover/public/components/view_mode_toggle/view_mode_toggle.tsx +++ b/src/plugins/discover/public/components/view_mode_toggle/view_mode_toggle.tsx @@ -130,7 +130,14 @@ export const DocumentViewModeToggle = ({ onClick={() => setDiscoverViewMode(VIEW_MODE.DOCUMENT_LEVEL)} data-test-subj="dscViewModeDocumentButton" > - + {isEsqlMode ? ( + + ) : ( + + )} diff --git a/src/plugins/discover/public/embeddable/components/saved_search_grid.tsx b/src/plugins/discover/public/embeddable/components/saved_search_grid.tsx index e45ad009db898..f6c77dc6cddf5 100644 --- a/src/plugins/discover/public/embeddable/components/saved_search_grid.tsx +++ b/src/plugins/discover/public/embeddable/components/saved_search_grid.tsx @@ -85,10 +85,10 @@ export function DiscoverGridEmbeddable(props: DiscoverGridEmbeddableProps) { getRenderCustomToolbarWithElements({ leftSide: typeof props.totalHitCount === 'number' ? ( - + ) : undefined, }), - [props.totalHitCount] + [props.totalHitCount, props.isPlainRecord] ); const getCellRenderersAccessor = useProfileAccessor('getCellRenderers'); diff --git a/x-pack/plugins/cases/public/components/configure_cases/connectors.test.tsx b/x-pack/plugins/cases/public/components/configure_cases/connectors.test.tsx index 0769e7a29cc59..1dc3346a72da6 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/connectors.test.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/connectors.test.tsx @@ -21,12 +21,20 @@ import { import { ConnectorsDropdown } from './connectors_dropdown'; import { connectors, actionTypes } from './__mock__'; import { ConnectorTypes } from '../../../common/types/domain'; +import userEvent from '@testing-library/user-event'; +import { useApplicationCapabilities } from '../../common/lib/kibana'; + +const useApplicationCapabilitiesMock = useApplicationCapabilities as jest.Mocked< + typeof useApplicationCapabilities +>; +jest.mock('../../common/lib/kibana'); describe('Connectors', () => { let wrapper: ReactWrapper; let appMockRender: AppMockRenderer; const onChangeConnector = jest.fn(); const handleShowEditFlyout = jest.fn(); + const onAddNewConnector = jest.fn(); const props: Props = { actionTypes, @@ -38,6 +46,7 @@ describe('Connectors', () => { onChangeConnector, selectedConnector: { id: 'none', type: ConnectorTypes.none }, updateConnectorDisabled: false, + onAddNewConnector, }; beforeAll(() => { @@ -104,12 +113,16 @@ describe('Connectors', () => { }); it('shows the add connector button', () => { - wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); - wrapper.update(); + appMockRender.render(); - expect( - wrapper.find('button[data-test-subj="dropdown-connector-add-connector"]').exists() - ).toBeTruthy(); + expect(screen.getByTestId('add-new-connector')).toBeInTheDocument(); + }); + + it('shows the add connector flyout when the button is clicked', async () => { + appMockRender.render(); + + await userEvent.click(await screen.findByTestId('add-new-connector')); + expect(onAddNewConnector).toHaveBeenCalled(); }); it('the text of the update button is shown correctly', () => { @@ -156,16 +169,14 @@ describe('Connectors', () => { }); it('shows the actions permission message if the user does not have read access to actions', async () => { - appMockRender.coreStart.application.capabilities = { - ...appMockRender.coreStart.application.capabilities, - actions: { save: false, show: false }, - }; + useApplicationCapabilitiesMock().actions = { crud: false, read: false }; + + appMockRender.render(); - const result = appMockRender.render(); expect( - result.getByTestId('configure-case-connector-permissions-error-msg') + await screen.findByTestId('configure-case-connector-permissions-error-msg') ).toBeInTheDocument(); - expect(result.queryByTestId('case-connectors-dropdown')).toBe(null); + expect(screen.queryByTestId('case-connectors-dropdown')).not.toBeInTheDocument(); }); it('shows the actions permission message if the user does not have access to case connector', async () => { @@ -177,4 +188,12 @@ describe('Connectors', () => { ).toBeInTheDocument(); expect(result.queryByTestId('case-connectors-dropdown')).toBe(null); }); + + it('it should hide the "Add Connector" button when the user lacks the capability to add a new connector', () => { + useApplicationCapabilitiesMock().actions = { crud: false, read: true }; + + appMockRender.render(); + + expect(screen.queryByTestId('add-new-connector')).not.toBeInTheDocument(); + }); }); diff --git a/x-pack/plugins/cases/public/components/configure_cases/connectors.tsx b/x-pack/plugins/cases/public/components/configure_cases/connectors.tsx index b1ab16109c28f..3d742a202a0b7 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/connectors.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/connectors.tsx @@ -13,10 +13,9 @@ import { EuiFlexItem, EuiLink, EuiText, + EuiButtonEmpty, } from '@elastic/eui'; -import { css } from '@emotion/react'; - import { ConnectorsDropdown } from './connectors_dropdown'; import * as i18n from './translations'; @@ -39,6 +38,7 @@ export interface Props { onChangeConnector: (id: string) => void; selectedConnector: { id: string; type: ConnectorTypes }; updateConnectorDisabled: boolean; + onAddNewConnector: () => void; } const ConnectorsComponent: React.FC = ({ actionTypes, @@ -50,8 +50,10 @@ const ConnectorsComponent: React.FC = ({ onChangeConnector, selectedConnector, updateConnectorDisabled, + onAddNewConnector, }) => { const { actions } = useApplicationCapabilities(); + const canSave = actions.crud; const connector = useMemo( () => connectors.find((c) => c.id === selectedConnector.id), [connectors, selectedConnector.id] @@ -95,13 +97,19 @@ const ConnectorsComponent: React.FC = ({ > + {i18n.ADD_CONNECTOR} + + ) : null + } > @@ -113,7 +121,6 @@ const ConnectorsComponent: React.FC = ({ isLoading={isLoading} onChange={onChangeConnector} data-test-subj="case-connectors-dropdown" - appendAddConnectorButton={true} /> ) : ( diff --git a/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.test.tsx b/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.test.tsx index faabf3f42c70f..30c45453ebc17 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.test.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.test.tsx @@ -15,13 +15,6 @@ import type { Props } from './connectors_dropdown'; import { ConnectorsDropdown } from './connectors_dropdown'; import { TestProviders } from '../../common/mock'; import { connectors } from './__mock__'; -import userEvent from '@testing-library/user-event'; -import { useApplicationCapabilities } from '../../common/lib/kibana'; - -const useApplicationCapabilitiesMock = useApplicationCapabilities as jest.Mocked< - typeof useApplicationCapabilities ->; -jest.mock('../../common/lib/kibana'); describe('ConnectorsDropdown', () => { let wrapper: ReactWrapper; @@ -388,23 +381,4 @@ describe('ConnectorsDropdown', () => { ); expect(tooltips[0]).toBeInTheDocument(); }); - - test('it should hide the "Add New Connector" button when the user lacks the capability to add a new connector', async () => { - const selectedConnector = 'none'; - useApplicationCapabilitiesMock().actions = { crud: false, read: true }; - render( - {}} - />, - { wrapper: ({ children }) => {children} } - ); - - await userEvent.click(screen.getByTestId('dropdown-connectors')); - expect(screen.queryByTestId('dropdown-connector-add-connector')).not.toBeInTheDocument(); - }); }); diff --git a/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.tsx b/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.tsx index 71df212399bc2..04fa9e3ef3647 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.tsx @@ -6,7 +6,6 @@ */ import React, { Suspense, useMemo } from 'react'; -import type { EuiThemeComputed } from '@elastic/eui'; import { EuiFlexGroup, EuiFlexItem, @@ -20,7 +19,7 @@ import { css } from '@emotion/react'; import type { ActionConnector } from '../../containers/configure/types'; import * as i18n from './translations'; -import { useApplicationCapabilities, useKibana } from '../../common/lib/kibana'; +import { useKibana } from '../../common/lib/kibana'; import { getConnectorIcon, isDeprecatedConnector } from '../utils'; export interface Props { @@ -29,7 +28,6 @@ export interface Props { isLoading: boolean; onChange: (id: string) => void; selectedConnector: string; - appendAddConnectorButton?: boolean; } const suspendedComponentWithProps = (ComponentToSuspend: React.ComponentType) => { @@ -65,37 +63,14 @@ const noConnectorOption = { 'data-test-subj': 'dropdown-connector-no-connector', }; -const addNewConnector = (euiTheme: EuiThemeComputed<{}>) => ({ - value: 'add-connector', - inputDisplay: ( - - {i18n.ADD_NEW_CONNECTOR} - - ), - 'data-test-subj': 'dropdown-connector-add-connector', -}); - const ConnectorsDropdownComponent: React.FC = ({ connectors, disabled, isLoading, onChange, selectedConnector, - appendAddConnectorButton = false, }) => { const { triggersActionsUi } = useKibana().services; - const { actions } = useApplicationCapabilities(); - const canSave = actions.crud; const { euiTheme } = useEuiTheme(); const connectorsAsOptions = useMemo(() => { const connectorsFormatted = connectors.reduce( @@ -152,10 +127,6 @@ const ConnectorsDropdownComponent: React.FC = ({ [noConnectorOption] ); - if (appendAddConnectorButton && canSave) { - return [...connectorsFormatted, addNewConnector(euiTheme)]; - } - return connectorsFormatted; // eslint-disable-next-line react-hooks/exhaustive-deps }, [connectors]); diff --git a/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx b/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx index 6c65eae41c78b..e058d982e7367 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx @@ -565,8 +565,7 @@ describe('ConfigureCases', () => { wrappingComponent: TestProviders as ComponentType>, }); - wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); - wrapper.find('button[data-test-subj="dropdown-connector-add-connector"]').simulate('click'); + wrapper.find('button[data-test-subj="add-new-connector"]').simulate('click'); await waitFor(() => { wrapper.update(); diff --git a/x-pack/plugins/cases/public/components/configure_cases/index.tsx b/x-pack/plugins/cases/public/components/configure_cases/index.tsx index 61f99a46a0b08..641482ceca4fe 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/index.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/index.tsx @@ -215,6 +215,10 @@ export const ConfigureCases: React.FC = React.memo(() => { [] ); + const onAddNewConnector = useCallback(() => { + setFlyOutVisibility({ type: 'addConnector', visible: true }); + }, []); + const onChangeConnector = useCallback( (id: string) => { if (id === 'add-connector') { @@ -577,6 +581,7 @@ export const ConfigureCases: React.FC = React.memo(() => { onChangeConnector={onChangeConnector} selectedConnector={connector} updateConnectorDisabled={updateConnectorDisabled || !permissions.update} + onAddNewConnector={onAddNewConnector} /> diff --git a/x-pack/plugins/cases/public/components/configure_cases/translations.ts b/x-pack/plugins/cases/public/components/configure_cases/translations.ts index 7a2e0e84b0306..4fe462655dcc1 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/translations.ts +++ b/x-pack/plugins/cases/public/components/configure_cases/translations.ts @@ -35,6 +35,10 @@ export const ADD_NEW_CONNECTOR = i18n.translate('xpack.cases.configureCases.addN defaultMessage: 'Add new connector', }); +export const ADD_CONNECTOR = i18n.translate('xpack.cases.configureCases.addConnector', { + defaultMessage: 'Add connector', +}); + export const CASE_CLOSURE_OPTIONS_TITLE = i18n.translate( 'xpack.cases.configureCases.caseClosureOptionsTitle', { diff --git a/x-pack/plugins/cases/public/components/connector_selector/form.tsx b/x-pack/plugins/cases/public/components/connector_selector/form.tsx index fa991bc5b9871..2419aa60b148f 100644 --- a/x-pack/plugins/cases/public/components/connector_selector/form.tsx +++ b/x-pack/plugins/cases/public/components/connector_selector/form.tsx @@ -12,9 +12,17 @@ import { css } from '@emotion/react'; import type { FieldHook } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { getFieldValidityAndErrorMessage } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { i18n } from '@kbn/i18n'; import type { ActionConnector } from '../../../common/types/domain'; import { ConnectorsDropdown } from '../configure_cases/connectors_dropdown'; +const ADD_CONNECTOR_HELPER_TEXT = i18n.translate( + 'xpack.cases.connectorSelector.addConnectorHelperText', + { + defaultMessage: 'Go to Cases > Settings to add an external incident management system', + } +); + interface ConnectorSelectorProps { connectors: ActionConnector[]; dataTestSubj: string; @@ -60,7 +68,7 @@ export const ConnectorSelector = ({ describedByIds={idAria ? [idAria] : undefined} error={errorMessage} fullWidth - helpText={field.helpText} + helpText={ADD_CONNECTOR_HELPER_TEXT} isInvalid={isInvalid} label={field.label} labelAppend={field.labelAppend} diff --git a/x-pack/plugins/cases/server/telemetry/queries/alerts.test.ts b/x-pack/plugins/cases/server/telemetry/queries/alerts.test.ts index 11636b50ebd4e..fd00aea939dc8 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/alerts.test.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/alerts.test.ts @@ -17,19 +17,20 @@ describe('alerts', () => { const telemetrySavedObjectsClient = new TelemetrySavedObjectsClient(savedObjectsClient); savedObjectsClient.find.mockResolvedValue({ - total: 5, + total: 3, saved_objects: [], per_page: 1, page: 1, aggregations: { counts: { buckets: [ - { doc_count: 1, key: 1 }, - { doc_count: 2, key: 2 }, - { doc_count: 3, key: 3 }, + { topAlertsPerBucket: { value: 12 } }, + { topAlertsPerBucket: { value: 5 } }, + { topAlertsPerBucket: { value: 3 } }, ], }, references: { cases: { max: { value: 1 } } }, + uniqueAlertCommentsCount: { value: 5 }, }, }); @@ -42,12 +43,13 @@ describe('alerts', () => { savedObjectsClient: telemetrySavedObjectsClient, logger, }); + expect(res).toEqual({ all: { total: 5, daily: 3, - weekly: 2, - monthly: 1, + weekly: 5, + monthly: 12, maxOnACase: 1, }, }); @@ -76,6 +78,13 @@ describe('alerts', () => { }, ], }, + aggregations: { + topAlertsPerBucket: { + cardinality: { + field: 'cases-comments.attributes.alertId', + }, + }, + }, }, references: { aggregations: { @@ -85,10 +94,22 @@ describe('alerts', () => { terms: { field: 'cases-comments.references.id', }, + aggregations: { + reverse: { + reverse_nested: {}, + aggregations: { + topAlerts: { + cardinality: { + field: 'cases-comments.attributes.alertId', + }, + }, + }, + }, + }, }, max: { max_bucket: { - buckets_path: 'ids._count', + buckets_path: 'ids>reverse.topAlerts', }, }, }, @@ -103,6 +124,11 @@ describe('alerts', () => { path: 'cases-comments.references', }, }, + uniqueAlertCommentsCount: { + cardinality: { + field: 'cases-comments.attributes.alertId', + }, + }, }, filter: { arguments: [ diff --git a/x-pack/plugins/cases/server/telemetry/queries/alerts.ts b/x-pack/plugins/cases/server/telemetry/queries/alerts.ts index 96aaec211acb8..88a9c25c88c3d 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/alerts.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/alerts.ts @@ -5,17 +5,14 @@ * 2.0. */ -import { CASE_COMMENT_SAVED_OBJECT } from '../../../common/constants'; import type { CasesTelemetry, CollectTelemetryDataParams } from '../types'; -import { getCountsAndMaxData, getOnlyAlertsCommentsFilter } from './utils'; +import { getCountsAndMaxAlertsData } from './utils'; export const getAlertsTelemetryData = async ({ savedObjectsClient, }: CollectTelemetryDataParams): Promise => { - const res = await getCountsAndMaxData({ + const res = await getCountsAndMaxAlertsData({ savedObjectsClient, - savedObjectType: CASE_COMMENT_SAVED_OBJECT, - filter: getOnlyAlertsCommentsFilter(), }); return res; diff --git a/x-pack/plugins/cases/server/telemetry/queries/utils.test.ts b/x-pack/plugins/cases/server/telemetry/queries/utils.test.ts index 6c66c5aab81c7..b4b18f231eb6a 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/utils.test.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/utils.test.ts @@ -16,10 +16,12 @@ import type { import { findValueInBuckets, getAggregationsBuckets, + getAlertsCountsFromBuckets, getAttachmentsFrameworkStats, getBucketFromAggregation, getConnectorsCardinalityAggregationQuery, getCountsAggregationQuery, + getCountsAndMaxAlertsData, getCountsAndMaxData, getCountsFromBuckets, getCustomFieldsTelemetry, @@ -28,6 +30,7 @@ import { getOnlyConnectorsFilter, getReferencesAggregationQuery, getSolutionValues, + getUniqueAlertCommentsCountQuery, } from './utils'; import { TelemetrySavedObjectsClient } from '../telemetry_saved_objects_client'; @@ -994,6 +997,63 @@ describe('utils', () => { }); }); + describe('getAlertsCountsFromBuckets', () => { + it('returns the correct counts', () => { + const buckets = [ + { topAlertsPerBucket: { value: 12 } }, + { topAlertsPerBucket: { value: 5 } }, + { topAlertsPerBucket: { value: 3 } }, + ]; + + expect(getAlertsCountsFromBuckets(buckets)).toEqual({ + daily: 3, + weekly: 5, + monthly: 12, + }); + }); + + it('returns zero counts when the bucket does not have the topAlertsPerBucket field', () => { + const buckets = [{}]; + // @ts-expect-error + expect(getAlertsCountsFromBuckets(buckets)).toEqual({ + daily: 0, + weekly: 0, + monthly: 0, + }); + }); + + it('returns zero counts when the bucket is undefined', () => { + // @ts-expect-error + expect(getAlertsCountsFromBuckets(undefined)).toEqual({ + daily: 0, + weekly: 0, + monthly: 0, + }); + }); + + it('returns zero counts when the topAlertsPerBucket field is missing in some buckets', () => { + const buckets = [{ doc_count: 1, key: 1, topAlertsPerBucket: { value: 5 } }, {}, {}]; + // @ts-expect-error + expect(getAlertsCountsFromBuckets(buckets)).toEqual({ + daily: 0, + weekly: 0, + monthly: 5, + }); + }); + }); + + describe('getUniqueAlertCommentsCountQuery', () => { + it('returns the correct query', () => { + expect(getUniqueAlertCommentsCountQuery()).toEqual({ + uniqueAlertCommentsCount: { + cardinality: { + field: 'cases-comments.attributes.alertId', + }, + }, + }); + }); + }); + describe('getCountsAndMaxData', () => { const savedObjectsClient = savedObjectsRepositoryMock.create(); savedObjectsClient.find.mockResolvedValue({ @@ -1125,6 +1185,174 @@ describe('utils', () => { }); }); + describe('getCountsAndMaxAlertsData', () => { + const savedObjectsClient = savedObjectsRepositoryMock.create(); + savedObjectsClient.find.mockResolvedValue({ + total: 3, + saved_objects: [], + per_page: 1, + page: 1, + aggregations: { + counts: { + buckets: [ + { doc_count: 1, key: 1, topAlertsPerBucket: { value: 5 } }, + { doc_count: 2, key: 2, topAlertsPerBucket: { value: 3 } }, + { doc_count: 3, key: 3, topAlertsPerBucket: { value: 1 } }, + ], + }, + references: { cases: { max: { value: 1 } } }, + uniqueAlertCommentsCount: { value: 5 }, + }, + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('returns the correct counts and max data', async () => { + const telemetrySavedObjectsClient = new TelemetrySavedObjectsClient(savedObjectsClient); + + const res = await getCountsAndMaxAlertsData({ + savedObjectsClient: telemetrySavedObjectsClient, + }); + expect(res).toEqual({ + all: { + total: 5, + daily: 1, + weekly: 3, + monthly: 5, + maxOnACase: 1, + }, + }); + }); + + it('returns zero data if the response aggregation is not as expected', async () => { + const telemetrySavedObjectsClient = new TelemetrySavedObjectsClient(savedObjectsClient); + savedObjectsClient.find.mockResolvedValue({ + total: 5, + saved_objects: [], + per_page: 1, + page: 1, + }); + + const res = await getCountsAndMaxAlertsData({ + savedObjectsClient: telemetrySavedObjectsClient, + }); + expect(res).toEqual({ + all: { + total: 0, + daily: 0, + weekly: 0, + monthly: 0, + maxOnACase: 0, + }, + }); + }); + + it('should call find with correct arguments', async () => { + const telemetrySavedObjectsClient = new TelemetrySavedObjectsClient(savedObjectsClient); + + await getCountsAndMaxAlertsData({ + savedObjectsClient: telemetrySavedObjectsClient, + }); + + expect(savedObjectsClient.find).toBeCalledWith({ + aggs: { + counts: { + date_range: { + field: 'cases-comments.attributes.created_at', + format: 'dd/MM/YYYY', + ranges: [ + { + from: 'now-1d', + to: 'now', + }, + { + from: 'now-1w', + to: 'now', + }, + { + from: 'now-1M', + to: 'now', + }, + ], + }, + aggregations: { + topAlertsPerBucket: { + cardinality: { + field: 'cases-comments.attributes.alertId', + }, + }, + }, + }, + references: { + aggregations: { + cases: { + aggregations: { + ids: { + terms: { + field: 'cases-comments.references.id', + }, + aggregations: { + reverse: { + reverse_nested: {}, + aggregations: { + topAlerts: { + cardinality: { + field: 'cases-comments.attributes.alertId', + }, + }, + }, + }, + }, + }, + max: { + max_bucket: { + buckets_path: 'ids>reverse.topAlerts', + }, + }, + }, + filter: { + term: { + 'cases-comments.references.type': 'cases', + }, + }, + }, + }, + nested: { + path: 'cases-comments.references', + }, + }, + uniqueAlertCommentsCount: { + cardinality: { + field: 'cases-comments.attributes.alertId', + }, + }, + }, + filter: { + arguments: [ + { + isQuoted: false, + type: 'literal', + value: 'cases-comments.attributes.type', + }, + { + isQuoted: false, + type: 'literal', + value: 'alert', + }, + ], + function: 'is', + type: 'function', + }, + page: 0, + perPage: 0, + type: 'cases-comments', + namespaces: ['*'], + }); + }); + }); + describe('getBucketFromAggregation', () => { it('returns the buckets', () => { expect( diff --git a/x-pack/plugins/cases/server/telemetry/queries/utils.ts b/x-pack/plugins/cases/server/telemetry/queries/utils.ts index 65b81e3362300..6992ed8f7ac06 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/utils.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/utils.ts @@ -27,6 +27,7 @@ import type { FileAttachmentAggsResult, AttachmentFrameworkAggsResult, CustomFieldsTelemetry, + AlertBuckets, } from '../types'; import { buildFilter } from '../../client/utils'; import type { Owner } from '../../../common/constants/types'; @@ -47,6 +48,27 @@ export const getCountsAggregationQuery = (savedObjectType: string) => ({ }, }); +export const getAlertsCountsAggregationQuery = () => ({ + counts: { + date_range: { + field: `${CASE_COMMENT_SAVED_OBJECT}.attributes.created_at`, + format: 'dd/MM/YYYY', + ranges: [ + { from: 'now-1d', to: 'now' }, + { from: 'now-1w', to: 'now' }, + { from: 'now-1M', to: 'now' }, + ], + }, + aggregations: { + topAlertsPerBucket: { + cardinality: { + field: `${CASE_COMMENT_SAVED_OBJECT}.attributes.alertId`, + }, + }, + }, + }, +}); + export const getMaxBucketOnCaseAggregationQuery = (savedObjectType: string) => ({ references: { nested: { @@ -76,6 +98,55 @@ export const getMaxBucketOnCaseAggregationQuery = (savedObjectType: string) => ( }, }); +export const getAlertsMaxBucketOnCaseAggregationQuery = () => ({ + references: { + nested: { + path: `${CASE_COMMENT_SAVED_OBJECT}.references`, + }, + aggregations: { + cases: { + filter: { + term: { + [`${CASE_COMMENT_SAVED_OBJECT}.references.type`]: CASE_SAVED_OBJECT, + }, + }, + aggregations: { + ids: { + terms: { + field: `${CASE_COMMENT_SAVED_OBJECT}.references.id`, + }, + aggregations: { + reverse: { + reverse_nested: {}, + aggregations: { + topAlerts: { + cardinality: { + field: `${CASE_COMMENT_SAVED_OBJECT}.attributes.alertId`, + }, + }, + }, + }, + }, + }, + max: { + max_bucket: { + buckets_path: 'ids>reverse.topAlerts', + }, + }, + }, + }, + }, + }, +}); + +export const getUniqueAlertCommentsCountQuery = () => ({ + uniqueAlertCommentsCount: { + cardinality: { + field: `${CASE_COMMENT_SAVED_OBJECT}.attributes.alertId`, + }, + }, +}); + export const getReferencesAggregationQuery = ({ savedObjectType, referenceType, @@ -121,6 +192,52 @@ export const getCountsFromBuckets = (buckets: Buckets['buckets']) => ({ monthly: buckets?.[0]?.doc_count ?? 0, }); +export const getAlertsCountsFromBuckets = (buckets: AlertBuckets['buckets']) => ({ + daily: buckets?.[2]?.topAlertsPerBucket?.value ?? 0, + weekly: buckets?.[1]?.topAlertsPerBucket?.value ?? 0, + monthly: buckets?.[0]?.topAlertsPerBucket?.value ?? 0, +}); + +export const getCountsAndMaxAlertsData = async ({ + savedObjectsClient, +}: { + savedObjectsClient: TelemetrySavedObjectsClient; +}) => { + const filter = getOnlyAlertsCommentsFilter(); + + const res = await savedObjectsClient.find< + unknown, + { + counts: AlertBuckets; + references: MaxBucketOnCaseAggregation['references']; + uniqueAlertCommentsCount: { value: number }; + } + >({ + page: 0, + perPage: 0, + filter, + type: CASE_COMMENT_SAVED_OBJECT, + namespaces: ['*'], + aggs: { + ...getAlertsCountsAggregationQuery(), + ...getAlertsMaxBucketOnCaseAggregationQuery(), + ...getUniqueAlertCommentsCountQuery(), + }, + }); + + const countsBuckets = res.aggregations?.counts?.buckets ?? []; + const totalAlerts = res.aggregations?.uniqueAlertCommentsCount.value ?? 0; + const maxOnACase = res.aggregations?.references?.cases?.max?.value ?? 0; + + return { + all: { + total: totalAlerts, + ...getAlertsCountsFromBuckets(countsBuckets), + maxOnACase, + }, + }; +}; + export const getCountsAndMaxData = async ({ savedObjectsClient, savedObjectType, @@ -132,7 +249,10 @@ export const getCountsAndMaxData = async ({ }) => { const res = await savedObjectsClient.find< unknown, - { counts: Buckets; references: MaxBucketOnCaseAggregation['references'] } + { + counts: Buckets; + references: MaxBucketOnCaseAggregation['references']; + } >({ page: 0, perPage: 0, diff --git a/x-pack/plugins/cases/server/telemetry/types.ts b/x-pack/plugins/cases/server/telemetry/types.ts index b4996da27f234..228aa0c7ae397 100644 --- a/x-pack/plugins/cases/server/telemetry/types.ts +++ b/x-pack/plugins/cases/server/telemetry/types.ts @@ -17,6 +17,10 @@ export interface Bucket { key: T; } +export interface AlertBuckets { + buckets: Array<{ topAlertsPerBucket: { value: number } }>; +} + export interface Buckets { buckets: Array>; } diff --git a/x-pack/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.test.tsx b/x-pack/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.test.tsx index d81737fea1ec2..9f5f2ec85d021 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.test.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.test.tsx @@ -45,6 +45,7 @@ describe('AccountsEvaluatedWidget', () => { expect(mockNavToFindings).toHaveBeenCalledWith( { 'cloud.provider': 'aws', + 'rule.benchmark.posture_type': 'cspm', }, ['cloud.account.name'] ); diff --git a/x-pack/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.tsx b/x-pack/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.tsx index 1d4f26274690d..5ae8a47a93e71 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/accounts_evaluated_widget.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem, useEuiTheme } from '@elastic/eui'; import { css } from '@emotion/react'; import { useNavigateFindings } from '@kbn/cloud-security-posture/src/hooks/use_navigate_findings'; +import { CSPM_POLICY_TEMPLATE } from '@kbn/cloud-security-posture-common'; import { CLOUD_PROVIDERS, getBenchmarkApplicableTo } from '../../common/utils/helpers'; import { CIS_AWS, CIS_GCP, CIS_AZURE, CIS_K8S, CIS_EKS } from '../../common/constants'; import { CISBenchmarkIcon } from './cis_benchmark_icon'; @@ -61,7 +62,10 @@ export const AccountsEvaluatedWidget = ({ const navToFindings = useNavigateFindings(); const navToFindingsByCloudProvider = (provider: string) => { - navToFindings({ 'cloud.provider': provider }, [FINDINGS_GROUPING_OPTIONS.CLOUD_ACCOUNT_NAME]); + navToFindings( + { 'cloud.provider': provider, 'rule.benchmark.posture_type': CSPM_POLICY_TEMPLATE }, + [FINDINGS_GROUPING_OPTIONS.CLOUD_ACCOUNT_NAME] + ); }; const navToFindingsByCisBenchmark = (cisBenchmark: string) => { diff --git a/x-pack/plugins/observability_solution/observability_onboarding/public/application/onboarding_flow_form/onboarding_flow_form.tsx b/x-pack/plugins/observability_solution/observability_onboarding/public/application/onboarding_flow_form/onboarding_flow_form.tsx index 9a46cf885b285..eb5ee3bc92369 100644 --- a/x-pack/plugins/observability_solution/observability_onboarding/public/application/onboarding_flow_form/onboarding_flow_form.tsx +++ b/x-pack/plugins/observability_solution/observability_onboarding/public/application/onboarding_flow_form/onboarding_flow_form.tsx @@ -316,33 +316,34 @@ export const OnboardingFlowForm: FunctionComponent = () => { flowCategory={searchParams.get('category')} /> -
- - - - - - - - card.type === 'virtual' && !card.isCollectionCard - ) - .concat(virtualSearchResults)} - excludePackageIdList={searchExcludePackageIdList} - joinCardLists - /> -
+ + +
+ + + + + + + + card.type === 'virtual' && !card.isCollectionCard + ) + .concat(virtualSearchResults)} + excludePackageIdList={searchExcludePackageIdList} + joinCardLists + />
); diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/artifacts/blocklist.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/artifacts/blocklist.cy.ts index 32dad9b0bbc0d..f0d3eb96e4581 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/artifacts/blocklist.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/artifacts/blocklist.cy.ts @@ -41,7 +41,7 @@ const { describe( 'Blocklist', { - tags: ['@ess', '@serverless', '@skipInServerlessMKI'], // @skipInServerlessMKI until kibana is rebuilt after merge + tags: ['@ess', '@serverless'], }, () => { let indexedPolicy: IndexedFleetEndpointPolicyResponse; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/get_alert_types.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/find_alerting_rules.sh similarity index 59% rename from x-pack/plugins/security_solution/server/lib/detection_engine/scripts/get_alert_types.sh rename to x-pack/plugins/security_solution/server/lib/detection_engine/scripts/find_alerting_rules.sh index 9b51c289ac2c3..c735dd333710c 100755 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/get_alert_types.sh +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/find_alerting_rules.sh @@ -10,9 +10,10 @@ set -e ./check_env_variables.sh -# Example: ./get_alert_types.sh -# https://github.com/elastic/kibana/blob/main/x-pack/plugins/alerting/README.md#get-apialerttypes-list-alert-types +# Example: ./find_alerting_rules.sh +# https://www.elastic.co/docs/api/doc/kibana/v8/operation/operation-findrules +# Related: use ./find_rules.sh to retrieve Detection Engine (Security) rules curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X GET ${KIBANA_URL}${SPACE_URL}/api/alerts/list_alert_types \ + -X GET ${KIBANA_URL}${SPACE_URL}/api/alerting/rules/_find \ | jq . diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/find_rules.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/find_rules.sh index ef8244ad6e200..422f3e2bb0545 100755 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/find_rules.sh +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/find_rules.sh @@ -12,5 +12,6 @@ set -e # Example: ./find_rules.sh curl -s -k \ + -H 'elastic-api-version: 2023-10-31' \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ -X GET ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_find | jq . diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/get_alert_instances.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/get_alerting_rule_types.sh similarity index 65% rename from x-pack/plugins/security_solution/server/lib/detection_engine/scripts/get_alert_instances.sh rename to x-pack/plugins/security_solution/server/lib/detection_engine/scripts/get_alerting_rule_types.sh index f2ba9bb70a7c6..59c960d67ba4d 100755 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/get_alert_instances.sh +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/get_alerting_rule_types.sh @@ -10,9 +10,9 @@ set -e ./check_env_variables.sh -# Example: ./get_alert_instances.sh -# https://github.com/elastic/kibana/blob/main/x-pack/plugins/alerting/README.md#get-apialert_find-find-alerts +# Example: ./get_rule_types.sh +# https://www.elastic.co/docs/api/doc/kibana/v8/operation/operation-getruletypes curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X GET ${KIBANA_URL}${SPACE_URL}/api/alerts/_find \ + -X GET ${KIBANA_URL}${SPACE_URL}/api/alerting/rule_types \ | jq . diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index e001f78227348..ab75f1543cc33 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -2423,7 +2423,6 @@ "discover.docTable.tableRow.toggleRowDetailsButtonAriaLabel": "Afficher/Masquer les détails de la ligne", "discover.docTable.tableRow.viewSingleDocumentLinkText": "Afficher un seul document", "discover.docTable.tableRow.viewSurroundingDocumentsLinkText": "Afficher les documents alentour", - "discover.docTable.totalDocuments": "{totalDocuments} documents", "discover.documentsAriaLabel": "Documents", "discover.docViews.table.scoreSortWarningTooltip": "Filtrez sur _score pour pouvoir récupérer les valeurs correspondantes.", "discover.dropZoneTableLabel": "Abandonner la zone pour ajouter un champ en tant que colonne dans la table", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 1bf00f7e0677e..a77f0348b572f 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2422,7 +2422,6 @@ "discover.docTable.tableRow.toggleRowDetailsButtonAriaLabel": "行の詳細を切り替える", "discover.docTable.tableRow.viewSingleDocumentLinkText": "単一のドキュメントを表示", "discover.docTable.tableRow.viewSurroundingDocumentsLinkText": "周りのドキュメントを表示", - "discover.docTable.totalDocuments": "{totalDocuments}ドキュメント", "discover.documentsAriaLabel": "ドキュメント", "discover.docViews.table.scoreSortWarningTooltip": "_scoreの値を取得するには、並べ替える必要があります。", "discover.dropZoneTableLabel": "フィールドを列として表に追加するには、ゾーンをドロップします", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 41dc4a29fdf3e..2b13eacdad26e 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2424,7 +2424,6 @@ "discover.docTable.tableRow.toggleRowDetailsButtonAriaLabel": "切换行详细信息", "discover.docTable.tableRow.viewSingleDocumentLinkText": "查看单个文档", "discover.docTable.tableRow.viewSurroundingDocumentsLinkText": "查看周围文档", - "discover.docTable.totalDocuments": "{totalDocuments} 个文档", "discover.documentsAriaLabel": "文档", "discover.docViews.table.scoreSortWarningTooltip": "要检索 _score 的值,必须按其筛选。", "discover.dropZoneTableLabel": "放置区域以将字段作为列添加到表中", diff --git a/x-pack/test/api_integration/apis/slos/config.ts b/x-pack/test/api_integration/apis/slos/config.ts deleted file mode 100644 index c755e2a46882d..0000000000000 --- a/x-pack/test/api_integration/apis/slos/config.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FtrConfigProviderContext } from '@kbn/test'; - -export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const baseIntegrationTestsConfig = await readConfigFile(require.resolve('../../config.ts')); - - return { - ...baseIntegrationTestsConfig.getAll(), - testFiles: [require.resolve('.')], - // overriding default timeouts from packages/kbn-test/src/functional_test_runner/lib/config/schema.ts - // so we can easily adjust them for serverless where needed - timeouts: { - find: 10 * 1000, - try: 120 * 1000, - waitFor: 20 * 1000, - esRequestTimeout: 30 * 1000, - kibanaReportCompletion: 60 * 1000, - kibanaStabilize: 15 * 1000, - navigateStatusPageCheck: 250, - waitForExists: 2500, - }, - }; -} diff --git a/x-pack/test/api_integration/apis/slos/create_slo.ts b/x-pack/test/api_integration/apis/slos/create_slo.ts deleted file mode 100644 index 71ce8434a61f1..0000000000000 --- a/x-pack/test/api_integration/apis/slos/create_slo.ts +++ /dev/null @@ -1,488 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import expect from '@kbn/expect'; -import { cleanup } from '@kbn/infra-forge'; -import type { CreateSLOInput } from '@kbn/slo-schema'; -import { SO_SLO_TYPE } from '@kbn/slo-plugin/server/saved_objects'; - -import { FtrProviderContext } from '../../ftr_provider_context'; -import { sloData } from './fixtures/create_slo'; -import { loadTestData } from './helper/load_test_data'; -import { SloEsClient } from './helper/es'; - -export default function ({ getService }: FtrProviderContext) { - describe('Create SLOs', function () { - this.tags('skipCloud'); - - const supertestAPI = getService('supertest'); - const kibanaServer = getService('kibanaServer'); - const esClient = getService('es'); - const slo = getService('slo'); - const retry = getService('retry'); - const logger = getService('log'); - const sloEsClient = new SloEsClient(esClient); - - let createSLOInput: CreateSLOInput; - - before(async () => { - await slo.createUser(); - await loadTestData(getService); - await slo.deleteAllSLOs(); - }); - - beforeEach(() => { - createSLOInput = sloData; - }); - - afterEach(async () => { - await slo.deleteAllSLOs(); - }); - - after(async () => { - await cleanup({ esClient, logger }); - await sloEsClient.deleteTestSourceData(); - }); - - it('creates a new slo and transforms', async () => { - const apiResponse = await slo.create(createSLOInput); - - expect(apiResponse.body).property('id'); - - const { id } = apiResponse.body; - - const savedObject = await kibanaServer.savedObjects.find({ - type: SO_SLO_TYPE, - }); - - expect(savedObject.saved_objects.length).eql(1); - - expect(savedObject.saved_objects[0].attributes).eql({ - budgetingMethod: 'occurrences', - updatedAt: savedObject.saved_objects[0].attributes.updatedAt, - createdAt: savedObject.saved_objects[0].attributes.createdAt, - description: 'Fixture for api integration tests', - enabled: true, - groupBy: 'tags', - id, - indicator: { - params: { - filter: 'system.network.name: eth1', - good: 'container.cpu.user.pct < 1', - index: 'kbn-data-forge*', - timestampField: '@timestamp', - total: 'container.cpu.user.pct: *', - }, - type: 'sli.kql.custom', - }, - name: 'Test SLO for api integration', - objective: { - target: 0.99, - }, - revision: 1, - settings: { - frequency: '1m', - syncDelay: '1m', - preventInitialBackfill: false, - }, - tags: ['test'], - timeWindow: { - duration: '7d', - type: 'rolling', - }, - version: 2, - }); - - const rollUpTransformResponse = await supertestAPI - .get(`/internal/transform/transforms/slo-${id}-1`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - - // expect roll up transform to be created - expect(rollUpTransformResponse.body).eql({ - count: 1, - transforms: [ - { - id: `slo-${id}-1`, - authorization: { roles: ['slo_editor', 'editor'] }, - version: '10.0.0', - create_time: rollUpTransformResponse.body.transforms[0].create_time, - source: { - index: ['kbn-data-forge*'], - query: { - bool: { - filter: [ - { range: { '@timestamp': { gte: 'now-7d/d' } } }, - { - bool: { - should: [ - { - match: { - 'system.network.name': 'eth1', - }, - }, - ], - minimum_should_match: 1, - }, - }, - { - exists: { - field: 'tags', - }, - }, - ], - }, - }, - }, - dest: { - index: '.slo-observability.sli-v3.3', - pipeline: `.slo-observability.sli.pipeline-${id}-1`, - }, - frequency: '1m', - sync: { time: { field: '@timestamp', delay: '1m' } }, - pivot: { - group_by: { - 'slo.groupings.tags': { terms: { field: 'tags' } }, - '@timestamp': { date_histogram: { field: '@timestamp', fixed_interval: '1m' } }, - }, - aggregations: { - 'slo.numerator': { - filter: { - bool: { - should: [{ range: { 'container.cpu.user.pct': { lt: '1' } } }], - minimum_should_match: 1, - }, - }, - }, - 'slo.denominator': { - filter: { - bool: { - should: [{ exists: { field: 'container.cpu.user.pct' } }], - minimum_should_match: 1, - }, - }, - }, - }, - }, - description: `Rolled-up SLI data for SLO: Test SLO for api integration [id: ${id}, revision: 1]`, - settings: { deduce_mappings: false, unattended: true }, - _meta: { version: 3.3, managed: true, managed_by: 'observability' }, - }, - ], - }); - - const summaryTransform = await supertestAPI - .get(`/internal/transform/transforms/slo-summary-${id}-1`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - - // expect summary transform to be created - expect(summaryTransform.body).eql({ - count: 1, - transforms: [ - { - id: `slo-summary-${id}-1`, - authorization: { roles: ['slo_editor', 'editor'] }, - version: '10.0.0', - create_time: summaryTransform.body.transforms[0].create_time, - source: { - index: ['.slo-observability.sli-v3.3*'], - query: { - bool: { - filter: [ - { range: { '@timestamp': { gte: 'now-7d/m', lte: 'now/m' } } }, - { term: { 'slo.id': id } }, - { term: { 'slo.revision': 1 } }, - ], - }, - }, - }, - dest: { - index: '.slo-observability.summary-v3.3', - pipeline: `.slo-observability.summary.pipeline-${id}-1`, - }, - frequency: '1m', - sync: { time: { field: 'event.ingested', delay: '65s' } }, - pivot: { - group_by: { - 'slo.id': { terms: { field: 'slo.id' } }, - 'slo.instanceId': { terms: { field: 'slo.instanceId' } }, - 'slo.revision': { terms: { field: 'slo.revision' } }, - 'slo.groupings.tags': { - terms: { field: 'slo.groupings.tags' }, - }, - 'monitor.config_id': { - terms: { - field: 'monitor.config_id', - missing_bucket: true, - }, - }, - 'monitor.name': { - terms: { - field: 'monitor.name', - missing_bucket: true, - }, - }, - 'observer.geo.name': { - terms: { - field: 'observer.geo.name', - missing_bucket: true, - }, - }, - 'observer.name': { - terms: { - field: 'observer.name', - missing_bucket: true, - }, - }, - 'service.name': { terms: { field: 'service.name', missing_bucket: true } }, - 'service.environment': { - terms: { field: 'service.environment', missing_bucket: true }, - }, - 'transaction.name': { terms: { field: 'transaction.name', missing_bucket: true } }, - 'transaction.type': { terms: { field: 'transaction.type', missing_bucket: true } }, - }, - aggregations: { - goodEvents: { sum: { field: 'slo.numerator' } }, - totalEvents: { sum: { field: 'slo.denominator' } }, - sliValue: { - bucket_script: { - buckets_path: { goodEvents: 'goodEvents', totalEvents: 'totalEvents' }, - script: - 'if (params.totalEvents == 0) { return -1 } else if (params.goodEvents >= params.totalEvents) { return 1 } else { return params.goodEvents / params.totalEvents }', - }, - }, - errorBudgetInitial: { bucket_script: { buckets_path: {}, script: '1 - 0.99' } }, - errorBudgetConsumed: { - bucket_script: { - buckets_path: { - sliValue: 'sliValue', - errorBudgetInitial: 'errorBudgetInitial', - }, - script: - 'if (params.sliValue == -1) { return 0 } else { return (1 - params.sliValue) / params.errorBudgetInitial }', - }, - }, - errorBudgetRemaining: { - bucket_script: { - buckets_path: { errorBudgetConsumed: 'errorBudgetConsumed' }, - script: '1 - params.errorBudgetConsumed', - }, - }, - statusCode: { - bucket_script: { - buckets_path: { - sliValue: 'sliValue', - errorBudgetRemaining: 'errorBudgetRemaining', - }, - script: { - source: - 'if (params.sliValue == -1) { return 0 } else if (params.sliValue >= 0.99) { return 4 } else if (params.errorBudgetRemaining > 0) { return 2 } else { return 1 }', - }, - }, - }, - latestSliTimestamp: { max: { field: '@timestamp' } }, - fiveMinuteBurnRate: { - filter: { - range: { - '@timestamp': { - gte: 'now-480s/m', - lte: 'now-180s/m', - }, - }, - }, - aggs: { - goodEvents: { - sum: { - field: 'slo.numerator', - }, - }, - totalEvents: { - sum: { - field: 'slo.denominator', - }, - }, - }, - }, - oneHourBurnRate: { - filter: { - range: { - '@timestamp': { - gte: 'now-3780s/m', - lte: 'now-180s/m', - }, - }, - }, - aggs: { - goodEvents: { - sum: { - field: 'slo.numerator', - }, - }, - totalEvents: { - sum: { - field: 'slo.denominator', - }, - }, - }, - }, - oneDayBurnRate: { - filter: { - range: { - '@timestamp': { - gte: 'now-86580s/m', - lte: 'now-180s/m', - }, - }, - }, - aggs: { - goodEvents: { - sum: { - field: 'slo.numerator', - }, - }, - totalEvents: { - sum: { - field: 'slo.denominator', - }, - }, - }, - }, - }, - }, - description: `Summarise the rollup data of SLO: Test SLO for api integration [id: ${id}, revision: 1].`, - settings: { deduce_mappings: false, unattended: true }, - _meta: { version: 3.3, managed: true, managed_by: 'observability' }, - }, - ], - }); - }); - - it('creates instanceId for SLOs with multi groupBy', async () => { - createSLOInput.groupBy = ['system.network.name', 'event.dataset']; - - const apiResponse = await slo.create(createSLOInput); - - expect(apiResponse.body).property('id'); - - const { id } = apiResponse.body; - - await retry.tryForTime(300 * 1000, async () => { - const response = await esClient.search(getEsQuery(id)); - - // @ts-ignore - expect(response.aggregations?.last_doc.hits?.hits[0]._source.slo.instanceId).eql( - 'eth1,system.network' - ); - }); - }); - - it('creates instanceId for SLOs with single groupBy', async () => { - createSLOInput.groupBy = 'system.network.name'; - - const apiResponse = await slo.create(createSLOInput); - - expect(apiResponse.body).property('id'); - - const { id } = apiResponse.body; - - await retry.tryForTime(300 * 1000, async () => { - const response = await esClient.search(getEsQuery(id)); - - // @ts-ignore - expect(response.aggregations?.last_doc.hits?.hits[0]._source.slo.instanceId).eql('eth1'); - }); - }); - - it('creates instanceId for SLOs without groupBy ([])', async () => { - createSLOInput.groupBy = []; - - const apiResponse = await slo.create(createSLOInput); - - expect(apiResponse.body).property('id'); - - const { id } = apiResponse.body; - - await retry.tryForTime(300 * 1000, async () => { - const response = await esClient.search(getEsQuery(id)); - - // @ts-ignore - expect(response.aggregations?.last_doc.hits?.hits[0]._source.slo.instanceId).eql('*'); - }); - }); - - it('creates instanceId for SLOs without groupBy (["*"])', async () => { - createSLOInput.groupBy = ['*']; - - const apiResponse = await slo.create(createSLOInput); - - expect(apiResponse.body).property('id'); - - const { id } = apiResponse.body; - - await retry.tryForTime(300 * 1000, async () => { - const response = await esClient.search(getEsQuery(id)); - - // @ts-ignore - expect(response.aggregations?.last_doc.hits?.hits[0]._source.slo.instanceId).eql('*'); - }); - }); - - it('creates instanceId for SLOs without groupBy ("")', async () => { - createSLOInput.groupBy = ''; - - const apiResponse = await slo.create(createSLOInput); - - expect(apiResponse.body).property('id'); - - const { id } = apiResponse.body; - - await retry.tryForTime(300 * 1000, async () => { - const response = await esClient.search(getEsQuery(id)); - - // @ts-ignore - expect(response.aggregations?.last_doc.hits?.hits[0]._source.slo.instanceId).eql('*'); - }); - }); - }); -} - -const getEsQuery = (id: string) => ({ - index: '.slo-observability.sli-v3*', - size: 0, - query: { - bool: { - filter: [ - { - term: { - 'slo.id': id, - }, - }, - ], - }, - }, - aggs: { - last_doc: { - top_hits: { - sort: [ - { - '@timestamp': { - order: 'desc', - }, - }, - ], - _source: { - includes: ['slo.instanceId'], - }, - size: 1, - }, - }, - }, -}); diff --git a/x-pack/test/api_integration/apis/slos/delete_slo.ts b/x-pack/test/api_integration/apis/slos/delete_slo.ts deleted file mode 100644 index 979564f06be55..0000000000000 --- a/x-pack/test/api_integration/apis/slos/delete_slo.ts +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { cleanup } from '@kbn/infra-forge'; -import expect from '@kbn/expect'; -import type { CreateSLOInput } from '@kbn/slo-schema'; -import { SO_SLO_TYPE } from '@kbn/slo-plugin/server/saved_objects'; - -import { FtrProviderContext } from '../../ftr_provider_context'; -import { sloData } from './fixtures/create_slo'; -import { loadTestData } from './helper/load_test_data'; -import { SloEsClient } from './helper/es'; - -export default function ({ getService }: FtrProviderContext) { - describe('Delete SLOs', function () { - this.tags('skipCloud'); - - const supertestAPI = getService('supertest'); - const kibanaServer = getService('kibanaServer'); - const esClient = getService('es'); - const logger = getService('log'); - const slo = getService('slo'); - const retry = getService('retry'); - const sloEsClient = new SloEsClient(esClient); - - let createSLOInput: CreateSLOInput; - - before(async () => { - await slo.createUser(); - await slo.deleteAllSLOs(); - await sloEsClient.deleteTestSourceData(); - await loadTestData(getService); - }); - - beforeEach(() => { - createSLOInput = sloData; - }); - - afterEach(async () => { - await slo.deleteAllSLOs(); - }); - - after(async () => { - await cleanup({ esClient, logger }); - await sloEsClient.deleteTestSourceData(); - }); - - it('deletes new slo saved object and transforms', async () => { - const response = await slo.create(createSLOInput); - - expect(response.body).property('id'); - - const { id } = response.body; - - await retry.tryForTime(10000, async () => { - const savedObject = await kibanaServer.savedObjects.find({ - type: SO_SLO_TYPE, - }); - - expect(savedObject.saved_objects.length).eql(1); - - expect(savedObject.saved_objects[0].attributes.id).eql(id); - }); - - await retry.tryForTime(300 * 1000, async () => { - // expect summary and rollup data to exist - const sloSummaryResponse = await sloEsClient.getSLOSummaryDataById(id); - const sloRollupResponse = await sloEsClient.getSLORollupDataById(id); - - expect(sloSummaryResponse.hits.hits.length > 0).eql(true); - expect(sloRollupResponse.hits.hits.length > 0).eql(true); - - const rollUpTransformResponse = await supertestAPI - .get(`/internal/transform/transforms/slo-${id}-1`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - - // expect roll up transform to be created - expect(rollUpTransformResponse.body.transforms[0].id).eql(`slo-${id}-1`); - - const summaryTransform = await supertestAPI - .get(`/internal/transform/transforms/slo-summary-${id}-1`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - - // expect summary transform to be created - expect(summaryTransform.body.transforms[0].id).eql(`slo-summary-${id}-1`); - - await slo.delete(id); - }); - - // await retry.tryForTime(150 * 1000, async () => { - const savedObjectAfterDelete = await kibanaServer.savedObjects.find({ - type: SO_SLO_TYPE, - }); - - // SO should now be deleted - expect(savedObjectAfterDelete.saved_objects.length).eql(0); - - // roll up transform should be deleted - await supertestAPI - .get(`/internal/transform/transforms/slo-${id}-1`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(404); - - // summary transform should be deleted - await supertestAPI - .get(`/internal/transform/transforms/slo-summary-${id}-1`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(404); - - // expect summary and rollup documents to be deleted - await retry.waitForWithTimeout('SLO summary data is deleted', 60 * 1000, async () => { - const sloSummaryResponseAfterDeletion = await sloEsClient.getSLOSummaryDataById(id); - if (sloSummaryResponseAfterDeletion.hits.hits.length > 0) { - throw new Error('SLO summary data not deleted yet'); - } - return true; - }); - - await retry.waitForWithTimeout('SLO rollup data is deleted', 60 * 1000, async () => { - const sloRollupResponseAfterDeletion = await sloEsClient.getSLORollupDataById(id); - if (sloRollupResponseAfterDeletion.hits.hits.length > 1) { - throw new Error('SLO rollup data not deleted yet'); - } - return true; - }); - }); - }); -} diff --git a/x-pack/test/api_integration/apis/slos/fetch_historical_summary.ts b/x-pack/test/api_integration/apis/slos/fetch_historical_summary.ts deleted file mode 100644 index 96f8e21c8c593..0000000000000 --- a/x-pack/test/api_integration/apis/slos/fetch_historical_summary.ts +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { - SLO_DESTINATION_INDEX_NAME, - SLO_DESTINATION_INDEX_PATTERN, -} from '@kbn/slo-plugin/common/constants'; -import { ALL_VALUE } from '@kbn/slo-schema'; -import moment from 'moment'; -import { FtrProviderContext } from '../../ftr_provider_context'; - -export default function ({ getService }: FtrProviderContext) { - const esClient = getService('es'); - const esDeleteAllIndices = getService('esDeleteAllIndices'); - const sloApi = getService('slo'); - - const SLO_ID = 'slo-fake-1'; - - describe('fetch historical summary', () => { - before(async () => { - await sloApi.createUser(); - const now = moment().startOf('minute'); - const curr = now.clone().subtract(30, 'days'); - const end = now.clone().add(5, 'minutes'); - - const batchOperations = []; - while (curr.isSameOrBefore(end)) { - batchOperations.push([ - { index: { _index: SLO_DESTINATION_INDEX_NAME } }, - { - '@timestamp': curr.toISOString(), - slo: { - id: SLO_ID, - revision: 1, - instanceId: ALL_VALUE, - numerator: 90, - denominator: 100, - isGoodSlice: 1, - groupings: {}, - }, - }, - ]); - curr.add(1, 'minute'); - } - - await esClient.bulk({ - index: SLO_DESTINATION_INDEX_NAME, - operations: batchOperations.flat(), - refresh: 'wait_for', - }); - - await esClient.indices.refresh({ index: SLO_DESTINATION_INDEX_NAME }); - }); - - after(async () => { - await esDeleteAllIndices(SLO_DESTINATION_INDEX_PATTERN); - }); - - it('computes the historical summary for a rolling occurrences SLO', async () => { - const response = await sloApi.fetchHistoricalSummary({ - list: [ - { - sloId: SLO_ID, - instanceId: ALL_VALUE, - timeWindow: { - duration: '7d', - type: 'rolling', - }, - budgetingMethod: 'occurrences', - objective: { - target: 0.9, - }, - groupBy: ALL_VALUE, - revision: 1, - }, - ], - }); - expect(response[0].sloId).to.eql(SLO_ID); - expect(response[0].instanceId).to.eql(ALL_VALUE); - const numberOfBuckets = response[0].data.length; - expect(numberOfBuckets).to.be.within(168, 170); // 7 days * 24 hours/day * 1 bucket/hour + 2 extra bucket due to histogram agg rounding - const last = response[0].data.pop(); - expect(last?.errorBudget).to.eql({ - consumed: 1, - initial: 0.1, - isEstimated: false, - remaining: 0, - }); - expect(last?.sliValue).to.eql(0.9); - expect(last?.status).to.eql('HEALTHY'); - }); - - it('computes the historical summary for a rolling timeslices SLO', async () => { - const response = await sloApi.fetchHistoricalSummary({ - list: [ - { - sloId: SLO_ID, - instanceId: ALL_VALUE, - timeWindow: { - duration: '7d', - type: 'rolling', - }, - budgetingMethod: 'timeslices', - objective: { - target: 0.9, - timesliceTarget: 0.8, - timesliceWindow: '1m', - }, - groupBy: ALL_VALUE, - revision: 1, - }, - ], - }); - expect(response[0].sloId).to.eql(SLO_ID); - expect(response[0].instanceId).to.eql(ALL_VALUE); - const numberOfBuckets = response[0].data.length; - expect(numberOfBuckets).to.be.within(168, 170); // 7 days * 24 hours/day * 1 bucket/hour + 2 extra bucket due to histogram agg rounding - const last = response[0].data.pop(); - expect(last?.errorBudget).to.eql({ - consumed: 0, - initial: 0.1, - isEstimated: false, - remaining: 1, - }); - expect(last?.sliValue).to.eql(1); - expect(last?.status).to.eql('HEALTHY'); - }); - }); -} diff --git a/x-pack/test/api_integration/apis/slos/get_slo.ts b/x-pack/test/api_integration/apis/slos/get_slo.ts deleted file mode 100644 index 815409853c7d6..0000000000000 --- a/x-pack/test/api_integration/apis/slos/get_slo.ts +++ /dev/null @@ -1,492 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { cleanup } from '@kbn/infra-forge'; -import expect from 'expect'; -import type { CreateSLOInput } from '@kbn/slo-schema'; - -import { FtrProviderContext } from '../../ftr_provider_context'; -import { loadTestData } from './helper/load_test_data'; -import { SloEsClient } from './helper/es'; -import { sloData } from './fixtures/create_slo'; - -export const expectSummary = (summary: Record) => { - expect(summary).toEqual({ - sliValue: expect.any(Number), - errorBudget: { - initial: expect.any(Number), - consumed: expect.any(Number), - remaining: expect.any(Number), - isEstimated: expect.any(Boolean), - }, - status: expect.any(String), - fiveMinuteBurnRate: expect.any(Number), - oneDayBurnRate: expect.any(Number), - oneHourBurnRate: expect.any(Number), - }); -}; - -export default function ({ getService }: FtrProviderContext) { - describe('GetSLOs', function () { - this.tags('skipCloud'); - - const supertestAPI = getService('supertest'); - const esClient = getService('es'); - const logger = getService('log'); - const retry = getService('retry'); - const slo = getService('slo'); - // const transform = getService('transform'); - const sloEsClient = new SloEsClient(esClient); - - // const onFailure = async () => { - // const allTransforms = await transform.api.getTransformList(); - // for (const tf of allTransforms.transforms) { - // await transform.api.scheduleTransform(tf.id); - // } - // }; - - let createSLOInput: CreateSLOInput; - - const createSLO = async (requestOverrides?: Record) => { - return await slo.create({ - ...createSLOInput, - ...requestOverrides, - }); - }; - - before(async () => { - await slo.createUser(); - await slo.deleteAllSLOs(); - await sloEsClient.deleteTestSourceData(); - await loadTestData(getService); - }); - - beforeEach(async () => { - createSLOInput = sloData; - }); - - afterEach(async () => { - await retry.tryForTime(60 * 1000, async () => { - await slo.deleteAllSLOs(); - }); - }); - - after(async () => { - await cleanup({ esClient, logger }); - await sloEsClient.deleteTestSourceData(); - }); - - it('gets slo by id and calculates SLI - occurrences rolling', async () => { - const response = await createSLO({ - groupBy: '*', - }); - const id = response.body.id; - - await retry.tryForTime(300 * 1000, async () => { - const getResponse = await supertestAPI - .get(`/api/observability/slos/${id}`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(getResponse.body).toEqual({ - name: 'Test SLO for api integration', - description: 'Fixture for api integration tests', - indicator: { - type: 'sli.kql.custom', - params: { - index: 'kbn-data-forge*', - filter: `system.network.name: eth1`, - good: 'container.cpu.user.pct < 1', - total: 'container.cpu.user.pct: *', - timestampField: '@timestamp', - }, - }, - budgetingMethod: 'occurrences', - timeWindow: { duration: '7d', type: 'rolling' }, - objective: { target: 0.99 }, - tags: ['test'], - groupBy: '*', - groupings: {}, - id, - settings: { syncDelay: '1m', frequency: '1m', preventInitialBackfill: false }, - revision: 1, - enabled: true, - createdAt: getResponse.body.createdAt, - updatedAt: getResponse.body.updatedAt, - version: 2, - instanceId: '*', - meta: {}, - summary: expect.any(Object), - }); - expectSummary(getResponse.body.summary); - }); - }); - - it('gets slo by id and calculates SLI - occurrences calendarAligned', async () => { - const response = await createSLO({ - groupBy: '*', - timeWindow: { - duration: '1w', - type: 'calendarAligned', - }, - }); - const id = response.body.id; - - await retry.tryForTime(300 * 1000, async () => { - const getResponse = await supertestAPI - .get(`/api/observability/slos/${id}`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - // expect summary transform to be created - expect(getResponse.body).toEqual({ - name: 'Test SLO for api integration', - description: 'Fixture for api integration tests', - indicator: { - type: 'sli.kql.custom', - params: { - index: 'kbn-data-forge*', - filter: `system.network.name: eth1`, - good: 'container.cpu.user.pct < 1', - total: 'container.cpu.user.pct: *', - timestampField: '@timestamp', - }, - }, - budgetingMethod: 'occurrences', - timeWindow: { duration: '1w', type: 'calendarAligned' }, - objective: { target: 0.99 }, - tags: ['test'], - groupBy: '*', - groupings: {}, - id, - settings: { syncDelay: '1m', frequency: '1m', preventInitialBackfill: false }, - revision: 1, - enabled: true, - createdAt: getResponse.body.createdAt, - updatedAt: getResponse.body.updatedAt, - version: 2, - instanceId: '*', - meta: {}, - summary: expect.any(Object), - }); - expectSummary(getResponse.body.summary); - }); - }); - - it('gets slo by id and calculates SLI - timeslices rolling', async () => { - const response = await createSLO({ - groupBy: '*', - timeWindow: { - duration: '7d', - type: 'rolling', - }, - budgetingMethod: 'timeslices', - objective: { - target: 0.99, - timesliceTarget: 0.95, - timesliceWindow: '1m', - }, - }); - const id = response.body.id; - - await retry.tryForTime(300 * 1000, async () => { - const getResponse = await supertestAPI - .get(`/api/observability/slos/${id}`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - // expect summary transform to be created - expect(getResponse.body).toEqual({ - name: 'Test SLO for api integration', - description: 'Fixture for api integration tests', - indicator: { - type: 'sli.kql.custom', - params: { - index: 'kbn-data-forge*', - filter: `system.network.name: eth1`, - good: 'container.cpu.user.pct < 1', - total: 'container.cpu.user.pct: *', - timestampField: '@timestamp', - }, - }, - budgetingMethod: 'timeslices', - timeWindow: { duration: '7d', type: 'rolling' }, - objective: { - target: 0.99, - timesliceTarget: 0.95, - timesliceWindow: '1m', - }, - tags: ['test'], - groupBy: '*', - groupings: {}, - id, - settings: { syncDelay: '1m', frequency: '1m', preventInitialBackfill: false }, - revision: 1, - enabled: true, - createdAt: getResponse.body.createdAt, - updatedAt: getResponse.body.updatedAt, - version: 2, - instanceId: '*', - meta: {}, - summary: expect.any(Object), - }); - expectSummary(getResponse.body.summary); - }); - }); - - it('gets slo by id and calculates SLI - timeslices calendarAligned', async () => { - const response = await createSLO({ - groupBy: '*', - timeWindow: { - duration: '1w', - type: 'calendarAligned', - }, - budgetingMethod: 'timeslices', - objective: { - target: 0.99, - timesliceTarget: 0.95, - timesliceWindow: '10m', - }, - }); - const id = response.body.id; - - await retry.tryForTime(300 * 1000, async () => { - const getResponse = await supertestAPI - .get(`/api/observability/slos/${id}`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(getResponse.body).toEqual({ - name: 'Test SLO for api integration', - description: 'Fixture for api integration tests', - indicator: { - type: 'sli.kql.custom', - params: { - index: 'kbn-data-forge*', - filter: `system.network.name: eth1`, - good: 'container.cpu.user.pct < 1', - total: 'container.cpu.user.pct: *', - timestampField: '@timestamp', - }, - }, - budgetingMethod: 'timeslices', - timeWindow: { duration: '1w', type: 'calendarAligned' }, - objective: { - target: 0.99, - timesliceTarget: 0.95, - timesliceWindow: '10m', - }, - tags: ['test'], - groupBy: '*', - groupings: {}, - id, - settings: { syncDelay: '1m', frequency: '1m', preventInitialBackfill: false }, - revision: 1, - enabled: true, - createdAt: getResponse.body.createdAt, - updatedAt: getResponse.body.updatedAt, - version: 2, - instanceId: '*', - meta: {}, - summary: expect.any(Object), - }); - expectSummary(getResponse.body.summary); - }); - }); - - it('gets slos by query', async () => { - await createSLO(); - await createSLO({ name: 'test int' }); - - await retry.tryForTime(360 * 1000, async () => { - const response = await supertestAPI - .get(`/api/observability/slos`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(response.body.results.length).toEqual(2); - - const searchResponse = await supertestAPI - .get(`/api/observability/slos?kqlQuery=slo.name%3Aapi*`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(searchResponse.body.results.length).toEqual(1); - - const searchResponse2 = await supertestAPI - .get(`/api/observability/slos?kqlQuery=slo.name%3Aint`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(searchResponse2.body.results.length).toEqual(1); - - const searchResponse3 = await supertestAPI - .get(`/api/observability/slos?kqlQuery=slo.name%3Aint*`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(searchResponse3.body.results.length).toEqual(2); - - const searchResponse4 = await supertestAPI - .get(`/api/observability/slos?kqlQuery=int*`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(searchResponse4.body.results.length).toEqual(2); - }); - }); - - // not possible for now to reliably fix this - // it.skip('gets slos instances', async () => { - // const createResponse = await createSLO(); - // const id = createResponse.body.id; - // - // await retry.tryForTime( - // 400 * 1000, - // async () => { - // const response = await supertestAPI - // .get(`/api/observability/slos`) - // .set('kbn-xsrf', 'true') - // .send() - // .expect(200); - // const res = response.body.results; - // expect(res.length).toEqual(3); - // const groups = res.map((r: any) => r.groupings.tags); - // - // expect(groups.sort()).toEqual(['1', '2', '3']); - // - // const instanceResponse = await supertestAPI - // .get(`/internal/observability/slos/${id}/_instances`) - // .set('kbn-xsrf', 'true') - // .send() - // .expect(200); - // - // // expect 3 instances to be created - // expect(instanceResponse.body.groupBy).toEqual('tags'); - // expect(instanceResponse.body.instances.sort()).toEqual(['1', '2', '3']); - // }, - // onFailure, - // 10 * 1000 - // ); - // }); - - it('gets slo definitions', async () => { - const createResponse = await createSLO(); - const id = createResponse.body.id; - const secondCreateResponse = await createSLO({ name: 'test name int' }); - const secondId = secondCreateResponse.body.id; - const response = await slo.getDefinitions(); - - expect(response.body).toEqual({ - page: 1, - perPage: 100, - results: [ - { - budgetingMethod: 'occurrences', - createdAt: response.body.results[0].createdAt, - description: 'Fixture for api integration tests', - enabled: true, - groupBy: 'tags', - id, - indicator: { - params: { - filter: 'system.network.name: eth1', - good: 'container.cpu.user.pct < 1', - index: 'kbn-data-forge*', - timestampField: '@timestamp', - total: 'container.cpu.user.pct: *', - }, - type: 'sli.kql.custom', - }, - name: 'Test SLO for api integration', - objective: { - target: 0.99, - }, - revision: 1, - settings: { - frequency: '1m', - syncDelay: '1m', - preventInitialBackfill: false, - }, - tags: ['test'], - timeWindow: { - duration: '7d', - type: 'rolling', - }, - updatedAt: response.body.results[0].updatedAt, - version: 2, - }, - { - budgetingMethod: 'occurrences', - createdAt: response.body.results[1].createdAt, - description: 'Fixture for api integration tests', - enabled: true, - groupBy: 'tags', - id: secondId, - indicator: { - params: { - filter: 'system.network.name: eth1', - good: 'container.cpu.user.pct < 1', - index: 'kbn-data-forge*', - timestampField: '@timestamp', - total: 'container.cpu.user.pct: *', - }, - type: 'sli.kql.custom', - }, - name: 'test name int', - objective: { - target: 0.99, - }, - revision: 1, - settings: { - frequency: '1m', - syncDelay: '1m', - preventInitialBackfill: false, - }, - tags: ['test'], - timeWindow: { - duration: '7d', - type: 'rolling', - }, - updatedAt: response.body.results[1].updatedAt, - version: 2, - }, - ], - total: 2, - }); - - // can search by name - const searchResponse = await slo.getDefinitions({ search: 'api' }); - - expect(searchResponse.body.total).toEqual(1); - - const searchResponse2 = await supertestAPI - .get(`/api/observability/slos/_definitions?search=int`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(searchResponse2.body.total).toEqual(1); - - const searchResponse3 = await supertestAPI - .get(`/api/observability/slos/_definitions?search=int*`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(searchResponse3.body.total).toEqual(2); - }); - }); -} diff --git a/x-pack/test/api_integration/apis/slos/index.ts b/x-pack/test/api_integration/apis/slos/index.ts deleted file mode 100644 index 3401b195ccee5..0000000000000 --- a/x-pack/test/api_integration/apis/slos/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FtrProviderContext } from '../../ftr_provider_context'; - -export default function ({ loadTestFile }: FtrProviderContext) { - describe('SLO API Tests', () => { - loadTestFile(require.resolve('./get_slo')); - loadTestFile(require.resolve('./create_slo')); - loadTestFile(require.resolve('./delete_slo')); - loadTestFile(require.resolve('./update_slo')); - loadTestFile(require.resolve('./reset_slo')); - loadTestFile(require.resolve('./fetch_historical_summary')); - }); -} diff --git a/x-pack/test/api_integration/apis/slos/reset_slo.ts b/x-pack/test/api_integration/apis/slos/reset_slo.ts deleted file mode 100644 index cccac8f1796be..0000000000000 --- a/x-pack/test/api_integration/apis/slos/reset_slo.ts +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { cleanup } from '@kbn/infra-forge'; -import expect from '@kbn/expect'; -import { SO_SLO_TYPE } from '@kbn/slo-plugin/server/saved_objects'; - -import { FtrProviderContext } from '../../ftr_provider_context'; -import { loadTestData } from './helper/load_test_data'; -import { SloEsClient } from './helper/es'; - -export default function ({ getService }: FtrProviderContext) { - describe('Reset SLOs', function () { - this.tags('skipCloud'); - - const kibanaServer = getService('kibanaServer'); - const esClient = getService('es'); - const logger = getService('log'); - const slo = getService('slo'); - const sloEsClient = new SloEsClient(esClient); - - before(async () => { - await sloEsClient.deleteTestSourceData(); - await slo.createUser(); - await slo.deleteAllSLOs(); - await loadTestData(getService); - }); - - afterEach(async () => { - await slo.deleteAllSLOs(); - }); - - after(async () => { - await cleanup({ esClient, logger }); - await sloEsClient.deleteTestSourceData(); - }); - - it('updates the SO and transforms', async () => { - // create mock old SLO - const id = 'bdaeccdd-dc63-4138-a1d5-92c075f88087'; - await kibanaServer.savedObjects.clean({ - types: [SO_SLO_TYPE], - }); - await kibanaServer.savedObjects.create({ - type: SO_SLO_TYPE, - overwrite: true, - id, - attributes: { - name: 'Test SLO for api integration', - description: 'Fixture for api integration tests', - indicator: { - type: 'sli.kql.custom', - params: { - index: 'kbn-data-forge*', - filter: 'system.network.name: eth1', - good: 'container.cpu.user.pct < 1', - total: 'container.cpu.user.pct: *', - timestampField: '@timestamp', - }, - }, - budgetingMethod: 'occurrences', - timeWindow: { duration: '7d', type: 'rolling' }, - objective: { target: 0.99 }, - tags: ['test'], - groupBy: '*', - id, - settings: { - syncDelay: '1m', - frequency: '1m', - }, - revision: 1, - enabled: true, - createdAt: '2023-12-14T01:12:35.638Z', - updatedAt: '2023-12-14T01:12:35.638Z', - version: 1, - }, - }); - - const responseBeforeReset = await slo.getDefinitions(); - - expect(responseBeforeReset.body.results[0].version).eql(1); - - await slo.reset(id); - - const responseAfterReset = await slo.getDefinitions(); - - expect(responseAfterReset.body.results[0].version).eql(2); - }); - }); -} diff --git a/x-pack/test/api_integration/apis/slos/update_slo.ts b/x-pack/test/api_integration/apis/slos/update_slo.ts deleted file mode 100644 index a8f4aa1a334f8..0000000000000 --- a/x-pack/test/api_integration/apis/slos/update_slo.ts +++ /dev/null @@ -1,764 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { cleanup } from '@kbn/infra-forge'; -import expect from '@kbn/expect'; -import type { CreateSLOInput } from '@kbn/slo-schema'; -import { SO_SLO_TYPE } from '@kbn/slo-plugin/server/saved_objects'; - -import { FtrProviderContext } from '../../ftr_provider_context'; -import { loadTestData } from './helper/load_test_data'; -import { sloData } from './fixtures/create_slo'; - -export default function ({ getService }: FtrProviderContext) { - describe('UpdateSLOs', function () { - this.tags('skipCloud'); - - const supertestAPI = getService('supertest'); - const kibanaServer = getService('kibanaServer'); - const esClient = getService('es'); - const logger = getService('log'); - const slo = getService('slo'); - - let createSLOInput: CreateSLOInput; - - before(async () => { - await slo.createUser(); - await slo.deleteAllSLOs(); - await loadTestData(getService); - }); - - beforeEach(() => { - createSLOInput = sloData; - }); - - afterEach(async () => { - await slo.deleteAllSLOs(); - }); - - after(async () => { - await cleanup({ esClient, logger }); - }); - - it('updates the SO and transforms', async () => { - const apiResponse = await supertestAPI - .post('/api/observability/slos') - .set('kbn-xsrf', 'true') - .send(createSLOInput) - .expect(200); - - expect(apiResponse.body).property('id'); - - const { id } = apiResponse.body; - - await supertestAPI - .put(`/api/observability/slos/${id}`) - .set('kbn-xsrf', 'true') - .send({ - ...createSLOInput, - groupBy: 'hosts', - }) - .expect(200); - - const savedObject = await kibanaServer.savedObjects.find({ - type: SO_SLO_TYPE, - }); - - expect(savedObject.saved_objects.length).eql(1); - - expect(savedObject.saved_objects[0].attributes).eql({ - budgetingMethod: 'occurrences', - updatedAt: savedObject.saved_objects[0].attributes.updatedAt, - createdAt: savedObject.saved_objects[0].attributes.createdAt, - description: 'Fixture for api integration tests', - enabled: true, - groupBy: 'hosts', - id, - indicator: { - params: { - filter: 'system.network.name: eth1', - good: 'container.cpu.user.pct < 1', - index: 'kbn-data-forge*', - timestampField: '@timestamp', - total: 'container.cpu.user.pct: *', - }, - type: 'sli.kql.custom', - }, - name: 'Test SLO for api integration', - objective: { - target: 0.99, - }, - revision: 2, - settings: { - frequency: '1m', - syncDelay: '1m', - preventInitialBackfill: false, - }, - tags: ['test'], - timeWindow: { - duration: '7d', - type: 'rolling', - }, - version: 2, - }); - - const rollUpTransformResponse = await supertestAPI - .get(`/internal/transform/transforms/slo-${id}-2`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - - // expect roll up transform to be created - expect(rollUpTransformResponse.body).eql({ - count: 1, - transforms: [ - { - id: `slo-${id}-2`, - authorization: { roles: ['superuser'] }, - version: '10.0.0', - create_time: rollUpTransformResponse.body.transforms[0].create_time, - source: { - index: ['kbn-data-forge*'], - query: { - bool: { - filter: [ - { range: { '@timestamp': { gte: 'now-7d/d' } } }, - { - bool: { - should: [ - { - match: { - 'system.network.name': 'eth1', - }, - }, - ], - minimum_should_match: 1, - }, - }, - { - exists: { - field: 'hosts', - }, - }, - ], - }, - }, - }, - dest: { - index: '.slo-observability.sli-v3.3', - pipeline: `.slo-observability.sli.pipeline-${id}-2`, - }, - frequency: '1m', - sync: { time: { field: '@timestamp', delay: '1m' } }, - pivot: { - group_by: { - 'slo.groupings.hosts': { terms: { field: 'hosts' } }, - '@timestamp': { date_histogram: { field: '@timestamp', fixed_interval: '1m' } }, - }, - aggregations: { - 'slo.numerator': { - filter: { - bool: { - should: [{ range: { 'container.cpu.user.pct': { lt: '1' } } }], - minimum_should_match: 1, - }, - }, - }, - 'slo.denominator': { - filter: { - bool: { - should: [{ exists: { field: 'container.cpu.user.pct' } }], - minimum_should_match: 1, - }, - }, - }, - }, - }, - description: `Rolled-up SLI data for SLO: Test SLO for api integration [id: ${id}, revision: 2]`, - settings: { deduce_mappings: false, unattended: true }, - _meta: { version: 3.3, managed: true, managed_by: 'observability' }, - }, - ], - }); - - const summaryTransform = await supertestAPI - .get(`/internal/transform/transforms/slo-summary-${id}-2`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - - // expect summary transform to be created - expect(summaryTransform.body).eql({ - count: 1, - transforms: [ - { - id: `slo-summary-${id}-2`, - authorization: { roles: ['superuser'] }, - version: '10.0.0', - create_time: summaryTransform.body.transforms[0].create_time, - source: { - index: ['.slo-observability.sli-v3.3*'], - query: { - bool: { - filter: [ - { range: { '@timestamp': { gte: 'now-7d/m', lte: 'now/m' } } }, - { term: { 'slo.id': id } }, - { term: { 'slo.revision': 2 } }, - ], - }, - }, - }, - dest: { - index: '.slo-observability.summary-v3.3', - pipeline: `.slo-observability.summary.pipeline-${id}-2`, - }, - frequency: '1m', - sync: { time: { field: 'event.ingested', delay: '65s' } }, - pivot: { - group_by: { - 'slo.id': { terms: { field: 'slo.id' } }, - 'slo.revision': { terms: { field: 'slo.revision' } }, - 'slo.instanceId': { terms: { field: 'slo.instanceId' } }, - 'slo.groupings.hosts': { - terms: { field: 'slo.groupings.hosts' }, - }, - 'monitor.config_id': { - terms: { - field: 'monitor.config_id', - missing_bucket: true, - }, - }, - 'monitor.name': { - terms: { - field: 'monitor.name', - missing_bucket: true, - }, - }, - 'observer.geo.name': { - terms: { - field: 'observer.geo.name', - missing_bucket: true, - }, - }, - 'observer.name': { - terms: { - field: 'observer.name', - missing_bucket: true, - }, - }, - 'service.name': { terms: { field: 'service.name', missing_bucket: true } }, - 'service.environment': { - terms: { field: 'service.environment', missing_bucket: true }, - }, - 'transaction.name': { terms: { field: 'transaction.name', missing_bucket: true } }, - 'transaction.type': { terms: { field: 'transaction.type', missing_bucket: true } }, - }, - aggregations: { - goodEvents: { sum: { field: 'slo.numerator' } }, - totalEvents: { sum: { field: 'slo.denominator' } }, - sliValue: { - bucket_script: { - buckets_path: { goodEvents: 'goodEvents', totalEvents: 'totalEvents' }, - script: - 'if (params.totalEvents == 0) { return -1 } else if (params.goodEvents >= params.totalEvents) { return 1 } else { return params.goodEvents / params.totalEvents }', - }, - }, - errorBudgetInitial: { bucket_script: { buckets_path: {}, script: '1 - 0.99' } }, - errorBudgetConsumed: { - bucket_script: { - buckets_path: { - sliValue: 'sliValue', - errorBudgetInitial: 'errorBudgetInitial', - }, - script: - 'if (params.sliValue == -1) { return 0 } else { return (1 - params.sliValue) / params.errorBudgetInitial }', - }, - }, - errorBudgetRemaining: { - bucket_script: { - buckets_path: { errorBudgetConsumed: 'errorBudgetConsumed' }, - script: '1 - params.errorBudgetConsumed', - }, - }, - statusCode: { - bucket_script: { - buckets_path: { - sliValue: 'sliValue', - errorBudgetRemaining: 'errorBudgetRemaining', - }, - script: { - source: - 'if (params.sliValue == -1) { return 0 } else if (params.sliValue >= 0.99) { return 4 } else if (params.errorBudgetRemaining > 0) { return 2 } else { return 1 }', - }, - }, - }, - latestSliTimestamp: { max: { field: '@timestamp' } }, - fiveMinuteBurnRate: { - filter: { - range: { - '@timestamp': { - gte: 'now-480s/m', - lte: 'now-180s/m', - }, - }, - }, - aggs: { - goodEvents: { - sum: { - field: 'slo.numerator', - }, - }, - totalEvents: { - sum: { - field: 'slo.denominator', - }, - }, - }, - }, - oneHourBurnRate: { - filter: { - range: { - '@timestamp': { - gte: 'now-3780s/m', - lte: 'now-180s/m', - }, - }, - }, - aggs: { - goodEvents: { - sum: { - field: 'slo.numerator', - }, - }, - totalEvents: { - sum: { - field: 'slo.denominator', - }, - }, - }, - }, - oneDayBurnRate: { - filter: { - range: { - '@timestamp': { - gte: 'now-86580s/m', - lte: 'now-180s/m', - }, - }, - }, - aggs: { - goodEvents: { - sum: { - field: 'slo.numerator', - }, - }, - totalEvents: { - sum: { - field: 'slo.denominator', - }, - }, - }, - }, - }, - }, - description: `Summarise the rollup data of SLO: Test SLO for api integration [id: ${id}, revision: 2].`, - settings: { deduce_mappings: false, unattended: true }, - _meta: { version: 3.3, managed: true, managed_by: 'observability' }, - }, - ], - }); - }); - - it('updates an existing slo and does not update transforms when relevant fields are changed', async () => { - const request = createSLOInput; - - const apiResponse = await supertestAPI - .post('/api/observability/slos') - .set('kbn-xsrf', 'true') - .send(request) - .expect(200); - - expect(apiResponse.body).property('id'); - - const { id } = apiResponse.body; - - const savedObject = await kibanaServer.savedObjects.find({ - type: SO_SLO_TYPE, - }); - - expect(savedObject.saved_objects.length).eql(1); - - await supertestAPI - .get(`/internal/transform/transforms/slo-${id}-1`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - - await supertestAPI - .get(`/internal/transform/transforms/slo-summary-${id}-1`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - - // change name - await supertestAPI - .put(`/api/observability/slos/${id}`) - .set('kbn-xsrf', 'true') - .send({ - ...request, - name: 'test name', - }) - .expect(200); - - await supertestAPI - .get(`/internal/transform/transforms/slo-${id}-1`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - - await supertestAPI - .get(`/internal/transform/transforms/slo-summary-${id}-1`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - - // change description - await supertestAPI - .put(`/api/observability/slos/${id}`) - .set('kbn-xsrf', 'true') - .send({ - ...request, - description: 'test description', - }) - .expect(200); - - await supertestAPI - .get(`/internal/transform/transforms/slo-${id}-1`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - - await supertestAPI - .get(`/internal/transform/transforms/slo-summary-${id}-1`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - - // change tags - await supertestAPI - .put(`/api/observability/slos/${id}`) - .set('kbn-xsrf', 'true') - .send({ - ...request, - tags: ['testTag'], - }) - .expect(200); - - await supertestAPI - .get(`/internal/transform/transforms/slo-${id}-1`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - - await supertestAPI - .get(`/internal/transform/transforms/slo-summary-${id}-1`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - }); - - it('updates an existing slo and updates transforms when relevant fields are changed', async () => { - const request = createSLOInput; - - const apiResponse = await supertestAPI - .post('/api/observability/slos') - .set('kbn-xsrf', 'true') - .send(request) - .expect(200); - - expect(apiResponse.body).property('id'); - - const { id } = apiResponse.body; - - const savedObject = await kibanaServer.savedObjects.find({ - type: SO_SLO_TYPE, - }); - - expect(savedObject.saved_objects.length).eql(1); - - await supertestAPI - .get(`/internal/transform/transforms/slo-${id}-1`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - - await supertestAPI - .get(`/internal/transform/transforms/slo-summary-${id}-1`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - - // change group by - await supertestAPI - .put(`/api/observability/slos/${id}`) - .set('kbn-xsrf', 'true') - .send({ - ...request, - groupBy: 'hosts', - }) - .expect(200); - - await supertestAPI - .get(`/internal/transform/transforms/slo-${id}-1`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(404); - - await supertestAPI - .get(`/internal/transform/transforms/slo-summary-${id}-1`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(404); - - await supertestAPI - .get(`/internal/transform/transforms/slo-${id}-2`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - - await supertestAPI - .get(`/internal/transform/transforms/slo-summary-${id}-2`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - - // change indicator - await supertestAPI - .put(`/api/observability/slos/${id}`) - .set('kbn-xsrf', 'true') - .send({ - ...request, - indicator: { - ...request.indicator, - params: { - ...request.indicator.params, - index: 'test-index-*', - }, - }, - }) - .expect(200); - - await supertestAPI - .get(`/internal/transform/transforms/slo-${id}-2`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(404); - - await supertestAPI - .get(`/internal/transform/transforms/slo-summary-${id}-2`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(404); - - await supertestAPI - .get(`/internal/transform/transforms/slo-${id}-3`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - - await supertestAPI - .get(`/internal/transform/transforms/slo-summary-${id}-3`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - - // change time window - await supertestAPI - .put(`/api/observability/slos/${id}`) - .set('kbn-xsrf', 'true') - .send({ - ...request, - timeWindow: { - ...request.timeWindow, - duration: '7d', - }, - }) - .expect(200); - - await supertestAPI - .get(`/internal/transform/transforms/slo-${id}-3`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(404); - - await supertestAPI - .get(`/internal/transform/transforms/slo-summary-${id}-3`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(404); - - await supertestAPI - .get(`/internal/transform/transforms/slo-${id}-4`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - - await supertestAPI - .get(`/internal/transform/transforms/slo-summary-${id}-4`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - - // change objective - await supertestAPI - .put(`/api/observability/slos/${id}`) - .set('kbn-xsrf', 'true') - .send({ - ...request, - objective: { - target: 0.97, - }, - }) - .expect(200); - - await supertestAPI - .get(`/internal/transform/transforms/slo-${id}-4`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(404); - - await supertestAPI - .get(`/internal/transform/transforms/slo-summary-${id}-4`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(404); - - await supertestAPI - .get(`/internal/transform/transforms/slo-${id}-5`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - - await supertestAPI - .get(`/internal/transform/transforms/slo-summary-${id}-5`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - - // change budgetingMethod - await supertestAPI - .put(`/api/observability/slos/${id}`) - .set('kbn-xsrf', 'true') - .send({ - ...request, - budgetingMethod: 'timeslices', - objective: { - target: 0.99, - timesliceTarget: 0.95, - timesliceWindow: '1m', - }, - }) - .expect(200); - - await supertestAPI - .get(`/internal/transform/transforms/slo-${id}-5`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(404); - - await supertestAPI - .get(`/internal/transform/transforms/slo-summary-${id}-5`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(404); - - await supertestAPI - .get(`/internal/transform/transforms/slo-${id}-6`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - - await supertestAPI - .get(`/internal/transform/transforms/slo-summary-${id}-6`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - - // change settings - await supertestAPI - .put(`/api/observability/slos/${id}`) - .set('kbn-xsrf', 'true') - .send({ - ...request, - settings: { - frequency: '2m', - syncDelay: '5m', - }, - }) - .expect(200); - - await supertestAPI - .get(`/internal/transform/transforms/slo-${id}-6`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(404); - - await supertestAPI - .get(`/internal/transform/transforms/slo-summary-${id}-6`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(404); - - await supertestAPI - .get(`/internal/transform/transforms/slo-${id}-7`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - - await supertestAPI - .get(`/internal/transform/transforms/slo-summary-${id}-7`) - .set('kbn-xsrf', 'true') - .set('elastic-api-version', '1') - .send() - .expect(200); - }); - }); -} diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/telemetry.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/telemetry.ts index 0c47e62fae79c..c83210e51e5d5 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/telemetry.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/telemetry.ts @@ -6,12 +6,14 @@ */ import expect from 'expect'; -import { getPostCaseRequest } from '../../../common/lib/mock'; +import { getPostCaseRequest, postCommentAlertReq } from '../../../common/lib/mock'; import { deleteAllCaseItems, createCase, getTelemetry, runTelemetryTask, + createComment, + bulkCreateAttachments, } from '../../../common/lib/api'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { superUser } from '../../../common/lib/authentication/users'; @@ -49,5 +51,52 @@ export default ({ getService }: FtrProviderContext): void => { expect(res.stats.stack_stats.kibana.plugins.cases.cases.all.total).toBe(2); }); }); + + it('should return the corect total number of alerts attached to cases', async () => { + const firstCase = await createCase(supertest, getPostCaseRequest()); + const secondCase = await createCase(supertest, getPostCaseRequest()); + + const firstCaseAlerts = [...Array(3).keys()].map((num) => `test-case-1-${num}`); + const secondCaseAlerts = [...Array(2).keys()].map((num) => `test-case-2-${num}`); + + await bulkCreateAttachments({ + supertest, + caseId: firstCase.id, + params: [ + { + ...postCommentAlertReq, + alertId: firstCaseAlerts, + index: firstCaseAlerts, + }, + ], + expectedHttpCode: 200, + }); + + await bulkCreateAttachments({ + supertest, + caseId: firstCase.id, + params: [ + { + ...postCommentAlertReq, + alertId: secondCaseAlerts, + index: secondCaseAlerts, + }, + ], + expectedHttpCode: 200, + }); + + await createComment({ + supertest, + caseId: secondCase.id, + params: { ...postCommentAlertReq, alertId: 'test-case-2-3', index: 'test-case-2-3' }, + }); + + await runTelemetryTask(supertest); + + await retry.try(async () => { + const res = await getTelemetry(supertest); + expect(res.stats.stack_stats.kibana.plugins.cases.alerts.all.total).toBe(6); + }); + }); }); }; diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/group2/configure.ts b/x-pack/test/functional_with_es_ssl/apps/cases/group2/configure.ts index ee013b882c487..8b0ade86ac580 100644 --- a/x-pack/test/functional_with_es_ssl/apps/cases/group2/configure.ts +++ b/x-pack/test/functional_with_es_ssl/apps/cases/group2/configure.ts @@ -52,8 +52,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); it('opens and closes the connectors flyout correctly', async () => { - await common.clickAndValidate('dropdown-connectors', 'dropdown-connector-add-connector'); - await common.clickAndValidate('dropdown-connector-add-connector', 'euiFlyoutCloseButton'); + await common.clickAndValidate('add-new-connector', 'euiFlyoutCloseButton'); await testSubjects.click('euiFlyoutCloseButton'); expect(await testSubjects.exists('euiFlyoutCloseButton')).to.be(false); }); diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/index.ts b/x-pack/test_serverless/api_integration/test_suites/observability/index.ts index 63f8236a335b6..bfe3fd4cbb2c6 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/index.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/index.ts @@ -15,7 +15,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./apm_api_integration/service_maps/service_maps')); loadTestFile(require.resolve('./apm_api_integration/traces/critical_path')); loadTestFile(require.resolve('./cases')); - loadTestFile(require.resolve('./slos')); loadTestFile(require.resolve('./synthetics')); loadTestFile(require.resolve('./dataset_quality_api_integration')); }); diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/slos/create_slo.ts b/x-pack/test_serverless/api_integration/test_suites/observability/slos/create_slo.ts deleted file mode 100644 index 93aaa77e4e215..0000000000000 --- a/x-pack/test_serverless/api_integration/test_suites/observability/slos/create_slo.ts +++ /dev/null @@ -1,371 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { cleanup, generate } from '@kbn/infra-forge'; -import expect from '@kbn/expect'; -import type { GetTransformsResponseSchema } from '@kbn/transform-plugin/server/routes/api_schemas/transforms'; -import { SO_SLO_TYPE } from '@kbn/slo-plugin/server/saved_objects'; -import { ALL_VALUE } from '@kbn/slo-schema'; -import { - getSLOPipelineId, - getSLOSummaryPipelineId, - SLO_SUMMARY_TEMP_INDEX_NAME, -} from '@kbn/slo-plugin/common/constants'; -import type { RoleCredentials } from '../../../../shared/services'; -import { FtrProviderContext } from '../../../ftr_provider_context'; - -interface ExpectedTransforms { - count: number; - typeOfVersion: string; - typeOfCreateTime: string; - results: Record; -} - -function assertTransformsResponseBody( - body: GetTransformsResponseSchema, - expectedTransforms: ExpectedTransforms -) { - expect(body.count).to.eql(expectedTransforms.count); - expect(body.transforms).to.have.length(expectedTransforms.count); - - body.transforms.forEach((transform, index) => { - const expectedTransform = expectedTransforms.results[`transform${index}`]; - expect(transform.id).to.eql(expectedTransform.id); - expect(transform.dest.index).to.eql(expectedTransform.destIndex); - expect(typeof transform.version).to.eql(expectedTransforms.typeOfVersion); - expect(typeof transform.create_time).to.eql(expectedTransforms.typeOfCreateTime); - }); -} - -export default function ({ getService }: FtrProviderContext) { - const esClient = getService('es'); - const supertest = getService('supertest'); - - const esDeleteAllIndices = getService('esDeleteAllIndices'); - const logger = getService('log'); - const dataViewApi = getService('dataViewApi'); - const sloApi = getService('sloApi'); - const kibanaServer = getService('kibanaServer'); - const transform = getService('transform'); - const svlUserManager = getService('svlUserManager'); - const svlCommonApi = getService('svlCommonApi'); - - describe('create_slo', () => { - // DATE_VIEW should match the index template: - // x-pack/packages/kbn-infra-forge/src/data_sources/composable/template.json - const DATE_VIEW = 'kbn-data-forge-fake_hosts'; - const DATA_VIEW_ID = 'data-view-id'; - let infraDataIndex: string; - let roleAuthc: RoleCredentials; - - before(async () => { - infraDataIndex = await generate({ - esClient, - lookback: 'now-15m', - logger, - }); - await dataViewApi.create({ - name: DATE_VIEW, - id: DATA_VIEW_ID, - title: DATE_VIEW, - }); - await kibanaServer.savedObjects.cleanStandardList(); - roleAuthc = await svlUserManager.createM2mApiKeyWithRoleScope('admin'); - }); - - after(async () => { - await dataViewApi.delete({ - id: DATA_VIEW_ID, - }); - await supertest - .delete('/api/observability/slos/my-custom-id1') - .set(svlCommonApi.getInternalRequestHeader()); - - await supertest - .delete('/api/observability/slos/my-custom-id2') - .set(svlCommonApi.getInternalRequestHeader()); - - await supertest - .delete('/api/observability/slos/my-custom-id3') - .set(svlCommonApi.getInternalRequestHeader()); - - await supertest - .delete('/api/observability/slos/my-custom-id4') - .set(svlCommonApi.getInternalRequestHeader()); - - await esDeleteAllIndices([infraDataIndex]); - await cleanup({ esClient, logger }); - await kibanaServer.savedObjects.clean({ types: [SO_SLO_TYPE] }); - await transform.api.cleanTransformIndices(); - await svlUserManager.invalidateM2mApiKeyWithRoleScope(roleAuthc); - }); - - describe('non partition by SLO', () => { - const sloId = 'my-custom-id1'; - - before(async () => { - await sloApi.create( - { - id: sloId, - name: 'my custom name', - description: 'my custom description', - indicator: { - type: 'sli.kql.custom', - params: { - index: infraDataIndex, - good: 'system.cpu.total.norm.pct > 1', - total: 'system.cpu.total.norm.pct: *', - timestampField: '@timestamp', - }, - }, - timeWindow: { - duration: '7d', - type: 'rolling', - }, - budgetingMethod: 'occurrences', - objective: { - target: 0.999, - }, - groupBy: ALL_VALUE, - }, - roleAuthc - ); - }); - - it('saves the SLO definition', async () => { - const savedObject = await kibanaServer.savedObjects.find({ - type: SO_SLO_TYPE, - }); - expect(savedObject.total).to.eql(1); - expect(savedObject.saved_objects[0].attributes.version).eql(2); - expect(savedObject.saved_objects[0].attributes.revision).eql(1); - }); - - it('creates the rollup and summary transforms', async () => { - const expectedTransforms: ExpectedTransforms = { - count: 2, - results: { - transform0: { id: 'slo-my-custom-id1-1', destIndex: '.slo-observability.sli-v3.3' }, - transform1: { - id: 'slo-summary-my-custom-id1-1', - destIndex: '.slo-observability.summary-v3.3', - }, - }, - typeOfVersion: 'string', - typeOfCreateTime: 'number', - }; - const { body, status } = await supertest - .get(`/internal/transform/transforms`) - .set('kbn-xsrf', 'foo') - .set('x-elastic-internal-origin', 'foo') - .set('elastic-api-version', '1') - .set(roleAuthc.apiKeyHeader) - .send(); - transform.api.assertResponseStatusCode(200, status, body); - assertTransformsResponseBody(body, expectedTransforms); - }); - - it('creates ingest pipelines', async () => { - const sloRevision = 1; - const rollupPipelineResponse = await esClient.ingest.getPipeline({ - id: getSLOPipelineId(sloId, sloRevision), - }); - const expectedRollupPipeline = `.slo-observability.sli.pipeline-${sloId}-${sloRevision}`; - expect(rollupPipelineResponse[expectedRollupPipeline]).not.to.be(undefined); - - const summaryPipelineResponse = await esClient.ingest.getPipeline({ - id: getSLOSummaryPipelineId(sloId, sloRevision), - }); - const expectedSummaryPipeline = `.slo-observability.summary.pipeline-${sloId}-${sloRevision}`; - expect(summaryPipelineResponse[expectedSummaryPipeline]).not.to.be(undefined); - expect(summaryPipelineResponse[expectedSummaryPipeline].description).to.be( - `Ingest pipeline for SLO summary data [id: ${sloId}, revision: ${sloRevision}]` - ); - }); - - it('creates summary TEMP index', async () => { - const result = await sloApi.waitForSloSummaryTempIndexToExist(SLO_SUMMARY_TEMP_INDEX_NAME); - expect(result).to.be(true); - }); - - it('finds the created SLO', async () => { - const createdSlo = await sloApi.waitForSloCreated({ - sloId, - roleAuthc, - }); - expect(createdSlo.id).to.be(sloId); - expect(createdSlo.groupBy).to.be(ALL_VALUE); - }); - }); - - describe('SLO with long description', () => { - it('creates an SLO with description over 256 characters', async () => { - const sloId = 'my-custom-id2'; - await sloApi.create( - { - id: sloId, - name: 'my super long SLO name and description', - description: - 'Lorem Ipsum has been the industry standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. ', - indicator: { - type: 'sli.kql.custom', - params: { - index: infraDataIndex, - good: 'system.cpu.total.norm.pct > 1', - total: 'system.cpu.total.norm.pct: *', - timestampField: '@timestamp', - }, - }, - timeWindow: { - duration: '7d', - type: 'rolling', - }, - budgetingMethod: 'occurrences', - objective: { - target: 0.999, - }, - groupBy: '*', - }, - roleAuthc - ); - - const createdSlo = await sloApi.waitForSloCreated({ - sloId, - roleAuthc, - }); - expect(createdSlo.id).to.be(sloId); - }); - }); - - describe('SLO with special characters in the description', () => { - it("creates an SLO that has ' character in the description", async () => { - const sloId = 'my-custom-id3'; - await sloApi.create( - { - id: sloId, - name: 'my SLO with weird characters in the description', - description: - "Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged.", - indicator: { - type: 'sli.kql.custom', - params: { - index: infraDataIndex, - good: 'system.cpu.total.norm.pct > 1', - total: 'system.cpu.total.norm.pct: *', - timestampField: '@timestamp', - }, - }, - timeWindow: { - duration: '7d', - type: 'rolling', - }, - budgetingMethod: 'occurrences', - objective: { - target: 0.999, - }, - groupBy: '*', - }, - roleAuthc - ); - const createdSlo = await sloApi.waitForSloCreated({ - sloId, - roleAuthc, - }); - expect(createdSlo.id).to.be(sloId); - }); - }); - - describe('partition by SLO', () => { - it('creates a partition by SLO', async () => { - const sloId = 'my-custom-id4'; - await sloApi.create( - { - id: sloId, - name: 'Group by SLO', - description: 'This is a group by SLO.', - indicator: { - type: 'sli.kql.custom', - params: { - index: infraDataIndex, - good: 'system.cpu.total.norm.pct > 1', - total: 'system.cpu.total.norm.pct: *', - timestampField: '@timestamp', - }, - }, - timeWindow: { - duration: '7d', - type: 'rolling', - }, - budgetingMethod: 'occurrences', - objective: { - target: 0.999, - }, - groupBy: 'host.name', - }, - roleAuthc - ); - const createdSlo = await sloApi.waitForSloCreated({ - sloId, - roleAuthc, - }); - expect(createdSlo.id).to.be(sloId); - expect(createdSlo.groupBy).not.to.be(ALL_VALUE); - expect(createdSlo.groupBy).to.be('host.name'); - }); - }); - - describe('Total transforms', () => { - it('returns all the transforms for above created SLOs', async () => { - const expectedTransforms: ExpectedTransforms = { - count: 8, - results: { - transform0: { id: 'slo-my-custom-id1-1', destIndex: '.slo-observability.sli-v3.3' }, - transform1: { id: 'slo-my-custom-id2-1', destIndex: '.slo-observability.sli-v3.3' }, - transform2: { id: 'slo-my-custom-id3-1', destIndex: '.slo-observability.sli-v3.3' }, - transform3: { id: 'slo-my-custom-id4-1', destIndex: '.slo-observability.sli-v3.3' }, - transform4: { - id: 'slo-summary-my-custom-id1-1', - destIndex: '.slo-observability.summary-v3.3', - }, - transform5: { - id: 'slo-summary-my-custom-id2-1', - destIndex: '.slo-observability.summary-v3.3', - }, - transform6: { - id: 'slo-summary-my-custom-id3-1', - destIndex: '.slo-observability.summary-v3.3', - }, - transform7: { - id: 'slo-summary-my-custom-id4-1', - destIndex: '.slo-observability.summary-v3.3', - }, - }, - typeOfVersion: 'string', - typeOfCreateTime: 'number', - }; - const { body, status } = await supertest - .get(`/internal/transform/transforms`) - .set('kbn-xsrf', 'foo') - .set('x-elastic-internal-origin', 'foo') - .set('elastic-api-version', '1') - .set(roleAuthc.apiKeyHeader) - .send(); - transform.api.assertResponseStatusCode(200, status, body); - assertTransformsResponseBody(body, expectedTransforms); - }); - }); - - describe('Total SO definitions', () => { - it('returns SO definitions for above created SLOs', async () => { - const savedObject = await kibanaServer.savedObjects.find({ - type: SO_SLO_TYPE, - }); - expect(savedObject.total).to.eql(4); - }); - }); - }); -} diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/slos/delete_slo.ts b/x-pack/test_serverless/api_integration/test_suites/observability/slos/delete_slo.ts deleted file mode 100644 index c33bde45e1720..0000000000000 --- a/x-pack/test_serverless/api_integration/test_suites/observability/slos/delete_slo.ts +++ /dev/null @@ -1,175 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { SO_SLO_TYPE } from '@kbn/slo-plugin/server/saved_objects'; -import { cleanup, generate } from '@kbn/infra-forge'; -import expect from '@kbn/expect'; -import { ALL_VALUE } from '@kbn/slo-schema'; -import { - getSLOSummaryTransformId, - getSLOTransformId, - getSLOSummaryPipelineId, -} from '@kbn/slo-plugin/common/constants'; -import { - SLO_DESTINATION_INDEX_PATTERN, - SLO_SUMMARY_DESTINATION_INDEX_PATTERN, -} from '@kbn/slo-plugin/common/constants'; -import type { RoleCredentials } from '../../../../shared/services'; -import { FtrProviderContext } from '../../../ftr_provider_context'; - -export default function ({ getService }: FtrProviderContext) { - const esClient = getService('es'); - const logger = getService('log'); - const kibanaServer = getService('kibanaServer'); - const sloApi = getService('sloApi'); - const transform = getService('transform'); - const retry = getService('retry'); - - const esDeleteAllIndices = getService('esDeleteAllIndices'); - const dataViewApi = getService('dataViewApi'); - const svlUserManager = getService('svlUserManager'); - - const fetchSloSummaryPipeline = async (sloId: string, sloRevision: number) => { - try { - return await esClient.ingest.getPipeline({ - id: getSLOSummaryPipelineId(sloId, sloRevision), - }); - } catch (error) { - // The GET /_ingest/pipeline API returns an empty object on 404 Not Found. If there are no SLO - // pipelines then return an empty record of pipelines - return {}; - } - }; - - describe('delete_slo', () => { - // DATE_VIEW should match the index template: - // x-pack/packages/kbn-infra-forge/src/data_sources/composable/template.json - const DATE_VIEW = 'kbn-data-forge-fake_hosts'; - const DATA_VIEW_ID = 'data-view-id'; - let infraDataIndex: string; - let sloId: string; - let roleAuthc: RoleCredentials; - - before(async () => { - await sloApi.deleteAllSLOs(); - - infraDataIndex = await generate({ - esClient, - lookback: 'now-15m', - logger, - }); - await dataViewApi.create({ - name: DATE_VIEW, - id: DATA_VIEW_ID, - title: DATE_VIEW, - }); - await kibanaServer.savedObjects.cleanStandardList(); - roleAuthc = await svlUserManager.createM2mApiKeyWithRoleScope('admin'); - }); - - after(async () => { - await dataViewApi.delete({ - id: DATA_VIEW_ID, - }); - await sloApi.deleteAllSLOs(); - await esDeleteAllIndices([infraDataIndex]); - await cleanup({ esClient, logger }); - await svlUserManager.invalidateM2mApiKeyWithRoleScope(roleAuthc); - }); - - describe('non partition by SLO', () => { - it('deletes the SLO definition, transforms, ingest pipeline and data', async () => { - const createdSlo = await sloApi.create( - { - name: 'my custom name', - description: 'my custom description', - indicator: { - type: 'sli.kql.custom', - params: { - index: infraDataIndex, - good: 'system.cpu.total.norm.pct > 1', - total: 'system.cpu.total.norm.pct: *', - timestampField: '@timestamp', - }, - }, - timeWindow: { - duration: '7d', - type: 'rolling', - }, - budgetingMethod: 'occurrences', - objective: { - target: 0.999, - }, - groupBy: ALL_VALUE, - }, - roleAuthc - ); - sloId = createdSlo.id; - await sloApi.waitForSloCreated({ sloId, roleAuthc }); - - // Saved Object - const savedObject = await kibanaServer.savedObjects.find({ - type: SO_SLO_TYPE, - }); - expect(savedObject.total).to.eql(1); - expect(savedObject.saved_objects[0].attributes.id).to.eql(sloId); - const sloRevision = savedObject.saved_objects[0].attributes.revision ?? 1; - - // Transforms - const sloTransformId = getSLOTransformId(sloId, sloRevision); - const sloSummaryTransformId = getSLOSummaryTransformId(sloId, sloRevision); - await transform.api.waitForTransformToExist(sloTransformId); - await transform.api.waitForTransformToExist(sloSummaryTransformId); - - // Ingest pipeline - const pipelineResponse = await fetchSloSummaryPipeline(sloId, sloRevision); - expect(pipelineResponse[getSLOSummaryPipelineId(sloId, sloRevision)]).not.to.be(undefined); - - // RollUp and Summary data - const sloRollupData = await sloApi.waitForSloData({ - sloId, - indexName: SLO_DESTINATION_INDEX_PATTERN, - }); - const sloSummaryData = await sloApi.waitForSloData({ - sloId, - indexName: SLO_SUMMARY_DESTINATION_INDEX_PATTERN, - }); - - expect(sloRollupData.hits.hits.length > 0).to.be(true); - expect(sloSummaryData.hits.hits.length > 0).to.be(true); - - // Delete the SLO - const response = await sloApi.waitForSloToBeDeleted({ - sloId, - roleAuthc, - }); - expect(response.status).to.be(204); - - // Saved object definition - const savedObjectAfterDelete = await kibanaServer.savedObjects.find({ - type: SO_SLO_TYPE, - }); - expect(savedObjectAfterDelete.total).to.eql(0); - - // Transforms - await transform.api.getTransform(sloTransformId, 404); - await transform.api.getTransform(sloSummaryTransformId, 404); - - await retry.waitForWithTimeout('SLO summary data is deleted', 60 * 1000, async () => { - const sloSummaryDataAfterDeletion = await sloApi.getSloData({ - sloId, - indexName: SLO_SUMMARY_DESTINATION_INDEX_PATTERN, - }); - if (sloSummaryDataAfterDeletion.hits.hits.length > 0) { - throw new Error('SLO summary data not deleted yet'); - } - return true; - }); - }); - }); - }); -} diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/slos/fetch_historical_summary.ts b/x-pack/test_serverless/api_integration/test_suites/observability/slos/fetch_historical_summary.ts deleted file mode 100644 index 2f8db3098fccf..0000000000000 --- a/x-pack/test_serverless/api_integration/test_suites/observability/slos/fetch_historical_summary.ts +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { - SLO_DESTINATION_INDEX_NAME, - SLO_DESTINATION_INDEX_PATTERN, -} from '@kbn/slo-plugin/common/constants'; - -import { ALL_VALUE } from '@kbn/slo-schema'; -import moment from 'moment'; -import { RoleCredentials } from '../../../../shared/services'; -import { FtrProviderContext } from '../../../ftr_provider_context'; - -export default function ({ getService }: FtrProviderContext) { - const esClient = getService('es'); - const esDeleteAllIndices = getService('esDeleteAllIndices'); - const sloApi = getService('sloApi'); - const svlUserManager = getService('svlUserManager'); - - const SLO_ID = 'slo-fake-1'; - describe('fetch historical summary', () => { - let roleAuthc: RoleCredentials; - - before(async () => { - const now = moment().startOf('minute'); - const curr = now.clone().subtract(30, 'days'); - const end = now.clone().add(5, 'minutes'); - - const batchOperations = []; - - while (curr.isSameOrBefore(end)) { - batchOperations.push([ - { index: { _index: SLO_DESTINATION_INDEX_NAME } }, - { - '@timestamp': curr.toISOString(), - slo: { - id: SLO_ID, - revision: 1, - instanceId: ALL_VALUE, - numerator: 90, - denominator: 100, - isGoodSlice: 1, - groupings: {}, - }, - }, - ]); - curr.add(1, 'minute'); - } - - await esClient.bulk({ - index: SLO_DESTINATION_INDEX_NAME, - operations: batchOperations.flat(), - refresh: 'wait_for', - }); - - await esClient.indices.refresh({ index: SLO_DESTINATION_INDEX_NAME }); - roleAuthc = await svlUserManager.createM2mApiKeyWithRoleScope('admin'); - }); - - after(async () => { - await esDeleteAllIndices(SLO_DESTINATION_INDEX_PATTERN); - await svlUserManager.invalidateM2mApiKeyWithRoleScope(roleAuthc); - }); - - it('computes the historical summary for a rolling occurrences SLO', async () => { - const response = await sloApi.fetchHistoricalSummary( - { - list: [ - { - sloId: SLO_ID, - instanceId: ALL_VALUE, - timeWindow: { - duration: '7d', - type: 'rolling', - }, - budgetingMethod: 'occurrences', - objective: { - target: 0.9, - }, - groupBy: ALL_VALUE, - revision: 1, - }, - ], - }, - roleAuthc - ); - expect(response[0].sloId).to.eql(SLO_ID); - expect(response[0].instanceId).to.eql(ALL_VALUE); - const numberOfBuckets = response[0].data.length; - expect(numberOfBuckets).to.be.within(168, 170); // 7 days * 24 hours/day * 1 bucket/hour + 2 extra bucket due to histogram agg rounding - const last = response[0].data.pop(); - expect(last?.errorBudget).to.eql({ - consumed: 1, - initial: 0.1, - isEstimated: false, - remaining: 0, - }); - expect(last?.sliValue).to.eql(0.9); - expect(last?.status).to.eql('HEALTHY'); - }); - - it('computes the historical summary for a rolling timeslices SLO', async () => { - const response = await sloApi.fetchHistoricalSummary( - { - list: [ - { - sloId: SLO_ID, - instanceId: ALL_VALUE, - timeWindow: { - duration: '7d', - type: 'rolling', - }, - budgetingMethod: 'timeslices', - objective: { - target: 0.9, - timesliceTarget: 0.8, - timesliceWindow: '1m', - }, - groupBy: ALL_VALUE, - revision: 1, - }, - ], - }, - roleAuthc - ); - expect(response[0].sloId).to.eql(SLO_ID); - expect(response[0].instanceId).to.eql(ALL_VALUE); - const numberOfBuckets = response[0].data.length; - expect(numberOfBuckets).to.be.within(168, 170); // 7 days * 24 hours/day * 1 bucket/hour + 2 extra bucket due to histogram agg rounding - const last = response[0].data.pop(); - expect(last?.errorBudget).to.eql({ - consumed: 0, - initial: 0.1, - isEstimated: false, - remaining: 1, - }); - expect(last?.sliValue).to.eql(1); - expect(last?.status).to.eql('HEALTHY'); - }); - }); -} diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/slos/index.ts b/x-pack/test_serverless/api_integration/test_suites/observability/slos/index.ts deleted file mode 100644 index 8df59e6f3b624..0000000000000 --- a/x-pack/test_serverless/api_integration/test_suites/observability/slos/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { FtrProviderContext } from '../../../ftr_provider_context'; - -export default function ({ loadTestFile }: FtrProviderContext) { - describe('SLOs', function () { - loadTestFile(require.resolve('./create_slo')); - loadTestFile(require.resolve('./delete_slo')); - loadTestFile(require.resolve('./fetch_historical_summary')); - }); -} diff --git a/x-pack/test_serverless/functional/test_suites/observability/cases/configure.ts b/x-pack/test_serverless/functional/test_suites/observability/cases/configure.ts index 1909408d05332..1887e76a65e62 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/cases/configure.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/cases/configure.ts @@ -66,8 +66,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); it('opens and closes the connectors flyout correctly', async () => { - await common.clickAndValidate('dropdown-connectors', 'dropdown-connector-add-connector'); - await common.clickAndValidate('dropdown-connector-add-connector', 'euiFlyoutCloseButton'); + await common.clickAndValidate('add-new-connector', 'euiFlyoutCloseButton'); await testSubjects.click('euiFlyoutCloseButton'); expect(await testSubjects.exists('euiFlyoutCloseButton')).to.be(false); }); diff --git a/x-pack/test_serverless/functional/test_suites/security/ftr/cases/configure.ts b/x-pack/test_serverless/functional/test_suites/security/ftr/cases/configure.ts index e185b4c470548..cc5486a354015 100644 --- a/x-pack/test_serverless/functional/test_suites/security/ftr/cases/configure.ts +++ b/x-pack/test_serverless/functional/test_suites/security/ftr/cases/configure.ts @@ -66,8 +66,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); it('opens and closes the connectors flyout correctly', async () => { - await common.clickAndValidate('dropdown-connectors', 'dropdown-connector-add-connector'); - await common.clickAndValidate('dropdown-connector-add-connector', 'euiFlyoutCloseButton'); + await common.clickAndValidate('add-new-connector', 'euiFlyoutCloseButton'); await testSubjects.click('euiFlyoutCloseButton'); expect(await testSubjects.exists('euiFlyoutCloseButton')).to.be(false); }); diff --git a/x-pack/test_serverless/tsconfig.json b/x-pack/test_serverless/tsconfig.json index 388a4732fdd9e..ed9a78fde0f6f 100644 --- a/x-pack/test_serverless/tsconfig.json +++ b/x-pack/test_serverless/tsconfig.json @@ -78,7 +78,6 @@ "@kbn/ftr-common-functional-ui-services", "@kbn/saved-objects-management-plugin", "@kbn/es", - "@kbn/infra-forge", "@kbn/reporting-common", "@kbn/es-query", "@kbn/slo-plugin",