diff --git a/config/serverless.oblt.yml b/config/serverless.oblt.yml index ba7664823834..945142d48d8d 100644 --- a/config/serverless.oblt.yml +++ b/config/serverless.oblt.yml @@ -1 +1,2 @@ +uiSettings.overrides.defaultRoute: /app/observability/overview xpack.infra.logs.app_target: discover diff --git a/docs/settings/alert-action-settings.asciidoc b/docs/settings/alert-action-settings.asciidoc index 383fae89bad6..ba41d20112ad 100644 --- a/docs/settings/alert-action-settings.asciidoc +++ b/docs/settings/alert-action-settings.asciidoc @@ -1,10 +1,14 @@ -[role="xpack"] [[alert-action-settings-kb]] -=== Alerting and action settings in {kib} +== Alerting and action settings in {kib} ++++ Alerting and action settings ++++ +:description: Learn about the settings that affect {kib} {alert-features}. +:tags-products: [kibana, alerting] +:tags-content-type: [reference] +:tags-user-goals: [configure] + Alerting and actions are enabled by default in {kib}, but require you to configure the following: . <>. @@ -15,7 +19,7 @@ You can configure the following settings in the `kibana.yml` file. [float] [[general-alert-action-settings]] -==== General settings +=== General settings `xpack.encryptedSavedObjects.encryptionKey`:: A string of 32 or more characters used to encrypt sensitive properties on alerting rules and actions before they're stored in {es}. Third party credentials — such as the username and password used to connect to an SMTP service — are an example of encrypted properties. @@ -29,7 +33,7 @@ Be sure to back up the encryption key value somewhere safe, as your alerting rul [float] [[action-settings]] -==== Action settings +=== Action settings `xpack.actions.allowedHosts` {ess-icon}:: A list of hostnames that {kib} is allowed to connect to when built-in actions are triggered. It defaults to `[*]`, allowing any host, but keep in mind the potential for SSRF attacks when hosts are not explicitly added to the allowed hosts. An empty list `[]` can be used to block built-in actions from making any external connections. @@ -71,7 +75,7 @@ xpack.actions.customHostSettings: ssl: verificationMode: 'none' -- - ++ The settings in `xpack.actions.customHostSettings` can be used to override the global option `xpack.actions.ssl.verificationMode` and provide customized TLS settings on a per-server basis. Set `xpack.actions.ssl.verificationMode` to the @@ -107,7 +111,7 @@ The options `smtp.ignoreTLS` and `smtp.requireTLS` can not both be set to true. Default: `false`. `xpack.actions.customHostSettings[n].ssl.rejectUnauthorized`:: -Deprecated. Use <> instead. A boolean value indicating whether to bypass server certificate validation. +deprecated:[8.0.0] Use <> instead. A boolean value indicating whether to bypass server certificate validation. Overrides the general `xpack.actions.rejectUnauthorized` configuration for requests made for this hostname/port. @@ -127,7 +131,7 @@ the files cannot be made available. [[action-config-email-domain-allowlist]] `xpack.actions.email.domain_allowlist` {ess-icon}:: A list of allowed email domains which can be used with the email connector. When this setting is not used, all email domains are allowed. When this setting is used, if any email is attempted to be sent that (a) includes an addressee with an email domain that is not in the allowlist, or (b) includes a from address domain that is not in the allowlist, it will fail with a message indicating the email is not allowed. - ++ WARNING: This feature is available in {kib} 7.17.4 and 8.3.0 onwards but is not supported in {kib} 8.0, 8.1 or 8.2. As such, this setting should be removed before upgrading from 7.17 to 8.0, 8.1 or 8.2. It is possible to configure the settings in 7.17.4 and then upgrade to 8.3.0 directly. `xpack.actions.enableFooterInEmail` {ess-icon}:: @@ -160,8 +164,6 @@ proxy in tunneling mode, and display some of the interaction between the client -- curl --verbose --proxytunnel --proxy http://localhost:8080 http://example.com -- -+ - `xpack.actions.proxyBypassHosts` {ess-icon}:: Specifies hostnames which should not use the proxy, if using a proxy for actions. The value is an array of hostnames as strings. By default, all hosts will use the proxy, but if an action's hostname is in this list, the proxy will not be used. The settings `xpack.actions.proxyBypassHosts` and `xpack.actions.proxyOnlyHosts` cannot be used at the same time. @@ -173,14 +175,14 @@ Specifies hostnames which should only use the proxy, if using a proxy for action Specifies HTTP headers for the proxy, if using a proxy for actions. Default: {}. `xpack.actions.proxyRejectUnauthorizedCertificates` {ess-icon}:: -Deprecated. Use <> instead. Set to `false` to bypass certificate validation for the proxy, if using a proxy for actions. Default: `true`. +deprecated:[8.0.0] Use <> instead. Set to `false` to bypass certificate validation for the proxy, if using a proxy for actions. Default: `true`. [[action-config-proxy-verification-mode]]`xpack.actions.ssl.proxyVerificationMode` {ess-icon}:: Controls the verification for the proxy server certificate that Kibana receives when making an outbound SSL/TLS connection to the proxy server. Valid values are `full`, `certificate`, and `none`. Use `full` to perform hostname verification, `certificate` to skip hostname verification, and `none` to skip verification. Default: `full`. <>. `xpack.actions.rejectUnauthorized` {ess-icon}:: -Deprecated. Use <> instead. Set to `false` to bypass certificate validation for actions. Default: `true`. +deprecated:[8.0.0] Use <> instead. Set to `false` to bypass certificate validation for actions. Default: `true`. + As an alternative to setting `xpack.actions.rejectUnauthorized`, you can use the setting `xpack.actions.customHostSettings` to set SSL options for specific servers. @@ -206,9 +208,8 @@ For example, `20m`, `24h`, `7d`, `1w`. Default: `60s`. Specifies the maximum number of times an action can be attempted to run. Can be minimum 1 and maximum 10. `xpack.actions.run.connectorTypeOverrides` {ess-icon}:: -Overrides the configs under `xpack.actions.run` for the connector type with the given ID. List the connector type identifier and its settings in an array of objects. +Overrides the configs under `xpack.actions.run` for the connector type with the given ID. List the connector type identifier and its settings in an array of objects. For example: + -For example: [source,yaml] -- xpack.actions.run: @@ -220,7 +221,7 @@ xpack.actions.run: [float] [[alert-settings]] -==== Alerting settings +=== Alerting settings `xpack.alerting.maxEphemeralActionsPerAlert` {ess-icon}:: deprecated:[8.8.0] @@ -257,9 +258,8 @@ Specifies the default timeout for tasks associated with all types of rules. The For example, `20m`, `24h`, `7d`, `1w`. Default: `5m`. `xpack.alerting.rules.run.ruleTypeOverrides` {ess-icon}:: -Overrides the configs under `xpack.alerting.rules.run` for the rule type with the given ID. List the rule identifier and its settings in an array of objects. +Overrides the configs under `xpack.alerting.rules.run` for the rule type with the given ID. List the rule identifier and its settings in an array of objects. For example: + -For example: [source,yaml] -- xpack.alerting.rules.run: @@ -270,9 +270,8 @@ xpack.alerting.rules.run: -- `xpack.alerting.rules.run.actions.connectorTypeOverrides` {ess-icon}:: -Overrides the configs under `xpack.alerting.rules.run.actions` for the connector type with the given ID. List the connector type identifier and its settings in an array of objects. +Overrides the configs under `xpack.alerting.rules.run.actions` for the connector type with the given ID. List the connector type identifier and its settings in an array of objects. For example: + -For example: [source,yaml] -- xpack.alerting.rules.run: diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index ca8eaf10583a..efa58a34d485 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -619,7 +619,7 @@ Set this value to false to disable the Upgrade Assistant UI. *Default: true* Set this value to change the {kib} interface language. Valid locales are: `en`, `zh-CN`, `ja-JP`. *Default: `en`* -include::{kib-repo-dir}/settings/alert-action-settings.asciidoc[] +include::{kib-repo-dir}/settings/alert-action-settings.asciidoc[leveloffset=+1] include::{kib-repo-dir}/settings/apm-settings.asciidoc[] include::{kib-repo-dir}/settings/banners-settings.asciidoc[] include::{kib-repo-dir}/settings/cases-settings.asciidoc[leveloffset=+1] diff --git a/packages/core/root/core-root-server-internal/src/bootstrap.test.mocks.ts b/packages/core/root/core-root-server-internal/src/bootstrap.test.mocks.ts new file mode 100644 index 000000000000..07277d565f69 --- /dev/null +++ b/packages/core/root/core-root-server-internal/src/bootstrap.test.mocks.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Env } from '@kbn/config'; +import { rawConfigServiceMock, configServiceMock } from '@kbn/config-mocks'; + +export const mockConfigService = configServiceMock.create(); +export const mockRawConfigService = rawConfigServiceMock.create(); +export const mockRawConfigServiceConstructor = jest.fn(() => mockRawConfigService); +jest.doMock('@kbn/config', () => ({ + ConfigService: jest.fn(() => mockConfigService), + Env, + RawConfigService: jest.fn(mockRawConfigServiceConstructor), +})); + +jest.doMock('./root', () => ({ + Root: jest.fn(() => ({ + shutdown: jest.fn(), + })), +})); diff --git a/packages/core/root/core-root-server-internal/src/bootstrap.test.ts b/packages/core/root/core-root-server-internal/src/bootstrap.test.ts new file mode 100644 index 000000000000..1bd413314aa9 --- /dev/null +++ b/packages/core/root/core-root-server-internal/src/bootstrap.test.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { of } from 'rxjs'; +import type { CliArgs } from '@kbn/config'; + +import { mockRawConfigService, mockRawConfigServiceConstructor } from './bootstrap.test.mocks'; + +jest.mock('@kbn/core-logging-server-internal'); + +import { bootstrap } from './bootstrap'; + +const bootstrapCfg = { + configs: ['config/kibana.yml'], + cliArgs: {} as unknown as CliArgs, + applyConfigOverrides: () => ({}), +}; + +describe('bootstrap', () => { + describe('serverless', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('should load additional serverless files for a valid project', async () => { + mockRawConfigService.getConfig$.mockReturnValue(of({ serverless: 'es' })); + await bootstrap(bootstrapCfg); + expect(mockRawConfigServiceConstructor).toHaveBeenCalledTimes(2); + expect(mockRawConfigServiceConstructor).toHaveBeenNthCalledWith( + 1, + bootstrapCfg.configs, + bootstrapCfg.applyConfigOverrides + ); + expect(mockRawConfigServiceConstructor).toHaveBeenNthCalledWith( + 2, + [ + expect.stringContaining('config/serverless.yml'), + expect.stringContaining('config/serverless.es.yml'), + ...bootstrapCfg.configs, + ], + bootstrapCfg.applyConfigOverrides + ); + }); + + test('should skip loading the serverless files for an invalid project', async () => { + mockRawConfigService.getConfig$.mockReturnValue(of({ serverless: 'not-valid' })); + await bootstrap(bootstrapCfg); + expect(mockRawConfigServiceConstructor).toHaveBeenCalledTimes(1); + expect(mockRawConfigServiceConstructor).toHaveBeenNthCalledWith( + 1, + bootstrapCfg.configs, + bootstrapCfg.applyConfigOverrides + ); + }); + }); +}); diff --git a/packages/core/root/core-root-server-internal/src/bootstrap.ts b/packages/core/root/core-root-server-internal/src/bootstrap.ts index 3b5a340a7b79..3f55b6493a6b 100644 --- a/packages/core/root/core-root-server-internal/src/bootstrap.ts +++ b/packages/core/root/core-root-server-internal/src/bootstrap.ts @@ -7,9 +7,14 @@ */ import chalk from 'chalk'; +import { firstValueFrom } from 'rxjs'; import { getPackages } from '@kbn/repo-packages'; import { CliArgs, Env, RawConfigService } from '@kbn/config'; import { CriticalError } from '@kbn/core-base-server-internal'; +import { resolve } from 'path'; +import { getConfigDirectory } from '@kbn/utils'; +import { statSync } from 'fs'; +import { VALID_SERVERLESS_PROJECT_TYPES } from './root/serverless_config'; import { Root } from './root'; import { MIGRATION_EXCEPTION_CODE } from './constants'; @@ -38,15 +43,40 @@ export async function bootstrap({ configs, cliArgs, applyConfigOverrides }: Boot // eslint-disable-next-line @typescript-eslint/no-var-requires const { REPO_ROOT } = require('@kbn/repo-info'); - const env = Env.createDefault(REPO_ROOT, { + let env = Env.createDefault(REPO_ROOT, { configs, cliArgs, repoPackages: getPackages(REPO_ROOT), }); - const rawConfigService = new RawConfigService(env.configs, applyConfigOverrides); + let rawConfigService = new RawConfigService(env.configs, applyConfigOverrides); rawConfigService.loadConfig(); + // Hack to load the extra serverless config files if `serverless: {projectType}` is found in it. + const rawConfig = await firstValueFrom(rawConfigService.getConfig$()); + const serverlessProjectType = rawConfig?.serverless; + if ( + typeof serverlessProjectType === 'string' && + VALID_SERVERLESS_PROJECT_TYPES.includes(serverlessProjectType) + ) { + const extendedConfigs = [ + ...['serverless.yml', `serverless.${serverlessProjectType}.yml`] + .map((name) => resolve(getConfigDirectory(), name)) + .filter(configFileExists), + ...configs, + ]; + + env = Env.createDefault(REPO_ROOT, { + configs: extendedConfigs, + cliArgs: { ...cliArgs, serverless: true }, + repoPackages: getPackages(REPO_ROOT), + }); + + rawConfigService.stop(); + rawConfigService = new RawConfigService(env.configs, applyConfigOverrides); + rawConfigService.loadConfig(); + } + const root = new Root(rawConfigService, env, onRootShutdown); process.on('SIGHUP', () => reloadConfiguration()); @@ -128,3 +158,15 @@ function onRootShutdown(reason?: any) { process.exit(0); } + +function configFileExists(path: string) { + try { + return statSync(path).isFile(); + } catch (err) { + if (err.code === 'ENOENT') { + return false; + } + + throw err; + } +} diff --git a/packages/core/root/core-root-server-internal/src/register_service_config.ts b/packages/core/root/core-root-server-internal/src/register_service_config.ts index a22ea56f25ee..f646f9e538ae 100644 --- a/packages/core/root/core-root-server-internal/src/register_service_config.ts +++ b/packages/core/root/core-root-server-internal/src/register_service_config.ts @@ -28,6 +28,7 @@ import { uiSettingsConfig } from '@kbn/core-ui-settings-server-internal'; import { config as pluginsConfig } from '@kbn/core-plugins-server-internal'; import { elasticApmConfig } from './root/elastic_config'; +import { serverlessConfig } from './root/serverless_config'; const rootConfigPath = ''; @@ -49,6 +50,7 @@ export function registerServiceConfig(configService: ConfigService) { pluginsConfig, savedObjectsConfig, savedObjectsMigrationConfig, + serverlessConfig, statusConfig, uiSettingsConfig, ]; diff --git a/packages/core/root/core-root-server-internal/src/root/serverless_config.ts b/packages/core/root/core-root-server-internal/src/root/serverless_config.ts new file mode 100644 index 000000000000..351065f7d83d --- /dev/null +++ b/packages/core/root/core-root-server-internal/src/root/serverless_config.ts @@ -0,0 +1,34 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { schema, TypeOf, Type } from '@kbn/config-schema'; +import { ServiceConfigDescriptor } from '@kbn/core-base-server-internal'; + +// Config validation for how to run Kibana in Serverless mode. +// Clients need to specify the project type to run in. +// Going for a simple `serverless` string because it serves as +// a direct replacement to the legacy --serverless CLI flag. +// If we even decide to extend this further, and converting it into an object, +// BWC can be ensured by adding the object definition as another alternative to `schema.oneOf`. + +export const VALID_SERVERLESS_PROJECT_TYPES = ['es', 'oblt', 'security']; + +const serverlessConfigSchema = schema.maybe( + schema.oneOf( + VALID_SERVERLESS_PROJECT_TYPES.map((projectName) => schema.literal(projectName)) as [ + Type // This cast is needed because it's different to Type[] :sight: + ] + ) +); + +export type ServerlessConfigType = TypeOf; + +export const serverlessConfig: ServiceConfigDescriptor = { + path: 'serverless', + schema: serverlessConfigSchema, +}; diff --git a/src/cli/serve/integration_tests/serverless_config_flag.test.ts b/src/cli/serve/integration_tests/serverless_config_flag.test.ts new file mode 100644 index 000000000000..6c67ba6261eb --- /dev/null +++ b/src/cli/serve/integration_tests/serverless_config_flag.test.ts @@ -0,0 +1,88 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { spawn, spawnSync } from 'child_process'; +import { readFileSync } from 'fs'; +import { resolve } from 'path'; +import { filter, firstValueFrom, from, take, concatMap } from 'rxjs'; + +import { REPO_ROOT } from '@kbn/repo-info'; +import { getConfigDirectory } from '@kbn/utils'; + +describe('cli serverless project type', () => { + it( + 'exits with statusCode 1 and logs an error when serverless project type is invalid', + () => { + const { error, status, stdout } = spawnSync( + process.execPath, + ['scripts/kibana', '--serverless=non-existing-project-type'], + { + cwd: REPO_ROOT, + } + ); + expect(error).toBe(undefined); + + expect(stdout.toString('utf8')).toContain( + 'FATAL CLI ERROR Error: invalid --serverless value, must be one of es, oblt, security' + ); + + expect(status).toBe(1); + }, + 20 * 1000 + ); + + // Skipping this one because on CI it fails to read the config file + it.skip.each(['es', 'oblt', 'security'])( + 'writes the serverless project type %s in config/serverless.recent.yml', + async (mode) => { + // Making sure `--serverless` translates into the `serverless` config entry, and validates against the accepted values + const child = spawn(process.execPath, ['scripts/kibana', `--serverless=${mode}`], { + cwd: REPO_ROOT, + }); + + // Wait for 5 lines in the logs + await firstValueFrom(from(child.stdout).pipe(take(5))); + + expect( + readFileSync(resolve(getConfigDirectory(), 'serverless.recent.yml'), 'utf-8') + ).toContain(`serverless: ${mode}\n`); + + child.kill('SIGKILL'); + } + ); + + it.each(['es', 'oblt', 'security'])( + 'Kibana does not crash when running project type %s', + async (mode) => { + const child = spawn(process.execPath, ['scripts/kibana', `--serverless=${mode}`], { + cwd: REPO_ROOT, + }); + + // Wait until Kibana starts listening to the port + let leftover = ''; + const found = await firstValueFrom( + from(child.stdout).pipe( + concatMap((chunk: Buffer) => { + const data = leftover + chunk.toString('utf-8'); + const msgs = data.split('\n'); + leftover = msgs.pop() ?? ''; + return msgs; + }), + filter( + (msg) => + msg.includes('http server running at http://localhost:5601') || msg.includes('FATAL') + ) + ) + ); + + child.kill('SIGKILL'); + + expect(found).not.toContain('FATAL'); + } + ); +}); diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index 9facf9440823..c1b9f04fd8d8 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -8,7 +8,7 @@ import { set as lodashSet } from '@kbn/safer-lodash-set'; import _ from 'lodash'; -import { statSync } from 'fs'; +import { statSync, existsSync, readFileSync, writeFileSync } from 'fs'; import { resolve } from 'path'; import url from 'url'; @@ -22,14 +22,14 @@ const VALID_SERVERLESS_PROJECT_MODE = ['es', 'oblt', 'security']; /** * @param {Record} opts - * @returns {ServerlessProjectMode | null} + * @returns {ServerlessProjectMode | true | null} */ function getServerlessProjectMode(opts) { if (!opts.serverless) { return null; } - if (VALID_SERVERLESS_PROJECT_MODE.includes(opts.serverless)) { + if (VALID_SERVERLESS_PROJECT_MODE.includes(opts.serverless) || opts.serverless === true) { return opts.serverless; } @@ -94,16 +94,6 @@ function configFileExists(name) { } } -/** - * @returns {boolean} Whether the distribution can run in Serverless mode - */ -function isServerlessCapableDistribution() { - // For now, checking if the `serverless.yml` config file exists should be enough - // We could also check the following as well, but I don't think it's necessary: - // VALID_SERVERLESS_PROJECT_MODE.some((projectType) => configFileExists(`serverless.${projectType}.yml`)) - return configFileExists('serverless.yml'); -} - /** * @param {string} name * @param {string[]} configs @@ -115,6 +105,48 @@ function maybeAddConfig(name, configs, method) { } } +/** + * @param {string} file + * @param {'es' | 'security' | 'oblt' | true} projectType + * @param {boolean} isDevMode + * @param {string[]} configs + * @param {'push' | 'unshift'} method + */ +function maybeSetRecentConfig(file, projectType, isDevMode, configs, method) { + const path = resolve(getConfigDirectory(), file); + + function writeMode(selectedProjectType) { + writeFileSync( + path, + `${ + isDevMode ? 'xpack.serverless.plugin.developer.projectSwitcher.enabled: true\n' : '' + }serverless: ${selectedProjectType}\n` + ); + } + + try { + if (projectType === true) { + if (!existsSync(path)) { + writeMode('es'); + } + } else { + const data = readFileSync(path, 'utf-8'); + const match = data.match(/serverless: (\w+)\n/); + if (!match || match[1] !== projectType) { + writeMode(projectType); + } + } + + configs[method](path); + } catch (err) { + if (err.code === 'ENOENT') { + return; + } + + throw err; + } +} + /** * @returns {string[]} */ @@ -251,13 +283,14 @@ export default function (program) { .option( '--run-examples', 'Adds plugin paths for all the Kibana example plugins and runs with no base path' + ) + .option( + '--serverless [oblt|security|es]', + 'Start Kibana in a specific serverless project mode. ' + + 'If no mode is provided, it starts Kibana in the most recent serverless project mode (default is es)' ); } - if (isServerlessCapableDistribution()) { - command.option('--serverless ', 'Start Kibana in a serverless project mode'); - } - if (DEV_MODE_SUPPORTED) { command .option('--dev', 'Run the server with development mode defaults') @@ -282,10 +315,8 @@ export default function (program) { const configs = [getConfigPath(), ...getEnvConfigs(), ...(opts.config || [])]; const serverlessMode = getServerlessProjectMode(opts); - // we "unshift" .serverless. config so that it only overrides defaults if (serverlessMode) { - maybeAddConfig(`serverless.yml`, configs, 'push'); - maybeAddConfig(`serverless.${serverlessMode}.yml`, configs, 'unshift'); + maybeSetRecentConfig('serverless.recent.yml', serverlessMode, opts.dev, configs, 'push'); } // .dev. configs are "pushed" so that they override all other config files @@ -293,7 +324,7 @@ export default function (program) { maybeAddConfig('kibana.dev.yml', configs, 'push'); if (serverlessMode) { maybeAddConfig(`serverless.dev.yml`, configs, 'push'); - maybeAddConfig(`serverless.${serverlessMode}.dev.yml`, configs, 'push'); + maybeAddConfig('serverless.recent.dev.yml', configs, 'push'); } } @@ -315,7 +346,6 @@ export default function (program) { oss: !!opts.oss, cache: !!opts.cache, dist: !!opts.dist, - serverless: !!opts.serverless, }; // In development mode, the main process uses the @kbn/dev-cli-mode diff --git a/src/plugins/discover/public/application/main/hooks/use_test_based_query_language.test.ts b/src/plugins/discover/public/application/main/hooks/use_test_based_query_language.test.ts index 7ac2fb6ef791..d512477d9f53 100644 --- a/src/plugins/discover/public/application/main/hooks/use_test_based_query_language.test.ts +++ b/src/plugins/discover/public/application/main/hooks/use_test_based_query_language.test.ts @@ -14,21 +14,24 @@ import { useTextBasedQueryLanguage } from './use_text_based_query_language'; import { BehaviorSubject } from 'rxjs'; import { FetchStatus } from '../../types'; import { DataDocuments$, RecordRawType } from '../services/discover_data_state_container'; +import { DiscoverAppState } from '../services/discover_app_state_container'; import { DataTableRecord } from '../../../types'; import { AggregateQuery, Query } from '@kbn/es-query'; import { dataViewMock } from '../../../__mocks__/data_view'; import { DataViewListItem } from '@kbn/data-views-plugin/common'; import { savedSearchMock } from '../../../__mocks__/saved_search'; import { getDiscoverStateMock } from '../../../__mocks__/discover_state.mock'; +import { VIEW_MODE } from '@kbn/saved-search-plugin/common'; function getHookProps( query: AggregateQuery | Query | undefined, - dataViewsService?: DataViewsContract + dataViewsService?: DataViewsContract, + appState?: Partial ) { const replaceUrlState = jest.fn(); const stateContainer = getDiscoverStateMock({ isTimeBased: true }); stateContainer.appState.replaceUrlState = replaceUrlState; - stateContainer.appState.update({ columns: [] }); + stateContainer.appState.update({ columns: [], ...appState }); stateContainer.internalState.transitions.setSavedDataViews([dataViewMock as DataViewListItem]); const msgLoading = { @@ -83,6 +86,20 @@ describe('useTextBasedQueryLanguage', () => { }); }); }); + test('should change viewMode to DOCUMENT_LEVEL if it was AGGREGATED_LEVEL', async () => { + const props = getHookProps(query, undefined, { + viewMode: VIEW_MODE.AGGREGATED_LEVEL, + }); + const { replaceUrlState } = props; + + renderHook(() => useTextBasedQueryLanguage(props)); + + await waitFor(() => expect(replaceUrlState).toHaveBeenCalledTimes(1)); + expect(replaceUrlState).toHaveBeenCalledWith({ + index: 'the-data-view-id', + viewMode: VIEW_MODE.DOCUMENT_LEVEL, + }); + }); test('changing a text based query with different result columns should change state when loading and finished', async () => { const props = getHookProps(query); const { documents$, replaceUrlState } = props; diff --git a/src/plugins/discover/public/application/main/hooks/use_text_based_query_language.ts b/src/plugins/discover/public/application/main/hooks/use_text_based_query_language.ts index 5e2eb33efdbe..ff4012127485 100644 --- a/src/plugins/discover/public/application/main/hooks/use_text_based_query_language.ts +++ b/src/plugins/discover/public/application/main/hooks/use_text_based_query_language.ts @@ -14,9 +14,10 @@ import { } from '@kbn/es-query'; import { useCallback, useEffect, useRef } from 'react'; import type { DataViewsContract } from '@kbn/data-views-plugin/public'; -import { SavedSearch } from '@kbn/saved-search-plugin/public'; +import { SavedSearch, VIEW_MODE } from '@kbn/saved-search-plugin/public'; import type { DiscoverStateContainer } from '../services/discover_state'; import type { DataDocuments$ } from '../services/discover_data_state_container'; +import { getValidViewMode } from '../utils/get_valid_view_mode'; import { FetchStatus } from '../../types'; const MAX_NUM_OF_COLUMNS = 50; @@ -57,7 +58,7 @@ export function useTextBasedQueryLanguage({ if (!query || next.fetchStatus === FetchStatus.ERROR) { return; } - const { columns: stateColumns, index } = stateContainer.appState.getState(); + const { columns: stateColumns, index, viewMode } = stateContainer.appState.getState(); let nextColumns: string[] = []; const isTextBasedQueryLang = recordRawType === 'plain' && isOfAggregateQueryType(query) && 'sql' in query; @@ -115,6 +116,9 @@ export function useTextBasedQueryLanguage({ const nextState = { ...(addDataViewToState && { index: dataViewObj.id }), ...(addColumnsToState && { columns: nextColumns }), + ...(viewMode === VIEW_MODE.AGGREGATED_LEVEL && { + viewMode: getValidViewMode({ viewMode, isTextBasedQueryMode: true }), + }), }; stateContainer.appState.replaceUrlState(nextState); } else { diff --git a/src/plugins/discover/public/application/main/utils/get_raw_record_type.ts b/src/plugins/discover/public/application/main/utils/get_raw_record_type.ts index 595d54420e7a..36d9544b9e85 100644 --- a/src/plugins/discover/public/application/main/utils/get_raw_record_type.ts +++ b/src/plugins/discover/public/application/main/utils/get_raw_record_type.ts @@ -25,7 +25,3 @@ export function getRawRecordType(query?: Query | AggregateQuery) { return RecordRawType.DOCUMENT; } - -export function isPlainRecord(query?: Query | AggregateQuery): query is AggregateQuery { - return getRawRecordType(query) === RecordRawType.PLAIN; -} diff --git a/src/plugins/discover/public/application/main/utils/get_state_defaults.test.ts b/src/plugins/discover/public/application/main/utils/get_state_defaults.test.ts index aed900821c74..a994adc340f2 100644 --- a/src/plugins/discover/public/application/main/utils/get_state_defaults.test.ts +++ b/src/plugins/discover/public/application/main/utils/get_state_defaults.test.ts @@ -8,8 +8,9 @@ import { getStateDefaults } from './get_state_defaults'; import { createSearchSourceMock } from '@kbn/data-plugin/public/mocks'; +import { VIEW_MODE } from '@kbn/saved-search-plugin/common'; import { dataViewWithTimefieldMock } from '../../../__mocks__/data_view_with_timefield'; -import { savedSearchMock } from '../../../__mocks__/saved_search'; +import { savedSearchMock, savedSearchMockWithSQL } from '../../../__mocks__/saved_search'; import { dataViewMock } from '../../../__mocks__/data_view'; import { discoverServiceMock } from '../../../__mocks__/services'; @@ -75,4 +76,42 @@ describe('getStateDefaults', () => { } `); }); + + test('should set view mode correctly', () => { + const actualForUndefinedViewMode = getStateDefaults({ + services: discoverServiceMock, + savedSearch: { + ...savedSearchMockWithSQL, + viewMode: undefined, + }, + }); + expect(actualForUndefinedViewMode.viewMode).toBeUndefined(); + + const actualForTextBasedWithInvalidViewMode = getStateDefaults({ + services: discoverServiceMock, + savedSearch: { + ...savedSearchMockWithSQL, + viewMode: VIEW_MODE.AGGREGATED_LEVEL, + }, + }); + expect(actualForTextBasedWithInvalidViewMode.viewMode).toBe(VIEW_MODE.DOCUMENT_LEVEL); + + const actualForTextBasedWithValidViewMode = getStateDefaults({ + services: discoverServiceMock, + savedSearch: { + ...savedSearchMockWithSQL, + viewMode: VIEW_MODE.DOCUMENT_LEVEL, + }, + }); + expect(actualForTextBasedWithValidViewMode.viewMode).toBe(VIEW_MODE.DOCUMENT_LEVEL); + + const actualForWithValidViewMode = getStateDefaults({ + services: discoverServiceMock, + savedSearch: { + ...savedSearchMock, + viewMode: VIEW_MODE.AGGREGATED_LEVEL, + }, + }); + expect(actualForWithValidViewMode.viewMode).toBe(VIEW_MODE.AGGREGATED_LEVEL); + }); }); diff --git a/src/plugins/discover/public/application/main/utils/get_state_defaults.ts b/src/plugins/discover/public/application/main/utils/get_state_defaults.ts index 8acdc9ce6728..1ec3be8c3ec1 100644 --- a/src/plugins/discover/public/application/main/utils/get_state_defaults.ts +++ b/src/plugins/discover/public/application/main/utils/get_state_defaults.ts @@ -19,6 +19,8 @@ import { SEARCH_FIELDS_FROM_SOURCE, SORT_DEFAULT_ORDER_SETTING, } from '../../../../common'; +import { isTextBasedQuery } from './is_text_based_query'; +import { getValidViewMode } from './get_valid_view_mode'; function getDefaultColumns(savedSearch: SavedSearch, uiSettings: IUiSettingsClient) { if (savedSearch.columns && savedSearch.columns.length > 0) { @@ -81,7 +83,10 @@ export function getStateDefaults({ defaultState.rowHeight = savedSearch.rowHeight; } if (savedSearch.viewMode) { - defaultState.viewMode = savedSearch.viewMode; + defaultState.viewMode = getValidViewMode({ + viewMode: savedSearch.viewMode, + isTextBasedQueryMode: isTextBasedQuery(query), + }); } if (savedSearch.hideAggregatedPreview) { defaultState.hideAggregatedPreview = savedSearch.hideAggregatedPreview; diff --git a/src/plugins/discover/public/application/main/utils/get_valid_view_mode.test.ts b/src/plugins/discover/public/application/main/utils/get_valid_view_mode.test.ts new file mode 100644 index 000000000000..33431ba09b7c --- /dev/null +++ b/src/plugins/discover/public/application/main/utils/get_valid_view_mode.test.ts @@ -0,0 +1,58 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { VIEW_MODE } from '@kbn/saved-search-plugin/common'; +import { getValidViewMode } from './get_valid_view_mode'; + +describe('getValidViewMode', () => { + test('should work correctly for regular mode', () => { + expect( + getValidViewMode({ + viewMode: undefined, + isTextBasedQueryMode: false, + }) + ).toBeUndefined(); + + expect( + getValidViewMode({ + viewMode: VIEW_MODE.DOCUMENT_LEVEL, + isTextBasedQueryMode: false, + }) + ).toBe(VIEW_MODE.DOCUMENT_LEVEL); + + expect( + getValidViewMode({ + viewMode: VIEW_MODE.AGGREGATED_LEVEL, + isTextBasedQueryMode: false, + }) + ).toBe(VIEW_MODE.AGGREGATED_LEVEL); + }); + + test('should work correctly for text-based mode', () => { + expect( + getValidViewMode({ + viewMode: undefined, + isTextBasedQueryMode: true, + }) + ).toBeUndefined(); + + expect( + getValidViewMode({ + viewMode: VIEW_MODE.DOCUMENT_LEVEL, + isTextBasedQueryMode: true, + }) + ).toBe(VIEW_MODE.DOCUMENT_LEVEL); + + expect( + getValidViewMode({ + viewMode: VIEW_MODE.AGGREGATED_LEVEL, + isTextBasedQueryMode: true, + }) + ).toBe(VIEW_MODE.DOCUMENT_LEVEL); + }); +}); diff --git a/src/plugins/discover/public/application/main/utils/get_valid_view_mode.ts b/src/plugins/discover/public/application/main/utils/get_valid_view_mode.ts new file mode 100644 index 000000000000..4d6d660c28ee --- /dev/null +++ b/src/plugins/discover/public/application/main/utils/get_valid_view_mode.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { VIEW_MODE } from '@kbn/saved-search-plugin/public'; + +/** + * Returns a valid view mode + * @param viewMode + * @param isTextBasedQueryMode + */ +export const getValidViewMode = ({ + viewMode, + isTextBasedQueryMode, +}: { + viewMode?: VIEW_MODE; + isTextBasedQueryMode: boolean; +}): VIEW_MODE | undefined => { + if (viewMode === VIEW_MODE.AGGREGATED_LEVEL && isTextBasedQueryMode) { + // only this mode is supported for text-based languages + return VIEW_MODE.DOCUMENT_LEVEL; + } + + return viewMode; +}; diff --git a/src/plugins/discover/public/application/main/utils/is_text_based_query.test.ts b/src/plugins/discover/public/application/main/utils/is_text_based_query.test.ts new file mode 100644 index 000000000000..53e85216ba0b --- /dev/null +++ b/src/plugins/discover/public/application/main/utils/is_text_based_query.test.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { isTextBasedQuery } from './is_text_based_query'; + +describe('isTextBasedQuery', () => { + it('should work correctly', () => { + expect(isTextBasedQuery({ query: '', language: 'lucene' })).toEqual(false); + expect(isTextBasedQuery({ sql: 'SELECT * from foo' })).toEqual(true); + expect(isTextBasedQuery()).toEqual(false); + }); +}); diff --git a/src/plugins/discover/public/application/main/utils/is_text_based_query.ts b/src/plugins/discover/public/application/main/utils/is_text_based_query.ts new file mode 100644 index 000000000000..bf86af930423 --- /dev/null +++ b/src/plugins/discover/public/application/main/utils/is_text_based_query.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import type { AggregateQuery, Query } from '@kbn/es-query'; +import { RecordRawType } from '../services/discover_data_state_container'; +import { getRawRecordType } from './get_raw_record_type'; + +/** + * Checks if the query is of AggregateQuery type + * @param query + */ +export function isTextBasedQuery(query?: Query | AggregateQuery): query is AggregateQuery { + return getRawRecordType(query) === RecordRawType.PLAIN; +} diff --git a/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx index 0e315d3b348e..131791658818 100644 --- a/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx +++ b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx @@ -37,7 +37,6 @@ import { SavedSearch } from '@kbn/saved-search-plugin/public'; import { METRIC_TYPE } from '@kbn/analytics'; import { VIEW_MODE } from '../../common/constants'; import { getSortForEmbeddable, SortPair } from '../utils/sorting'; -import { RecordRawType } from '../application/main/services/discover_data_state_container'; import { buildDataTableRecord } from '../utils/build_data_record'; import { DataTableRecord, EsHitRecord } from '../types'; import { ISearchEmbeddable, SearchInput, SearchOutput } from './types'; @@ -59,7 +58,8 @@ import { DiscoverGridSettings } from '../components/discover_grid/types'; import { DocTableProps } from '../components/doc_table/doc_table_wrapper'; import { updateSearchSource } from './utils/update_search_source'; import { FieldStatisticsTable } from '../application/main/components/field_stats_table'; -import { getRawRecordType } from '../application/main/utils/get_raw_record_type'; +import { isTextBasedQuery } from '../application/main/utils/is_text_based_query'; +import { getValidViewMode } from '../application/main/utils/get_valid_view_mode'; import { fetchSql } from '../application/main/utils/fetch_sql'; import { ADHOC_DATA_VIEW_RENDER_EVENT } from '../constants'; @@ -169,6 +169,11 @@ export class SavedSearchEmbeddable return true; } + private isTextBasedSearch = (savedSearch: SavedSearch): boolean => { + const query = savedSearch.searchSource.getField('query'); + return isTextBasedQuery(query); + }; + private fetch = async () => { const searchSessionId = this.input.searchSessionId; const useNewFieldsApi = !this.services.uiSettings.get(SEARCH_FIELDS_FROM_SOURCE, false); @@ -229,8 +234,7 @@ export class SavedSearchEmbeddable const query = this.savedSearch.searchSource.getField('query'); const dataView = this.savedSearch.searchSource.getField('index')!; - const recordRawType = getRawRecordType(query); - const useSql = recordRawType === RecordRawType.PLAIN; + const useSql = this.isTextBasedSearch(this.savedSearch); try { // Request SQL data @@ -515,9 +519,14 @@ export class SavedSearchEmbeddable return; } + const viewMode = getValidViewMode({ + viewMode: this.savedSearch.viewMode, + isTextBasedQueryMode: this.isTextBasedSearch(this.savedSearch), + }); + if ( this.services.uiSettings.get(SHOW_FIELD_STATISTICS) === true && - this.savedSearch.viewMode === VIEW_MODE.AGGREGATED_LEVEL && + viewMode === VIEW_MODE.AGGREGATED_LEVEL && searchProps.services && searchProps.dataView && Array.isArray(searchProps.columns) diff --git a/src/plugins/unified_field_list/public/components/error_boundary/error_boundary.tsx b/src/plugins/unified_field_list/public/components/error_boundary/error_boundary.tsx new file mode 100644 index 000000000000..3fe938b3cbdf --- /dev/null +++ b/src/plugins/unified_field_list/public/components/error_boundary/error_boundary.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; + +/** + * Renders nothing instead of a component which triggered an exception. + */ +export class ErrorBoundary extends React.Component<{}, { hasError: boolean }> { + constructor(props: {}) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError() { + return { hasError: true }; + } + + render() { + if (this.state.hasError) { + return null; + } + + return this.props.children; + } +} diff --git a/src/plugins/unified_field_list/public/components/error_boundary/index.tsx b/src/plugins/unified_field_list/public/components/error_boundary/index.tsx new file mode 100644 index 000000000000..4e2cd318ddcf --- /dev/null +++ b/src/plugins/unified_field_list/public/components/error_boundary/index.tsx @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { ErrorBoundary } from './error_boundary'; diff --git a/src/plugins/unified_field_list/public/components/field_popover/field_popover.tsx b/src/plugins/unified_field_list/public/components/field_popover/field_popover.tsx index 59c6b9621f8c..42806637b8cd 100644 --- a/src/plugins/unified_field_list/public/components/field_popover/field_popover.tsx +++ b/src/plugins/unified_field_list/public/components/field_popover/field_popover.tsx @@ -22,8 +22,24 @@ export const FieldPopover: React.FC = ({ renderContent, ...otherPopoverProps }) => { - const header = (isOpen && renderHeader?.()) || null; - const content = (isOpen && renderContent?.()) || null; + let header = null; + let content = null; + + if (isOpen) { + try { + header = renderHeader?.() || null; + } catch (error) { + // eslint-disable-next-line no-console + console.error(error); + } + + try { + content = renderContent?.() || null; + } catch (error) { + // eslint-disable-next-line no-console + console.error(error); + } + } return ( = (props) => { +const FieldPopoverFooterComponent: React.FC = (props) => { const [visualizeButton, setVisualizeButton] = useState(null); const [categorizeButton, setCategorizeButton] = useState(null); useEffect(() => { - getFieldVisualizeButton(props).then(setVisualizeButton); - getFieldCategorizeButton(props).then(setCategorizeButton); + getFieldVisualizeButton(props) + .then(setVisualizeButton) + .catch((error) => { + // eslint-disable-next-line no-console + console.error(error); + }); + getFieldCategorizeButton(props) + .then(setCategorizeButton) + .catch((error) => { + // eslint-disable-next-line no-console + console.error(error); + }); }, [props]); return visualizeButton || categorizeButton ? ( @@ -30,3 +41,11 @@ export const FieldPopoverFooter: React.FC = (props) => ) : null; }; + +export const FieldPopoverFooter: React.FC = (props) => { + return ( + + + + ); +}; diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx index ae5857c4feaf..028ceddc080c 100755 --- a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx +++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx @@ -47,6 +47,7 @@ import { getDefaultColor, } from './field_top_values'; import { FieldSummaryMessage } from './field_summary_message'; +import { ErrorBoundary } from '../error_boundary'; export interface FieldStatsState { isLoading: boolean; @@ -540,25 +541,6 @@ const FieldStatsComponent: React.FC = ({ return null; }; -class ErrorBoundary extends React.Component<{}, { hasError: boolean }> { - constructor(props: FieldStatsProps) { - super(props); - this.state = { hasError: false }; - } - - static getDerivedStateFromError() { - return { hasError: true }; - } - - render() { - if (this.state.hasError) { - return null; - } - - return this.props.children; - } -} - /** * Component which fetches and renders stats for a data view field * @param props diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.test.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.test.ts index 92977df6021d..4875c7872a20 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.test.ts @@ -166,6 +166,12 @@ describe('Enterprise Search Managed Indices', () => { mockTrainedModelsProvider = { getTrainedModels: jest.fn(), getTrainedModelsStats: jest.fn(), + startTrainedModelDeployment: jest.fn(), + stopTrainedModelDeployment: jest.fn(), + inferTrainedModel: jest.fn(), + deleteTrainedModel: jest.fn(), + updateTrainedModelDeployment: jest.fn(), + putTrainedModel: jest.fn(), } as MlTrainedModels; mockMl = { @@ -1060,6 +1066,12 @@ describe('Enterprise Search Managed Indices', () => { mockTrainedModelsProvider = { getTrainedModels: jest.fn(), getTrainedModelsStats: jest.fn(), + startTrainedModelDeployment: jest.fn(), + stopTrainedModelDeployment: jest.fn(), + inferTrainedModel: jest.fn(), + deleteTrainedModel: jest.fn(), + updateTrainedModelDeployment: jest.fn(), + putTrainedModel: jest.fn(), } as MlTrainedModels; mockMl = { diff --git a/x-pack/plugins/ml/server/lib/ml_client/types.ts b/x-pack/plugins/ml/server/lib/ml_client/types.ts index bba5e9b49a0a..88d1475b23ff 100644 --- a/x-pack/plugins/ml/server/lib/ml_client/types.ts +++ b/x-pack/plugins/ml/server/lib/ml_client/types.ts @@ -13,12 +13,15 @@ export interface UpdateTrainedModelDeploymentRequest { model_id: string; number_of_allocations: number; } +export interface UpdateTrainedModelDeploymentResponse { + acknowledge: boolean; +} export interface MlClient extends OrigMlClient { anomalySearch: ReturnType['anomalySearch']; updateTrainedModelDeployment: ( payload: UpdateTrainedModelDeploymentRequest - ) => Promise<{ acknowledge: boolean }>; + ) => Promise; } export type MlClientParams = diff --git a/x-pack/plugins/ml/server/shared_services/providers/trained_models.ts b/x-pack/plugins/ml/server/shared_services/providers/trained_models.ts index 808b1a2f0d4a..ca2b08702ce7 100644 --- a/x-pack/plugins/ml/server/shared_services/providers/trained_models.ts +++ b/x-pack/plugins/ml/server/shared_services/providers/trained_models.ts @@ -7,7 +7,10 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { KibanaRequest, SavedObjectsClientContract } from '@kbn/core/server'; -import { UpdateTrainedModelDeploymentRequest } from '../../lib/ml_client/types'; +import type { + UpdateTrainedModelDeploymentRequest, + UpdateTrainedModelDeploymentResponse, +} from '../../lib/ml_client/types'; import type { GetGuards } from '../shared_services'; export interface TrainedModelsProvider { @@ -21,6 +24,24 @@ export interface TrainedModelsProvider { getTrainedModelsStats( params: estypes.MlGetTrainedModelsStatsRequest ): Promise; + startTrainedModelDeployment( + params: estypes.MlStartTrainedModelDeploymentRequest + ): Promise; + stopTrainedModelDeployment( + params: estypes.MlStopTrainedModelDeploymentRequest + ): Promise; + inferTrainedModel( + params: estypes.MlInferTrainedModelRequest + ): Promise; + deleteTrainedModel( + params: estypes.MlDeleteTrainedModelRequest + ): Promise; + updateTrainedModelDeployment( + params: UpdateTrainedModelDeploymentRequest + ): Promise; + putTrainedModel( + params: estypes.MlPutTrainedModelRequest + ): Promise; }; } @@ -80,11 +101,19 @@ export function getTrainedModelsProvider(getGuards: GetGuards): TrainedModelsPro async updateTrainedModelDeployment(params: UpdateTrainedModelDeploymentRequest) { return await guards .isFullLicense() - .hasMlCapabilities(['canStartStopTrainedModels']) + .hasMlCapabilities(['canCreateTrainedModels']) .ok(async ({ mlClient }) => { return mlClient.updateTrainedModelDeployment(params); }); }, + async putTrainedModel(params: estypes.MlPutTrainedModelRequest) { + return await guards + .isFullLicense() + .hasMlCapabilities(['canCreateTrainedModels']) + .ok(async ({ mlClient }) => { + return mlClient.putTrainedModel(params); + }); + }, }; }, }; diff --git a/x-pack/test/functional/apps/ingest_pipelines/ingest_pipelines.ts b/x-pack/test/functional/apps/ingest_pipelines/ingest_pipelines.ts index baae09f47c53..479d5972a867 100644 --- a/x-pack/test/functional/apps/ingest_pipelines/ingest_pipelines.ts +++ b/x-pack/test/functional/apps/ingest_pipelines/ingest_pipelines.ts @@ -26,7 +26,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const es = getService('es'); const security = getService('security'); - describe('Ingest Pipelines', function () { + // Failing: See https://github.com/elastic/kibana/issues/156014 + // Failing: See https://github.com/elastic/kibana/issues/118593 + // Failing: See https://github.com/elastic/kibana/issues/156015 + describe.skip('Ingest Pipelines', function () { this.tags('smoke'); before(async () => { await security.testUser.setRoles(['ingest_pipelines_user']);