[Synthetics] Enable private locations via elastic agent (#135782)

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Nicolas Chaulet <nicolas.chaulet@elastic.co>
This commit is contained in:
Shahzad 2022-07-22 14:53:09 +02:00 committed by GitHub
parent c3ac0a163a
commit 0958e55c17
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
111 changed files with 3622 additions and 331 deletions

View file

@ -99,6 +99,8 @@ const previouslyRegisteredTypes = [
'siem-ui-timeline-pinned-event',
'space',
'spaces-usage-stats',
'synthetics-monitor',
'synthetics-privates-locations',
'tag',
'task',
'telemetry',
@ -110,6 +112,7 @@ const previouslyRegisteredTypes = [
'upgrade-assistant-reindex-operation',
'upgrade-assistant-telemetry',
'uptime-dynamic-settings',
'uptime-synthetics-api-key',
'url',
'usage-counters',
'visualization',

View file

@ -160,7 +160,6 @@ describe('Fleet preconfiguration reset', () => {
perPage: 10000,
});
expect(packagePolicies.total).toBe(3);
expect(
packagePolicies.saved_objects.find((so) => so.id === 'elastic-cloud-fleet-server')
).toBeDefined();

View file

@ -298,7 +298,7 @@ describe('Fleet setup preconfiguration with multiple instances Kibana', () => {
type: 'epm-packages',
perPage: 10000,
});
expect(packages.saved_objects).toHaveLength(2);
expect(packages.saved_objects.length).toBeGreaterThanOrEqual(2);
expect(packages.saved_objects.map((p) => p.attributes.name)).toEqual(
expect.arrayContaining(['fleet_server', 'apm'])
);

View file

@ -180,7 +180,7 @@ describe('Uprade package install version', () => {
type: PACKAGES_SAVED_OBJECT_TYPE,
perPage: SO_SEARCH_LIMIT,
});
expect(res.saved_objects).toHaveLength(4);
res.saved_objects.forEach((so) => {
expect(so.attributes.install_format_schema_version).toBe(FLEET_INSTALL_FORMAT_VERSION);
if (!OUTDATED_PACKAGES.includes(so.attributes.name)) {

View file

@ -37,6 +37,7 @@ export const DEFAULT_COMMON_FIELDS: CommonFields = {
unit: ScheduleUnit.MINUTES,
},
[ConfigKey.APM_SERVICE_NAME]: '',
[ConfigKey.CONFIG_ID]: '',
[ConfigKey.TAGS]: [],
[ConfigKey.TIMEOUT]: '16',
[ConfigKey.NAME]: '',

View file

@ -9,6 +9,7 @@
export enum ConfigKey {
APM_SERVICE_NAME = 'service.name',
CUSTOM_HEARTBEAT_ID = 'custom_heartbeat_id',
CONFIG_ID = 'config_id',
ENABLED = 'enabled',
HOSTS = 'hosts',
IGNORE_HTTPS_ERRORS = 'ignore_https_errors',

View file

@ -4,8 +4,8 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { BrowserFields, ConfigKey } from '../../runtime_types/monitor_management';
import { BrowserFields, ConfigKey } from '../types';
import {
Formatter,
commonFormatters,

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { CommonFields, MonitorFields, ConfigKey } from '../types';
import { CommonFields, ConfigKey, MonitorFields } from '../../runtime_types/monitor_management';
export type Formatter = null | ((fields: Partial<MonitorFields>) => string | null);
@ -16,6 +16,7 @@ export const commonFormatters: CommonFormatMap = {
[ConfigKey.LOCATIONS]: null,
[ConfigKey.MONITOR_TYPE]: null,
[ConfigKey.ENABLED]: null,
[ConfigKey.CONFIG_ID]: null,
[ConfigKey.SCHEDULE]: (fields) =>
JSON.stringify(
`@every ${fields[ConfigKey.SCHEDULE]?.number}${fields[ConfigKey.SCHEDULE]?.unit}`

View file

@ -0,0 +1,48 @@
/*
* 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 { NewPackagePolicy } from '@kbn/fleet-plugin/common';
import { formatters } from './formatters';
import { ConfigKey, DataStream, MonitorFields } from '../runtime_types';
export const formatSyntheticsPolicy = (
newPolicy: NewPackagePolicy,
monitorType: DataStream,
config: Partial<MonitorFields & { location_name: string }>
) => {
const configKeys = Object.keys(config) as ConfigKey[];
const formattedPolicy = { ...newPolicy };
const currentInput = formattedPolicy.inputs.find(
(input) => input.type === `synthetics/${monitorType}`
);
const dataStream = currentInput?.streams.find(
(stream) => stream.data_stream.dataset === monitorType
);
formattedPolicy.inputs.forEach((input) => (input.enabled = false));
if (currentInput && dataStream) {
// reset all data streams to enabled false
formattedPolicy.inputs.forEach((input) => (input.enabled = false));
// enable only the input type and data stream that matches the monitor type.
currentInput.enabled = true;
dataStream.enabled = true;
}
configKeys.forEach((key) => {
const configItem = dataStream?.vars?.[key];
if (configItem) {
if (formatters[key]) {
configItem.value = formatters[key]?.(config);
} else {
configItem.value = config[key] === undefined || config[key] === null ? null : config[key];
}
}
});
return { formattedPolicy, dataStream, currentInput };
};

View file

@ -5,13 +5,12 @@
* 2.0.
*/
import { DataStream } from '../types';
import { httpFormatters, HTTPFormatMap } from '../http/formatters';
import { tcpFormatters, TCPFormatMap } from '../tcp/formatters';
import { icmpFormatters, ICMPFormatMap } from '../icmp/formatters';
import { browserFormatters, BrowserFormatMap } from '../browser/formatters';
import { commonFormatters, CommonFormatMap } from '../common/formatters';
import { DataStream } from '../runtime_types';
import { httpFormatters, HTTPFormatMap } from './http/formatters';
import { tcpFormatters, TCPFormatMap } from './tcp/formatters';
import { icmpFormatters, ICMPFormatMap } from './icmp/formatters';
import { browserFormatters, BrowserFormatMap } from './browser/formatters';
import { commonFormatters, CommonFormatMap } from './common/formatters';
type Formatters = HTTPFormatMap & TCPFormatMap & ICMPFormatMap & BrowserFormatMap & CommonFormatMap;

View file

@ -5,14 +5,15 @@
* 2.0.
*/
import { HTTPFields, ConfigKey } from '../types';
import { tlsFormatters } from '../tls/formatters';
import { HTTPFields, ConfigKey } from '../../runtime_types/monitor_management';
import {
Formatter,
commonFormatters,
objectToJsonFormatter,
arrayToJsonFormatter,
} from '../common/formatters';
import { tlsFormatters } from '../tls/formatters';
export type HTTPFormatMap = Record<keyof HTTPFields, Formatter>;

View file

@ -5,7 +5,8 @@
* 2.0.
*/
import { ICMPFields, ConfigKey } from '../types';
import { ICMPFields, ConfigKey } from '../../runtime_types/monitor_management';
import { Formatter, commonFormatters, secondsToCronFormatter } from '../common/formatters';
export type ICMPFormatMap = Record<keyof ICMPFields, Formatter>;

View file

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

View file

@ -5,7 +5,8 @@
* 2.0.
*/
import { TCPFields, ConfigKey } from '../types';
import { TCPFields, ConfigKey } from '../../runtime_types/monitor_management';
import { Formatter, commonFormatters, objectToJsonFormatter } from '../common/formatters';
import { tlsFormatters } from '../tls/formatters';

View file

@ -4,8 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { TLSFields, ConfigKey } from '../types';
import { TLSFields, ConfigKey } from '../../runtime_types/monitor_management';
import { Formatter } from '../common/formatters';
type TLSFormatMap = Record<keyof TLSFields, Formatter>;

View file

@ -14,3 +14,4 @@ export * from './ping';
export * from './snapshot';
export * from './network_events';
export * from './monitor_management';
export * from './monitor_management/synthetics_private_locations';

View file

@ -53,14 +53,19 @@ export const ManifestLocationCodec = t.interface({
status: LocationStatusCodec,
});
export const ServiceLocationCodec = t.interface({
id: t.string,
label: t.string,
geo: LocationGeoCodec,
url: t.string,
isServiceManaged: t.boolean,
status: LocationStatusCodec,
});
export const ServiceLocationCodec = t.intersection([
t.interface({
id: t.string,
label: t.string,
geo: LocationGeoCodec,
url: t.string,
isServiceManaged: t.boolean,
status: LocationStatusCodec,
}),
t.partial({
isInvalid: t.boolean,
}),
]);
export const MonitorServiceLocationCodec = t.intersection([
t.interface({
@ -71,6 +76,7 @@ export const MonitorServiceLocationCodec = t.intersection([
label: t.string,
geo: LocationGeoCodec,
url: t.string,
isInvalid: t.boolean,
}),
]);

View file

@ -82,6 +82,7 @@ export const CommonFieldsCodec = t.intersection([
[ConfigKey.TIMEOUT]: t.union([t.string, t.null]),
[ConfigKey.REVISION]: t.number,
[ConfigKey.MONITOR_SOURCE_TYPE]: SourceTypeCodec,
[ConfigKey.CONFIG_ID]: t.string,
}),
]);

View file

@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import * as t from 'io-ts';
export const PrivateLocationType = t.type({
name: t.string,
id: t.string,
policyHostId: t.string,
concurrentMonitors: t.number,
latLon: t.string,
});
export const SyntheticsPrivateLocationsType = t.type({
locations: t.array(PrivateLocationType),
});
export type PrivateLocation = t.TypeOf<typeof PrivateLocationType>;
export type SyntheticsPrivateLocations = t.TypeOf<typeof SyntheticsPrivateLocationsType>;

View file

@ -105,6 +105,7 @@ export const MonitorType = t.intersection([
gte: t.string,
lt: t.string,
}),
fleet_managed: t.boolean,
}),
]);

View file

@ -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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export const privateLocationsSavedObjectId = 'synthetics-privates-locations-singleton';
export const privateLocationsSavedObjectName = 'synthetics-privates-locations';

View file

@ -16,3 +16,4 @@ export * from './monitor_management.journey';
export * from './monitor_management_enablement.journey';
export * from './monitor_details';
export * from './locations';
export * from './private_locations';

View file

@ -0,0 +1,70 @@
/*
* 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 { journey, step, expect, before } from '@elastic/synthetics';
import { assertText, byTestId, TIMEOUT_60_SEC } from '@kbn/observability-plugin/e2e/utils';
import { monitorManagementPageProvider } from '../../page_objects/monitor_management';
journey('AddPrivateLocationMonitor', async ({ page, params: { kibanaUrl } }) => {
const uptime = monitorManagementPageProvider({ page, kibanaUrl });
before(async () => {
await uptime.waitForLoadingToFinish();
});
step('Go to monitor-management', async () => {
await uptime.navigateToMonitorManagement();
});
step('login to Kibana', async () => {
await uptime.loginToKibana();
const invalid = await page.locator(`text=Username or password is incorrect. Please try again.`);
expect(await invalid.isVisible()).toBeFalsy();
});
step('enable management', async () => {
await uptime.enableMonitorManagement();
});
step('Click text=Add monitor', async () => {
await page.click('text=Add monitor');
expect(page.url()).toBe(`${kibanaUrl}/app/uptime/add-monitor`);
await uptime.waitForLoadingToFinish();
await page.click('input[name="name"]');
await page.fill('input[name="name"]', 'Private location monitor');
await page.click('label:has-text("Test private location Private")', TIMEOUT_60_SEC);
await page.selectOption('select', 'http');
await page.click(byTestId('syntheticsUrlField'));
await page.fill(byTestId('syntheticsUrlField'), 'https://www.google.com');
await page.click('text=Save monitor');
await page.click('text=Private location monitor');
await page.click('text=Private location monitorLast 15 minutes1 mRefresh >> span');
});
step('Click [placeholder="Find apps, content, and more. Ex: Discover"]', async () => {
await page.click('[placeholder="Find apps, content, and more. Ex: Discover"]');
await page.fill('[placeholder="Find apps, content, and more. Ex: Discover"]', 'integ');
await Promise.all([
page.waitForNavigation(/* { url: '${kibanaUrl}/app/integrations/browse' }*/),
page.click('text=Integrations'),
]);
await page.click('text=Installed integrations');
expect(page.url()).toBe(`${kibanaUrl}/app/integrations/installed`);
await page.click(`text=Elastic Synthetics`);
expect(page.url()).toBe(`${kibanaUrl}/app/integrations/detail/synthetics-0.9.5/overview`);
await page.click('text=Integration policies');
expect(page.url()).toBe(`${kibanaUrl}/app/integrations/detail/synthetics-0.9.5/policies`);
});
step('Click text=Edit Elastic Synthetics integration', async () => {
await assertText({ page, text: 'This table contains 1 rows out of 1 rows; Page 1 of 1.' });
await page.click('[data-test-subj="integrationNameLink"]');
await page.click('text=Edit in uptime');
await page.click('text=Private location monitor');
});
});

View file

@ -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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export * from './manage_locations';
export * from './add_monitor_private_location';

View file

