[Ingest] Add Global settings flyout (#64276)

This commit is contained in:
Nicolas Chaulet 2020-04-27 09:06:33 -04:00 committed by GitHub
parent 8c05a724e7
commit 6c98b2368b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 899 additions and 54 deletions

View file

@ -12,3 +12,4 @@ export * from './datasource';
export * from './epm';
export * from './output';
export * from './enrollment_api_key';
export * from './settings';

View file

@ -48,6 +48,19 @@ export const AGENT_CONFIG_API_ROUTES = {
FULL_INFO_PATTERN: `${AGENT_CONFIG_API_ROOT}/{agentConfigId}/full`,
};
// Output API routes
export const OUTPUT_API_ROUTES = {
LIST_PATTERN: `${API_ROOT}/outputs`,
INFO_PATTERN: `${API_ROOT}/outputs/{outputId}`,
UPDATE_PATTERN: `${API_ROOT}/outputs/{outputId}`,
};
// Settings API routes
export const SETTINGS_API_ROUTES = {
INFO_PATTERN: `${API_ROOT}/settings`,
UPDATE_PATTERN: `${API_ROOT}/settings`,
};
// Agent API routes
export const AGENT_API_ROUTES = {
LIST_PATTERN: `${FLEET_API_ROOT}/agents`,

View file

@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export const GLOBAL_SETTINGS_SAVED_OBJECT_TYPE = 'ingest_manager_settings';

View file

@ -13,6 +13,8 @@ import {
AGENT_API_ROUTES,
ENROLLMENT_API_KEY_ROUTES,
SETUP_API_ROUTE,
OUTPUT_API_ROUTES,
SETTINGS_API_ROUTES,
} from '../constants';
export const epmRouteService = {
@ -112,6 +114,18 @@ export const agentRouteService = {
getStatusPath: () => AGENT_API_ROUTES.STATUS_PATTERN,
};
export const outputRoutesService = {
getInfoPath: (outputId: string) => OUTPUT_API_ROUTES.INFO_PATTERN.replace('{outputId}', outputId),
getUpdatePath: (outputId: string) =>
OUTPUT_API_ROUTES.UPDATE_PATTERN.replace('{outputId}', outputId),
getListPath: () => OUTPUT_API_ROUTES.LIST_PATTERN,
};
export const settingsRoutesService = {
getInfoPath: () => SETTINGS_API_ROUTES.INFO_PATTERN,
getUpdatePath: () => SETTINGS_API_ROUTES.UPDATE_PATTERN,
};
export const enrollmentAPIKeyRouteService = {
getListPath: () => ENROLLMENT_API_KEY_ROUTES.LIST_PATTERN,
getCreatePath: () => ENROLLMENT_API_KEY_ROUTES.CREATE_PATTERN,

View file

@ -11,3 +11,4 @@ export * from './data_stream';
export * from './output';
export * from './epm';
export * from './enrollment_api_key';
export * from './settings';

View file

@ -0,0 +1,19 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { SavedObjectAttributes } from 'src/core/public';
interface BaseSettings {
agent_auto_upgrade?: boolean;
package_auto_upgrade?: boolean;
kibana_url?: string;
kibana_ca_sha256?: string;
}
export interface Settings extends BaseSettings {
id: string;
}
export interface SettingsSOAttributes extends BaseSettings, SavedObjectAttributes {}

View file

@ -12,3 +12,5 @@ export * from './fleet_setup';
export * from './epm';
export * from './enrollment_api_key';
export * from './install_script';
export * from './output';
export * from './settings';

View file

@ -0,0 +1,40 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Output } from '../models';
export interface GetOneOutputResponse {
item: Output;
success: boolean;
}
export interface GetOneOutputRequest {
params: {
outputId: string;
};
}
export interface PutOutputRequest {
params: {
outputId: string;
};
body: {
hosts?: string[];
ca_sha256?: string;
};
}
export interface PutOutputResponse {
item: Output;
success: boolean;
}
export interface GetOutputsResponse {
items: Output[];
total: number;
page: number;
perPage: number;
success: boolean;
}

View file

@ -0,0 +1,20 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Settings } from '../models';
export interface GetSettingsResponse {
item: Settings;
success: boolean;
}
export interface PutSettingsRequest {
body: Partial<Omit<Settings, 'id'>>;
}
export interface PutSettingsResponse {
item: Settings;
success: boolean;
}

View file

@ -7,3 +7,4 @@ export { Loading } from './loading';
export { Error } from './error';
export { Header, HeaderProps } from './header';
export { AlphaMessaging } from './alpha_messaging';
export * from './settings_flyout';

View file

@ -0,0 +1,240 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useEffect } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiFlyout,
EuiFlyoutBody,
EuiFlyoutHeader,
EuiTitle,
EuiFlexGroup,
EuiFlexItem,
EuiButtonEmpty,
EuiSpacer,
EuiButton,
EuiFlyoutFooter,
EuiForm,
EuiFormRow,
EuiFieldText,
EuiRadioGroup,
EuiComboBox,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiText } from '@elastic/eui';
import { useInput, useComboInput, useCore, useGetSettings, sendPutSettings } from '../hooks';
import { useGetOutputs, sendPutOutput } from '../hooks/use_request/outputs';
interface Props {
onClose: () => void;
}
function useSettingsForm(outputId: string | undefined) {
const { notifications } = useCore();
const kibanaUrlInput = useInput();
const elasticsearchUrlInput = useComboInput([]);
return {
onSubmit: async () => {
try {
if (!outputId) {
throw new Error('Unable to load outputs');
}
await sendPutOutput(outputId, {
hosts: elasticsearchUrlInput.value,
});
await sendPutSettings({
kibana_url: kibanaUrlInput.value,
});
} catch (error) {
notifications.toasts.addError(error, {
title: 'Error',
});
}
notifications.toasts.addSuccess(
i18n.translate('xpack.ingestManager.settings.success.message', {
defaultMessage: 'Settings saved',
})
);
},
inputs: {
kibanaUrl: kibanaUrlInput,
elasticsearchUrl: elasticsearchUrlInput,
},
};
}
export const SettingFlyout: React.FunctionComponent<Props> = ({ onClose }) => {
const core = useCore();
const settingsRequest = useGetSettings();
const settings = settingsRequest?.data?.item;
const outputsRequest = useGetOutputs();
const output = outputsRequest.data?.items?.[0];
const { inputs, onSubmit } = useSettingsForm(output?.id);
useEffect(() => {
if (output) {
inputs.elasticsearchUrl.setValue(output.hosts || []);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [output]);
useEffect(() => {
if (settings) {
inputs.kibanaUrl.setValue(
settings.kibana_url || `${window.location.origin}${core.http.basePath.get()}`
);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [settings]);
const body = (
<EuiForm>
<EuiRadioGroup
options={[
{
id: 'enabled',
label: i18n.translate('xpack.ingestManager.settings.autoUpgradeEnabledLabel', {
defaultMessage:
'Automatically update agent binaries to use the latest minor version.',
}),
},
{
id: 'disabled',
disabled: true,
label: i18n.translate('xpack.ingestManager.settings.autoUpgradeDisabledLabel', {
defaultMessage: 'Manually manage agent binary versions. Requires Gold license.',
}),
},
]}
idSelected={'enabled'}
onChange={id => {}}
legend={{
children: (
<EuiTitle size="xs">
<h3>
<FormattedMessage
id="xpack.ingestManager.settings.autoUpgradeFieldLabel"
defaultMessage="Elastic Agent binary version"
/>
</h3>
</EuiTitle>
),
}}
/>
<EuiSpacer size="l" />
<EuiRadioGroup
options={[
{
id: 'enabled',
label: i18n.translate(
'xpack.ingestManager.settings.integrationUpgradeEnabledFieldLabel',
{
defaultMessage:
'Automatically update Integrations to the latest version to receive the latest assets. Agent configurations may need to be updated in order to use new features.',
}
),
},
{
id: 'disabled',
disabled: true,
label: i18n.translate(
'xpack.ingestManager.settings.integrationUpgradeDisabledFieldLabel',
{
defaultMessage: 'Manually manage integration versions yourself.',
}
),
},
]}
idSelected={'enabled'}
onChange={id => {}}
legend={{
children: (
<EuiTitle size="xs">
<h3>
<FormattedMessage
id="xpack.ingestManager.settings.integrationUpgradeFieldLabel"
defaultMessage="Elastic integration version"
/>
</h3>
</EuiTitle>
),
}}
/>
<EuiSpacer size="l" />
<EuiTitle size="s">
<h3>
<FormattedMessage
id="xpack.ingestManager.settings.globalOutputTitle"
defaultMessage="Global output"
/>
</h3>
</EuiTitle>
<EuiSpacer size="s" />
<EuiText color="subdued" size="s">
<FormattedMessage
id="xpack.ingestManager.settings.globalOutputDescription"
defaultMessage="The global output is applied to all agent configurations and specifies where data is sent."
/>
</EuiText>
<EuiSpacer size="m" />
<EuiFormRow>
<EuiFormRow
label={i18n.translate('xpack.ingestManager.settings.kibanaUrlLabel', {
defaultMessage: 'Kibana URL',
})}
>
<EuiFieldText required={true} {...inputs.kibanaUrl.props} name="kibanaUrl" />
</EuiFormRow>
</EuiFormRow>
<EuiSpacer size="m" />
<EuiFormRow>
<EuiFormRow
label={i18n.translate('xpack.ingestManager.settings.elasticsearchUrlLabel', {
defaultMessage: 'Elasticsearch URL',
})}
>
<EuiComboBox noSuggestions {...inputs.elasticsearchUrl.props} />
</EuiFormRow>
</EuiFormRow>
</EuiForm>
);
return (
<EuiFlyout onClose={onClose} size="l" maxWidth={640}>
<EuiFlyoutHeader hasBorder aria-labelledby="IngestManagerSettingsFlyoutTitle">
<EuiTitle size="m">
<h2 id="IngestManagerSettingsFlyoutTitle">
<FormattedMessage
id="xpack.ingestManager.settings.flyoutTitle"
defaultMessage="Ingest Management settings"
/>
</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>{body}</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty iconType="cross" onClick={onClose} flush="left">
<FormattedMessage
id="xpack.ingestManager.settings.cancelButtonLabel"
defaultMessage="Cancel"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton onClick={onSubmit} iconType="save">
<FormattedMessage
id="xpack.ingestManager.settings.saveButtonLabel"
defaultMessage="Save settings"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</EuiFlyout>
);
};

View file

@ -20,5 +20,27 @@ export function useInput(defaultValue = '') {
clear: () => {
setValue('');
},
setValue,
};
}
export function useComboInput(defaultValue = []) {
const [value, setValue] = React.useState<string[]>(defaultValue);
return {
props: {
selectedOptions: value.map((val: string) => ({ label: val })),
onCreateOption: (newVal: any) => {
setValue([...value, newVal]);
},
onChange: (newVals: any[]) => {
setValue(newVals.map(val => val.label));
},
},
value,
clear: () => {
setValue([]);
},
setValue,
};
}

View file

@ -10,3 +10,5 @@ export * from './data_stream';
export * from './agents';
export * from './enrollment_api_keys';
export * from './epm';
export * from './outputs';
export * from './settings';

View file

@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { sendRequest, useRequest } from './use_request';
import { outputRoutesService } from '../../services';
import { PutOutputRequest, GetOutputsResponse } from '../../types';
export function useGetOutputs() {
return useRequest<GetOutputsResponse>({
method: 'get',
path: outputRoutesService.getListPath(),
});
}
export function sendPutOutput(outputId: string, body: PutOutputRequest['body']) {
return sendRequest({
method: 'put',
path: outputRoutesService.getUpdatePath(outputId),
body,
});
}

View file

@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { sendRequest, useRequest } from './use_request';
import { settingsRoutesService } from '../../services';
import { PutSettingsResponse, PutSettingsRequest, GetSettingsResponse } from '../../types';
export function useGetSettings() {
return useRequest<GetSettingsResponse>({
method: 'get',
path: settingsRoutesService.getInfoPath(),
});
}
export function sendPutSettings(body: PutSettingsRequest['body']) {
return sendRequest<PutSettingsResponse>({
method: 'put',
path: settingsRoutesService.getUpdatePath(),
body,
});
}

View file

@ -5,10 +5,10 @@
*/
import React from 'react';
import styled from 'styled-components';
import { EuiTabs, EuiTab, EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui';
import { EuiTabs, EuiTab, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiButtonEmpty } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { Section } from '../sections';
import { AlphaMessaging } from '../components';
import { AlphaMessaging, SettingFlyout } from '../components';
import { useLink, useConfig } from '../hooks';
import { EPM_PATH, FLEET_PATH, AGENT_CONFIG_PATH, DATA_STREAM_PATH } from '../constants';
@ -35,59 +35,79 @@ const Nav = styled.nav`
export const DefaultLayout: React.FunctionComponent<Props> = ({ section, children }) => {
const { epm, fleet } = useConfig();
const [isSettingsFlyoutOpen, setIsSettingsFlyoutOpen] = React.useState(false);
return (
<Container>
<Nav>
<EuiFlexGroup gutterSize="l" alignItems="center">
<EuiFlexItem grow={false}>
<EuiIcon type="savedObjectsApp" size="l" />
</EuiFlexItem>
<EuiFlexItem>
<EuiTabs display="condensed">
<EuiTab isSelected={section === 'overview'} href={useLink()}>
<>
{isSettingsFlyoutOpen && (
<SettingFlyout
onClose={() => {
setIsSettingsFlyoutOpen(false);
}}
/>
)}
<Container>
<Nav>
<EuiFlexGroup gutterSize="l" alignItems="center">
<EuiFlexItem grow={false}>
<EuiIcon type="savedObjectsApp" size="l" />
</EuiFlexItem>
<EuiFlexItem>
<EuiTabs display="condensed">
<EuiTab isSelected={section === 'overview'} href={useLink()}>
<FormattedMessage
id="xpack.ingestManager.appNavigation.overviewLinkText"
defaultMessage="Overview"
/>
</EuiTab>
<EuiTab
isSelected={section === 'epm'}
href={useLink(EPM_PATH)}
disabled={!epm?.enabled}
>
<FormattedMessage
id="xpack.ingestManager.appNavigation.epmLinkText"
defaultMessage="Integrations"
/>
</EuiTab>
<EuiTab isSelected={section === 'agent_config'} href={useLink(AGENT_CONFIG_PATH)}>
<FormattedMessage
id="xpack.ingestManager.appNavigation.configurationsLinkText"
defaultMessage="Configurations"
/>
</EuiTab>
<EuiTab
isSelected={section === 'fleet'}
href={useLink(FLEET_PATH)}
disabled={!fleet?.enabled}
>
<FormattedMessage
id="xpack.ingestManager.appNavigation.fleetLinkText"
defaultMessage="Fleet"
/>
</EuiTab>
<EuiTab isSelected={section === 'data_stream'} href={useLink(DATA_STREAM_PATH)}>
<FormattedMessage
id="xpack.ingestManager.appNavigation.dataStreamsLinkText"
defaultMessage="Data streams"
/>
</EuiTab>
</EuiTabs>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty iconType="gear" onClick={() => setIsSettingsFlyoutOpen(true)}>
<FormattedMessage
id="xpack.ingestManager.appNavigation.overviewLinkText"
defaultMessage="Overview"
id="xpack.ingestManager.appNavigation.settingsButton"
defaultMessage="General settings"
/>
</EuiTab>
<EuiTab
isSelected={section === 'epm'}
href={useLink(EPM_PATH)}
disabled={!epm?.enabled}
>
<FormattedMessage
id="xpack.ingestManager.appNavigation.epmLinkText"
defaultMessage="Integrations"
/>
</EuiTab>
<EuiTab isSelected={section === 'agent_config'} href={useLink(AGENT_CONFIG_PATH)}>
<FormattedMessage
id="xpack.ingestManager.appNavigation.configurationsLinkText"
defaultMessage="Configurations"
/>
</EuiTab>
<EuiTab
isSelected={section === 'fleet'}
href={useLink(FLEET_PATH)}
disabled={!fleet?.enabled}
>
<FormattedMessage
id="xpack.ingestManager.appNavigation.fleetLinkText"
defaultMessage="Fleet"
/>
</EuiTab>
<EuiTab isSelected={section === 'data_stream'} href={useLink(DATA_STREAM_PATH)}>
<FormattedMessage
id="xpack.ingestManager.appNavigation.dataStreamsLinkText"
defaultMessage="Data streams"
/>
</EuiTab>
</EuiTabs>
</EuiFlexItem>
</EuiFlexGroup>
</Nav>
{children}
<AlphaMessaging />
</Container>
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</Nav>
{children}
<AlphaMessaging />
</Container>
</>
);
};

View file

@ -15,6 +15,8 @@ export {
enrollmentAPIKeyRouteService,
epmRouteService,
setupRouteService,
outputRoutesService,
settingsRoutesService,
packageToConfigDatasourceInputs,
storedDatasourceToAgentDatasource,
AgentStatusKueryHelper,

View file

@ -17,6 +17,7 @@ export {
DatasourceInput,
DatasourceInputStream,
DatasourceConfigRecordEntry,
Output,
DataStream,
// API schemas - Agent Config
GetAgentConfigsResponse,
@ -48,6 +49,14 @@ export {
GetEnrollmentAPIKeysResponse,
GetEnrollmentAPIKeysRequest,
GetOneEnrollmentAPIKeyResponse,
// API schemas - Outputs
GetOutputsResponse,
PutOutputRequest,
PutOutputResponse,
// API schemas - Settings
GetSettingsResponse,
PutSettingsRequest,
PutSettingsResponse,
// EPM types
AssetReference,
AssetsGroupedByServiceByType,

View file

@ -19,7 +19,9 @@ export {
FLEET_SETUP_API_ROUTES,
ENROLLMENT_API_KEY_ROUTES,
INSTALL_SCRIPT_API_ROUTES,
OUTPUT_API_ROUTES,
SETUP_API_ROUTE,
SETTINGS_API_ROUTES,
// Saved object types
AGENT_SAVED_OBJECT_TYPE,
AGENT_EVENT_SAVED_OBJECT_TYPE,
@ -30,6 +32,7 @@ export {
PACKAGES_SAVED_OBJECT_TYPE,
INDEX_PATTERN_SAVED_OBJECT_TYPE,
ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE,
GLOBAL_SETTINGS_SAVED_OBJECT_TYPE as GLOBAL_SETTINGS_SAVED_OBJET_TYPE,
// Defaults
DEFAULT_AGENT_CONFIG,
DEFAULT_OUTPUT,

View file

@ -39,6 +39,8 @@ import {
registerAgentRoutes,
registerEnrollmentApiKeyRoutes,
registerInstallScriptRoutes,
registerOutputRoutes,
registerSettingsRoutes,
} from './routes';
import { IngestManagerConfigType } from '../common';
@ -150,6 +152,8 @@ export class IngestManagerPlugin
// Register routes
registerAgentConfigRoutes(router);
registerDatasourceRoutes(router);
registerOutputRoutes(router);
registerSettingsRoutes(router);
registerDataStreamRoutes(router);
// Conditional routes

View file

@ -11,3 +11,5 @@ export { registerRoutes as registerSetupRoutes } from './setup';
export { registerRoutes as registerAgentRoutes } from './agent';
export { registerRoutes as registerEnrollmentApiKeyRoutes } from './enrollment_api_key';
export { registerRoutes as registerInstallScriptRoutes } from './install_script';
export { registerRoutes as registerOutputRoutes } from './output';
export { registerRoutes as registerSettingsRoutes } from './settings';

View file

@ -0,0 +1,90 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { RequestHandler } from 'src/core/server';
import { TypeOf } from '@kbn/config-schema';
import { GetOneOutputRequestSchema, PutOutputRequestSchema } from '../../types';
import { GetOneOutputResponse, GetOutputsResponse } from '../../../common';
import { outputService } from '../../services/output';
export const getOutputsHandler: RequestHandler = async (context, request, response) => {
const soClient = context.core.savedObjects.client;
try {
const outputs = await outputService.list(soClient);
const body: GetOutputsResponse = {
items: outputs.items,
page: outputs.page,
perPage: outputs.perPage,
total: outputs.total,
success: true,
};
return response.ok({ body });
} catch (e) {
return response.customError({
statusCode: 500,
body: { message: e.message },
});
}
};
export const getOneOuputHandler: RequestHandler<TypeOf<
typeof GetOneOutputRequestSchema.params
>> = async (context, request, response) => {
const soClient = context.core.savedObjects.client;
try {
const output = await outputService.get(soClient, request.params.outputId);
const body: GetOneOutputResponse = {
item: output,
success: true,
};
return response.ok({ body });
} catch (e) {
if (e.isBoom && e.output.statusCode === 404) {
return response.notFound({
body: { message: `Output ${request.params.outputId} not found` },
});
}
return response.customError({
statusCode: 500,
body: { message: e.message },
});
}
};
export const putOuputHandler: RequestHandler<
TypeOf<typeof PutOutputRequestSchema.params>,
undefined,
TypeOf<typeof PutOutputRequestSchema.body>
> = async (context, request, response) => {
const soClient = context.core.savedObjects.client;
try {
await outputService.update(soClient, request.params.outputId, request.body);
const output = await outputService.get(soClient, request.params.outputId);
const body: GetOneOutputResponse = {
item: output,
success: true,
};
return response.ok({ body });
} catch (e) {
if (e.isBoom && e.output.statusCode === 404) {
return response.notFound({
body: { message: `Output ${request.params.outputId} not found` },
});
}
return response.customError({
statusCode: 500,
body: { message: e.message },
});
}
};

View file

@ -0,0 +1,41 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { IRouter } from 'src/core/server';
import { PLUGIN_ID, OUTPUT_API_ROUTES } from '../../constants';
import { getOneOuputHandler, getOutputsHandler, putOuputHandler } from './handler';
import {
GetOneOutputRequestSchema,
GetOutputsRequestSchema,
PutOutputRequestSchema,
} from '../../types';
export const registerRoutes = (router: IRouter) => {
router.get(
{
path: OUTPUT_API_ROUTES.LIST_PATTERN,
validate: GetOutputsRequestSchema,
options: { tags: [`access:${PLUGIN_ID}-read`] },
},
getOutputsHandler
);
router.get(
{
path: OUTPUT_API_ROUTES.INFO_PATTERN,
validate: GetOneOutputRequestSchema,
options: { tags: [`access:${PLUGIN_ID}-read`] },
},
getOneOuputHandler
);
router.put(
{
path: OUTPUT_API_ROUTES.UPDATE_PATTERN,
validate: PutOutputRequestSchema,
options: { tags: [`access:${PLUGIN_ID}-read`] },
},
putOuputHandler
);
};

View file

@ -0,0 +1,81 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { IRouter, RequestHandler } from 'src/core/server';
import { TypeOf } from '@kbn/config-schema';
import { PLUGIN_ID, SETTINGS_API_ROUTES } from '../../constants';
import { PutSettingsRequestSchema, GetSettingsRequestSchema } from '../../types';
import { settingsService } from '../../services';
export const getSettingsHandler: RequestHandler = async (context, request, response) => {
const soClient = context.core.savedObjects.client;
try {
const settings = await settingsService.getSettings(soClient);
const body = {
success: true,
item: settings,
};
return response.ok({ body });
} catch (e) {
if (e.isBoom && e.output.statusCode === 404) {
return response.notFound({
body: { message: `Setings not found` },
});
}
return response.customError({
statusCode: 500,
body: { message: e.message },
});
}
};
export const putSettingsHandler: RequestHandler<
undefined,
undefined,
TypeOf<typeof PutSettingsRequestSchema.body>
> = async (context, request, response) => {
const soClient = context.core.savedObjects.client;
try {
const settings = await settingsService.saveSettings(soClient, request.body);
const body = {
success: true,
item: settings,
};
return response.ok({ body });
} catch (e) {
if (e.isBoom && e.output.statusCode === 404) {
return response.notFound({
body: { message: `Setings not found` },
});
}
return response.customError({
statusCode: 500,
body: { message: e.message },
});
}
};
export const registerRoutes = (router: IRouter) => {
router.get(
{
path: SETTINGS_API_ROUTES.INFO_PATTERN,
validate: GetSettingsRequestSchema,
options: { tags: [`access:${PLUGIN_ID}-read`] },
},
getSettingsHandler
);
router.put(
{
path: SETTINGS_API_ROUTES.UPDATE_PATTERN,
validate: PutSettingsRequestSchema,
options: { tags: [`access:${PLUGIN_ID}-all`] },
},
putSettingsHandler
);
};

View file

@ -15,6 +15,7 @@ import {
AGENT_EVENT_SAVED_OBJECT_TYPE,
AGENT_ACTION_SAVED_OBJECT_TYPE,
ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE,
GLOBAL_SETTINGS_SAVED_OBJET_TYPE,
} from './constants';
/*
@ -22,7 +23,24 @@ import {
*
* Please update typings in `/common/types` if mappings are updated.
*/
const savedObjectTypes: { [key: string]: SavedObjectsType } = {
[GLOBAL_SETTINGS_SAVED_OBJET_TYPE]: {
name: GLOBAL_SETTINGS_SAVED_OBJET_TYPE,
hidden: false,
namespaceType: 'agnostic',
management: {
importableAndExportable: false,
},
mappings: {
properties: {
agent_auto_upgrade: { type: 'keyword' },
package_auto_upgrade: { type: 'keyword' },
kibana_url: { type: 'keyword' },
kibana_ca_sha256: { type: 'keyword' },
},
},
},
[AGENT_SAVED_OBJECT_TYPE]: {
name: AGENT_SAVED_OBJECT_TYPE,
hidden: false,

View file

@ -3,9 +3,10 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { SavedObjectsClientContract } from 'kibana/server';
import { AgentStatus } from '../../common/types/models';
import * as settingsService from './settings';
export { ESIndexPatternSavedObjectService } from './es_index_pattern';
/**
@ -35,6 +36,7 @@ export interface AgentService {
export { datasourceService } from './datasource';
export { agentConfigService } from './agent_config';
export { outputService } from './output';
export { settingsService };
// Plugin services
export { appContextService } from './app_context';

View file

@ -95,6 +95,34 @@ class OutputService {
...outputSO.attributes,
};
}
public async update(soClient: SavedObjectsClientContract, id: string, data: Partial<Output>) {
const outputSO = await soClient.update<Output>(SAVED_OBJECT_TYPE, id, data);
if (outputSO.error) {
throw new Error(outputSO.error.message);
}
}
public async list(soClient: SavedObjectsClientContract) {
const outputs = await soClient.find<Output>({
type: SAVED_OBJECT_TYPE,
page: 1,
perPage: 1000,
});
return {
items: outputs.saved_objects.map<Output>(outputSO => {
return {
id: outputSO.id,
...outputSO.attributes,
};
}),
total: outputs.total,
page: 1,
perPage: 1000,
};
}
}
export const outputService = new OutputService();

View file

@ -0,0 +1,57 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import Boom from 'boom';
import { SavedObjectsClientContract } from 'kibana/server';
import { GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, SettingsSOAttributes, Settings } from '../../common';
export async function getSettings(soClient: SavedObjectsClientContract): Promise<Settings> {
const res = await soClient.find<SettingsSOAttributes>({
type: GLOBAL_SETTINGS_SAVED_OBJECT_TYPE,
});
if (res.total === 0) {
throw Boom.notFound('Global settings not found');
}
const settingsSo = res.saved_objects[0];
return {
id: settingsSo.id,
...settingsSo.attributes,
};
}
export async function saveSettings(
soClient: SavedObjectsClientContract,
newData: Partial<Omit<Settings, 'id'>>
): Promise<Settings> {
try {
const settings = await getSettings(soClient);
const res = await soClient.update<SettingsSOAttributes>(
GLOBAL_SETTINGS_SAVED_OBJECT_TYPE,
settings.id,
newData
);
return {
id: settings.id,
...res.attributes,
};
} catch (e) {
if (e.isBoom && e.output.statusCode === 404) {
const res = await soClient.create<SettingsSOAttributes>(
GLOBAL_SETTINGS_SAVED_OBJECT_TYPE,
newData
);
return {
id: res.id,
...res.attributes,
};
}
throw e;
}
}

View file

@ -21,6 +21,8 @@ import {
import { getPackageInfo } from './epm/packages';
import { datasourceService } from './datasource';
import { generateEnrollmentAPIKey } from './api_keys';
import { settingsService } from '.';
import { appContextService } from './app_context';
const FLEET_ENROLL_USERNAME = 'fleet_enroll';
const FLEET_ENROLL_ROLE = 'fleet_enroll';
@ -34,6 +36,17 @@ export async function setupIngestManager(
ensureInstalledDefaultPackages(soClient, callCluster),
outputService.ensureDefaultOutput(soClient),
agentConfigService.ensureDefaultAgentConfig(soClient),
settingsService.getSettings(soClient).catch((e: any) => {
if (e.isBoom && e.output.statusCode === 404) {
return settingsService.saveSettings(soClient, {
agent_auto_upgrade: true,
package_auto_upgrade: true,
kibana_url: appContextService.getConfig()?.fleet?.kibana?.host,
});
}
return Promise.reject(e);
}),
]);
// ensure default packages are added to the default conifg

View file

@ -50,6 +50,8 @@ export {
DefaultPackages,
TemplateRef,
IndexTemplateMappings,
Settings,
SettingsSOAttributes,
} from '../../common';
export type CallESAsCurrentUser = ScopedClusterClient['callAsCurrentUser'];

View file

@ -10,3 +10,5 @@ export * from './datasource';
export * from './epm';
export * from './enrollment_api_key';
export * from './install_script';
export * from './output';
export * from './settings';

View file

@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { schema } from '@kbn/config-schema';
export const GetOneOutputRequestSchema = {
params: schema.object({
outputId: schema.string(),
}),
};
export const GetOutputsRequestSchema = {};
export const PutOutputRequestSchema = {
params: schema.object({
outputId: schema.string(),
}),
body: schema.object({
hosts: schema.maybe(schema.arrayOf(schema.string())),
ca_sha256: schema.maybe(schema.string()),
}),
};

View file

@ -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;
* you may not use this file except in compliance with the Elastic License.
*/
import { schema } from '@kbn/config-schema';
export const GetSettingsRequestSchema = {};
export const PutSettingsRequestSchema = {
body: schema.object({
agent_auto_upgrade: schema.maybe(schema.boolean()),
package_auto_upgrade: schema.maybe(schema.boolean()),
kibana_url: schema.maybe(schema.string()),
kibana_ca_sha256: schema.maybe(schema.string()),
}),
};