@ -0,0 +1,79 @@
/*
* 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 { journey, step, expect, before } from '@elastic/synthetics';
import { monitorManagementPageProvider } from '../../page_objects/monitor_management';
journey('ManagePrivateLocation', async ({ page, params: { kibanaUrl } }) => {
const uptime = monitorManagementPageProvider({ page, kibanaUrl });
before(async () => {
await uptime.waitForLoadingToFinish();
});
step('Go to monitor-management', async () => {
await uptime.navigateToMonitorManagement();
});
step('login to Kibana', async () => {
await uptime.loginToKibana();
const invalid = await page.locator(`text=Username or password is incorrect. Please try again.`);
expect(await invalid.isVisible()).toBeFalsy();
});
step('enable management', async () => {
await uptime.enableMonitorManagement();
});
step('Open manage location', async () => {
await page.click('button:has-text("Manage private locations")');
await page.click('button:has-text("Add location")');
});
step('Click text=Add two agent policies', async () => {
await page.click('text=Create agent policy');
await addAgentPolicy('Fleet test policy');
await page.click('text=Create agent policy');
await addAgentPolicy('Fleet test policy 2');
await page.goBack({ waitUntil: 'networkidle' });
await page.goBack({ waitUntil: 'networkidle' });
await page.goBack({ waitUntil: 'networkidle' });
});
step('Add new private location', async () => {
await page.waitForTimeout(30 * 1000);
await page.click('button:has-text("Close")');
await page.click('button:has-text("Manage private locations")');
await page.click('button:has-text("Add location")');
await addPrivateLocation('Test private location', 'Fleet test policy');
});
step('Add another location', async () => {
await page.click('button:has-text("Add location")');
await page.click('[aria-label="Select agent policy"]');
await page.isDisabled(`button[role="option"]:has-text("Fleet test policyAgents: 0")`);
await addPrivateLocation('Test private location 2', 'Fleet test policy 2');
});
const addPrivateLocation = async (name: string, policy: string) => {
await page.click('[aria-label="Location name"]');
await page.fill('[aria-label="Location name"]', name);
await page.click('[aria-label="Select agent policy"]');
await page.click(`button[role="option"]:has-text("${policy}Agents: 0")`);
await page.click('button:has-text("Create location")');
};
const addAgentPolicy = async (name: string) => {
await page.click('[placeholder="Choose a name"]');
await page.fill('[placeholder="Choose a name"]', name);
await page.click('div[role="dialog"] button:has-text("Create agent policy")');
};
});

View file

@ -112,6 +112,7 @@ const Application = (props: UptimeAppProps) => {
...plugins,
storage,
data: startPlugins.data,
fleet: startPlugins.fleet,
inspector: startPlugins.inspector,
triggersActionsUi: startPlugins.triggersActionsUi,
observability: startPlugins.observability,

View file

@ -84,6 +84,7 @@ export const commonNormalizers: CommonNormalizerMap = {
}
},
[ConfigKey.APM_SERVICE_NAME]: getCommonNormalizer(ConfigKey.APM_SERVICE_NAME),
[ConfigKey.CONFIG_ID]: getCommonNormalizer(ConfigKey.CONFIG_ID),
[ConfigKey.TAGS]: getCommonjsonToJavascriptNormalizer(ConfigKey.TAGS),
[ConfigKey.TIMEOUT]: getCommonCronToSecondsNormalizer(ConfigKey.TIMEOUT),
[ConfigKey.NAMESPACE]: (fields) =>

View file

@ -6,8 +6,8 @@
*/
import { useEffect, useRef, useState } from 'react';
import { NewPackagePolicy } from '@kbn/fleet-plugin/public';
import { formatSyntheticsPolicy } from '../../../../../common/formatters/format_synthetics_policy';
import { ConfigKey, DataStream, Validation, MonitorFields } from '../types';
import { formatters } from '../helpers/formatters';
interface Props {
monitorType: DataStream;
@ -41,32 +41,15 @@ export const useUpdatePolicy = ({
const configDidUpdate = configKeys.some((key) => config[key] !== currentConfig.current[key]);
const isValid =
!!newPolicy.name && !validationKeys.find((key) => validate[monitorType]?.[key]?.(config));
const formattedPolicy = { ...newPolicy };
const currentInput = formattedPolicy.inputs.find(
(input) => input.type === `synthetics/${monitorType}`
const { formattedPolicy, dataStream, currentInput } = formatSyntheticsPolicy(
newPolicy,
monitorType,
config
);
const dataStream = currentInput?.streams.find(
(stream) => stream.data_stream.dataset === monitorType
);
formattedPolicy.inputs.forEach((input) => (input.enabled = false));
if (currentInput && dataStream) {
// reset all data streams to enabled false
formattedPolicy.inputs.forEach((input) => (input.enabled = false));
// enable only the input type and data stream that matches the monitor type.
currentInput.enabled = true;
dataStream.enabled = true;
}
// prevent an infinite loop of updating the policy
if (currentInput && dataStream && configDidUpdate) {
configKeys.forEach((key) => {
const configItem = dataStream.vars?.[key];
if (configItem && formatters[key]) {
configItem.value = formatters[key]?.(config);
} else if (configItem) {
configItem.value = config[key] === undefined || config[key] === null ? null : config[key];
}
});
currentConfig.current = config;
setUpdatedPolicy(formattedPolicy);
onChange({

View file

@ -65,6 +65,8 @@ export const ActionBar = ({
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean | undefined>(undefined);
const isReadOnly = monitor[ConfigKey.MONITOR_SOURCE_TYPE] === SourceType.PROJECT;
const hasServiceManagedLocation = monitor.locations?.some((loc) => loc.isServiceManaged);
const { data, status } = useFetcher(() => {
if (!isSaving || !isValid) {
return;
@ -150,7 +152,7 @@ export const ActionBar = ({
size="s"
color="success"
iconType="play"
disabled={!isValid || isTestRunInProgress}
disabled={!isValid || isTestRunInProgress || !hasServiceManagedLocation}
data-test-subj={'monitorTestNowRunBtn'}
onClick={() => onTestNow()}
onMouseEnter={() => {

View file

@ -10,6 +10,8 @@ import { i18n } from '@kbn/i18n';
import { EuiButton, EuiFlexItem, EuiFlexGroup, EuiToolTip, EuiSwitch } from '@elastic/eui';
import { useHistory } from 'react-router-dom';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { useLocations } from './hooks/use_locations';
import { ClientPluginsSetup, ClientPluginsStart } from '../../../plugin';
import { kibanaService } from '../../state/kibana_service';
import { MONITOR_ADD_ROUTE } from '../../../../common/constants';
import { useEnablement } from './hooks/use_enablement';
@ -29,6 +31,8 @@ export const AddMonitorBtn = () => {
} = useEnablement();
const { isEnabled, canEnable, areApiKeysEnabled } = enablement || {};
const { locations } = useLocations();
useEffect(() => {
if (isEnabling && isEnabled) {
setIsEnabling(false);
@ -90,7 +94,16 @@ export const AddMonitorBtn = () => {
const loading = allowedLoading || enablementLoading;
const canSave: boolean = !!useKibana().services?.application?.capabilities.uptime.save;
const kServices = useKibana<ClientPluginsStart>().services;
const canSave: boolean = !!kServices?.application?.capabilities.uptime.save;
const canSaveIntegrations: boolean =
!!kServices?.fleet?.authz.integrations.writeIntegrationPolicies;
const isCloud = useKibana<ClientPluginsSetup>().services?.cloud?.isCloudEnabled;
const canSavePrivate: boolean = Boolean(isCloud) || canSaveIntegrations;
return (
<EuiFlexGroup alignItems="center">
@ -120,7 +133,9 @@ export const AddMonitorBtn = () => {
<EuiButton
isLoading={loading}
fill
isDisabled={!canSave || !isEnabled || !isAllowed}
isDisabled={
!canSave || !isEnabled || !isAllowed || !canSavePrivate || locations.length === 0
}
iconType="plus"
data-test-subj="syntheticsAddMonitorBtn"
href={history.createHref({

View file

@ -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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { ClientPluginsStart } from '../../../../plugin';
import { BrowserFields, ConfigKey } from '../../../../../common/runtime_types';
export function usePrivateLocationPermissions(monitor?: BrowserFields) {
const kServices = useKibana<ClientPluginsStart>().services;
const canSaveIntegrations: boolean =
!!kServices?.fleet?.authz.integrations.writeIntegrationPolicies;
const locations = (monitor as BrowserFields)?.[ConfigKey.LOCATIONS];
const hasPrivateLocation = locations?.some((location) => !location.isServiceManaged);
const canUpdatePrivateMonitor = !(hasPrivateLocation && !canSaveIntegrations);
return { canUpdatePrivateMonitor };
}

View file

@ -0,0 +1,79 @@
/*
* 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 React from 'react';
import { EuiEmptyPrompt, EuiButton, EuiTitle, EuiLink } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useDispatch } from 'react-redux';
import { setManageFlyoutOpen } from '../../../state/private_locations';
export const EmptyLocations = ({
setIsAddingNew,
disabled,
}: {
disabled?: boolean;
setIsAddingNew?: (val: boolean) => void;
}) => {
const dispatch = useDispatch();
return (
<EuiEmptyPrompt
iconType="visMapCoordinate"
title={<h2>{START_ADDING_LOCATIONS}</h2>}
body={<p>{START_ADDING_LOCATIONS_DESCRIPTION}</p>}
actions={
<EuiButton
disabled={disabled}
color="primary"
fill
onClick={() => {
setIsAddingNew?.(true);
dispatch(setManageFlyoutOpen(true));
}}
>
{ADD_LOCATION}
</EuiButton>
}
footer={
<>
<EuiTitle size="xxs">
<h3>{LEARN_MORE}</h3>
</EuiTitle>
<EuiLink href="#" target="_blank">
{READ_DOCS}
</EuiLink>
</>
}
/>
);
};
const START_ADDING_LOCATIONS = i18n.translate(
'xpack.synthetics.monitorManagement.startAddingLocations',
{
defaultMessage: 'Start adding private locations',
}
);
const START_ADDING_LOCATIONS_DESCRIPTION = i18n.translate(
'xpack.synthetics.monitorManagement.startAddingLocationsDescription',
{
defaultMessage: 'Add your first private location to run monitors on premiss via Elastic agent.',
}
);
const ADD_LOCATION = i18n.translate('xpack.synthetics.monitorManagement.addLocation', {
defaultMessage: 'Add location',
});
const READ_DOCS = i18n.translate('xpack.synthetics.monitorManagement.readDocs', {
defaultMessage: 'Read the docs',
});
const LEARN_MORE = i18n.translate('xpack.synthetics.monitorManagement.learnMore', {
defaultMessage: 'Want to learn more?',
});

View file

@ -0,0 +1,62 @@
/*
* 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 { renderHook } from '@testing-library/react-hooks';
import { useLocationMonitors } from './use_location_monitors';
import { defaultCore, WrappedHelper } from '../../../../../apps/synthetics/utils/testing';
describe('useLocationMonitors', () => {
it('returns expected results', () => {
const { result } = renderHook(() => useLocationMonitors(), { wrapper: WrappedHelper });
expect(result.current).toStrictEqual({ locations: [] });
expect(defaultCore.savedObjects.client.find).toHaveBeenCalledWith({
aggs: {
locations: {
terms: { field: 'synthetics-monitor.attributes.locations.id', size: 10000 },
},
},
perPage: 0,
type: 'synthetics-monitor',
});
});
it('returns expected results after data', async () => {
defaultCore.savedObjects.client.find = jest.fn().mockReturnValue({
aggregations: {
locations: {
buckets: [
{ key: 'Test', doc_count: 5 },
{ key: 'Test 1', doc_count: 0 },
],
},
},
});
const { result, waitForNextUpdate } = renderHook(() => useLocationMonitors(), {
wrapper: WrappedHelper,
});
expect(result.current).toStrictEqual({ locations: [] });
await waitForNextUpdate();
expect(result.current).toStrictEqual({
locations: [
{
id: 'Test',
count: 5,
},
{
id: 'Test 1',
count: 0,
},
],
});
});
});

View file

@ -0,0 +1,54 @@
/*
* 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 { useMemo } from 'react';
import { useFetcher } from '@kbn/observability-plugin/public';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import {
monitorAttributes,
syntheticsMonitorType,
} from '../../../../../../common/types/saved_objects';
interface AggsResponse {
locations: {
buckets: Array<{
key: string;
doc_count: number;
}>;
};
}
export const useLocationMonitors = () => {
const { savedObjects } = useKibana().services;
const { data } = useFetcher(() => {
const aggs = {
locations: {
terms: {
field: `${monitorAttributes}.locations.id`,
size: 10000,
},
},
};
return savedObjects?.client.find<unknown, typeof aggs>({
type: syntheticsMonitorType,
perPage: 0,
aggs,
});
}, []);
return useMemo(() => {
if (data?.aggregations) {
const newValues = (data.aggregations as AggsResponse)?.locations.buckets.map(
({ key, doc_count: count }) => ({ id: key, count })
);
return { locations: newValues };
}
return { locations: [] };
}, [data]);
};

View file

@ -0,0 +1,144 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { renderHook } from '@testing-library/react-hooks';
import { defaultCore, WrappedHelper } from '../../../../../apps/synthetics/utils/testing';
import { useLocationsAPI } from './use_locations_api';
describe('useLocationsAPI', () => {
it('returns expected results', () => {
const { result } = renderHook(() => useLocationsAPI({ isOpen: false }), {
wrapper: WrappedHelper,
});
expect(result.current).toEqual(
expect.objectContaining({
fetchLoading: true,
deleteLoading: true,
privateLocations: [],
})
);
expect(defaultCore.savedObjects.client.get).toHaveBeenCalledWith(
'synthetics-privates-locations',
'synthetics-privates-locations-singleton'
);
});
defaultCore.savedObjects.client.get = jest.fn().mockReturnValue({
attributes: {
locations: [
{
id: 'Test',
policyHostId: 'testPolicy',
},
],
},
});
it('returns expected results after data', async () => {
const { result, waitForNextUpdate } = renderHook(() => useLocationsAPI({ isOpen: true }), {
wrapper: WrappedHelper,
});
expect(result.current).toEqual(
expect.objectContaining({
deleteLoading: true,
fetchLoading: true,
privateLocations: [],
})
);
await waitForNextUpdate();
expect(result.current).toEqual(
expect.objectContaining({
deleteLoading: false,
fetchLoading: false,
privateLocations: [
{
id: 'Test',
policyHostId: 'testPolicy',
},
],
})
);
});
it('adds location on submit', async () => {
const { result, waitForNextUpdate } = renderHook(() => useLocationsAPI({ isOpen: true }), {
wrapper: WrappedHelper,
});
await waitForNextUpdate();
result.current.onSubmit({
id: 'new',
policyHostId: 'newPolicy',
name: 'new',
concurrentMonitors: 1,
latLon: '',
});
await waitForNextUpdate();
expect(defaultCore.savedObjects.client.create).toHaveBeenCalledWith(
'synthetics-privates-locations',
{
locations: [
{ id: 'Test', policyHostId: 'testPolicy' },
{
concurrentMonitors: 1,
id: 'newPolicy',
latLon: '',
name: 'new',
policyHostId: 'newPolicy',
},
],
},
{ id: 'synthetics-privates-locations-singleton', overwrite: true }
);
});
it('deletes location on delete', async () => {
defaultCore.savedObjects.client.get = jest.fn().mockReturnValue({
attributes: {
locations: [
{
id: 'Test',
policyHostId: 'testPolicy',
},
{
id: 'Test1',
policyHostId: 'testPolicy1',
},
],
},
});
const { result, waitForNextUpdate } = renderHook(() => useLocationsAPI({ isOpen: true }), {
wrapper: WrappedHelper,
});
await waitForNextUpdate();
result.current.onDelete('Test');
await waitForNextUpdate();
expect(defaultCore.savedObjects.client.create).toHaveBeenLastCalledWith(
'synthetics-privates-locations',
{
locations: [
{
id: 'Test1',
policyHostId: 'testPolicy1',
},
],
},
{ id: 'synthetics-privates-locations-singleton', overwrite: true }
);
});
});

View file

@ -0,0 +1,68 @@
/*
* 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 { useFetcher } from '@kbn/observability-plugin/public';
import { useState } from 'react';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { PrivateLocation } from '../../../../../../common/runtime_types';
import {
setSyntheticsPrivateLocations,
getSyntheticsPrivateLocations,
} from '../../../../state/private_locations/api';
export const useLocationsAPI = ({ isOpen }: { isOpen: boolean }) => {
const [formData, setFormData] = useState<PrivateLocation>();
const [deleteId, setDeleteId] = useState<string>();
const { savedObjects } = useKibana().services;
const { data: currentPrivateLocations, loading: fetchLoading } = useFetcher(() => {
if (!formData) return getSyntheticsPrivateLocations(savedObjects?.client!);
return Promise.resolve(null);
}, [formData, deleteId, isOpen]);
const { loading: saveLoading } = useFetcher(async () => {
if (currentPrivateLocations && formData) {
const existingLocations = currentPrivateLocations.filter((loc) => loc.id !== formData.id);
const result = await setSyntheticsPrivateLocations(savedObjects?.client!, {
locations: [...(existingLocations ?? []), { ...formData, id: formData.policyHostId }],
});
setFormData(undefined);
return result;
}
return Promise.resolve(null);
}, [formData, currentPrivateLocations]);
const onSubmit = (data: PrivateLocation) => {
setFormData(data);
};
const onDelete = (id: string) => {
setDeleteId(id);
};
const { loading: deleteLoading } = useFetcher(async () => {
if (deleteId) {
const result = await setSyntheticsPrivateLocations(savedObjects?.client!, {
locations: (currentPrivateLocations ?? []).filter((loc) => loc.id !== deleteId),
});
setDeleteId(undefined);
return result;
}
return Promise.resolve(null);
}, [deleteId, currentPrivateLocations]);
return {
onSubmit,
onDelete,
fetchLoading: Boolean(fetchLoading || Boolean(formData)),
saveLoading: Boolean(saveLoading),
deleteLoading: Boolean(deleteLoading),
privateLocations: currentPrivateLocations ?? [],
};
};

View file

@ -0,0 +1,150 @@
/*
* 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 React from 'react';
import {
EuiFieldText,
EuiForm,
EuiFormRow,
EuiFlexGroup,
EuiFlexItem,
EuiButton,
EuiSpacer,
EuiButtonEmpty,
} from '@elastic/eui';
import { isEmpty } from 'lodash';
import { useSelector } from 'react-redux';
import { i18n } from '@kbn/i18n';
import { PolicyHostNeeded } from './policy_host_needed';
import { PrivateLocation } from '../../../../../common/runtime_types';
import { PolicyHostsField } from './policy_hosts';
import { useFormWrapped } from '../../../../hooks/use_form_wrapped';
import { selectAgentPolicies } from '../../../state/private_locations';
export const LocationForm = ({
onSubmit,
loading,
location,
onDiscard,
privateLocations,
}: {
onSubmit: (val: PrivateLocation) => void;
onDiscard?: () => void;
loading?: boolean;
location?: PrivateLocation;
privateLocations: PrivateLocation[];
}) => {
const {
control,
register,
handleSubmit,
reset,
formState: { errors, isValid, isSubmitted, isDirty },
} = useFormWrapped({
mode: 'onTouched',
reValidateMode: 'onChange',
shouldFocusError: true,
defaultValues: location || {
name: '',
policyHostId: '',
id: '',
latLon: '',
concurrentMonitors: 1,
},
});
const { data } = useSelector(selectAgentPolicies);
return (
<>
{data?.items.length === 0 && <PolicyHostNeeded />}
<EuiForm
component="form"
onSubmit={handleSubmit(onSubmit)}
isInvalid={isSubmitted && !isValid && !isEmpty(errors)}
noValidate
>
<EuiFlexGroup>
<EuiFlexItem>
<EuiFormRow
label={LOCATION_NAME_LABEL}
isInvalid={Boolean(errors?.name)}
error={errors?.name?.message}
>
<EuiFieldText
disabled={Boolean(location)}
aria-label={LOCATION_NAME_LABEL}
{...register('name', {
required: {
value: true,
message: NAME_REQUIRED,
},
validate: (val: string) => {
return privateLocations.some((loc) => loc.name === val)
? NAME_ALREADY_EXISTS
: undefined;
},
})}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem>
<PolicyHostsField
errors={errors}
control={control}
isDisabled={Boolean(location)}
privateLocations={privateLocations}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
size="s"
isDisabled={!isDirty && Boolean(location)}
onClick={() => {
reset();
onDiscard?.();
}}
>
{DISCARD_LABEL}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton size="s" type="submit" fill isLoading={loading} isDisabled={!isDirty}>
{location ? UPDATE_LOCATION_LABEL : CREATE_LOCATION_LABEL}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiForm>
</>
);
};
const LOCATION_NAME_LABEL = i18n.translate('xpack.synthetics.monitorManagement.locationName', {
defaultMessage: 'Location name',
});
const DISCARD_LABEL = i18n.translate('xpack.synthetics.monitorManagement.discard', {
defaultMessage: 'Discard',
});
const UPDATE_LOCATION_LABEL = i18n.translate('xpack.synthetics.monitorManagement.updateLocation', {
defaultMessage: 'Update location',
});
const CREATE_LOCATION_LABEL = i18n.translate('xpack.synthetics.monitorManagement.createLocation', {
defaultMessage: 'Create location',
});
const NAME_ALREADY_EXISTS = i18n.translate('xpack.synthetics.monitorManagement.alreadyExists', {
defaultMessage: 'Location name already exists.',
});
const NAME_REQUIRED = i18n.translate('xpack.synthetics.monitorManagement.nameRequired', {
defaultMessage: 'Location name is required',
});

View file

@ -0,0 +1,183 @@
/*
* 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 React, { useState } from 'react';
import {
EuiAccordion,
EuiText,
EuiTextColor,
EuiIcon,
EuiFlexGroup,
EuiFlexItem,
EuiTitle,
EuiButtonIcon,
EuiLink,
EuiSpacer,
EuiLoadingSpinner,
EuiToolTip,
} from '@elastic/eui';
import { useSelector } from 'react-redux';
import styled from 'styled-components';
import { i18n } from '@kbn/i18n';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { useLocationMonitors } from './hooks/use_location_monitors';
import { PrivateLocation } from '../../../../../common/runtime_types';
import { useUptimeSettingsContext } from '../../../contexts/uptime_settings_context';
import { LocationForm } from './location_form';
import { selectAgentPolicies } from '../../../state/private_locations';
export const PrivateLocationsList = ({
privateLocations,
onSubmit,
loading,
onDelete,
hasFleetPermissions,
}: {
loading: boolean;
privateLocations: PrivateLocation[];
hasFleetPermissions: boolean;
onSubmit: (location: PrivateLocation) => void;
onDelete: (id: string) => void;
}) => {
const { basePath } = useUptimeSettingsContext();
const { data: policies } = useSelector(selectAgentPolicies);
const { locations } = useLocationMonitors();
const [openLocationMap, setOpenLocationMap] = useState<Record<string, boolean>>({});
const canSave: boolean = !!useKibana().services?.application?.capabilities.uptime.save;
if (loading) {
return <EuiLoadingSpinner />;
}
return (
<Wrapper>
{privateLocations.map((location, index) => {
const monCount = locations?.find((l) => l.id === location.id)?.count ?? 0;
const canDelete = monCount === 0 || !hasFleetPermissions;
const policy = policies?.items.find((policyT) => policyT.id === location.policyHostId);
return (
<div key={location.id}>
<EuiAccordion
data-test-subj={`location-accordion-` + index}
id={location.id}
element="fieldset"
className="euiAccordionForm"
buttonClassName="euiAccordionForm__button"
onToggle={(val) => {
setOpenLocationMap((prevState) => ({ [location.id]: val, ...prevState }));
}}
buttonContent={
<div>
<EuiFlexGroup gutterSize="s" alignItems="center" responsive={false}>
<EuiFlexItem grow={false}>
<EuiIcon type="visMapCoordinate" size="m" />
</EuiFlexItem>
<EuiFlexItem>
<EuiTitle size="xs" className="eui-textNoWrap">
<h3>{location.name}</h3>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem>
<EuiText size="xs">
<EuiTextColor color="subdued">
{RUNNING_MONITORS}: {monCount}
</EuiTextColor>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
<EuiText size="s">
<p>
{hasFleetPermissions && (
<EuiTextColor color="subdued">
{AGENT_POLICY_LABEL}:{' '}
{policy ? (
<EuiLink
href={`${basePath}/app/fleet/policies/${location.policyHostId}`}
>
{policy?.name}
</EuiLink>
) : (
<EuiText color="danger" size="s" className="eui-displayInline">
{POLICY_IS_DELETED}
</EuiText>
)}
</EuiTextColor>
)}
</p>
</EuiText>
</div>
}
extraAction={
<EuiToolTip
content={
canDelete
? DELETE_LABEL
: i18n.translate('xpack.synthetics.monitorManagement.cannotDelete', {
defaultMessage: `This location cannot be deleted, because it has {monCount} monitors running. Please remove this location from your monitors before deleting this location.`,
values: { monCount },
})
}
>
<EuiButtonIcon
iconType="trash"
color="danger"
aria-label={DELETE_LABEL}
onClick={() => onDelete(location.id)}
isDisabled={!canDelete || !canSave}
/>
</EuiToolTip>
}
paddingSize="l"
>
{openLocationMap[location.id] && (
<LocationForm
onSubmit={onSubmit}
loading={loading}
location={location}
privateLocations={privateLocations}
/>
)}
</EuiAccordion>
<EuiSpacer />
</div>
);
})}
</Wrapper>
);
};
const Wrapper = styled.div`
&&& {
.euiAccordion__button {
text-decoration: none;
}
}
`;
const DELETE_LABEL = i18n.translate('xpack.synthetics.monitorManagement.delete', {
defaultMessage: 'Delete location',
});
const RUNNING_MONITORS = i18n.translate('xpack.synthetics.monitorManagement.runningMonitors', {
defaultMessage: 'Running monitors',
});
const POLICY_IS_DELETED = i18n.translate('xpack.synthetics.monitorManagement.deletedPolicy', {
defaultMessage: 'Policy is deleted',
});
const AGENT_POLICY_LABEL = i18n.translate('xpack.synthetics.monitorManagement.agentPolicy', {
defaultMessage: 'Agent Policy',
});

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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { InPortal } from 'react-reverse-portal';
import { ManageLocationsFlyout } from './manage_locations_flyout';
import { ManageLocationsPortalNode } from '../../../pages/monitor_management/portals';
export const ManageLocationsPortal = () => {
return (
<InPortal node={ManageLocationsPortalNode}>
<ManageLocationsFlyout />
</InPortal>
);
};

View file

@ -0,0 +1,164 @@
/*
* 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 React, { useEffect, useState } from 'react';
import {
EuiFlyout,
EuiButtonEmpty,
EuiFlyoutHeader,
EuiTitle,
EuiSpacer,
EuiFlyoutBody,
EuiFlyoutFooter,
EuiFlexGroup,
EuiFlexItem,
EuiButton,
EuiCallOut,
} from '@elastic/eui';
import { useDispatch, useSelector } from 'react-redux';
import { i18n } from '@kbn/i18n';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { ClientPluginsStart } from '../../../../plugin';
import { EmptyLocations } from './empty_locations';
import { getServiceLocations } from '../../../state/actions';
import { LocationForm } from './location_form';
import { PrivateLocationsList } from './locations_list';
import { useLocationsAPI } from './hooks/use_locations_api';
import {
getAgentPoliciesAction,
selectManageFlyoutOpen,
setManageFlyoutOpen,
} from '../../../state/private_locations';
export const ManageLocationsFlyout = () => {
const [isAddingNew, setIsAddingNew] = useState(false);
const dispatch = useDispatch();
const setIsOpen = (val: boolean) => dispatch(setManageFlyoutOpen(val));
const isOpen = useSelector(selectManageFlyoutOpen);
const { onSubmit, saveLoading, fetchLoading, deleteLoading, privateLocations, onDelete } =
useLocationsAPI({
isOpen,
});
const { fleet } = useKibana<ClientPluginsStart>().services;
const hasFleetPermissions = Boolean(fleet?.authz.fleet.readAgentPolicies);
const canSave: boolean = !!useKibana().services?.application?.capabilities.uptime.save;
useEffect(() => {
if (isOpen) {
dispatch(getAgentPoliciesAction.get());
}
}, [dispatch, isOpen]);
const closeFlyout = () => {
setIsOpen(false);
dispatch(getServiceLocations());
};
const flyout = (
<EuiFlyout onClose={closeFlyout}>
<EuiFlyoutHeader hasBorder>
<EuiTitle size="m">
<h2>{MANAGE_PRIVATE_LOCATIONS}</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
{!hasFleetPermissions && (
<EuiCallOut title={NEED_PERMISSIONS} color="warning" iconType="help">
<p>{NEED_FLEET_READ_AGENT_POLICIES_PERMISSION}</p>
</EuiCallOut>
)}
{privateLocations.length === 0 && !(saveLoading || fetchLoading) && !isAddingNew ? (
<EmptyLocations setIsAddingNew={setIsAddingNew} disabled={!hasFleetPermissions} />
) : (
<PrivateLocationsList
privateLocations={privateLocations}
loading={fetchLoading}
onDelete={onDelete}
onSubmit={onSubmit}
hasFleetPermissions={hasFleetPermissions}
/>
)}
<EuiSpacer />
{isAddingNew && (
<LocationForm
privateLocations={privateLocations}
onSubmit={(val) => {
onSubmit(val);
setIsAddingNew(false);
}}
onDiscard={() => setIsAddingNew(false)}
/>
)}
{!isAddingNew && privateLocations.length > 0 && (
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButton
fill
isLoading={saveLoading || fetchLoading || deleteLoading}
disabled={!hasFleetPermissions || !canSave}
onClick={() => setIsAddingNew(true)}
>
{ADD_LABEL}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
)}
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty iconType="cross" onClick={closeFlyout} flush="left">
{CLOSE_LABEL}
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</EuiFlyout>
);
return (
<div>
<EuiButtonEmpty onClick={() => setIsOpen(true)} iconType="visMapCoordinate">
{MANAGE_PRIVATE_LOCATIONS}
</EuiButtonEmpty>
{isOpen ? flyout : null}
</div>
);
};
const MANAGE_PRIVATE_LOCATIONS = i18n.translate(
'xpack.synthetics.monitorManagement.managePrivateLocations',
{
defaultMessage: 'Manage private locations',
}
);
const CLOSE_LABEL = i18n.translate('xpack.synthetics.monitorManagement.closeLabel', {
defaultMessage: 'Close',
});
const ADD_LABEL = i18n.translate('xpack.synthetics.monitorManagement.addLocation', {
defaultMessage: 'Add location',
});
const NEED_PERMISSIONS = i18n.translate('xpack.synthetics.monitorManagement.needPermissions', {
defaultMessage: 'Need permissions',
});
const NEED_FLEET_READ_AGENT_POLICIES_PERMISSION = i18n.translate(
'xpack.synthetics.monitorManagement.needFleetReadAgentPoliciesPermission',
{
defaultMessage:
'You are not authorized to access Fleet. Fleet permissions are required to create new private locations.',
}
);

View file

@ -0,0 +1,45 @@
/*
* 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 React from 'react';
import { EuiCallOut, EuiLink, EuiButton } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useUptimeSettingsContext } from '../../../contexts/uptime_settings_context';
export const PolicyHostNeeded = () => {
const { basePath } = useUptimeSettingsContext();
return (
<EuiCallOut title={AGENT_POLICY_NEEDED} color="warning" iconType="help">
<p>
{ADD_AGENT_POLICY_DESCRIPTION}
<EuiLink href="#">{READ_THE_DOCS}</EuiLink>.
</p>
<EuiButton href={`${basePath}/app/fleet/policies?create`} color="primary">
{CREATE_AGENT_POLICY}
</EuiButton>
</EuiCallOut>
);
};
const CREATE_AGENT_POLICY = i18n.translate('xpack.synthetics.monitorManagement.createAgentPolicy', {
defaultMessage: 'Create agent policy',
});
const AGENT_POLICY_NEEDED = i18n.translate('xpack.synthetics.monitorManagement.agentPolicyNeeded', {
defaultMessage: 'No agent policy found. Please create one.',
});
const ADD_AGENT_POLICY_DESCRIPTION = i18n.translate(
'xpack.synthetics.monitorManagement.addAgentPolicyDesc',
{
defaultMessage:
'To add a synthetics private location, a Fleet policy with active elastic Agent is needed.',
}
);
const READ_THE_DOCS = i18n.translate('xpack.synthetics.monitorManagement.readDocs', {
defaultMessage: 'Read the docs',
});

View file

@ -0,0 +1,133 @@
/*
* 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 React from 'react';
import { Controller, FieldErrors, Control } from 'react-hook-form';
import { useSelector } from 'react-redux';
import {
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
EuiHealth,
EuiSuperSelect,
EuiText,
EuiToolTip,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { PrivateLocation } from '../../../../../common/runtime_types';
import { selectAgentPolicies } from '../../../state/private_locations';
export const PolicyHostsField = ({
isDisabled,
errors,
control,
privateLocations,
}: {
isDisabled: boolean;
errors: FieldErrors;
control: Control<PrivateLocation, any>;
privateLocations: PrivateLocation[];
}) => {
const { data } = useSelector(selectAgentPolicies);
const policyHostsOptions = data?.items.map((item) => {
const hasLocation = privateLocations.find((location) => location.policyHostId === item.id);
return {
disabled: Boolean(hasLocation),
value: item.id,
inputDisplay: (
<EuiHealth
color={item.status === 'active' ? 'success' : 'warning'}
style={{ lineHeight: 'inherit' }}
>
{item.name}
</EuiHealth>
),
'data-test-subj': item.name,
dropdownDisplay: (
<EuiToolTip
content={
hasLocation?.name
? i18n.translate('xpack.synthetics.monitorManagement.anotherPrivateLocation', {
defaultMessage:
'This agent policy is already attached to location: {locationName}.',
values: { locationName: hasLocation?.name },
})
: undefined
}
>
<>
<EuiHealth
color={item.status === 'active' ? 'success' : 'warning'}
style={{ lineHeight: 'inherit' }}
>
<strong>{item.name}</strong>
</EuiHealth>
<EuiFlexGroup>
<EuiFlexItem>
<EuiText size="s" color="subdued">
<p>
{AGENTS_LABEL} {item.agents}
</p>
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<EuiText size="s" color="subdued">
<p>{item.description}</p>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</>
</EuiToolTip>
),
};
});
return (
<EuiFormRow
label={POLICY_HOST_LABEL}
helpText={!errors?.policyHostId ? SELECT_POLICY_HOSTS : undefined}
isInvalid={!!errors?.policyHostId}
error={SELECT_POLICY_HOSTS}
>
<Controller
name="policyHostId"
control={control}
rules={{ required: true }}
render={({ field }) => (
<EuiSuperSelect
disabled={isDisabled}
fullWidth
aria-label={SELECT_POLICY_HOSTS}
placeholder={SELECT_POLICY_HOSTS}
valueOfSelected={field.value}
itemLayoutAlign="top"
popoverProps={{ repositionOnScroll: true }}
hasDividers
isInvalid={!!errors?.policyHostId}
options={policyHostsOptions ?? []}
{...field}
/>
)}
/>
</EuiFormRow>
);
};
const AGENTS_LABEL = i18n.translate('xpack.synthetics.monitorManagement.agentsLabel', {
defaultMessage: 'Agents: ',
});
const SELECT_POLICY_HOSTS = i18n.translate('xpack.synthetics.monitorManagement.selectPolicyHost', {
defaultMessage: 'Select agent policy',
});
const POLICY_HOST_LABEL = i18n.translate('xpack.synthetics.monitorManagement.policyHost', {
defaultMessage: 'Agent policy',
});

View file

@ -22,6 +22,7 @@ describe('<ServiceLocations />', () => {
lon: 1,
},
url: 'url',
isServiceManaged: true,
};
const locationTestSubId = `syntheticsServiceLocation--${location.id}`;
const state = {

View file

@ -8,9 +8,13 @@
import React, { useState, useEffect } from 'react';
import { useSelector } from 'react-redux';
import { i18n } from '@kbn/i18n';
import { EuiCheckboxGroup, EuiFormRow, EuiText, EuiBadge } from '@elastic/eui';
import { EuiCheckboxGroup, EuiFormRow, EuiText, EuiBadge, EuiIconTip } from '@elastic/eui';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { useRouteMatch } from 'react-router-dom';
import { monitorManagementListSelector } from '../../../state/selectors';
import { MonitorServiceLocations, LocationStatus } from '../../../../../common/runtime_types';
import { ClientPluginsStart } from '../../../../plugin';
import { MONITOR_EDIT_ROUTE } from '../../../../../common/constants';
interface Props {
selectedLocations: MonitorServiceLocations;
@ -33,6 +37,8 @@ export const ServiceLocations = ({
);
const { locations } = useSelector(monitorManagementListSelector);
const isEditMonitor = useRouteMatch(MONITOR_EDIT_ROUTE);
const onLocationChange = (optionId: string) => {
const isSelected = !checkboxIdToSelectedMap[optionId];
const location = locations.find((loc) => loc.id === optionId);
@ -50,6 +56,11 @@ export const ServiceLocations = ({
const errorMessage = error ?? (isInvalid ? VALIDATION_ERROR : null);
const kServices = useKibana<ClientPluginsStart>().services;
const canSaveIntegrations: boolean =
!!kServices?.fleet?.authz.integrations.writeIntegrationPolicies;
useEffect(() => {
const newCheckboxIdToSelectedMap = selectedLocations.reduce<Record<string, boolean>>(
(acc, location) => {
@ -65,18 +76,35 @@ export const ServiceLocations = ({
<EuiFormRow label={LOCATIONS_LABEL} error={errorMessage} isInvalid={errorMessage !== null}>
<EuiCheckboxGroup
options={locations.map((location) => {
const badge =
let badge =
location.status !== LocationStatus.GA ? (
<EuiBadge color="warning">Tech Preview</EuiBadge>
<EuiBadge color="warning">{TECH_PREVIEW_LABEL}</EuiBadge>
) : null;
if (!location.isServiceManaged) {
badge = <EuiBadge color="primary">{PRIVATE_LABEL}</EuiBadge>;
}
const invalidBadge = location.isInvalid ? (
<EuiBadge color="danger">{INVALID_LABEL}</EuiBadge>
) : null;
const isPrivateDisabled =
!location.isServiceManaged && (Boolean(location.isInvalid) || !canSaveIntegrations);
const iconTip =
isPrivateDisabled && !canSaveIntegrations ? (
<EuiIconTip content={CANNOT_SAVE_INTEGRATION_LABEL} position="right" />
) : null;
const label = (
<EuiText size="s" data-test-subj={`syntheticsServiceLocationText--${location.id}`}>
{location.label} {badge}
{location.label} {badge} {invalidBadge}
{iconTip}
</EuiText>
);
return {
...location,
label,
disabled: isPrivateDisabled && !isEditMonitor?.isExact,
'data-test-subj': `syntheticsServiceLocation--${location.id}`,
};
})}
@ -102,3 +130,26 @@ export const LOCATIONS_LABEL = i18n.translate(
defaultMessage: 'Monitor locations',
}
);
export const TECH_PREVIEW_LABEL = i18n.translate(
'xpack.synthetics.monitorManagement.techPreviewLabel',
{
defaultMessage: 'Tech Preview',
}
);
export const PRIVATE_LABEL = i18n.translate('xpack.synthetics.monitorManagement.privateLabel', {
defaultMessage: 'Private',
});
export const INVALID_LABEL = i18n.translate('xpack.synthetics.monitorManagement.invalidLabel', {
defaultMessage: 'Invalid',
});
export const CANNOT_SAVE_INTEGRATION_LABEL = i18n.translate(
'xpack.synthetics.monitorManagement.cannotSaveIntegration',
{
defaultMessage:
'You are not authorized to update integrations. Integrations write permissions are required.',
}
);

View file

@ -9,6 +9,8 @@ import React, { useContext } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiButtonIcon, EuiFlexItem, EuiFlexGroup, EuiToolTip } from '@elastic/eui';
import moment from 'moment';
import { usePrivateLocationPermissions } from '../hooks/use_private_location_permission';
import { CANNOT_SAVE_INTEGRATION_LABEL } from '../monitor_config/locations';
import { UptimeSettingsContext } from '../../../contexts';
import { DeleteMonitor } from './delete_monitor';
import { InlineError } from './inline_error';
@ -47,16 +49,22 @@ export const Actions = ({ id, name, onUpdate, isDisabled, errorSummaries, monito
}
}
const { canUpdatePrivateMonitor } = usePrivateLocationPermissions(
monitor?.attributes as BrowserFields
);
return (
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiButtonIcon
isDisabled={isDisabled}
iconType="pencil"
href={`${basePath}/app/uptime/edit-monitor/${id}`}
aria-label={EDIT_MONITOR_LABEL}
data-test-subj="monitorManagementEditMonitor"
/>
<EuiToolTip content={!canUpdatePrivateMonitor ? CANNOT_SAVE_INTEGRATION_LABEL : ''}>
<EuiButtonIcon
isDisabled={isDisabled || !canUpdatePrivateMonitor}
iconType="pencil"
href={`${basePath}/app/uptime/edit-monitor/${id}`}
aria-label={EDIT_MONITOR_LABEL}
data-test-subj="monitorManagementEditMonitor"
/>
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiToolTip
@ -73,7 +81,7 @@ export const Actions = ({ id, name, onUpdate, isDisabled, errorSummaries, monito
onUpdate={onUpdate}
name={name}
id={id}
isDisabled={isDisabled || isProjectMonitor}
isDisabled={isDisabled || isProjectMonitor || !canUpdatePrivateMonitor}
/>
</EuiToolTip>
</EuiFlexItem>

View file

@ -7,7 +7,16 @@
import React, { useState, useEffect, useRef } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiEmptyPrompt, EuiButton, EuiTitle, EuiLink } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import {
EuiEmptyPrompt,
EuiButton,
EuiTitle,
EuiLink,
EuiCallOut,
EuiText,
EuiSpacer,
} from '@elastic/eui';
import { useEnablement } from '../hooks/use_enablement';
import { kibanaService } from '../../../state/kibana_service';
import { SYNTHETICS_ENABLE_SUCCESS, SYNTHETICS_DISABLE_SUCCESS } from '../content';
@ -46,44 +55,81 @@ export const EnablementEmptyState = ({ focusButton }: { focusButton: boolean })
}, [focusButton]);
return !isEnabled && !loading ? (
<EuiEmptyPrompt
title={
<h2>
{canEnable ? MONITOR_MANAGEMENT_ENABLEMENT_LABEL : MONITOR_MANAGEMENT_DISABLED_LABEL}
</h2>
}
body={
<p>
{canEnable ? MONITOR_MANAGEMENT_ENABLEMENT_MESSAGE : MONITOR_MANAGEMENT_DISABLED_MESSAGE}
</p>
}
actions={
canEnable ? (
<EuiButton
color="primary"
fill
onClick={handleEnableSynthetics}
data-test-subj="syntheticsEnableButton"
buttonRef={buttonRef}
>
{MONITOR_MANAGEMENT_ENABLEMENT_BTN_LABEL}
</EuiButton>
) : null
}
footer={
<>
<EuiTitle size="xxs">
<h3>{LEARN_MORE_LABEL}</h3>
</EuiTitle>
<EuiLink
href="https://docs.google.com/document/d/1hkzFibu9LggPWXQqfbAd0mMlV75wCME7_BebXlEH-oI"
target="_blank"
>
{DOCS_LABEL}
</EuiLink>
</>
}
/>
<>
<EuiEmptyPrompt
title={
<h2>
{canEnable ? MONITOR_MANAGEMENT_ENABLEMENT_LABEL : MONITOR_MANAGEMENT_DISABLED_LABEL}
</h2>
}
body={
<p>
{canEnable
? MONITOR_MANAGEMENT_ENABLEMENT_MESSAGE
: MONITOR_MANAGEMENT_DISABLED_MESSAGE}
</p>
}
actions={
canEnable ? (
<EuiButton
color="primary"
fill
onClick={handleEnableSynthetics}
data-test-subj="syntheticsEnableButton"
buttonRef={buttonRef}
>
{MONITOR_MANAGEMENT_ENABLEMENT_BTN_LABEL}
</EuiButton>
) : null
}
footer={
<>
<EuiTitle size="xxs">
<h3>{LEARN_MORE_LABEL}</h3>
</EuiTitle>
<EuiLink
href="https://docs.google.com/document/d/1hkzFibu9LggPWXQqfbAd0mMlV75wCME7_BebXlEH-oI"
target="_blank"
>
{DOCS_LABEL}
</EuiLink>
</>
}
/>
<EuiSpacer />
<EuiEmptyPrompt
paddingSize="none"
body={
<EuiCallOut title="Please note" className="eui-textLeft">
<EuiText size="s">
<FormattedMessage
id="xpack.synthetics.monitorManagement.disclaimer"
defaultMessage="By using this feature, Customer acknowledges that it has read and agrees to {link}. "
values={{
link: (
<EuiLink
href="https://www.elastic.co/agreements/beta-release-terms"
target="_blank"
>
{"Elastic's Beta Release Terms"}
</EuiLink>
),
}}
/>
</EuiText>
<EuiSpacer size="s" />
<EuiText size="s">
<FormattedMessage
id="xpack.synthetics.monitorManagement.disclaimerLinkLabel"
defaultMessage="There is no cost for the use of the service to execute your tests during the beta period. A fair usage policy will apply. Normal data storage costs apply for test results stored in your Elastic cluster.', "
/>
</EuiText>
</EuiCallOut>
}
/>
</>
) : null;
};

View file

@ -10,7 +10,12 @@ import { i18n } from '@kbn/i18n';
import React, { useEffect, useState } from 'react';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { FETCH_STATUS, useFetcher } from '@kbn/observability-plugin/public';
import { ConfigKey, EncryptedSyntheticsMonitor } from '../../../../../common/runtime_types';
import { usePrivateLocationPermissions } from '../hooks/use_private_location_permission';
import {
BrowserFields,
ConfigKey,
EncryptedSyntheticsMonitor,
} from '../../../../../common/runtime_types';
import { setMonitor } from '../../../state/api';
interface Props {
@ -25,6 +30,8 @@ export const MonitorEnabled = ({ id, monitor, onUpdate, isDisabled }: Props) =>
const { notifications } = useKibana();
const { canUpdatePrivateMonitor } = usePrivateLocationPermissions(monitor as BrowserFields);
const { status } = useFetcher(() => {
if (isEnabled !== null) {
return setMonitor({ id, monitor: { ...monitor, [ConfigKey.ENABLED]: isEnabled } });
@ -69,7 +76,7 @@ export const MonitorEnabled = ({ id, monitor, onUpdate, isDisabled }: Props) =>
<div css={{ position: 'relative' }} aria-busy={isLoading}>
<EuiSwitch
checked={enabled}
disabled={isLoading || isDisabled}
disabled={isLoading || isDisabled || !canUpdatePrivateMonitor}
showLabel={false}
label={enabled ? DISABLE_MONITOR_LABEL : ENABLE_MONITOR_LABEL}
title={enabled ? DISABLE_MONITOR_LABEL : ENABLE_MONITOR_LABEL}

View file

@ -9,6 +9,8 @@ import React, { useCallback, Dispatch } from 'react';
import { useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
import { useTrackPageview } from '@kbn/observability-plugin/public';
import { useLocations } from '../hooks/use_locations';
import { EmptyLocations } from '../manage_locations/empty_locations';
import { monitorManagementListSelector } from '../../../state/selectors';
import { MonitorAsyncError } from './monitor_async_error';
import { useInlineErrors } from '../hooks/use_inline_errors';
@ -53,10 +55,16 @@ export const MonitorListContainer = ({
const { data: monitorSavedObjects, loading: objectsLoading } = useInvalidMonitors(errorSummaries);
const { locations } = useLocations();
if (!isEnabled && monitorList.list.total === 0) {
return null;
}
if (isEnabled && monitorList.list.total === 0 && locations.length === 0) {
return <EmptyLocations />;
}
return (
<>
<MonitorAsyncError />

View file

@ -9,20 +9,31 @@ import { EuiButtonIcon, EuiLoadingSpinner, EuiToolTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Ping } from '../../../../../../common/runtime_types';
import { testNowMonitorAction } from '../../../../state/actions';
import { testNowRunSelector, TestRunStats } from '../../../../state/reducers/test_now_runs';
export const TestNowColumn = ({
monitorId,
configId,
selectedMonitor,
}: {
monitorId: string;
configId?: string;
selectedMonitor: Ping;
}) => {
const dispatch = useDispatch();
const testNowRun = useSelector(testNowRunSelector(configId));
if (selectedMonitor.monitor.fleet_managed) {
return (
<EuiToolTip content={PRIVATE_AVAILABLE_LABEL}>
<>--</>
</EuiToolTip>
);
}
if (!configId) {
return (
<EuiToolTip content={TEST_NOW_AVAILABLE_LABEL}>
@ -68,6 +79,13 @@ export const TEST_NOW_AVAILABLE_LABEL = i18n.translate(
}
);
export const PRIVATE_AVAILABLE_LABEL = i18n.translate(
'xpack.synthetics.monitorList.testNow.available.private',
{
defaultMessage: 'For now, Test now is disabled for private locations monitors.',
}
);
export const TEST_NOW_LABEL = i18n.translate('xpack.synthetics.monitorList.testNow.label', {
defaultMessage: 'Test now',
});

View file

@ -208,7 +208,11 @@ export const MonitorListComponent: ({
name: TEST_NOW_COLUMN,
width: '100px',
render: (item: MonitorSummary) => (
<TestNowColumn monitorId={item.monitor_id} configId={item.configId} />
<TestNowColumn
monitorId={item.monitor_id}
configId={item.configId}
selectedMonitor={item.state.summaryPings[0]}
/>
),
},
...(!hideExtraColumns

View file

@ -128,4 +128,5 @@ export const mockState: AppState = {
hitCount: [],
},
testNowRuns: {},
agentPolicies: { loading: false, data: null, error: null },
};

View file

@ -5,15 +5,17 @@
* 2.0.
*/
import React from 'react';
import React, { useEffect } from 'react';
import { i18n } from '@kbn/i18n';
import { useTrackPageview } from '@kbn/observability-plugin/public';
import { useDispatch } from 'react-redux';
import { ScheduleUnit } from '../../../../common/runtime_types';
import { SyntheticsProviders } from '../../components/fleet_package/contexts';
import { Loader } from '../../components/monitor_management/loader/loader';
import { MonitorConfig } from '../../components/monitor_management/monitor_config/monitor_config';
import { useLocations } from '../../components/monitor_management/hooks/use_locations';
import { useMonitorManagementBreadcrumbs } from './use_monitor_management_breadcrumbs';
import { getAgentPoliciesAction } from '../../state/private_locations';
export const AddMonitorPage: React.FC = () => {
useTrackPageview({ app: 'uptime', path: 'add-monitor' });
@ -21,8 +23,14 @@ export const AddMonitorPage: React.FC = () => {
const { error, loading, locations, throttling } = useLocations();
const dispatch = useDispatch();
useMonitorManagementBreadcrumbs({ isAddMonitor: true });
useEffect(() => {
dispatch(getAgentPoliciesAction.get());
}, [dispatch]);
return (
<Loader
error={Boolean(error) || (locations && locations.length === 0)}

View file

@ -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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { OutPortal } from 'react-reverse-portal';
import { ManageLocationsPortalNode } from './portals';
export const ManageLocations = () => {
return (
<div>
<OutPortal node={ManageLocationsPortalNode} />
</div>
);
};

View file

@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n';
import { useSelector } from 'react-redux';
import { EuiCallOut, EuiButton, EuiSpacer, EuiLink } from '@elastic/eui';
import { useTrackPageview } from '@kbn/observability-plugin/public';
import { ManageLocationsPortal } from '../../components/monitor_management/manage_locations/manage_locations';
import { monitorManagementListSelector } from '../../state/selectors';
import { useMonitorManagementBreadcrumbs } from './use_monitor_management_breadcrumbs';
import { MonitorListContainer } from '../../components/monitor_management/monitor_list/monitor_list_container';
@ -91,6 +92,7 @@ export const MonitorManagementPage: React.FC = () => {
pageState={pageState}
dispatchPageAction={dispatchPageAction}
/>
<ManageLocationsPortal />
</Loader>
{showEmptyState && <EnablementEmptyState focusButton={shouldFocusEnablementButton} />}
</>

View file

@ -10,3 +10,5 @@ import { createPortalNode } from 'react-reverse-portal';
export const ActionBarPortalNode = createPortalNode();
export const APIKeysPortalNode = createPortalNode();
export const ManageLocationsPortalNode = createPortalNode();

View file

@ -12,6 +12,7 @@ import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { APP_WRAPPER_CLASS } from '@kbn/core/public';
import { useInspectorContext } from '@kbn/observability-plugin/public';
import { ManageLocations } from './pages/monitor_management/manage_locations';
import {
CERTIFICATES_ROUTE,
MAPPING_ERROR_ROUTE,
@ -209,6 +210,7 @@ const getRoutes = (): RouteProps[] => {
defaultMessage="Add Monitor"
/>
),
rightSideItems: [<APIKeysButton />, <ManageLocations />],
},
bottomBar: <MonitorManagementBottomBar />,
bottomBarProps: { paddingSize: 'm' as const },
@ -233,6 +235,7 @@ const getRoutes = (): RouteProps[] => {
defaultMessage="Edit Monitor"
/>
),
rightSideItems: [<APIKeysButton />, <ManageLocations />],
},
bottomBar: <MonitorManagementBottomBar />,
bottomBarProps: { paddingSize: 'm' as const },
@ -273,7 +276,7 @@ const getRoutes = (): RouteProps[] => {
</EuiFlexItem>
</EuiFlexGroup>
),
rightSideItems: [<AddMonitorBtn />, <APIKeysButton />],
rightSideItems: [<AddMonitorBtn />, <APIKeysButton />, <ManageLocations />],
},
},
];

View file

@ -6,6 +6,7 @@
*/
import { fork } from 'redux-saga/effects';
import { fetchAgentPoliciesEffect } from '../private_locations';
import { fetchMonitorDetailsEffect } from './monitor';
import { fetchMonitorListEffect, fetchUpdatedMonitorEffect } from './monitor_list';
import {
@ -49,4 +50,5 @@ export function* rootEffect() {
yield fork(generateBlockStatsOnPut);
yield fork(pruneBlockCache);
yield fork(fetchSyntheticsServiceAllowedEffect);
yield fork(fetchAgentPoliciesEffect);
}

View file

@ -0,0 +1,16 @@
/*
* 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 { createAction } from '@reduxjs/toolkit';
import { AgentPoliciesList } from '.';
import { createAsyncAction } from '../../../apps/synthetics/state/utils/actions';
export const getAgentPoliciesAction = createAsyncAction<void, AgentPoliciesList>(
'[AGENT POLICIES] GET'
);
export const setManageFlyoutOpen = createAction<boolean>('SET MANAGE FLYOUT OPEN');

View file

@ -0,0 +1,49 @@
/*
* 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 { SavedObjectsClientContract } from '@kbn/core/public';
import { AgentPoliciesList } from '.';
import {
privateLocationsSavedObjectId,
privateLocationsSavedObjectName,
} from '../../../../common/saved_objects/private_locations';
import { apiService } from '../api/utils';
import { SyntheticsPrivateLocations } from '../../../../common/runtime_types';
const FLEET_URLS = {
AGENT_POLICIES: '/api/fleet/agent_policies',
};
export const fetchAgentPolicies = async (): Promise<AgentPoliciesList> => {
return await apiService.get(
FLEET_URLS.AGENT_POLICIES,
{ page: 1, perPage: 10000, sortField: 'name', sortOrder: 'asc', full: true },
null
);
};
export const setSyntheticsPrivateLocations = async (
client: SavedObjectsClientContract,
privateLocations: SyntheticsPrivateLocations
) => {
await client.create(privateLocationsSavedObjectName, privateLocations, {
id: privateLocationsSavedObjectId,
overwrite: true,
});
};
export const getSyntheticsPrivateLocations = async (client: SavedObjectsClientContract) => {
try {
const obj = await client.get<SyntheticsPrivateLocations>(
privateLocationsSavedObjectName,
privateLocationsSavedObjectId
);
return obj?.attributes.locations ?? [];
} catch (getErr) {
return [];
}
};

View file

@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { takeLeading } from 'redux-saga/effects';
import { fetchAgentPolicies } from './api';
import { getAgentPoliciesAction } from './actions';
import { fetchEffectFactory } from '../../../apps/synthetics/state/utils/fetch_effect';
export function* fetchAgentPoliciesEffect() {
yield takeLeading(
getAgentPoliciesAction.get,
fetchEffectFactory(
fetchAgentPolicies,
getAgentPoliciesAction.success,
getAgentPoliciesAction.fail
)
);
}

View file

@ -0,0 +1,55 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { IHttpFetchError, ResponseErrorBody } from '@kbn/core/public';
import { createReducer } from '@reduxjs/toolkit';
import { AgentPolicy } from '@kbn/fleet-plugin/common';
import { getAgentPoliciesAction, setManageFlyoutOpen } from './actions';
export interface AgentPoliciesList {
items: AgentPolicy[];
total: number;
page: number;
perPage: number;
}
export interface AgentPoliciesState {
data: AgentPoliciesList | null;
loading: boolean;
error: IHttpFetchError<ResponseErrorBody> | null;
isManageFlyoutOpen?: boolean;
}
const initialState: AgentPoliciesState = {
data: null,
loading: false,
error: null,
isManageFlyoutOpen: false,
};
export const agentPoliciesReducer = createReducer(initialState, (builder) => {
builder
.addCase(getAgentPoliciesAction.get, (state) => {
state.loading = true;
})
.addCase(getAgentPoliciesAction.success, (state, action) => {
state.data = action.payload;
state.loading = false;
})
.addCase(getAgentPoliciesAction.fail, (state, action) => {
state.error = action.payload as IHttpFetchError<ResponseErrorBody>;
state.loading = false;
})
.addCase(setManageFlyoutOpen, (state, action) => {
state.isManageFlyoutOpen = action.payload;
state.loading = false;
});
});
export * from './actions';
export * from './effects';
export * from './selectors';

View file

@ -0,0 +1,15 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { createSelector } from 'reselect';
import { AppState } from '..';
const getState = (appState: AppState) => appState.agentPolicies;
export const selectAgentPolicies = createSelector(getState, (state) => state);
export const selectManageFlyoutOpen = createSelector(getState, (state) =>
Boolean(state.isManageFlyoutOpen)
);

View file

@ -6,6 +6,7 @@
*/
import { combineReducers } from 'redux';
import { agentPoliciesReducer } from '../private_locations';
import { monitorReducer } from './monitor';
import { uiReducer } from './ui';
import { monitorStatusReducer } from './monitor_status';
@ -44,4 +45,5 @@ export const rootReducer = combineReducers({
networkEvents: networkEventsReducer,
synthetics: syntheticsReducer,
testNowRuns: testNowRunsReducer,
agentPolicies: agentPoliciesReducer,
});

View file

@ -38,7 +38,7 @@ import {
import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public';
import { Start as InspectorPluginStart } from '@kbn/inspector-plugin/public';
import { CasesUiStart } from '@kbn/cases-plugin/public';
import { CloudSetup } from '@kbn/cloud-plugin/public';
import { CloudSetup, CloudStart } from '@kbn/cloud-plugin/public';
import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import { PLUGIN } from '../common/constants/plugin';
import { MONITORS_ROUTE } from '../common/constants/ui';
@ -74,6 +74,7 @@ export interface ClientPluginsStart {
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
cases: CasesUiStart;
dataViews: DataViewsPublicPluginStart;
cloud?: CloudStart;
}
export interface UptimePluginServices extends Partial<CoreStart> {

View file

@ -6,6 +6,7 @@
*/
import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server';
import { privateLocationsSavedObjectName } from '../common/saved_objects/private_locations';
import { PLUGIN } from '../common/constants/plugin';
import { UPTIME_RULE_TYPES } from '../common/constants/alerts';
import { umDynamicSettings } from './legacy_uptime/lib/saved_objects/uptime_settings';
@ -29,7 +30,12 @@ export const uptimeFeature = {
catalogue: ['uptime'],
api: ['uptime-read', 'uptime-write', 'lists-all'],
savedObject: {
all: [umDynamicSettings.name, syntheticsMonitorType, syntheticsApiKeyObjectType],
all: [
umDynamicSettings.name,
syntheticsMonitorType,
syntheticsApiKeyObjectType,
privateLocationsSavedObjectName,
],
read: [],
},
alerting: {
@ -51,7 +57,12 @@ export const uptimeFeature = {
api: ['uptime-read', 'lists-read'],
savedObject: {
all: [],
read: [umDynamicSettings.name, syntheticsMonitorType, syntheticsApiKeyObjectType],
read: [
umDynamicSettings.name,
syntheticsMonitorType,
syntheticsApiKeyObjectType,
privateLocationsSavedObjectName,
],
},
alerting: {
rule: {

View file

@ -33,7 +33,6 @@ import { UptimeESClient } from '../../lib';
import type { TelemetryEventsSender } from '../../telemetry/sender';
import type { UptimeRouter } from '../../../../types';
import { UptimeConfig } from '../../../../../common/config';
import { SyntheticsService } from '../../../../synthetics_service/synthetics_service';
export type UMElasticsearchQueryFn<P, R = any> = (
params: {
@ -57,7 +56,6 @@ export interface UptimeServerSetup {
savedObjectsClient?: SavedObjectsClientContract;
authSavedObjectsClient?: SavedObjectsClientContract;
encryptedSavedObjects: EncryptedSavedObjectsPluginStart;
syntheticsService: SyntheticsService;
kibanaVersion: string;
logger: Logger;
telemetry: TelemetryEventsSender;

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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import {
SavedObjectsClientContract,
SavedObjectsErrorHelpers,
SavedObjectsType,
} from '@kbn/core/server';
import { privateLocationsSavedObjectName } from '../../../../common/saved_objects/private_locations';
import { SyntheticsPrivateLocations } from '../../../../common/runtime_types';
export const privateLocationsSavedObjectId = 'synthetics-privates-locations-singleton';
export const privateLocationsSavedObject: SavedObjectsType = {
name: privateLocationsSavedObjectName,
hidden: false,
namespaceType: 'agnostic',
mappings: {
dynamic: false,
properties: {
/* Leaving these commented to make it clear that these fields exist, even though we don't want them indexed.
When adding new fields please add them here. If they need to be searchable put them in the uncommented
part of properties.
*/
},
},
management: {
importableAndExportable: true,
},
};
export const getSyntheticsPrivateLocations = async (client: SavedObjectsClientContract) => {
try {
const obj = await client.get<SyntheticsPrivateLocations>(
privateLocationsSavedObject.name,
privateLocationsSavedObjectId
);
return obj?.attributes.locations ?? [];
} catch (getErr) {
if (SavedObjectsErrorHelpers.isNotFoundError(getErr)) {
return [];
}
throw getErr;
}
};
export const setSyntheticsPrivateLocations = async (
client: SavedObjectsClientContract,
privateLocations: SyntheticsPrivateLocations
) => {
await client.create(privateLocationsSavedObject.name, privateLocations, {
id: privateLocationsSavedObjectId,
overwrite: true,
});
};

View file

@ -8,6 +8,7 @@
import { SavedObjectsErrorHelpers, SavedObjectsServiceSetup } from '@kbn/core/server';
import { EncryptedSavedObjectsPluginSetup } from '@kbn/encrypted-saved-objects-plugin/server';
import { privateLocationsSavedObject } from './private_locations';
import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants';
import { secretKeys } from '../../../../common/constants/monitor_management';
import { DynamicSettings } from '../../../../common/runtime_types';
@ -19,34 +20,32 @@ import { syntheticsServiceApiKey } from './service_api_key';
export const registerUptimeSavedObjects = (
savedObjectsService: SavedObjectsServiceSetup,
encryptedSavedObjects: EncryptedSavedObjectsPluginSetup,
isServiceEnabled: boolean
encryptedSavedObjects: EncryptedSavedObjectsPluginSetup
) => {
savedObjectsService.registerType(umDynamicSettings);
savedObjectsService.registerType(privateLocationsSavedObject);
if (isServiceEnabled) {
savedObjectsService.registerType(syntheticsMonitor);
savedObjectsService.registerType(syntheticsServiceApiKey);
savedObjectsService.registerType(syntheticsMonitor);
savedObjectsService.registerType(syntheticsServiceApiKey);
encryptedSavedObjects.registerType({
type: syntheticsServiceApiKey.name,
attributesToEncrypt: new Set(['apiKey']),
});
encryptedSavedObjects.registerType({
type: syntheticsServiceApiKey.name,
attributesToEncrypt: new Set(['apiKey']),
});
encryptedSavedObjects.registerType({
type: syntheticsMonitor.name,
attributesToEncrypt: new Set([
'secrets',
/* adding secretKeys to the list of attributes to encrypt ensures
* that secrets are never stored on the resulting saved object,
* even in the presence of developer error.
*
* In practice, all secrets should be stored as a single JSON
* payload on the `secrets` key. This ensures performant decryption. */
...secretKeys,
]),
});
}
encryptedSavedObjects.registerType({
type: syntheticsMonitor.name,
attributesToEncrypt: new Set([
'secrets',
/* adding secretKeys to the list of attributes to encrypt ensures
* that secrets are never stored on the resulting saved object,
* even in the presence of developer error.
*
* In practice, all secrets should be stored as a single JSON
* payload on the `secrets` key. This ensures performant decryption. */
...secretKeys,
]),
});
};
export interface UMSavedObjectsAdapter {

View file

@ -15,6 +15,7 @@ import {
KibanaResponseFactory,
IKibanaResponse,
} from '@kbn/core/server';
import { SyntheticsMonitorClient } from '../../synthetics_service/synthetics_monitor/synthetics_monitor_client';
import { UMServerLibs, UptimeESClient } from '../lib/lib';
import type { UptimeRequestHandlerContext } from '../../types';
import { UptimeServerSetup } from '../lib/adapters';
@ -54,6 +55,7 @@ export type UptimeRoute = UMRouteDefinition<UMRouteHandler>;
* Functions of this type accept custom lib functions and outputs a route object.
*/
export type UMRestApiRouteFactory = (libs: UMServerLibs) => UptimeRoute;
export type SyntheticsRestApiRouteFactory = (libs: UMServerLibs) => SyntheticsRoute;
/**
* Functions of this type accept our internal route format and output a route
@ -64,6 +66,14 @@ export type UMKibanaRouteWrapper = (
server: UptimeServerSetup
) => UMKibanaRoute;
export type SyntheticsRoute = UMRouteDefinition<SyntheticsRouteHandler>;
export type SyntheticsRouteWrapper = (
uptimeRoute: SyntheticsRoute,
server: UptimeServerSetup,
syntheticsMonitorClient: SyntheticsMonitorClient
) => UMKibanaRoute;
/**
* This is the contract we specify internally for route handling.
*/
@ -82,3 +92,20 @@ export type UMRouteHandler = ({
savedObjectsClient: SavedObjectsClientContract;
server: UptimeServerSetup;
}) => IKibanaResponse<any> | Promise<IKibanaResponse<any>>;
export type SyntheticsRouteHandler = ({
uptimeEsClient,
context,
request,
response,
server,
savedObjectsClient,
}: {
uptimeEsClient: UptimeESClient;
context: UptimeRequestHandlerContext;
request: KibanaRequest<Record<string, any>, Record<string, any>, Record<string, any>>;
response: KibanaResponseFactory;
savedObjectsClient: SavedObjectsClientContract;
server: UptimeServerSetup;
syntheticsMonitorClient: SyntheticsMonitorClient;
}) => IKibanaResponse<any> | Promise<IKibanaResponse<any>>;

View file

@ -16,6 +16,7 @@ import {
import { mappingFromFieldMap } from '@kbn/rule-registry-plugin/common/mapping_from_field_map';
import { experimentalRuleFieldMap } from '@kbn/rule-registry-plugin/common/assets/field_maps/experimental_rule_field_map';
import { Dataset } from '@kbn/rule-registry-plugin/server';
import { SyntheticsMonitorClient } from './synthetics_service/synthetics_monitor/synthetics_monitor_client';
import { initSyntheticsServer } from './server';
import { initUptimeServer } from './legacy_uptime/uptime_server';
import { uptimeFeature } from './feature';
@ -42,7 +43,8 @@ export class Plugin implements PluginType {
private initContext: PluginInitializerContext;
private logger: Logger;
private server?: UptimeServerSetup;
private syntheticService?: SyntheticsService;
private syntheticsService?: SyntheticsService;
private syntheticsMonitorClient?: SyntheticsMonitorClient;
private readonly telemetryEventsSender: TelemetryEventsSender;
constructor(initializerContext: PluginInitializerContext<UptimeConfig>) {
@ -87,28 +89,21 @@ export class Plugin implements PluginType {
spaces: plugins.spaces,
} as UptimeServerSetup;
if (this.server.config.service) {
this.syntheticService = new SyntheticsService(
this.logger,
this.server,
this.server.config.service
);
this.syntheticsService = new SyntheticsService(this.server);
this.syntheticService.registerSyncTask(plugins.taskManager);
this.telemetryEventsSender.setup(plugins.telemetry);
}
this.syntheticsService.setup(plugins.taskManager);
this.syntheticsMonitorClient = new SyntheticsMonitorClient(this.syntheticsService, this.server);
this.telemetryEventsSender.setup(plugins.telemetry);
plugins.features.registerKibanaFeature(uptimeFeature);
initUptimeServer(this.server, plugins, ruleDataClient, this.logger);
initSyntheticsServer(this.server);
initSyntheticsServer(this.server, this.syntheticsMonitorClient);
registerUptimeSavedObjects(
core.savedObjects,
plugins.encryptedSavedObjects,
Boolean(this.server.config.service)
);
registerUptimeSavedObjects(core.savedObjects, plugins.encryptedSavedObjects);
KibanaTelemetryAdapter.registerUsageCollector(
plugins.usageCollection,
@ -120,32 +115,21 @@ export class Plugin implements PluginType {
};
}
public start(coreStart: CoreStart, plugins: UptimeCorePluginsStart) {
if (this.server?.config.service) {
this.savedObjectsClient = new SavedObjectsClient(
coreStart.savedObjects.createInternalRepository([syntheticsServiceApiKey.name])
);
} else {
this.savedObjectsClient = new SavedObjectsClient(
coreStart.savedObjects.createInternalRepository()
);
}
public start(coreStart: CoreStart, pluginsStart: UptimeCorePluginsStart) {
this.savedObjectsClient = new SavedObjectsClient(
coreStart.savedObjects.createInternalRepository([syntheticsServiceApiKey.name])
);
if (this.server) {
this.server.security = plugins.security;
this.server.fleet = plugins.fleet;
this.server.encryptedSavedObjects = plugins.encryptedSavedObjects;
this.server.security = pluginsStart.security;
this.server.fleet = pluginsStart.fleet;
this.server.encryptedSavedObjects = pluginsStart.encryptedSavedObjects;
this.server.savedObjectsClient = this.savedObjectsClient;
}
if (this.server?.config.service) {
this.syntheticService?.init();
this.syntheticService?.scheduleSyncTask(plugins.taskManager);
if (this.server && this.syntheticService) {
this.server.syntheticsService = this.syntheticService;
}
this.telemetryEventsSender.start(plugins.telemetry, coreStart);
}
this.syntheticsService?.start(pluginsStart.taskManager);
this.telemetryEventsSender.start(pluginsStart.telemetry, coreStart);
}
public stop() {}

View file

@ -0,0 +1,53 @@
/*
* 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 { UMServerLibs } from '../legacy_uptime/lib/lib';
import {
SyntheticsRestApiRouteFactory,
SyntheticsRoute,
SyntheticsRouteHandler,
} from '../legacy_uptime/routes';
export const createSyntheticsRouteWithAuth = (
libs: UMServerLibs,
routeCreator: SyntheticsRestApiRouteFactory
): SyntheticsRoute => {
const restRoute = routeCreator(libs);
const { handler, method, path, options, ...rest } = restRoute;
const licenseCheckHandler: SyntheticsRouteHandler = async ({
context,
response,
...restProps
}) => {
const { statusCode, message } = libs.license((await context.licensing).license);
if (statusCode === 200) {
return handler({
context,
response,
...restProps,
});
}
switch (statusCode) {
case 400:
return response.badRequest({ body: { message } });
case 401:
return response.unauthorized({ body: { message } });
case 403:
return response.forbidden({ body: { message } });
default:
throw new Error('Failed to validate the license');
}
};
return {
method,
path,
options,
handler: licenseCheckHandler,
...rest,
};
};

View file

@ -26,9 +26,9 @@ import { installIndexTemplatesRoute } from './synthetics_service/install_index_t
import { editSyntheticsMonitorRoute } from './monitor_cruds/edit_monitor';
import { addSyntheticsMonitorRoute } from './monitor_cruds/add_monitor';
import { addSyntheticsProjectMonitorRoute } from './monitor_cruds/add_monitor_project';
import { UMRestApiRouteFactory } from '../legacy_uptime/routes';
import { SyntheticsRestApiRouteFactory } from '../legacy_uptime/routes';
export const syntheticsAppRestApiRoutes: UMRestApiRouteFactory[] = [
export const syntheticsAppRestApiRoutes: SyntheticsRestApiRouteFactory[] = [
addSyntheticsProjectMonitorRoute,
addSyntheticsMonitorRoute,
getSyntheticsEnablementRoute,

View file

@ -6,23 +6,23 @@
*/
import { schema } from '@kbn/config-schema';
import { SavedObject, SavedObjectsErrorHelpers } from '@kbn/core/server';
import { SyntheticsMonitorClient } from '../../synthetics_service/synthetics_monitor/synthetics_monitor_client';
import {
ConfigKey,
MonitorFields,
SyntheticsMonitor,
EncryptedSyntheticsMonitor,
} from '../../../common/runtime_types';
import { UMRestApiRouteFactory } from '../../legacy_uptime/routes/types';
import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes/types';
import { API_URLS } from '../../../common/constants';
import { DEFAULT_FIELDS } from '../../../common/constants/monitor_defaults';
import { syntheticsMonitorType } from '../../legacy_uptime/lib/saved_objects/synthetics_monitor';
import { validateMonitor } from './monitor_validation';
import { sendTelemetryEvents, formatTelemetryEvent } from '../telemetry/monitor_upgrade_sender';
import { formatHeartbeatRequest } from '../../synthetics_service/formatters/format_configs';
import { formatSecrets } from '../../synthetics_service/utils/secrets';
import type { UptimeServerSetup } from '../../legacy_uptime/lib/adapters/framework';
export const addSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({
export const addSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
method: 'POST',
path: API_URLS.SYNTHETICS_MONITORS,
validate: {
@ -31,7 +31,13 @@ export const addSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({
id: schema.maybe(schema.string()),
}),
},
handler: async ({ request, response, savedObjectsClient, server }): Promise<any> => {
handler: async ({
request,
response,
savedObjectsClient,
server,
syntheticsMonitorClient,
}): Promise<any> => {
// usually id is auto generated, but this is useful for testing
const { id } = request.query;
@ -78,7 +84,12 @@ export const addSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({
});
}
const errors = await syncNewMonitor({ monitor, monitorSavedObject: newMonitor, server });
const errors = await syncNewMonitor({
monitor,
monitorSavedObject: newMonitor,
server,
syntheticsMonitorClient,
});
if (errors && errors.length > 0) {
return response.ok({
@ -98,17 +109,16 @@ export const syncNewMonitor = async ({
monitor,
monitorSavedObject,
server,
syntheticsMonitorClient,
}: {
monitor: SyntheticsMonitor;
monitorSavedObject: SavedObject<EncryptedSyntheticsMonitor>;
server: UptimeServerSetup;
syntheticsMonitorClient: SyntheticsMonitorClient;
}) => {
const errors = await server.syntheticsService.addConfig(
formatHeartbeatRequest({
monitor,
monitorId: monitorSavedObject.id,
customHeartbeatId: (monitor as MonitorFields)[ConfigKey.CUSTOM_HEARTBEAT_ID],
})
const errors = await syntheticsMonitorClient.addMonitor(
monitor as MonitorFields,
monitorSavedObject.id
);
sendTelemetryEvents(

View file

@ -8,12 +8,14 @@ import { schema } from '@kbn/config-schema';
import { UMServerLibs } from '../../legacy_uptime/lib/lib';
import { ProjectBrowserMonitor, Locations } from '../../../common/runtime_types';
import { UMRestApiRouteFactory } from '../../legacy_uptime/routes/types';
import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes/types';
import { API_URLS } from '../../../common/constants';
import { getServiceLocations } from '../../synthetics_service/get_service_locations';
import { ProjectMonitorFormatter } from '../../synthetics_service/project_monitor_formatter';
export const addSyntheticsProjectMonitorRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({
export const addSyntheticsProjectMonitorRoute: SyntheticsRestApiRouteFactory = (
libs: UMServerLibs
) => ({
method: 'PUT',
path: API_URLS.SYNTHETICS_MONITORS_PROJECT,
validate: {
@ -23,7 +25,13 @@ export const addSyntheticsProjectMonitorRoute: UMRestApiRouteFactory = (libs: UM
monitors: schema.arrayOf(schema.any()),
}),
},
handler: async ({ request, response, savedObjectsClient, server }): Promise<any> => {
handler: async ({
request,
response,
savedObjectsClient,
server,
syntheticsMonitorClient,
}): Promise<any> => {
const monitors = (request.body?.monitors as ProjectBrowserMonitor[]) || [];
const spaceId = server.spaces.spacesService.getSpaceId(request);
const { keep_stale: keepStale, project: projectId } = request.body || {};
@ -39,6 +47,7 @@ export const addSyntheticsProjectMonitorRoute: UMRestApiRouteFactory = (libs: UM
savedObjectsClient,
monitors,
server,
syntheticsMonitorClient,
});
await pushMonitorFormatter.configureAllProjectMonitors();

View file

@ -6,13 +6,14 @@
*/
import { schema } from '@kbn/config-schema';
import { SavedObjectsClientContract, SavedObjectsErrorHelpers } from '@kbn/core/server';
import { SyntheticsMonitorClient } from '../../synthetics_service/synthetics_monitor/synthetics_monitor_client';
import {
ConfigKey,
MonitorFields,
EncryptedSyntheticsMonitor,
SyntheticsMonitorWithSecrets,
} from '../../../common/runtime_types';
import { UMRestApiRouteFactory } from '../../legacy_uptime/routes/types';
import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes/types';
import { API_URLS } from '../../../common/constants';
import {
syntheticsMonitorType,
@ -26,7 +27,7 @@ import {
import { normalizeSecrets } from '../../synthetics_service/utils/secrets';
import type { UptimeServerSetup } from '../../legacy_uptime/lib/adapters/framework';
export const deleteSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({
export const deleteSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
method: 'DELETE',
path: API_URLS.SYNTHETICS_MONITORS + '/{monitorId}',
validate: {
@ -34,11 +35,22 @@ export const deleteSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({
monitorId: schema.string({ minLength: 1, maxLength: 1024 }),
}),
},
handler: async ({ request, response, savedObjectsClient, server }): Promise<any> => {
handler: async ({
request,
response,
savedObjectsClient,
server,
syntheticsMonitorClient,
}): Promise<any> => {
const { monitorId } = request.params;
try {
const errors = await deleteMonitor({ savedObjectsClient, server, monitorId });
const errors = await deleteMonitor({
savedObjectsClient,
server,
monitorId,
syntheticsMonitorClient,
});
if (errors && errors.length > 0) {
return response.ok({
@ -61,12 +73,14 @@ export const deleteMonitor = async ({
savedObjectsClient,
server,
monitorId,
syntheticsMonitorClient,
}: {
savedObjectsClient: SavedObjectsClientContract;
server: UptimeServerSetup;
monitorId: string;
syntheticsMonitorClient: SyntheticsMonitorClient;
}) => {
const { syntheticsService, logger, telemetry, kibanaVersion, encryptedSavedObjects } = server;
const { logger, telemetry, kibanaVersion, encryptedSavedObjects } = server;
const encryptedSavedObjectsClient = encryptedSavedObjects.getClient();
try {
const encryptedMonitor = await savedObjectsClient.get<EncryptedSyntheticsMonitor>(
@ -86,14 +100,11 @@ export const deleteMonitor = async ({
const normalizedMonitor = normalizeSecrets(monitor);
await savedObjectsClient.delete(syntheticsMonitorType, monitorId);
const errors = await syntheticsService.deleteConfigs([
{
...normalizedMonitor.attributes,
id:
(normalizedMonitor.attributes as MonitorFields)[ConfigKey.CUSTOM_HEARTBEAT_ID] ||
monitorId,
},
]);
const errors = await syntheticsMonitorClient.deleteMonitor({
...normalizedMonitor.attributes,
id:
(normalizedMonitor.attributes as MonitorFields)[ConfigKey.CUSTOM_HEARTBEAT_ID] || monitorId,
});
sendTelemetryEvents(
logger,

View file

@ -11,6 +11,7 @@ import { SavedObjectsUpdateResponse, SavedObject } from '@kbn/core/server';
import { EncryptedSyntheticsMonitor, SyntheticsMonitor } from '../../../common/runtime_types';
import { UptimeServerSetup } from '../../legacy_uptime/lib/adapters';
import { SyntheticsService } from '../../synthetics_service/synthetics_service';
import { SyntheticsMonitorClient } from '../../synthetics_service/synthetics_monitor/synthetics_monitor_client';
jest.mock('../telemetry/monitor_upgrade_sender', () => ({
sendTelemetryEvents: jest.fn(),
@ -25,18 +26,19 @@ describe('syncEditedMonitor', () => {
kibanaVersion: null,
authSavedObjectsClient: { bulkUpdate: jest.fn() },
logger,
config: {
service: {
username: 'dev',
password: '12345',
},
},
} as unknown as UptimeServerSetup;
const syntheticsService = new SyntheticsService(logger, serverMock, {
username: 'dev',
password: '12345',
});
const syntheticsService = new SyntheticsService(serverMock);
const fakePush = jest.fn();
jest.spyOn(syntheticsService, 'pushConfigs').mockImplementationOnce(fakePush);
serverMock.syntheticsService = syntheticsService;
jest.spyOn(syntheticsService, 'editConfig').mockImplementationOnce(fakePush);
const editedMonitor = {
type: 'http',
@ -61,21 +63,21 @@ describe('syncEditedMonitor', () => {
id: 'saved-obj-id',
} as SavedObjectsUpdateResponse<EncryptedSyntheticsMonitor>;
const syntheticsMonitorClient = new SyntheticsMonitorClient(syntheticsService, serverMock);
it('includes the isEdit flag', () => {
syncEditedMonitor({
editedMonitor,
editedMonitorSavedObject,
previousMonitor,
syntheticsMonitorClient,
server: serverMock,
});
expect(fakePush).toHaveBeenCalledWith(
expect.arrayContaining([
expect.objectContaining({
id: 'saved-obj-id',
}),
]),
true
expect.objectContaining({
id: 'saved-obj-id',
})
);
});
});

View file

@ -8,6 +8,7 @@
import { schema } from '@kbn/config-schema';
import { SavedObjectsUpdateResponse, SavedObject } from '@kbn/core/server';
import { SavedObjectsErrorHelpers } from '@kbn/core/server';
import { SyntheticsMonitorClient } from '../../synthetics_service/synthetics_monitor/synthetics_monitor_client';
import {
MonitorFields,
EncryptedSyntheticsMonitor,
@ -15,7 +16,7 @@ import {
SyntheticsMonitor,
ConfigKey,
} from '../../../common/runtime_types';
import { UMRestApiRouteFactory } from '../../legacy_uptime/routes/types';
import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes/types';
import { API_URLS } from '../../../common/constants';
import {
syntheticsMonitorType,
@ -27,12 +28,11 @@ import {
sendTelemetryEvents,
formatTelemetryUpdateEvent,
} from '../telemetry/monitor_upgrade_sender';
import { formatHeartbeatRequest } from '../../synthetics_service/formatters/format_configs';
import { formatSecrets, normalizeSecrets } from '../../synthetics_service/utils/secrets';
import type { UptimeServerSetup } from '../../legacy_uptime/lib/adapters/framework';
// Simplify return promise type and type it with runtime_types
export const editSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({
export const editSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
method: 'PUT',
path: API_URLS.SYNTHETICS_MONITORS + '/{monitorId}',
validate: {
@ -41,7 +41,13 @@ export const editSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({
}),
body: schema.any(),
},
handler: async ({ request, response, savedObjectsClient, server }): Promise<any> => {
handler: async ({
request,
response,
savedObjectsClient,
server,
syntheticsMonitorClient,
}): Promise<any> => {
const { encryptedSavedObjects, logger } = server;
const encryptedSavedObjectsClient = encryptedSavedObjects.getClient();
const monitor = request.body as SyntheticsMonitor;
@ -95,6 +101,7 @@ export const editSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({
editedMonitor,
editedMonitorSavedObject,
previousMonitor,
syntheticsMonitorClient,
});
// Return service sync errors in OK response
@ -121,21 +128,17 @@ export const syncEditedMonitor = async ({
editedMonitorSavedObject,
previousMonitor,
server,
syntheticsMonitorClient,
}: {
editedMonitor: SyntheticsMonitor;
editedMonitorSavedObject: SavedObjectsUpdateResponse<EncryptedSyntheticsMonitor>;
previousMonitor: SavedObject<EncryptedSyntheticsMonitor>;
server: UptimeServerSetup;
syntheticsMonitorClient: SyntheticsMonitorClient;
}) => {
const errors = await server.syntheticsService.pushConfigs(
[
formatHeartbeatRequest({
monitor: editedMonitor,
monitorId: editedMonitorSavedObject.id,
customHeartbeatId: (editedMonitor as MonitorFields)[ConfigKey.CUSTOM_HEARTBEAT_ID],
}),
],
true
const errors = await syntheticsMonitorClient.editMonitor(
editedMonitor as MonitorFields,
editedMonitorSavedObject.id
);
sendTelemetryEvents(

View file

@ -4,15 +4,15 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { UMRestApiRouteFactory } from '../../legacy_uptime/routes';
import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes';
import { generateAPIKey } from '../../synthetics_service/get_api_key';
import { API_URLS } from '../../../common/constants';
export const getAPIKeySyntheticsRoute: UMRestApiRouteFactory = (libs) => ({
export const getAPIKeySyntheticsRoute: SyntheticsRestApiRouteFactory = (libs) => ({
method: 'GET',
path: API_URLS.SYNTHETICS_APIKEY,
validate: {},
handler: async ({ request, response, server }): Promise<any> => {
handler: async ({ request, server }): Promise<any> => {
const { security } = server;
const apiKey = await generateAPIKey({

View file

@ -10,6 +10,7 @@ import {
SavedObjectsErrorHelpers,
SavedObjectsFindResponse,
} from '@kbn/core/server';
import { SyntheticsService } from '../../synthetics_service/synthetics_service';
import {
ConfigKey,
EncryptedSyntheticsMonitor,
@ -17,11 +18,10 @@ import {
} from '../../../common/runtime_types';
import { monitorAttributes } from '../../../common/types/saved_objects';
import { UMServerLibs } from '../../legacy_uptime/lib/lib';
import { UMRestApiRouteFactory } from '../../legacy_uptime/routes/types';
import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes/types';
import { API_URLS, SYNTHETICS_API_URLS } from '../../../common/constants';
import { syntheticsMonitorType } from '../../legacy_uptime/lib/saved_objects/synthetics_monitor';
import { getMonitorNotFoundResponse } from '../synthetics_service/service_errors';
import { UptimeServerSetup } from '../../legacy_uptime/lib/adapters';
const querySchema = schema.object({
page: schema.maybe(schema.number()),
@ -40,7 +40,7 @@ type MonitorsQuery = TypeOf<typeof querySchema>;
const getMonitors = (
request: MonitorsQuery,
server: UptimeServerSetup,
syntheticsService: SyntheticsService,
savedObjectsClient: SavedObjectsClientContract
): Promise<SavedObjectsFindResponse<EncryptedSyntheticsMonitor>> => {
const {
@ -55,7 +55,7 @@ const getMonitors = (
filter = '',
} = request as MonitorsQuery;
const locationFilter = parseLocationFilter(server.syntheticsService.locations, locations);
const locationFilter = parseLocationFilter(syntheticsService.locations, locations);
const filters =
getFilter('tags', tags) +
@ -74,7 +74,7 @@ const getMonitors = (
});
};
export const getSyntheticsMonitorRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({
export const getSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = (libs: UMServerLibs) => ({
method: 'GET',
path: API_URLS.SYNTHETICS_MONITORS + '/{monitorId}',
validate: {
@ -106,15 +106,19 @@ export const getSyntheticsMonitorRoute: UMRestApiRouteFactory = (libs: UMServerL
},
});
export const getAllSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({
export const getAllSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
method: 'GET',
path: API_URLS.SYNTHETICS_MONITORS,
validate: {
query: querySchema,
},
handler: async ({ request, savedObjectsClient, server }): Promise<any> => {
handler: async ({ request, savedObjectsClient, syntheticsMonitorClient }): Promise<any> => {
const { filters, query } = request.query;
const monitorsPromise = getMonitors(request.query, server, savedObjectsClient);
const monitorsPromise = getMonitors(
request.query,
syntheticsMonitorClient.syntheticsService,
savedObjectsClient
);
if (filters || query) {
const totalMonitorsPromise = savedObjectsClient.find({
@ -132,7 +136,7 @@ export const getAllSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({
monitors,
perPage: perPageT,
absoluteTotal: total,
syncErrors: server.syntheticsService.syncErrors,
syncErrors: syntheticsMonitorClient.syntheticsService.syncErrors,
};
}
@ -143,7 +147,7 @@ export const getAllSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({
monitors,
perPage: perPageT,
absoluteTotal: rest.total,
syncErrors: server.syntheticsService.syncErrors,
syncErrors: syntheticsMonitorClient.syntheticsService.syncErrors,
};
},
});
@ -180,13 +184,13 @@ export const findLocationItem = (query: string, locations: ServiceLocations) =>
return locations.find(({ id, label }) => query === id || label === query);
};
export const getSyntheticsMonitorOverviewRoute: UMRestApiRouteFactory = () => ({
export const getSyntheticsMonitorOverviewRoute: SyntheticsRestApiRouteFactory = () => ({
method: 'GET',
path: SYNTHETICS_API_URLS.SYNTHETICS_OVERVIEW,
validate: {
query: querySchema,
},
handler: async ({ request, savedObjectsClient, server }): Promise<any> => {
handler: async ({ request, savedObjectsClient, syntheticsMonitorClient }): Promise<any> => {
const { perPage = 5 } = request.query;
const { saved_objects: monitors } = await getMonitors(
{
@ -195,7 +199,7 @@ export const getSyntheticsMonitorOverviewRoute: UMRestApiRouteFactory = () => ({
sortOrder: 'asc',
page: 1,
},
server,
syntheticsMonitorClient.syntheticsService,
savedObjectsClient
);

View file

@ -56,6 +56,7 @@ describe('validateMonitor', () => {
testCommonFields = {
[ConfigKey.MONITOR_TYPE]: DataStream.ICMP,
[ConfigKey.NAME]: 'test-monitor-name',
[ConfigKey.CONFIG_ID]: 'test-monitor-id',
[ConfigKey.ENABLED]: true,
[ConfigKey.TAGS]: testTags,
[ConfigKey.SCHEDULE]: testSchedule,
@ -434,6 +435,7 @@ function getJsonPayload() {
' "TLSv1.2"' +
' ],' +
' "name": "test-monitor-name",' +
' "config_id": "test-monitor-id",' +
' "namespace": "testnamespace",' +
' "locations": [{' +
' "id": "eu-west-01",' +

View file

@ -8,7 +8,7 @@
import { schema, TypeOf } from '@kbn/config-schema';
import { UMServerLibs } from '../../legacy_uptime/uptime_server';
import { syntheticsMonitorType } from '../../../common/types/saved_objects';
import { UMRestApiRouteFactory } from '../../legacy_uptime/routes';
import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes';
import { SYNTHETICS_API_URLS } from '../../../common/constants';
import { ConfigKey, MonitorFields } from '../../../common/runtime_types';
@ -20,7 +20,7 @@ const queryParams = schema.object({
type QueryParams = TypeOf<typeof queryParams>;
export const createGetMonitorStatusRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({
export const createGetMonitorStatusRoute: SyntheticsRestApiRouteFactory = (libs: UMServerLibs) => ({
method: 'GET',
path: SYNTHETICS_API_URLS.MONITOR_STATUS,
validate: {

View file

@ -4,7 +4,10 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { UMRestApiRouteFactory } from '../../legacy_uptime/routes/types';
import {
SyntheticsRestApiRouteFactory,
UMRestApiRouteFactory,
} from '../../legacy_uptime/routes/types';
import { API_URLS } from '../../../common/constants';
import { SyntheticsForbiddenError } from '../../synthetics_service/get_api_key';
@ -27,12 +30,19 @@ export const getSyntheticsEnablementRoute: UMRestApiRouteFactory = (libs) => ({
},
});
export const disableSyntheticsRoute: UMRestApiRouteFactory = (libs) => ({
export const disableSyntheticsRoute: SyntheticsRestApiRouteFactory = (libs) => ({
method: 'DELETE',
path: API_URLS.SYNTHETICS_ENABLEMENT,
validate: {},
handler: async ({ response, request, server, savedObjectsClient }): Promise<any> => {
const { syntheticsService, security } = server;
handler: async ({
response,
request,
server,
savedObjectsClient,
syntheticsMonitorClient,
}): Promise<any> => {
const { security } = server;
const { syntheticsService } = syntheticsMonitorClient;
try {
const { canEnable } = await libs.requests.getSyntheticsEnablement({ request, server });
if (!canEnable) {

View file

@ -5,17 +5,17 @@
* 2.0.
*/
import { UMRestApiRouteFactory } from '../../legacy_uptime/routes/types';
import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes/types';
import { API_URLS } from '../../../common/constants';
export const getServiceAllowedRoute: UMRestApiRouteFactory = () => ({
export const getServiceAllowedRoute: SyntheticsRestApiRouteFactory = () => ({
method: 'GET',
path: API_URLS.SERVICE_ALLOWED,
validate: {},
handler: async ({ server }): Promise<any> => {
handler: async ({ syntheticsMonitorClient }): Promise<any> => {
return {
serviceAllowed: server.syntheticsService.isAllowed,
signupUrl: server.syntheticsService.signupUrl,
serviceAllowed: true,
signupUrl: syntheticsMonitorClient.syntheticsService.signupUrl,
};
},
});

View file

@ -6,19 +6,41 @@
*/
import { getServiceLocations } from '../../synthetics_service/get_service_locations';
import { UMRestApiRouteFactory } from '../../legacy_uptime/routes';
import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes';
import { API_URLS } from '../../../common/constants';
import { getSyntheticsPrivateLocations } from '../../legacy_uptime/lib/saved_objects/private_locations';
export const getServiceLocationsRoute: UMRestApiRouteFactory = () => ({
export const getServiceLocationsRoute: SyntheticsRestApiRouteFactory = () => ({
method: 'GET',
path: API_URLS.SERVICE_LOCATIONS,
validate: {},
handler: async ({ server }): Promise<any> => {
if (server.syntheticsService.locations.length > 0) {
const { throttling, locations } = server.syntheticsService;
return { throttling, locations };
handler: async ({ server, savedObjectsClient, syntheticsMonitorClient }): Promise<any> => {
const { syntheticsService, privateLocationAPI } = syntheticsMonitorClient;
const privateLocations = await getSyntheticsPrivateLocations(savedObjectsClient);
const agentPolicies = await privateLocationAPI.getAgentPolicies();
const privateLocs =
privateLocations?.map((loc) => ({
label: loc.name,
isServiceManaged: false,
isInvalid: agentPolicies.find((policy) => policy.id === loc.policyHostId) === undefined,
...loc,
})) ?? [];
if (syntheticsService.locations.length > 0) {
const { throttling, locations } = syntheticsService;
return {
throttling,
locations: [...locations, ...privateLocs],
};
}
return getServiceLocations(server);
const { locations, throttling } = await getServiceLocations(server);
return {
locations: [...locations, ...privateLocs],
throttling,
};
},
});

View file

@ -5,11 +5,11 @@
* 2.0.
*/
import { UMRestApiRouteFactory } from '../../legacy_uptime/routes/types';
import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes/types';
import { API_URLS } from '../../../common/constants';
import { UptimeServerSetup } from '../../legacy_uptime/lib/adapters';
export const installIndexTemplatesRoute: UMRestApiRouteFactory = () => ({
export const installIndexTemplatesRoute: SyntheticsRestApiRouteFactory = () => ({
method: 'GET',
path: API_URLS.INDEX_TEMPLATES,
validate: {},

View file

@ -6,12 +6,12 @@
*/
import { schema } from '@kbn/config-schema';
import { MonitorFields } from '../../../common/runtime_types';
import { UMRestApiRouteFactory } from '../../legacy_uptime/routes/types';
import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes/types';
import { API_URLS } from '../../../common/constants';
import { formatHeartbeatRequest } from '../../synthetics_service/formatters/format_configs';
import { validateMonitor } from '../monitor_cruds/monitor_validation';
export const runOnceSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({
export const runOnceSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
method: 'POST',
path: API_URLS.RUN_ONCE_MONITOR + '/{monitorId}',
validate: {
@ -20,7 +20,7 @@ export const runOnceSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({
monitorId: schema.string({ minLength: 1, maxLength: 1024 }),
}),
},
handler: async ({ request, response, server }): Promise<any> => {
handler: async ({ request, response, server, syntheticsMonitorClient }): Promise<any> => {
const monitor = request.body as MonitorFields;
const { monitorId } = request.params;
@ -31,7 +31,7 @@ export const runOnceSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({
return response.badRequest({ body: { message, attributes: { details, ...payload } } });
}
const { syntheticsService } = server;
const { syntheticsService } = syntheticsMonitorClient;
const errors = await syntheticsService.runOnceConfigs([
formatHeartbeatRequest({

View file

@ -12,7 +12,7 @@ import {
SyntheticsMonitor,
SyntheticsMonitorWithSecrets,
} from '../../../common/runtime_types';
import { UMRestApiRouteFactory } from '../../legacy_uptime/routes/types';
import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes/types';
import { API_URLS } from '../../../common/constants';
import {
syntheticsMonitor,
@ -21,7 +21,7 @@ import {
import { formatHeartbeatRequest } from '../../synthetics_service/formatters/format_configs';
import { normalizeSecrets } from '../../synthetics_service/utils/secrets';
export const testNowMonitorRoute: UMRestApiRouteFactory = () => ({
export const testNowMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
method: 'GET',
path: API_URLS.TRIGGER_MONITOR + '/{monitorId}',
validate: {
@ -29,7 +29,12 @@ export const testNowMonitorRoute: UMRestApiRouteFactory = () => ({
monitorId: schema.string({ minLength: 1, maxLength: 1024 }),
}),
},
handler: async ({ request, savedObjectsClient, server }): Promise<any> => {
handler: async ({
request,
savedObjectsClient,
server,
syntheticsMonitorClient,
}): Promise<any> => {
const { monitorId } = request.params;
const monitor = await savedObjectsClient.get<SyntheticsMonitor>(
syntheticsMonitorType,
@ -47,7 +52,7 @@ export const testNowMonitorRoute: UMRestApiRouteFactory = () => ({
const { [ConfigKey.SCHEDULE]: schedule, [ConfigKey.LOCATIONS]: locations } = monitor.attributes;
const { syntheticsService } = server;
const { syntheticsService } = syntheticsMonitorClient;
const testRunId = uuidv4();

View file

@ -5,14 +5,18 @@
* 2.0.
*/
import { createSyntheticsRouteWithAuth } from './routes/create_route_with_auth';
import { SyntheticsMonitorClient } from './synthetics_service/synthetics_monitor/synthetics_monitor_client';
import { syntheticsRouteWrapper } from './synthetics_route_wrapper';
import { uptimeRequests } from './legacy_uptime/lib/requests';
import { syntheticsAppRestApiRoutes } from './routes';
import { createRouteWithAuth } from './legacy_uptime/routes';
import { UptimeServerSetup } from './legacy_uptime/lib/adapters';
import { licenseCheck } from './legacy_uptime/lib/domains';
export const initSyntheticsServer = (server: UptimeServerSetup) => {
export const initSyntheticsServer = (
server: UptimeServerSetup,
syntheticsMonitorClient: SyntheticsMonitorClient
) => {
const libs = {
requests: uptimeRequests,
license: licenseCheck,
@ -20,8 +24,9 @@ export const initSyntheticsServer = (server: UptimeServerSetup) => {
syntheticsAppRestApiRoutes.forEach((route) => {
const { method, options, handler, validate, path } = syntheticsRouteWrapper(
createRouteWithAuth(libs, route),
server
createSyntheticsRouteWithAuth(libs, route),
server,
syntheticsMonitorClient
);
const routeDefinition = {

View file

@ -9,10 +9,14 @@ import { KibanaResponse } from '@kbn/core-http-router-server-internal';
import { enableInspectEsQueries } from '@kbn/observability-plugin/common';
import { createUptimeESClient, inspectableEsQueriesMap } from './legacy_uptime/lib/lib';
import { syntheticsServiceApiKey } from './legacy_uptime/lib/saved_objects/service_api_key';
import { UMKibanaRouteWrapper } from './legacy_uptime/routes';
import { SyntheticsRouteWrapper } from './legacy_uptime/routes';
import { API_URLS } from '../common/constants';
export const syntheticsRouteWrapper: UMKibanaRouteWrapper = (uptimeRoute, server) => ({
export const syntheticsRouteWrapper: SyntheticsRouteWrapper = (
uptimeRoute,
server,
syntheticsMonitorClient
) => ({
...uptimeRoute,
options: {
tags: ['access:uptime-read', ...(uptimeRoute?.writeAccess ? ['access:uptime-write'] : [])],
@ -54,6 +58,7 @@ export const syntheticsRouteWrapper: UMKibanaRouteWrapper = (uptimeRoute, server
request,
response,
server,
syntheticsMonitorClient,
});
if (res instanceof KibanaResponse) {

View file

@ -18,6 +18,7 @@ export const commonFormatters: CommonFormatMap = {
[ConfigKey.LOCATIONS]: null,
[ConfigKey.ENABLED]: null,
[ConfigKey.MONITOR_TYPE]: null,
[ConfigKey.CONFIG_ID]: null,
[ConfigKey.LOCATIONS]: null,
[ConfigKey.SCHEDULE]: (fields) =>
`@every ${fields[ConfigKey.SCHEDULE]?.number}${fields[ConfigKey.SCHEDULE]?.unit}`,

View file

@ -69,6 +69,7 @@ describe('getServiceLocations', function () {
lon: -95.86,
},
id: 'us_central',
isInvalid: false,
label: 'US Central',
url: 'https://local.dev',
isServiceManaged: true,
@ -80,6 +81,7 @@ describe('getServiceLocations', function () {
lon: -95.86,
},
id: 'us_east',
isInvalid: false,
label: 'US East',
url: 'https://local.dev',
isServiceManaged: true,
@ -118,6 +120,7 @@ describe('getServiceLocations', function () {
lon: -95.86,
},
id: 'us_central',
isInvalid: false,
label: 'US Central',
url: 'https://local.dev',
isServiceManaged: true,
@ -154,6 +157,7 @@ describe('getServiceLocations', function () {
lon: -95.86,
},
id: 'us_central',
isInvalid: false,
label: 'US Central',
url: 'https://local.dev',
isServiceManaged: true,
@ -165,6 +169,7 @@ describe('getServiceLocations', function () {
lon: -95.86,
},
id: 'us_east',
isInvalid: false,
label: 'US East',
url: 'https://local.dev',
isServiceManaged: true,

View file

@ -24,6 +24,7 @@ export const getDevLocation = (devUrl: string): ServiceLocation => ({
url: devUrl,
isServiceManaged: true,
status: LocationStatus.EXPERIMENTAL,
isInvalid: false,
});
export async function getServiceLocations(server: UptimeServerSetup) {
@ -58,6 +59,7 @@ export async function getServiceLocations(server: UptimeServerSetup) {
url: location.url,
isServiceManaged: true,
status: location.status,
isInvalid: false,
});
});

View file

@ -0,0 +1,193 @@
/*
* 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 { testMonitorPolicy } from './test_policy';
import { formatSyntheticsPolicy } from '../../../common/formatters/format_synthetics_policy';
import { DataStream, MonitorFields, ScheduleUnit, SourceType } from '../../../common/runtime_types';
describe('SyntheticsPrivateLocation', () => {
it('formats monitors stream properly', () => {
const test = formatSyntheticsPolicy(testMonitorPolicy, DataStream.BROWSER, dummyBrowserConfig);
expect(test.formattedPolicy.inputs[3].streams[1]).toStrictEqual({
data_stream: {
dataset: 'browser',
type: 'synthetics',
},
enabled: true,
vars: {
__ui: {
type: 'yaml',
value:
'{"script_source":{"is_generated_script":false,"file_name":""},"is_zip_url_tls_enabled":false,"is_tls_enabled":true}',
},
config_id: {
type: 'text',
value: '75cdd125-5b62-4459-870c-46f59bf37e89',
},
enabled: {
type: 'bool',
value: true,
},
'filter_journeys.match': {
type: 'text',
value: null,
},
'filter_journeys.tags': {
type: 'yaml',
value: null,
},
ignore_https_errors: {
type: 'bool',
value: false,
},
location_name: {
type: 'text',
value: 'Fleet managed',
},
name: {
type: 'text',
value: 'Browser monitor',
},
params: {
type: 'yaml',
value: '',
},
run_once: {
type: 'bool',
value: false,
},
schedule: {
type: 'text',
value: '"@every 10m"',
},
screenshots: {
type: 'text',
value: 'on',
},
'service.name': {
type: 'text',
value: '',
},
'source.inline.script': {
type: 'yaml',
value:
"\"step('Go to https://www.elastic.co/', async () => {\\n await page.goto('https://www.elastic.co/');\\n});\"",
},
'source.zip_url.folder': {
type: 'text',
value: '',
},
'source.zip_url.password': {
type: 'password',
value: '',
},
'source.zip_url.proxy_url': {
type: 'text',
value: '',
},
'source.zip_url.ssl.certificate': {
type: 'yaml',
},
'source.zip_url.ssl.certificate_authorities': {
type: 'yaml',
},
'source.zip_url.ssl.key': {
type: 'yaml',
},
'source.zip_url.ssl.key_passphrase': {
type: 'text',
},
'source.zip_url.ssl.supported_protocols': {
type: 'yaml',
},
'source.zip_url.ssl.verification_mode': {
type: 'text',
},
'source.zip_url.url': {
type: 'text',
value: '',
},
'source.zip_url.username': {
type: 'text',
value: '',
},
synthetics_args: {
type: 'text',
value: null,
},
tags: {
type: 'yaml',
value: null,
},
'throttling.config': {
type: 'text',
value: '5d/3u/20l',
},
timeout: {
type: 'text',
value: null,
},
type: {
type: 'text',
value: 'browser',
},
},
});
});
});
const dummyBrowserConfig: Partial<MonitorFields> & {
id: string;
fields: Record<string, string | boolean>;
fields_under_root: boolean;
} = {
type: DataStream.BROWSER,
enabled: true,
schedule: { unit: ScheduleUnit.MINUTES, number: '10' },
'service.name': '',
tags: [],
timeout: null,
name: 'Browser monitor',
locations: [{ isServiceManaged: false, id: '1' }],
namespace: 'default',
origin: SourceType.UI,
journey_id: '',
project_id: '',
playwright_options: '',
__ui: {
script_source: { is_generated_script: false, file_name: '' },
is_zip_url_tls_enabled: false,
is_tls_enabled: true,
},
params: '',
'url.port': 443,
'source.inline.script':
"step('Go to https://www.elastic.co/', async () => {\n await page.goto('https://www.elastic.co/');\n});",
'source.project.content': '',
'source.zip_url.url': '',
'source.zip_url.username': '',
'source.zip_url.password': '',
'source.zip_url.folder': '',
'source.zip_url.proxy_url': '',
urls: 'https://www.elastic.co/',
screenshots: 'on',
synthetics_args: [],
'filter_journeys.match': '',
'filter_journeys.tags': [],
ignore_https_errors: false,
'throttling.is_enabled': true,
'throttling.download_speed': '5',
'throttling.upload_speed': '3',
'throttling.latency': '20',
'throttling.config': '5d/3u/20l',
id: '75cdd125-5b62-4459-870c-46f59bf37e89',
config_id: '75cdd125-5b62-4459-870c-46f59bf37e89',
fields: { config_id: '75cdd125-5b62-4459-870c-46f59bf37e89', run_once: true },
fields_under_root: true,
max_redirects: '0',
};

View file

@ -0,0 +1,207 @@
/*
* 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 { NewPackagePolicy, PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '@kbn/fleet-plugin/common';
import { SyntheticsConfig } from '../formatters/format_configs';
import { formatSyntheticsPolicy } from '../../../common/formatters/format_synthetics_policy';
import { getSyntheticsPrivateLocations } from '../../legacy_uptime/lib/saved_objects/private_locations';
import {
ConfigKey,
MonitorFields,
PrivateLocation,
SyntheticsMonitorWithId,
} from '../../../common/runtime_types';
import { UptimeServerSetup } from '../../legacy_uptime/lib/adapters';
const getPolicyId = (config: SyntheticsMonitorWithId, privateLocation: PrivateLocation) =>
config.id + '-' + privateLocation.id;
export class SyntheticsPrivateLocation {
private readonly server: UptimeServerSetup;
constructor(_server: UptimeServerSetup) {
this.server = _server;
}
async generateNewPolicy(
config: SyntheticsMonitorWithId,
privateLocation: PrivateLocation
): Promise<NewPackagePolicy> {
if (!this.server.authSavedObjectsClient) {
throw new Error('Could not find authSavedObjectsClient');
}
const newPolicy = await this.server.fleet.packagePolicyService.buildPackagePolicyFromPackage(
this.server.authSavedObjectsClient,
'synthetics'
);
if (!newPolicy) {
throw new Error('Could not create new synthetics policy');
}
newPolicy.is_managed = true;
newPolicy.policy_id = privateLocation.policyHostId;
newPolicy.name = config[ConfigKey.NAME] + '-' + privateLocation.name;
newPolicy.output_id = '';
newPolicy.namespace = 'default';
const { formattedPolicy } = formatSyntheticsPolicy(newPolicy, config.type, {
...(config as Partial<MonitorFields>),
config_id: config.id,
location_name: privateLocation.name,
});
return formattedPolicy;
}
async createMonitor(config: SyntheticsMonitorWithId) {
try {
const { locations } = config;
const privateLocations = await getSyntheticsPrivateLocations(
this.server.authSavedObjectsClient!
);
const fleetManagedLocations = locations.filter((loc) => !loc.isServiceManaged);
for (const privateLocation of fleetManagedLocations) {
const location = privateLocations?.find((loc) => loc.id === privateLocation.id)!;
const newPolicy = await this.generateNewPolicy(config, location);
await this.createPolicy(newPolicy, getPolicyId(config, location));
}
} catch (e) {
this.server.logger.error(e);
}
}
async editMonitor(config: SyntheticsConfig) {
const { locations } = config;
const allPrivateLocations = await getSyntheticsPrivateLocations(
this.server.authSavedObjectsClient!
);
const monitorPrivateLocations = locations.filter((loc) => !loc.isServiceManaged);
for (const privateLocation of allPrivateLocations) {
const hasLocation = monitorPrivateLocations?.some((loc) => loc.id === privateLocation.id);
const currId = getPolicyId(config, privateLocation);
const hasPolicy = await this.getMonitor(currId);
if (hasLocation) {
const newPolicy = await this.generateNewPolicy(config, privateLocation);
if (hasPolicy) {
await this.updatePolicy(newPolicy, currId);
} else {
await this.createPolicy(newPolicy, currId);
}
} else if (hasPolicy) {
const soClient = this.server.authSavedObjectsClient!;
const esClient = this.server.uptimeEsClient.baseESClient;
await this.server.fleet.packagePolicyService.delete(soClient, esClient, [currId], {
force: true,
});
}
}
}
async createPolicy(newPolicy: NewPackagePolicy, id: string) {
const soClient = this.server.authSavedObjectsClient;
const esClient = this.server.uptimeEsClient.baseESClient;
if (soClient && esClient) {
return await this.server.fleet.packagePolicyService.create(soClient, esClient, newPolicy, {
id,
overwrite: true,
});
}
}
async updatePolicy(updatedPolicy: NewPackagePolicy, id: string) {
const soClient = this.server.authSavedObjectsClient;
const esClient = this.server.uptimeEsClient.baseESClient;
if (soClient && esClient) {
return await this.server.fleet.packagePolicyService.update(
soClient,
esClient,
id,
updatedPolicy,
{
force: true,
}
);
}
}
async getMonitor(id: string) {
try {
const soClient = this.server.authSavedObjectsClient;
return await this.server.fleet.packagePolicyService.get(soClient!, id);
} catch (e) {
return;
}
}
async findMonitor(config: SyntheticsMonitorWithId) {
const soClient = this.server.authSavedObjectsClient;
const list = await this.server.fleet.packagePolicyService.list(soClient!, {
page: 1,
perPage: 10000,
kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:synthetics`,
});
const { locations } = config;
const fleetManagedLocationIds = locations
.filter((loc) => !loc.isServiceManaged)
.map((loc) => config.id + '-' + loc.id);
return list.items.filter((policy) => {
return fleetManagedLocationIds.includes(policy.name);
});
}
async deleteMonitor(config: SyntheticsMonitorWithId) {
const soClient = this.server.authSavedObjectsClient;
const esClient = this.server.uptimeEsClient.baseESClient;
if (soClient && esClient) {
const { locations } = config;
const allPrivateLocations = await getSyntheticsPrivateLocations(soClient);
const monitorPrivateLocations = locations.filter((loc) => !loc.isServiceManaged);
for (const privateLocation of monitorPrivateLocations) {
const location = allPrivateLocations?.find((loc) => loc.id === privateLocation.id);
if (location) {
await this.server.fleet.packagePolicyService.delete(
soClient,
esClient,
[getPolicyId(config, location)],
{
force: true,
}
);
}
}
}
}
async getAgentPolicies() {
const agentPolicies = await this.server.fleet.agentPolicyService.list(
this.server.savedObjectsClient!,
{
page: 1,
perPage: 10000,
}
);
return agentPolicies.items;
}
}

View file

@ -0,0 +1,167 @@
/*
* 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.
*/
export const testMonitorPolicy = {
name: 'synthetics-1',
namespace: '',
package: { name: 'synthetics', title: 'Elastic Synthetics', version: '0.9.10' },
enabled: true,
policy_id: '',
output_id: 'fleet-default-output',
inputs: [
{
type: 'synthetics/http',
policy_template: 'synthetics',
enabled: false,
streams: [
{
enabled: false,
data_stream: { type: 'synthetics', dataset: 'http' },
vars: {
__ui: { type: 'yaml' },
enabled: { value: true, type: 'bool' },
type: { value: 'http', type: 'text' },
name: { type: 'text' },
schedule: { value: '"@every 3m"', type: 'text' },
urls: { type: 'text' },
'service.name': { type: 'text' },
timeout: { type: 'text' },
max_redirects: { type: 'integer' },
proxy_url: { type: 'text' },
tags: { type: 'yaml' },
username: { type: 'text' },
password: { type: 'password' },
'response.include_headers': { type: 'bool' },
'response.include_body': { type: 'text' },
'check.request.method': { type: 'text' },
'check.request.headers': { type: 'yaml' },
'check.request.body': { type: 'yaml' },
'check.response.status': { type: 'yaml' },
'check.response.headers': { type: 'yaml' },
'check.response.body.positive': { type: 'yaml' },
'check.response.body.negative': { type: 'yaml' },
'ssl.certificate_authorities': { type: 'yaml' },
'ssl.certificate': { type: 'yaml' },
'ssl.key': { type: 'yaml' },
'ssl.key_passphrase': { type: 'text' },
'ssl.verification_mode': { type: 'text' },
'ssl.supported_protocols': { type: 'yaml' },
location_name: { value: 'Fleet managed', type: 'text' },
config_id: { type: 'text' },
run_once: { value: false, type: 'bool' },
},
},
],
},
{
type: 'synthetics/tcp',
policy_template: 'synthetics',
enabled: false,
streams: [
{
enabled: false,
data_stream: { type: 'synthetics', dataset: 'tcp' },
vars: {
__ui: { type: 'yaml' },
enabled: { value: true, type: 'bool' },
type: { value: 'tcp', type: 'text' },
name: { type: 'text' },
schedule: { value: '"@every 3m"', type: 'text' },
hosts: { type: 'text' },
'service.name': { type: 'text' },
timeout: { type: 'text' },
proxy_url: { type: 'text' },
proxy_use_local_resolver: { value: false, type: 'bool' },
tags: { type: 'yaml' },
'check.send': { type: 'text' },
'check.receive': { type: 'text' },
'ssl.certificate_authorities': { type: 'yaml' },
'ssl.certificate': { type: 'yaml' },
'ssl.key': { type: 'yaml' },
'ssl.key_passphrase': { type: 'text' },
'ssl.verification_mode': { type: 'text' },
'ssl.supported_protocols': { type: 'yaml' },
location_name: { value: 'Fleet managed', type: 'text' },
config_id: { type: 'text' },
run_once: { value: false, type: 'bool' },
},
},
],
},
{
type: 'synthetics/icmp',
policy_template: 'synthetics',
enabled: false,
streams: [
{
enabled: false,
data_stream: { type: 'synthetics', dataset: 'icmp' },
vars: {
__ui: { type: 'yaml' },
enabled: { value: true, type: 'bool' },
type: { value: 'icmp', type: 'text' },
name: { type: 'text' },
schedule: { value: '"@every 3m"', type: 'text' },
wait: { value: '1s', type: 'text' },
hosts: { type: 'text' },
'service.name': { type: 'text' },
timeout: { type: 'text' },
tags: { type: 'yaml' },
location_name: { value: 'Fleet managed', type: 'text' },
config_id: { type: 'text' },
run_once: { value: false, type: 'bool' },
},
},
],
},
{
type: 'synthetics/browser',
policy_template: 'synthetics',
enabled: true,
streams: [
{ enabled: true, data_stream: { type: 'synthetics', dataset: 'browser.network' } },
{
enabled: true,
data_stream: { type: 'synthetics', dataset: 'browser' },
vars: {
__ui: { type: 'yaml' },
enabled: { value: true, type: 'bool' },
type: { value: 'browser', type: 'text' },
name: { type: 'text' },
schedule: { value: '"@every 3m"', type: 'text' },
'service.name': { type: 'text' },
timeout: { type: 'text' },
tags: { type: 'yaml' },
'source.zip_url.url': { type: 'text' },
'source.zip_url.username': { type: 'text' },
'source.zip_url.folder': { type: 'text' },
'source.zip_url.password': { type: 'password' },
'source.inline.script': { type: 'yaml' },
params: { type: 'yaml' },
screenshots: { type: 'text' },
synthetics_args: { type: 'text' },
ignore_https_errors: { type: 'bool' },
'throttling.config': { type: 'text' },
'filter_journeys.tags': { type: 'yaml' },
'filter_journeys.match': { type: 'text' },
'source.zip_url.ssl.certificate_authorities': { type: 'yaml' },
'source.zip_url.ssl.certificate': { type: 'yaml' },
'source.zip_url.ssl.key': { type: 'yaml' },
'source.zip_url.ssl.key_passphrase': { type: 'text' },
'source.zip_url.ssl.verification_mode': { type: 'text' },
'source.zip_url.ssl.supported_protocols': { type: 'yaml' },
'source.zip_url.proxy_url': { type: 'text' },
location_name: { value: 'Fleet managed', type: 'text' },
config_id: { type: 'text' },
run_once: { value: false, type: 'bool' },
},
},
{ enabled: true, data_stream: { type: 'synthetics', dataset: 'browser.screenshot' } },
],
},
],
};

View file

@ -11,6 +11,7 @@ import {
SavedObjectsFindResult,
} from '@kbn/core/server';
import { EncryptedSavedObjectsClient } from '@kbn/encrypted-saved-objects-plugin/server';
import { SyntheticsMonitorClient } from './synthetics_monitor/synthetics_monitor_client';
import {
BrowserFields,
ConfigKey,
@ -58,6 +59,7 @@ export class ProjectMonitorFormatter {
public failedStaleMonitors: FailedMonitors = [];
private server: UptimeServerSetup;
private projectFilter: string;
private syntheticsMonitorClient: SyntheticsMonitorClient;
constructor({
locations,
@ -68,6 +70,7 @@ export class ProjectMonitorFormatter {
spaceId,
monitors,
server,
syntheticsMonitorClient,
}: {
locations: Locations;
keepStale: boolean;
@ -77,6 +80,7 @@ export class ProjectMonitorFormatter {
spaceId: string;
monitors: ProjectBrowserMonitor[];
server: UptimeServerSetup;
syntheticsMonitorClient: SyntheticsMonitorClient;
}) {
this.projectId = projectId;
this.spaceId = spaceId;
@ -84,6 +88,7 @@ export class ProjectMonitorFormatter {
this.keepStale = keepStale;
this.savedObjectsClient = savedObjectsClient;
this.encryptedSavedObjectsClient = encryptedSavedObjectsClient;
this.syntheticsMonitorClient = syntheticsMonitorClient;
this.monitors = monitors;
this.server = server;
this.projectFilter = `${syntheticsMonitorType}.attributes.${ConfigKey.PROJECT_ID}: "${this.projectId}"`;
@ -148,6 +153,7 @@ export class ProjectMonitorFormatter {
server: this.server,
monitor: normalizedMonitor,
monitorSavedObject: newMonitor,
syntheticsMonitorClient: this.syntheticsMonitorClient,
});
this.createdMonitors.push(monitor.id);
}
@ -249,6 +255,7 @@ export class ProjectMonitorFormatter {
editedMonitorSavedObject: editedMonitor,
previousMonitor,
server: this.server,
syntheticsMonitorClient: this.syntheticsMonitorClient,
});
}
@ -290,6 +297,7 @@ export class ProjectMonitorFormatter {
savedObjectsClient: this.savedObjectsClient,
server: this.server,
monitorId,
syntheticsMonitorClient: this.syntheticsMonitorClient,
});
this.deletedMonitors.push(journeyId);
} catch (e) {

View file

@ -34,13 +34,13 @@ export class ServiceAPIClient {
private readonly authorization: string;
public locations: ServiceLocations;
private logger: Logger;
private readonly config: ServiceConfig;
private readonly config?: ServiceConfig;
private readonly kibanaVersion: string;
private readonly server: UptimeServerSetup;
constructor(logger: Logger, config: ServiceConfig, server: UptimeServerSetup) {
this.config = config;
const { username, password } = config;
const { username, password } = config ?? {};
this.username = username;
this.kibanaVersion = server.kibanaVersion;
@ -61,7 +61,7 @@ export class ServiceAPIClient {
const rejectUnauthorized = parsedTargetUrl.hostname !== 'localhost' || !this.server.isDev;
const baseHttpsAgent = new https.Agent({ rejectUnauthorized });
const config = this.config;
const config = this.config ?? {};
// If using basic-auth, ignore certificate configs
if (this.authorization) return baseHttpsAgent;
@ -171,9 +171,8 @@ export class ServiceAPIClient {
const promises: Array<Observable<unknown>> = [];
this.locations.forEach(({ id, url }) => {
const locMonitors = allMonitors.filter(
({ locations }) =>
!locations || locations.length === 0 || locations?.find((loc) => loc.id === id)
const locMonitors = allMonitors.filter(({ locations }) =>
locations?.find((loc) => loc.id === id && loc.isServiceManaged)
);
if (locMonitors.length > 0) {
promises.push(

View file

@ -0,0 +1,117 @@
/*
* 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 { SyntheticsMonitorClient } from './synthetics_monitor_client';
import { UptimeServerSetup } from '../../legacy_uptime/lib/adapters';
import { SyntheticsService } from '../synthetics_service';
import { loggerMock } from '@kbn/logging-mocks';
import times from 'lodash/times';
import {
LocationStatus,
MonitorFields,
SyntheticsMonitorWithId,
} from '../../../common/runtime_types';
describe('SyntheticsMonitorClient', () => {
const mockEsClient = {
search: jest.fn(),
};
const logger = loggerMock.create();
const serverMock: UptimeServerSetup = {
logger,
uptimeEsClient: mockEsClient,
authSavedObjectsClient: {
bulkUpdate: jest.fn(),
get: jest.fn(),
},
config: {
service: {
username: 'dev',
password: '12345',
manifestUrl: 'http://localhost:8080/api/manifest',
},
},
} as unknown as UptimeServerSetup;
const syntheticsService = new SyntheticsService(serverMock);
syntheticsService.addConfig = jest.fn();
syntheticsService.editConfig = jest.fn();
syntheticsService.deleteConfigs = jest.fn();
const locations = times(3).map((n) => {
return {
id: `loc-${n}`,
label: `Location ${n}`,
url: `https://example.com/${n}`,
geo: {
lat: 0,
lon: 0,
},
isServiceManaged: true,
status: LocationStatus.GA,
};
});
const monitor = {
type: 'http',
enabled: true,
schedule: {
number: '3',
unit: 'm',
},
name: 'my mon',
locations,
urls: 'http://google.com',
max_redirects: '0',
password: '',
proxy_url: '',
id: '7af7e2f0-d5dc-11ec-87ac-bdfdb894c53d',
fields: { config_id: '7af7e2f0-d5dc-11ec-87ac-bdfdb894c53d' },
fields_under_root: true,
} as unknown as MonitorFields;
it('should add a monitor', async () => {
locations[1].isServiceManaged = false;
const id = 'test-id-1';
const client = new SyntheticsMonitorClient(syntheticsService, serverMock);
client.privateLocationAPI.createMonitor = jest.fn();
await client.addMonitor(monitor, id);
expect(syntheticsService.addConfig).toHaveBeenCalledTimes(1);
expect(client.privateLocationAPI.createMonitor).toHaveBeenCalledTimes(1);
});
it('should edit a monitor', async () => {
locations[1].isServiceManaged = false;
const id = 'test-id-1';
const client = new SyntheticsMonitorClient(syntheticsService, serverMock);
client.privateLocationAPI.editMonitor = jest.fn();
await client.editMonitor(monitor, id);
expect(syntheticsService.editConfig).toHaveBeenCalledTimes(1);
expect(client.privateLocationAPI.editMonitor).toHaveBeenCalledTimes(1);
});
it('should delete a monitor', async () => {
locations[1].isServiceManaged = false;
const client = new SyntheticsMonitorClient(syntheticsService, serverMock);
client.privateLocationAPI.deleteMonitor = jest.fn();
await client.deleteMonitor(monitor as unknown as SyntheticsMonitorWithId);
expect(syntheticsService.deleteConfigs).toHaveBeenCalledTimes(1);
expect(client.privateLocationAPI.deleteMonitor).toHaveBeenCalledTimes(1);
});
});

View file

@ -0,0 +1,75 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { UptimeServerSetup } from '../../legacy_uptime/lib/adapters';
import { SyntheticsPrivateLocation } from '../private_location/synthetics_private_location';
import { SyntheticsService } from '../synthetics_service';
import { formatHeartbeatRequest, SyntheticsConfig } from '../formatters/format_configs';
import { ConfigKey, MonitorFields, SyntheticsMonitorWithId } from '../../../common/runtime_types';
export class SyntheticsMonitorClient {
public syntheticsService: SyntheticsService;
public privateLocationAPI: SyntheticsPrivateLocation;
constructor(syntheticsService: SyntheticsService, server: UptimeServerSetup) {
this.syntheticsService = syntheticsService;
this.privateLocationAPI = new SyntheticsPrivateLocation(server);
}
async addMonitor(monitor: MonitorFields, id: string) {
await this.syntheticsService.setupIndexTemplates();
const config = formatHeartbeatRequest({
monitor,
monitorId: id,
customHeartbeatId: monitor[ConfigKey.CUSTOM_HEARTBEAT_ID],
});
const { privateLocations, publicLocations } = this.parseLocations(config);
if (privateLocations.length > 0) {
await this.privateLocationAPI.createMonitor(config);
}
if (publicLocations.length > 0) {
return await this.syntheticsService.addConfig(config);
}
}
async editMonitor(editedMonitor: MonitorFields, id: string) {
const editedConfig = formatHeartbeatRequest({
monitor: editedMonitor,
monitorId: id,
customHeartbeatId: (editedMonitor as MonitorFields)[ConfigKey.CUSTOM_HEARTBEAT_ID],
});
const { privateLocations, publicLocations } = this.parseLocations(editedConfig);
if (privateLocations.length > 0) {
await this.privateLocationAPI.editMonitor(editedConfig);
}
if (publicLocations.length > 0) {
return await this.syntheticsService.editConfig(editedConfig);
}
await this.syntheticsService.editConfig(editedConfig);
}
async deleteMonitor(monitor: SyntheticsMonitorWithId) {
await this.privateLocationAPI.deleteMonitor(monitor);
return await this.syntheticsService.deleteConfigs([monitor]);
}
parseLocations(config: SyntheticsConfig) {
const { locations } = config;
const privateLocations = locations.filter((loc) => !loc.isServiceManaged);
const publicLocations = locations.filter((loc) => loc.isServiceManaged);
return { privateLocations, publicLocations };
}
}

Some files were not shown because too many files have changed in this diff Show more