[Synthetics] Public API's refactor (#190531)

## Summary

Public API's refactor !!

Fixes https://github.com/elastic/kibana/issues/189906 !!


### Testing 

Please test HTTP, TCP, ICMP and Browser monitors creation and updates
via API

----------------

### params field 

return `params` always as an object `{}`

----------------

### `ssl` field 

ssl field isn't returned anymore for browser monitor

and is returned always as an object for other monitor types  as in
```

    "ssl": {
        "certificate_authorities": "",
        "certificate": "",
        "key": "i am a key",
        "key_passphrase": "",
        "verification_mode": "full",
        "supported_protocols": [
            "TLSv1.1",
            "TLSv1.2",
            "TLSv1.3"
        ]
    },
```

----------------

### `response` field 
also returned as nested object
```
    "response": {
        "include_body": "on_error",
        "include_headers": true,
        "include_body_max_bytes": "1024"
    },
```
-----
### `check` field
is also returned as nested object now

```
    "check": {
        "response.body.negative": [],
        "response.body.positive": [],
        "response.json": [],
        "response.headers": {},
        "response.status": [],
        "request.body": {
            "value": "",
            "type": "text"
        },
        "request.headers": {},
        "request.method": "GET"
    }
```

-----


### `retest_on_failure`

retest_on_failure is always returned now, max_attempts abstraction is
omitted

-----

### `max_redirects`
always return `max_redirects` as a number

---------

Co-authored-by: Justin Kambic <jk@elastic.co>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Shahzad 2024-09-24 14:37:24 +02:00 committed by GitHub
parent ca61209d13
commit a333373d9e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
50 changed files with 769 additions and 444 deletions

View file

@ -173,7 +173,8 @@ export type ICMPFields = t.TypeOf<typeof ICMPFieldsCodec>;
export const HTTPSimpleFieldsCodec = t.intersection([
t.interface({
[ConfigKey.METADATA]: MetadataCodec,
[ConfigKey.MAX_REDIRECTS]: t.string,
// string is for yaml config and number for public api
[ConfigKey.MAX_REDIRECTS]: t.union([t.string, t.number]),
[ConfigKey.URLS]: getNonEmptyStringCodec('url'),
[ConfigKey.PORT]: t.union([t.number, t.null]),
}),
@ -318,6 +319,11 @@ export const MonitorFieldsCodec = t.intersection([
BrowserFieldsCodec,
]);
export const MonitorFieldsResultCodec = t.intersection([
MonitorFieldsCodec,
t.interface({ id: t.string, updated_at: t.string, created_at: t.string }),
]);
// Monitor, represents one of (Icmp | Tcp | Http | Browser) decrypted
export const SyntheticsMonitorCodec = t.union([
HTTPFieldsCodec,
@ -336,7 +342,7 @@ export const EncryptedSyntheticsMonitorCodec = t.union([
export const SyntheticsMonitorWithIdCodec = t.intersection([
SyntheticsMonitorCodec,
t.interface({ id: t.string }),
t.interface({ id: t.string, updated_at: t.string, created_at: t.string }),
]);
const HeartbeatFieldsCodec = t.intersection([
@ -355,7 +361,10 @@ const HeartbeatFieldsCodec = t.intersection([
]);
export const HeartbeatConfigCodec = t.intersection([
SyntheticsMonitorWithIdCodec,
SyntheticsMonitorCodec,
t.interface({
id: t.string,
}),
t.partial({
fields_under_root: t.boolean,
fields: HeartbeatFieldsCodec,
@ -400,6 +409,7 @@ export type BrowserFields = t.TypeOf<typeof BrowserFieldsCodec>;
export type BrowserSimpleFields = t.TypeOf<typeof BrowserSimpleFieldsCodec>;
export type BrowserAdvancedFields = t.TypeOf<typeof BrowserAdvancedFieldsCodec>;
export type MonitorFields = t.TypeOf<typeof MonitorFieldsCodec>;
export type MonitorFieldsResult = t.TypeOf<typeof MonitorFieldsResultCodec>;
export type HeartbeatFields = t.TypeOf<typeof HeartbeatFieldsCodec>;
export type SyntheticsMonitor = t.TypeOf<typeof SyntheticsMonitorCodec>;
export type SyntheticsMonitorWithId = t.TypeOf<typeof SyntheticsMonitorWithIdCodec>;

View file

@ -62,7 +62,7 @@ export const ProjectMonitorCodec = t.intersection([
hash: t.string,
namespace: t.string,
retestOnFailure: t.boolean,
labels: t.record(t.string, t.string),
fields: t.record(t.string, t.string),
}),
]);
@ -91,14 +91,10 @@ export const ProjectMonitorsResponseCodec = t.intersection([
}),
]);
export type ProjectMonitorThrottlingConfig = t.TypeOf<typeof ProjectMonitorThrottlingConfigCodec>;
export type ProjectMonitor = t.TypeOf<typeof ProjectMonitorCodec>;
export type LegacyProjectMonitorsRequest = t.TypeOf<typeof LegacyProjectMonitorsRequestCodec>;
export type ProjectMonitorsRequest = t.TypeOf<typeof ProjectMonitorsRequestCodec>;
export type ProjectMonitorsResponse = t.TypeOf<typeof ProjectMonitorsResponseCodec>;
export type ProjectMonitorMetaData = t.TypeOf<typeof ProjectMonitorMetaDataCodec>;

View file

@ -10,6 +10,7 @@ import { CA_CERT_PATH } from '@kbn/dev-utils';
import { get } from 'lodash';
import { commonFunctionalServices } from '@kbn/ftr-common-functional-services';
import { commonFunctionalUIServices } from '@kbn/ftr-common-functional-ui-services';
import { readKibanaConfig } from './tasks/read_kibana_config';
const MANIFEST_KEY = 'xpack.uptime.service.manifestUrl';
const SERVICE_PASSWORD = 'xpack.uptime.service.password';

View file

@ -6,9 +6,10 @@
*/
import { after, before, expect, journey, step } from '@elastic/synthetics';
import { omit } from 'lodash';
import { SyntheticsMonitor } from '@kbn/synthetics-plugin/common/runtime_types';
import { SyntheticsServices } from './services/synthetics_services';
import { cleanTestMonitors, enableMonitorManagedViaApi } from './services/add_monitor';
import { cleanTestMonitors } from './services/add_monitor';
import { addTestMonitorProject } from './services/add_monitor_project';
import { syntheticsAppPageProvider } from '../page_objects/synthetics_app';
@ -23,14 +24,13 @@ journey('ProjectMonitorReadOnly', async ({ page, params }) => {
before(async () => {
await cleanTestMonitors(params);
await enableMonitorManagedViaApi(params.kibanaUrl);
await addTestMonitorProject(params.kibanaUrl, monitorName);
await syntheticsApp.waitForLoadingToFinish();
});
step('Go to monitor-management', async () => {
await addTestMonitorProject(params.kibanaUrl, monitorName);
await syntheticsApp.waitForLoadingToFinish();
await syntheticsApp.navigateToMonitorManagement();
});
@ -62,11 +62,16 @@ journey('ProjectMonitorReadOnly', async ({ page, params }) => {
// hash is always reset to empty string when monitor is edited
// this ensures that when the monitor is pushed again, the monitor
// config in the process takes precedence
expect(newConfiguration).toEqual({
...originalMonitorConfiguration,
hash: '',
revision: 2,
});
expect(omit(newConfiguration, ['updated_at'])).toEqual(
omit(
{
...originalMonitorConfiguration,
hash: '',
revision: 2,
},
['updated_at']
)
);
});
step('Navigate to edit monitor', async () => {
@ -83,29 +88,39 @@ journey('ProjectMonitorReadOnly', async ({ page, params }) => {
// hash is always reset to empty string when monitor is edited
// this ensures that when the monitor is pushed again, the monitor
// config in the process takes precedence
expect(newConfiguration).toEqual({
...originalMonitorConfiguration,
hash: '',
revision: 3,
alert: {
status: {
enabled: !(originalMonitorConfiguration?.alert?.status?.enabled as boolean),
expect(omit(newConfiguration, ['updated_at'])).toEqual(
omit(
{
...originalMonitorConfiguration,
hash: '',
revision: 3,
alert: {
status: {
enabled: !(originalMonitorConfiguration?.alert?.status?.enabled as boolean),
},
tls: {
enabled: originalMonitorConfiguration?.alert?.tls?.enabled as boolean,
},
},
enabled: !originalMonitorConfiguration?.enabled,
},
tls: {
enabled: originalMonitorConfiguration?.alert?.tls?.enabled as boolean,
},
},
enabled: !originalMonitorConfiguration?.enabled,
});
['updated_at']
)
);
});
step('Monitor can be re-pushed and overwrite any changes', async () => {
await addTestMonitorProject(params.kibanaUrl, monitorName);
const repushedConfiguration = await services.getMonitor(monitorId);
expect(repushedConfiguration).toEqual({
...originalMonitorConfiguration,
revision: 4,
});
expect(omit(repushedConfiguration, ['updated_at'])).toEqual(
omit(
{
...originalMonitorConfiguration,
revision: 4,
},
['updated_at']
)
);
});
step('Navigate to edit monitor', async () => {

View file

@ -17,23 +17,15 @@ export const addTestMonitorProject = async (
const testData = {
...testProjectMonitorBrowser(name, config),
};
try {
return await axios.put(
kibanaUrl +
SYNTHETICS_API_URLS.SYNTHETICS_MONITORS_PROJECT_UPDATE.replace(
'{projectName}',
projectName
),
testData,
{
auth: { username: 'elastic', password: 'changeme' },
headers: { 'kbn-xsrf': 'true', 'x-elastic-internal-origin': 'synthetics-e2e' },
}
);
} catch (e) {
// eslint-disable-next-line no-console
console.log(e);
}
return await axios.put(
kibanaUrl +
SYNTHETICS_API_URLS.SYNTHETICS_MONITORS_PROJECT_UPDATE.replace('{projectName}', projectName),
testData,
{
auth: { username: 'elastic', password: 'changeme' },
headers: { 'kbn-xsrf': 'true', 'x-elastic-internal-origin': 'synthetics-e2e' },
}
);
};
const testProjectMonitorBrowser = (name: string, config?: Record<string, unknown>) => ({

View file

@ -28,10 +28,9 @@ export class SyntheticsServices {
try {
const { data } = await this.requester.request({
description: 'get monitor by id',
path: SYNTHETICS_API_URLS.GET_SYNTHETICS_MONITOR.replace('{monitorId}', monitorId),
query: {
decrypted: true,
},
path:
SYNTHETICS_API_URLS.GET_SYNTHETICS_MONITOR.replace('{monitorId}', monitorId) +
'?internal=true',
method: 'GET',
});
return data as SyntheticsMonitor;

View file

@ -28,6 +28,7 @@ import {
EncryptedSyntheticsSavedMonitor,
MonitorFields,
Ping,
SyntheticsMonitorWithId,
} from '../../../../../../common/runtime_types';
import { MonitorTypeBadge } from './monitor_type_badge';
import { useDateFormat } from '../../../../../hooks/use_date_format';
@ -36,7 +37,7 @@ export interface MonitorDetailsPanelProps {
latestPing?: Ping;
loading: boolean;
configId: string;
monitor: EncryptedSyntheticsSavedMonitor | null;
monitor: SyntheticsMonitorWithId | EncryptedSyntheticsSavedMonitor | null;
hideEnabled?: boolean;
hideLocations?: boolean;
hasBorder?: boolean;

View file

@ -15,7 +15,12 @@ import { LastRefreshed } from '../components/last_refreshed';
import { AutoRefreshButton } from '../components/auto_refresh_button';
import { useSyntheticsSettingsContext } from '../../../contexts';
import { useGetUrlParams } from '../../../hooks';
import { MONITOR_ROUTE, SETTINGS_ROUTE } from '../../../../../../common/constants';
import {
MONITOR_ADD_ROUTE,
MONITOR_EDIT_ROUTE,
MONITOR_ROUTE,
SETTINGS_ROUTE,
} from '../../../../../../common/constants';
import { stringifyUrlParams } from '../../../utils/url_params';
import { InspectorHeaderLink } from './inspector_header_link';
import { ToggleAlertFlyoutButton } from '../../alerts/toggle_alert_flyout_button';
@ -41,6 +46,8 @@ export function ActionMenuContent(): React.ReactElement {
}; /* useSelector(monitorStatusSelector) TODO: Implement state for monitor status */
const detailRouteMatch = useRouteMatch(MONITOR_ROUTE);
const isEditRoute = useRouteMatch(MONITOR_EDIT_ROUTE);
const isAddRoute = useRouteMatch(MONITOR_ADD_ROUTE);
const monitorId = selectedMonitor?.monitor?.id;
const syntheticExploratoryViewLink = createExploratoryViewUrl(
@ -69,8 +76,12 @@ export function ActionMenuContent(): React.ReactElement {
return (
<EuiHeaderLinks gutterSize="xs">
<LastRefreshed />
<AutoRefreshButton />
{!isEditRoute && !isAddRoute && (
<>
<LastRefreshed />
<AutoRefreshButton />
</>
)}
<ToggleAlertFlyoutButton />
<EuiHeaderLink

View file

@ -70,6 +70,26 @@ export const formatDefaultFormValues = (monitor?: SyntheticsMonitor) => {
schedule.number = `${schedule.number}s`;
}
const params = monitorWithFormMonitorType[ConfigKey.PARAMS];
if (typeof params !== 'string' && params) {
try {
monitorWithFormMonitorType[ConfigKey.PARAMS] = JSON.stringify(params);
} catch (e) {
// ignore
}
}
const browserMonitor = monitor as BrowserFields;
const pwOptions = browserMonitor[ConfigKey.PLAYWRIGHT_OPTIONS];
if (typeof pwOptions !== 'string' && pwOptions) {
try {
(monitorWithFormMonitorType as BrowserFields)[ConfigKey.PLAYWRIGHT_OPTIONS] =
JSON.stringify(pwOptions);
} catch (e) {
// ignore
}
}
// handle default monitor types from Uptime, which don't contain `ConfigKey.FORM_MONITOR_TYPE`
if (!formMonitorType) {
formMonitorType =
@ -81,7 +101,6 @@ export const formatDefaultFormValues = (monitor?: SyntheticsMonitor) => {
switch (formMonitorType) {
case FormMonitorType.MULTISTEP:
const browserMonitor = monitor as BrowserFields;
return {
...monitorWithFormMonitorType,
'source.inline': {

View file

@ -148,6 +148,7 @@ export const BROWSER_ADVANCED = (readOnly: boolean) => [
FIELD(readOnly)[ConfigKey.IGNORE_HTTPS_ERRORS],
FIELD(readOnly)[ConfigKey.SYNTHETICS_ARGS],
FIELD(readOnly)[ConfigKey.PLAYWRIGHT_OPTIONS],
FIELD(readOnly)[ConfigKey.PARAMS],
],
},
];

View file

@ -117,8 +117,7 @@ const validateHTTP: ValidationLibrary = {
return validateHeaders(headers);
},
[ConfigKey.MAX_REDIRECTS]: ({ [ConfigKey.MAX_REDIRECTS]: value }) =>
(!!value && !`${value}`.match(DIGITS_ONLY)) ||
parseFloat(value as MonitorFields[ConfigKey.MAX_REDIRECTS]) < 0,
(!!value && !`${value}`.match(DIGITS_ONLY)) || parseFloat(value as string) < 0,
[ConfigKey.URLS]: ({ [ConfigKey.URLS]: value }) => !value,
...validateCommon,
};

View file

@ -6,13 +6,13 @@
*/
import { useFetcher } from '@kbn/observability-shared-plugin/public';
import { fetchSyntheticsMonitor } from '../../../state/monitor_details/api';
import { useGetUrlParams } from '../../../hooks';
import { getDecryptedMonitorAPI } from '../../../state/monitor_management/api';
export const useCloneMonitor = () => {
const { cloneId } = useGetUrlParams();
return useFetcher(() => {
if (!cloneId) return Promise.resolve(undefined);
return getDecryptedMonitorAPI({ id: cloneId });
return fetchSyntheticsMonitor({ monitorId: cloneId });
}, [cloneId]);
};

View file

@ -6,10 +6,10 @@
*/
import { useEffect } from 'react';
import { IHttpFetchError, ResponseErrorBody } from '@kbn/core-http-browser';
import { useGetUrlParams, useUrlParams } from '../../../hooks';
import { IHttpSerializedFetchError } from '../../../state';
export const useMonitorNotFound = (error?: IHttpFetchError<ResponseErrorBody>, id?: string) => {
export const useMonitorNotFound = (error?: IHttpSerializedFetchError | null, id?: string) => {
const { packagePolicyId } = useGetUrlParams();
const updateUrlParams = useUrlParams()[1];

View file

@ -22,17 +22,21 @@ interface Props {
export const MonitorDetailsLinkPortal = ({ name, configId, locationId, updateUrl }: Props) => {
return (
<InPortal node={MonitorDetailsLinkPortalNode}>
<MonitorDetailsLink
name={name}
configId={configId}
locationId={locationId}
updateUrl={updateUrl}
/>
{locationId ? (
<MonitorDetailsLinkWithLocation
name={name}
configId={configId}
locationId={locationId}
updateUrl={updateUrl}
/>
) : (
<MonitorDetailsLink name={name} configId={configId} />
)}
</InPortal>
);
};
export const MonitorDetailsLink = ({ name, configId, locationId, updateUrl }: Props) => {
const MonitorDetailsLinkWithLocation = ({ name, configId, locationId, updateUrl }: Props) => {
const selectedLocation = useSelectedLocation(updateUrl);
let locId = locationId;
@ -45,6 +49,18 @@ export const MonitorDetailsLink = ({ name, configId, locationId, updateUrl }: Pr
const href = history.createHref({
pathname: locId ? `monitor/${configId}?locationId=${locId}` : `monitor/${configId}`,
});
return <MonitorLink href={href} name={name} />;
};
const MonitorDetailsLink = ({ name, configId }: Props) => {
const history = useHistory();
const href = history.createHref({
pathname: `monitor/${configId}`,
});
return <MonitorLink href={href} name={name} />;
};
const MonitorLink = ({ href, name }: { href: string; name: string }) => {
return (
<EuiLink data-test-subj="syntheticsMonitorDetailsLinkLink" href={href}>
<EuiIcon type="arrowLeft" /> {name}

View file

@ -115,6 +115,9 @@ describe('MonitorEditPage', () => {
locationsLoaded: true,
loading: false,
},
monitorDetails: {
syntheticsMonitorLoading: true,
},
},
});
@ -173,6 +176,10 @@ describe('MonitorEditPage', () => {
locationsLoaded: true,
loading: false,
},
monitorDetails: {
syntheticsMonitorLoading: false,
syntheticsMonitorError: new Error('test error'),
},
},
});

View file

@ -10,22 +10,27 @@ import { useParams } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import { EuiEmptyPrompt } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useTrackPageview, useFetcher } from '@kbn/observability-shared-plugin/public';
import { IHttpFetchError, ResponseErrorBody } from '@kbn/core-http-browser';
import { useTrackPageview } from '@kbn/observability-shared-plugin/public';
import { CanUsePublicLocationsCallout } from './steps/can_use_public_locations_callout';
import { DisabledCallout } from '../monitors_page/management/disabled_callout';
import { useCanUsePublicLocations } from '../../../../hooks/use_capabilities';
import { EditMonitorNotFound } from './edit_monitor_not_found';
import { LoadingState } from '../monitors_page/overview/overview/monitor_detail_flyout';
import { ConfigKey, SourceType } from '../../../../../common/runtime_types';
import { getServiceLocations, selectServiceLocationsState } from '../../state';
import {
getMonitorAction,
getServiceLocations,
selectServiceLocationsState,
selectSyntheticsMonitor,
selectSyntheticsMonitorError,
selectSyntheticsMonitorLoading,
} from '../../state';
import { AlertingCallout } from '../common/alerting_callout/alerting_callout';
import { MonitorSteps } from './steps';
import { MonitorForm } from './form';
import { LocationsLoadingError } from './locations_loading_error';
import { MonitorDetailsLinkPortal } from './monitor_details_portal';
import { useMonitorAddEditBreadcrumbs } from './use_breadcrumbs';
import { getDecryptedMonitorAPI } from '../../state/monitor_management/api';
import { EDIT_MONITOR_STEPS } from './steps/step_config';
import { useMonitorNotFound } from './hooks/use_monitor_not_found';
@ -43,17 +48,15 @@ export const MonitorEditPage: React.FC = () => {
}
}, [locationsLoaded, dispatch]);
const { data, loading, error } = useFetcher(() => {
return getDecryptedMonitorAPI({ id: monitorId });
// FIXME: Dario thinks there is a better way to do this but
// he's getting tired and maybe the Synthetics folks can fix it
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const data = useSelector(selectSyntheticsMonitor);
const isLoading = useSelector(selectSyntheticsMonitorLoading);
const error = useSelector(selectSyntheticsMonitorError);
const monitorNotFoundError = useMonitorNotFound(
error as IHttpFetchError<ResponseErrorBody>,
data?.id
);
useEffect(() => {
dispatch(getMonitorAction.get({ monitorId }));
}, [dispatch, monitorId]);
const monitorNotFoundError = useMonitorNotFound(error, data?.id);
const canUsePublicLocations = useCanUsePublicLocations(data?.[ConfigKey.LOCATIONS]);
@ -93,7 +96,7 @@ export const MonitorEditPage: React.FC = () => {
);
}
return data && locationsLoaded && !loading && !error ? (
return data && locationsLoaded && !isLoading && !error ? (
<>
<DisabledCallout />
<CanUsePublicLocationsCallout canUsePublicLocations={canUsePublicLocations} />

View file

@ -7,7 +7,7 @@
import { useEffect, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
import { ConfigKey, EncryptedSyntheticsSavedMonitor } from '../../../../../../common/runtime_types';
import { ConfigKey } from '../../../../../../common/runtime_types';
import { useSyntheticsRefreshContext } from '../../../contexts';
import {
getMonitorAction,
@ -40,7 +40,8 @@ export const useSelectedMonitor = (monId?: string) => {
monitorId && monitorFromList && monitorFromList[ConfigKey.CONFIG_ID] === monitorId;
const isLoadedSyntheticsMonitorValid =
monitorId && syntheticsMonitor && syntheticsMonitor[ConfigKey.CONFIG_ID] === monitorId;
const availableMonitor: EncryptedSyntheticsSavedMonitor | null = isLoadedSyntheticsMonitorValid
const availableMonitor = isLoadedSyntheticsMonitorValid
? syntheticsMonitor
: isMonitorFromListValid
? monitorFromList

View file

@ -23,11 +23,6 @@ TagsListMock.mockReturnValue(<div>Tags list</div>);
describe('Monitor Detail Flyout', () => {
beforeEach(() => {
jest.spyOn(observabilitySharedPublic, 'useFetcher').mockReturnValue({
status: observabilitySharedPublic.FETCH_STATUS.PENDING,
data: null,
refetch: () => null,
});
jest
.spyOn(observabilitySharedPublic, 'useTheme')
.mockReturnValue({ eui: { euiColorVis0: 'red', euiColorVis9: 'red' } } as any);
@ -76,11 +71,6 @@ describe('Monitor Detail Flyout', () => {
it('renders error boundary for fetch failure', () => {
const testErrorText = 'This is a test error';
jest.spyOn(observabilitySharedPublic, 'useFetcher').mockReturnValue({
status: observabilitySharedPublic.FETCH_STATUS.FAILURE,
error: new Error('This is a test error'),
refetch: () => null,
});
const { getByText } = render(
<MonitorDetailFlyout
@ -91,17 +81,19 @@ describe('Monitor Detail Flyout', () => {
onClose={jest.fn()}
onEnabledChange={jest.fn()}
onLocationChange={jest.fn()}
/>
/>,
{
state: {
monitorDetails: {
syntheticsMonitorError: { body: { message: 'This is a test error' } },
},
},
}
);
getByText(testErrorText);
});
it('renders loading state while fetching', () => {
jest.spyOn(observabilitySharedPublic, 'useFetcher').mockReturnValue({
status: observabilitySharedPublic.FETCH_STATUS.LOADING,
refetch: jest.fn(),
});
const { getByRole } = render(
<MonitorDetailFlyout
configId="123456"
@ -111,28 +103,20 @@ describe('Monitor Detail Flyout', () => {
onClose={jest.fn()}
onEnabledChange={jest.fn()}
onLocationChange={jest.fn()}
/>
/>,
{
state: {
monitorDetails: {
syntheticsMonitorLoading: true,
},
},
}
);
expect(getByRole('progressbar'));
});
it('renders details for fetch success', () => {
jest.spyOn(observabilitySharedPublic, 'useFetcher').mockReturnValue({
status: observabilitySharedPublic.FETCH_STATUS.SUCCESS,
data: {
enabled: true,
type: 'http',
name: 'test-monitor',
schedule: {
number: '1',
unit: 'm',
},
tags: ['prod'],
config_id: 'test-id',
},
refetch: jest.fn(),
});
const detailLink = '/app/synthetics/monitor/test-id';
jest.spyOn(monitorDetailLocator, 'useMonitorDetailLocator').mockReturnValue(detailLink);
jest.spyOn(monitorDetailLocator, 'useMonitorDetailLocator').mockReturnValue(detailLink);
@ -146,7 +130,24 @@ describe('Monitor Detail Flyout', () => {
onClose={jest.fn()}
onEnabledChange={jest.fn()}
onLocationChange={jest.fn()}
/>
/>,
{
state: {
monitorDetails: {
syntheticsMonitor: {
enabled: true,
type: 'http',
name: 'test-monitor',
schedule: {
number: '1',
unit: 'm',
},
tags: ['prod'],
config_id: 'test-id',
} as any,
},
},
}
);
expect(getByText('Every 1 minute'));

View file

@ -29,7 +29,7 @@ import { i18n } from '@kbn/i18n';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useTheme, FETCH_STATUS, useFetcher } from '@kbn/observability-shared-plugin/public';
import { useTheme } from '@kbn/observability-shared-plugin/public';
import { useOverviewStatus } from '../../hooks/use_overview_status';
import { MonitorDetailsPanel } from '../../../common/components/monitor_details_panel';
import { ClientPluginsStart } from '../../../../../../plugin';
@ -37,8 +37,12 @@ import { LocationsStatus, useStatusByLocation } from '../../../../hooks/use_stat
import { MonitorEnabled } from '../../management/monitor_list_table/monitor_enabled';
import { ActionsPopover } from './actions_popover';
import {
getMonitorAction,
selectMonitorUpsertStatus,
selectServiceLocationsState,
selectSyntheticsMonitor,
selectSyntheticsMonitorError,
selectSyntheticsMonitorLoading,
setFlyoutConfig,
} from '../../../../state';
import { useMonitorDetail } from '../../../../hooks/use_monitor_detail';
@ -46,7 +50,6 @@ import { ConfigKey, EncryptedSyntheticsMonitor, OverviewStatusMetaData } from '.
import { useMonitorDetailLocator } from '../../../../hooks/use_monitor_detail_locator';
import { MonitorStatus } from '../../../common/components/monitor_status';
import { MonitorLocationSelect } from '../../../common/components/monitor_location_select';
import { fetchSyntheticsMonitor } from '../../../../state/monitor_details/api';
interface Props {
configId: string;
@ -250,21 +253,15 @@ export function MonitorDetailFlyout(props: Props) {
}, [dispatch]);
const upsertStatus = useSelector(selectMonitorUpsertStatus(configId));
const monitorObject = useSelector(selectSyntheticsMonitor);
const isLoading = useSelector(selectSyntheticsMonitorLoading);
const error = useSelector(selectSyntheticsMonitorError);
const upsertSuccess = upsertStatus?.status === 'success';
const {
data: monitorObject,
error,
status,
loading,
} = useFetcher(
() => fetchSyntheticsMonitor({ monitorId: configId }),
// FIXME: Dario thinks there is a better way to do this but
// he's getting tired and maybe the Synthetics folks can fix it
// eslint-disable-next-line react-hooks/exhaustive-deps
[configId, upsertSuccess]
);
useEffect(() => {
dispatch(getMonitorAction.get({ monitorId: configId }));
}, [configId, dispatch, upsertSuccess]);
const [isActionsPopoverOpen, setIsActionsPopoverOpen] = useState(false);
@ -283,9 +280,9 @@ export function MonitorDetailFlyout(props: Props) {
onClose={props.onClose}
paddingSize="none"
>
{status === FETCH_STATUS.FAILURE && <EuiErrorBoundary>{error?.message}</EuiErrorBoundary>}
{status === FETCH_STATUS.LOADING && <LoadingState />}
{status === FETCH_STATUS.SUCCESS && monitorObject && (
{error && !isLoading && <EuiErrorBoundary>{error?.body?.message}</EuiErrorBoundary>}
{isLoading && <LoadingState />}
{monitorObject && (
<>
<EuiFlyoutHeader hasBorder>
<EuiPanel hasBorder={false} hasShadow={false} paddingSize="l">
@ -332,7 +329,7 @@ export function MonitorDetailFlyout(props: Props) {
...monitorObject,
id,
}}
loading={Boolean(loading)}
loading={Boolean(isLoading)}
/>
</EuiFlyoutBody>
<EuiFlyoutFooter>

View file

@ -7,21 +7,16 @@
import { createAction } from '@reduxjs/toolkit';
import { MostRecentPingsRequest } from './api';
import {
Ping,
PingsResponse,
EncryptedSyntheticsSavedMonitor,
} from '../../../../../common/runtime_types';
import { Ping, PingsResponse, SyntheticsMonitorWithId } from '../../../../../common/runtime_types';
import { createAsyncAction } from '../utils/actions';
export const setMonitorDetailsLocationAction = createAction<string>(
'[MONITOR SUMMARY] SET LOCATION'
);
export const getMonitorAction = createAsyncAction<
{ monitorId: string },
EncryptedSyntheticsSavedMonitor
>('[MONITOR DETAILS] GET MONITOR');
export const getMonitorAction = createAsyncAction<{ monitorId: string }, SyntheticsMonitorWithId>(
'[MONITOR DETAILS] GET MONITOR'
);
export const getMonitorLastRunAction = createAsyncAction<
{ monitorId: string; locationId: string },

View file

@ -8,10 +8,10 @@
import moment from 'moment';
import { apiService } from '../../../../utils/api_service';
import {
EncryptedSyntheticsSavedMonitor,
EncryptedSyntheticsMonitorCodec,
PingsResponse,
PingsResponseType,
SyntheticsMonitorWithId,
} from '../../../../../common/runtime_types';
import { INITIAL_REST_VERSION, SYNTHETICS_API_URLS } from '../../../../../common/constants';
@ -67,11 +67,13 @@ export const fetchSyntheticsMonitor = async ({
monitorId,
}: {
monitorId: string;
}): Promise<EncryptedSyntheticsSavedMonitor> =>
apiService.get<EncryptedSyntheticsSavedMonitor>(
}): Promise<SyntheticsMonitorWithId> => {
return apiService.get<SyntheticsMonitorWithId>(
SYNTHETICS_API_URLS.GET_SYNTHETICS_MONITOR.replace('{monitorId}', monitorId),
{
internal: true,
version: INITIAL_REST_VERSION,
},
EncryptedSyntheticsMonitorCodec
);
};

View file

@ -6,7 +6,7 @@
*/
import { createReducer } from '@reduxjs/toolkit';
import { EncryptedSyntheticsSavedMonitor, Ping } from '../../../../../common/runtime_types';
import { Ping, SyntheticsMonitorWithId } from '../../../../../common/runtime_types';
import { checkIsStalePing } from '../../utils/monitor_test_result/check_pings';
import { enableMonitorAlertAction } from '../monitor_list/actions';
@ -34,7 +34,8 @@ export interface MonitorDetailsState {
loaded: boolean;
};
syntheticsMonitorLoading: boolean;
syntheticsMonitor: EncryptedSyntheticsSavedMonitor | null;
syntheticsMonitor: SyntheticsMonitorWithId | null;
syntheticsMonitorError?: IHttpSerializedFetchError | null;
syntheticsMonitorDispatchedAt: number;
error: IHttpSerializedFetchError | null;
selectedLocationId: string | null;
@ -97,15 +98,15 @@ export const monitorDetailsReducer = createReducer(initialState, (builder) => {
.addCase(getMonitorAction.get, (state, action) => {
state.syntheticsMonitorDispatchedAt = action.meta.dispatchedAt;
state.syntheticsMonitorLoading = true;
state.error = null;
state.syntheticsMonitorError = null;
})
.addCase(getMonitorAction.success, (state, action) => {
state.syntheticsMonitor = action.payload;
state.syntheticsMonitorLoading = false;
state.error = null;
state.syntheticsMonitorError = null;
})
.addCase(getMonitorAction.fail, (state, action) => {
state.error = action.payload;
state.syntheticsMonitorError = action.payload;
state.syntheticsMonitorLoading = false;
})
.addCase(enableMonitorAlertAction.success, (state, action) => {

View file

@ -27,3 +27,14 @@ export const selectMonitorPingsMetadata = createSelector(getState, (state) => st
export const selectPingsError = createSelector(getState, (state) => state.error);
export const selectStatusFilter = createSelector(getState, (state) => state.statusFilter);
export const selectSyntheticsMonitor = createSelector(getState, (state) => state.syntheticsMonitor);
export const selectSyntheticsMonitorError = createSelector(
getState,
(state) => state.syntheticsMonitorError
);
export const selectSyntheticsMonitorLoading = createSelector(
getState,
(state) => state.syntheticsMonitorLoading
);

View file

@ -11,6 +11,7 @@ import {
MonitorManagementListResult,
MonitorFiltersResult,
EncryptedSyntheticsSavedMonitor,
SyntheticsMonitorWithId,
} from '../../../../../common/runtime_types';
import { createAsyncAction } from '../utils/actions';
@ -34,7 +35,7 @@ export const fetchUpsertFailureAction = createAction<UpsertMonitorError>(
export const enableMonitorAlertAction = createAsyncAction<
UpsertMonitorRequest,
EncryptedSyntheticsSavedMonitor,
SyntheticsMonitorWithId,
UpsertMonitorError
>('enableMonitorAlertAction');

View file

@ -70,7 +70,7 @@ export const fetchUpsertMonitor = async ({
null,
{
version: INITIAL_REST_VERSION,
ui: true,
internal: true,
}
);
} else {

View file

@ -9,7 +9,11 @@ import { PayloadAction } from '@reduxjs/toolkit';
import { call, put, takeEvery, select, takeLatest, debounce } from 'redux-saga/effects';
import { quietFetchOverviewStatusAction } from '../overview_status';
import { enableDefaultAlertingAction } from '../alert_rules';
import { ConfigKey, EncryptedSyntheticsSavedMonitor } from '../../../../../common/runtime_types';
import {
ConfigKey,
EncryptedSyntheticsSavedMonitor,
SyntheticsMonitorWithId,
} from '../../../../../common/runtime_types';
import { kibanaService } from '../../../../utils/kibana_service';
import { MonitorOverviewPageState } from '../overview';
import { selectOverviewState } from '../overview/selectors';
@ -48,7 +52,7 @@ export function* enableMonitorAlertEffect() {
function* (action: PayloadAction<UpsertMonitorRequest>): Generator {
try {
const response = yield call(fetchUpsertMonitor, action.payload);
yield put(enableMonitorAlertAction.success(response as EncryptedSyntheticsSavedMonitor));
yield put(enableMonitorAlertAction.success(response as SyntheticsMonitorWithId));
sendSuccessToast(action.payload.success);
if (
(response as EncryptedSyntheticsSavedMonitor)[ConfigKey.ALERT_CONFIG]?.status?.enabled

View file

@ -11,12 +11,12 @@ import { apiService } from '../../../../utils/api_service';
import {
EncryptedSyntheticsMonitor,
SyntheticsMonitor,
SyntheticsMonitorCodec,
ServiceLocationErrorsResponse,
SyntheticsMonitorWithId,
} from '../../../../../common/runtime_types';
import { INITIAL_REST_VERSION, SYNTHETICS_API_URLS } from '../../../../../common/constants';
export type UpsertMonitorResponse = ServiceLocationErrorsResponse | EncryptedSyntheticsMonitor;
export type UpsertMonitorResponse = ServiceLocationErrorsResponse | SyntheticsMonitorWithId;
export const createMonitorAPI = async ({
monitor,
@ -25,6 +25,7 @@ export const createMonitorAPI = async ({
}): Promise<UpsertMonitorResponse> => {
return await apiService.post(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS, monitor, null, {
version: INITIAL_REST_VERSION,
internal: true,
});
};
@ -53,21 +54,11 @@ export const updateMonitorAPI = async ({
id: string;
}): Promise<UpsertMonitorResponse> => {
return await apiService.put(`${SYNTHETICS_API_URLS.SYNTHETICS_MONITORS}/${id}`, monitor, null, {
ui: true,
internal: true,
version: INITIAL_REST_VERSION,
});
};
export const getDecryptedMonitorAPI = async ({ id }: { id: string }): Promise<SyntheticsMonitor> =>
apiService.get(
SYNTHETICS_API_URLS.GET_SYNTHETICS_MONITOR.replace('{monitorId}', id),
{
decrypted: true,
version: INITIAL_REST_VERSION,
},
SyntheticsMonitorCodec
);
export const fetchProjectAPIKey = async (
accessToElasticManagedLocations: boolean
): Promise<ProjectAPIKeyResponse> => {

View file

@ -5,12 +5,11 @@
* 2.0.
*/
import { SavedObjectsClientContract } from '@kbn/core/server';
import { SavedObject } from '@kbn/core/server';
import { EncryptedSavedObjectsClient } from '@kbn/encrypted-saved-objects-plugin/server';
import { syntheticsMonitorType } from '../../common/types/saved_objects';
import {
SyntheticsMonitorWithSecretsAttributes,
EncryptedSyntheticsMonitorAttributes,
SyntheticsMonitor,
} from '../../common/runtime_types';
import { normalizeSecrets } from '../synthetics_service/utils/secrets';
@ -18,28 +17,22 @@ import { normalizeSecrets } from '../synthetics_service/utils/secrets';
export const getSyntheticsMonitor = async ({
monitorId,
encryptedSavedObjectsClient,
savedObjectsClient,
spaceId,
}: {
monitorId: string;
spaceId: string;
encryptedSavedObjectsClient: EncryptedSavedObjectsClient;
savedObjectsClient: SavedObjectsClientContract;
}): Promise<SyntheticsMonitor> => {
}): Promise<SavedObject<SyntheticsMonitor>> => {
try {
const encryptedMonitor = await savedObjectsClient.get<EncryptedSyntheticsMonitorAttributes>(
syntheticsMonitorType,
monitorId
);
const decryptedMonitor =
await encryptedSavedObjectsClient.getDecryptedAsInternalUser<SyntheticsMonitorWithSecretsAttributes>(
syntheticsMonitorType,
monitorId,
{
namespace: encryptedMonitor.namespaces?.[0],
namespace: spaceId,
}
);
const { attributes } = normalizeSecrets(decryptedMonitor);
return attributes;
return normalizeSecrets(decryptedMonitor);
} catch (e) {
throw e;
}

View file

@ -13,7 +13,7 @@ import { AddEditMonitorAPI, CreateMonitorPayLoad } from './add_monitor/add_monit
import { SyntheticsRestApiRouteFactory } from '../types';
import { SYNTHETICS_API_URLS } from '../../../common/constants';
import { normalizeAPIConfig, validateMonitor } from './monitor_validation';
import { mapSavedObjectToMonitor } from './helper';
import { mapSavedObjectToMonitor } from './formatters/saved_object_to_monitor';
export const addSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
method: 'POST',
@ -26,13 +26,18 @@ export const addSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
id: schema.maybe(schema.string()),
preserve_namespace: schema.maybe(schema.boolean()),
gettingStarted: schema.maybe(schema.boolean()),
internal: schema.maybe(
schema.boolean({
defaultValue: false,
})
),
}),
},
},
handler: async (routeContext): Promise<any> => {
const { request, response, server } = routeContext;
// usually id is auto generated, but this is useful for testing
const { id } = request.query;
const { id, internal } = request.query;
const addMonitorAPI = new AddEditMonitorAPI(routeContext);
@ -113,7 +118,7 @@ export const addSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
addMonitorAPI.initDefaultAlerts(newMonitor.attributes.name);
addMonitorAPI.setupGettingStarted(newMonitor.id);
return mapSavedObjectToMonitor(newMonitor);
return mapSavedObjectToMonitor({ monitor: newMonitor, internal });
} catch (getErr) {
server.logger.error(getErr);
if (getErr instanceof InvalidLocationError) {

View file

@ -188,7 +188,7 @@ export class AddEditMonitorAPI {
prevLocations?: MonitorFields['locations']
) {
const { savedObjectsClient, syntheticsMonitorClient, request } = this.routeContext;
const ui = Boolean((request.query as { ui?: boolean })?.ui);
const internal = Boolean((request.query as { internal?: boolean })?.internal);
const {
locations,
private_locations: privateLocations,
@ -212,7 +212,7 @@ export class AddEditMonitorAPI {
if (!locations && !privateLocations && prevLocations) {
locationsVal = prevLocations;
} else {
const monitorLocations = parseMonitorLocations(monitorPayload, prevLocations, ui);
const monitorLocations = parseMonitorLocations(monitorPayload, prevLocations, internal);
if (monitorLocations.privateLocations.length > 0) {
this.allPrivateLocations = await getPrivateLocations(savedObjectsClient);

View file

@ -25,7 +25,7 @@ export const getPrivateLocationsForMonitor = async (
export const parseMonitorLocations = (
monitorPayload: CreateMonitorPayLoad,
prevLocations?: MonitorFields['locations'],
ui: boolean = false
internal: boolean = false
) => {
const { locations, private_locations: privateLocations } = monitorPayload;
@ -37,11 +37,11 @@ export const parseMonitorLocations = (
let pvtLocs = [...(privateLocations ?? []), ...extractPvtLocs]?.map((loc) =>
typeof loc === 'string' ? loc : loc.id
);
if (ui && !privateLocations && !locations && prevLocations) {
if (internal && !privateLocations && !locations && prevLocations) {
locs = prevLocations.filter((loc) => loc.isServiceManaged).map((loc) => loc.id);
pvtLocs = prevLocations.filter((loc) => !loc.isServiceManaged).map((loc) => loc.id);
} else {
if (prevLocations && !ui) {
if (prevLocations && !internal) {
if (!locations && !privateLocations) {
locs = prevLocations.filter((loc) => loc.isServiceManaged).map((loc) => loc.id);
pvtLocs = prevLocations.filter((loc) => !loc.isServiceManaged).map((loc) => loc.id);

View file

@ -14,7 +14,7 @@ import { AddEditMonitorAPI, CreateMonitorPayLoad } from './add_monitor/add_monit
import { ELASTIC_MANAGED_LOCATIONS_DISABLED } from './add_monitor_project';
import { getDecryptedMonitor } from '../../saved_objects/synthetics_monitor';
import { getPrivateLocations } from '../../synthetics_service/get_private_locations';
import { mergeSourceMonitor } from './helper';
import { mergeSourceMonitor } from './formatters/saved_object_to_monitor';
import { RouteContext, SyntheticsRestApiRouteFactory } from '../types';
import { syntheticsMonitorType } from '../../../common/types/saved_objects';
import {
@ -33,7 +33,7 @@ import {
formatTelemetryUpdateEvent,
} from '../telemetry/monitor_upgrade_sender';
import { formatSecrets, normalizeSecrets } from '../../synthetics_service/utils/secrets';
import { mapSavedObjectToMonitor } from './helper';
import { mapSavedObjectToMonitor } from './formatters/saved_object_to_monitor';
// Simplify return promise type and type it with runtime_types
export const editSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
@ -46,7 +46,11 @@ export const editSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => (
monitorId: schema.string(),
}),
query: schema.object({
ui: schema.maybe(schema.boolean()),
internal: schema.maybe(
schema.boolean({
defaultValue: false,
})
),
}),
body: schema.any(),
},
@ -55,7 +59,7 @@ export const editSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => (
const { request, response, spaceId, server } = routeContext;
const { logger } = server;
const monitor = request.body as SyntheticsMonitor;
const reqQuery = request.query as { ui?: boolean };
const reqQuery = request.query as { internal?: boolean };
const { monitorId } = request.params;
if (!monitor || typeof monitor !== 'object' || isEmpty(monitor) || Array.isArray(monitor)) {
@ -86,7 +90,7 @@ export const editSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => (
const previousMonitor = await getDecryptedMonitor(server, monitorId, spaceId);
const normalizedPreviousMonitor = normalizeSecrets(previousMonitor).attributes;
if (normalizedPreviousMonitor.origin !== 'ui' && !reqQuery.ui) {
if (normalizedPreviousMonitor.origin !== 'ui' && !reqQuery.internal) {
return response.badRequest(getInvalidOriginError(monitor));
}
@ -170,9 +174,13 @@ export const editSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => (
});
}
return mapSavedObjectToMonitor(
editedMonitorSavedObject as SavedObject<EncryptedSyntheticsMonitorAttributes>
);
return mapSavedObjectToMonitor({
internal: reqQuery.internal,
monitor: {
...(editedMonitorSavedObject as SavedObject<EncryptedSyntheticsMonitorAttributes>),
created_at: previousMonitor.created_at,
},
});
} catch (updateErr) {
if (SavedObjectsErrorHelpers.isNotFoundError(updateErr)) {
return getMonitorNotFoundResponse(response, monitorId);

View file

@ -5,8 +5,8 @@
* 2.0.
*/
import { mapSavedObjectToMonitor, mergeSourceMonitor } from './helper';
import { EncryptedSyntheticsMonitor } from '../../../common/runtime_types';
import { mapSavedObjectToMonitor, mergeSourceMonitor } from './saved_object_to_monitor';
import { EncryptedSyntheticsMonitor } from '../../../../common/runtime_types';
describe('mergeSourceMonitor', () => {
it('should merge keys', function () {
@ -66,8 +66,82 @@ describe('mergeSourceMonitor', () => {
]);
});
it('should omit null or undefined values', () => {
const result = mapSavedObjectToMonitor({ attributes: testMonitor } as any);
it('should not omit null or undefined values', () => {
const result = mapSavedObjectToMonitor({ monitor: { attributes: testMonitor } } as any);
expect(result).toEqual({
alert: {
status: {
enabled: true,
},
tls: {
enabled: true,
},
},
config_id: 'ae88f0aa-9c7d-4a5f-96dc-89d65a0ca947',
custom_heartbeat_id: 'todos-lightweight-test-projects-default',
enabled: true,
id: 'todos-lightweight-test-projects-default',
ipv4: true,
ipv6: true,
locations: [
{
geo: {
lat: 41.25,
lon: -95.86,
},
id: 'us_central',
isServiceManaged: true,
label: 'North America - US Central',
},
],
max_redirects: 0,
mode: 'any',
name: 'Todos Lightweight',
namespace: 'default',
original_space: 'default',
proxy_url: '',
retest_on_failure: true,
revision: 21,
schedule: {
number: '3',
unit: 'm',
},
'service.name': '',
tags: [],
timeout: '16',
type: 'http',
url: '${devUrl}',
'url.port': null,
ssl: {
certificate: '',
certificate_authorities: '',
supported_protocols: ['TLSv1.1', 'TLSv1.2', 'TLSv1.3'],
verification_mode: 'full',
key: 'test-key',
},
response: {
include_body: 'on_error',
include_body_max_bytes: '1024',
include_headers: true,
},
check: {
request: {
method: 'GET',
},
response: {
status: ['404'],
},
},
params: {},
});
});
it('should not omit null or undefined values with ui', () => {
const result = mapSavedObjectToMonitor({
monitor: { attributes: { ...testMonitor } },
internal: true,
} as any);
expect(result).toEqual({
__ui: {
@ -100,27 +174,37 @@ describe('mergeSourceMonitor', () => {
label: 'North America - US Central',
},
],
max_attempts: 2,
max_redirects: '0',
mode: 'any',
name: 'Todos Lightweight',
namespace: 'default',
origin: 'project',
original_space: 'default',
project_id: 'test-projects',
'response.include_body': 'on_error',
'response.include_body_max_bytes': '1024',
'response.include_headers': true,
proxy_url: '',
revision: 21,
schedule: {
number: '3',
unit: 'm',
},
'service.name': '',
'ssl.certificate': '',
'ssl.certificate_authorities': '',
'ssl.key': 'test-key',
'ssl.supported_protocols': ['TLSv1.1', 'TLSv1.2', 'TLSv1.3'],
'ssl.verification_mode': 'full',
'response.include_body': 'on_error',
'response.include_body_max_bytes': '1024',
'response.include_headers': true,
tags: [],
timeout: '16',
type: 'http',
url: '${devUrl}',
'url.port': null,
form_monitor_type: 'http',
hash: 'f4b6u3Q/PMK5KzEtPeMNzXJBA46rt+yilohaAoqMzqk=',
journey_id: 'todos-lightweight',
max_attempts: 2,
origin: 'project',
urls: '${devUrl}',
});
});
});
@ -179,6 +263,7 @@ const testMonitor = {
ipv4: true,
ipv6: true,
'ssl.certificate_authorities': '',
'ssl.key': 'test-key',
'ssl.certificate': '',
'ssl.verification_mode': 'full',
'ssl.supported_protocols': ['TLSv1.1', 'TLSv1.2', 'TLSv1.3'],

View file

@ -0,0 +1,158 @@
/*
* 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 { SavedObject } from '@kbn/core/server';
import { mergeWith, omit, omitBy } from 'lodash';
import {
ConfigKey,
EncryptedSyntheticsMonitor,
MonitorFields,
MonitorFieldsResult,
} from '../../../../common/runtime_types';
const keysToOmit = [
ConfigKey.URLS,
ConfigKey.SOURCE_INLINE,
ConfigKey.HOSTS,
ConfigKey.CONFIG_HASH,
ConfigKey.JOURNEY_ID,
ConfigKey.FORM_MONITOR_TYPE,
ConfigKey.MAX_ATTEMPTS,
ConfigKey.MONITOR_SOURCE_TYPE,
ConfigKey.METADATA,
ConfigKey.SOURCE_PROJECT_CONTENT,
ConfigKey.PROJECT_ID,
ConfigKey.JOURNEY_FILTERS_MATCH,
ConfigKey.JOURNEY_FILTERS_TAGS,
ConfigKey.MONITOR_SOURCE_TYPE,
];
type Result = MonitorFieldsResult & {
url?: string;
host?: string;
inline_script?: string;
ssl: Record<string, any>;
response: Record<string, any>;
check: Record<string, any>;
};
export const transformPublicKeys = (result: Result) => {
let formattedResult = {
...result,
[ConfigKey.PARAMS]: formatParams(result),
retest_on_failure: (result[ConfigKey.MAX_ATTEMPTS] ?? 1) > 1,
...(result[ConfigKey.HOSTS] && { host: result[ConfigKey.HOSTS] }),
...(result[ConfigKey.URLS] && { url: result[ConfigKey.URLS] }),
};
if (formattedResult[ConfigKey.MONITOR_TYPE] === 'browser') {
formattedResult = {
...formattedResult,
...(result[ConfigKey.SOURCE_INLINE] && { inline_script: result[ConfigKey.SOURCE_INLINE] }),
[ConfigKey.PLAYWRIGHT_OPTIONS]: formatPWOptions(result),
};
} else {
formattedResult.ssl = formatNestedFields(formattedResult, 'ssl');
formattedResult.response = formatNestedFields(formattedResult, 'response');
formattedResult.check = formatNestedFields(formattedResult, 'check');
if (formattedResult[ConfigKey.MAX_REDIRECTS]) {
formattedResult[ConfigKey.MAX_REDIRECTS] = Number(formattedResult[ConfigKey.MAX_REDIRECTS]);
}
}
const res = omit(formattedResult, keysToOmit) as Result;
return omitBy(
res,
(value, key) =>
key.startsWith('response.') || key.startsWith('ssl.') || key.startsWith('check.')
);
};
export function mapSavedObjectToMonitor({
monitor,
internal = false,
}: {
monitor: SavedObject<MonitorFields | EncryptedSyntheticsMonitor>;
internal?: boolean;
}) {
const result = {
...monitor.attributes,
created_at: monitor.created_at,
updated_at: monitor.updated_at,
} as Result;
if (internal) {
return result;
}
return transformPublicKeys(result);
}
export function mergeSourceMonitor(
normalizedPreviousMonitor: EncryptedSyntheticsMonitor,
monitor: EncryptedSyntheticsMonitor
) {
return mergeWith({ ...normalizedPreviousMonitor }, monitor, customizer);
}
// Ensure that METADATA is merged deeply, to protect AAD and prevent decryption errors
const customizer = (destVal: any, srcValue: any, key: string) => {
if (key === ConfigKey.ALERT_CONFIG) {
return { ...destVal, ...srcValue };
}
if (key !== ConfigKey.METADATA) {
return srcValue;
}
};
const formatParams = (config: MonitorFields) => {
if (config[ConfigKey.PARAMS]) {
try {
return (config[ConfigKey.PARAMS] = JSON.parse(config[ConfigKey.PARAMS] ?? '{}'));
} catch (e) {
// ignore
return {};
}
}
return {};
};
const formatPWOptions = (config: MonitorFields) => {
if (config[ConfigKey.PLAYWRIGHT_OPTIONS]) {
try {
return (config[ConfigKey.PLAYWRIGHT_OPTIONS] = JSON.parse(
config[ConfigKey.PLAYWRIGHT_OPTIONS] ?? '{}'
));
} catch (e) {
// ignore
return {};
}
}
return {};
};
// combine same nested fields into same object
const formatNestedFields = (
config: MonitorFields | Record<string, any>,
nestedKey: 'ssl' | 'response' | 'check' | 'request'
): Record<string, any> => {
const nestedFields = Object.keys(config).filter((key) =>
key.startsWith(`${nestedKey}.`)
) as ConfigKey[];
const obj: Record<string, any> = {};
nestedFields.forEach((key) => {
const newKey = key.replace(`${nestedKey}.`, '');
obj[newKey] = config[key];
delete config[key];
});
if (nestedKey === 'check') {
return {
request: formatNestedFields(obj, 'request'),
response: formatNestedFields(obj, 'response'),
};
}
return obj;
};

View file

@ -12,7 +12,7 @@ import { isStatusEnabled } from '../../../common/runtime_types/monitor_managemen
import { ConfigKey, EncryptedSyntheticsMonitorAttributes } from '../../../common/runtime_types';
import { SYNTHETICS_API_URLS } from '../../../common/constants';
import { getMonitorNotFoundResponse } from '../synthetics_service/service_errors';
import { mapSavedObjectToMonitor } from './helper';
import { mapSavedObjectToMonitor } from './formatters/saved_object_to_monitor';
import { getSyntheticsMonitor } from '../../queries/get_monitor';
export const getSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
@ -25,7 +25,11 @@ export const getSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
monitorId: schema.string({ minLength: 1, maxLength: 1024 }),
}),
query: schema.object({
decrypted: schema.maybe(schema.boolean()),
internal: schema.maybe(
schema.boolean({
defaultValue: false,
})
),
}),
},
},
@ -34,36 +38,36 @@ export const getSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
response,
server: { encryptedSavedObjects, coreStart },
savedObjectsClient,
spaceId,
}): Promise<any> => {
const { monitorId } = request.params;
try {
const { decrypted } = request.query;
const { internal } = request.query;
if (!decrypted) {
return mapSavedObjectToMonitor(
await savedObjectsClient.get<EncryptedSyntheticsMonitorAttributes>(
syntheticsMonitorType,
monitorId
)
);
} else {
const canSave =
(
await coreStart?.capabilities.resolveCapabilities(request, {
capabilityPath: 'uptime.*',
})
).uptime.save ?? false;
if (Boolean(canSave)) {
// only user with write permissions can decrypt the monitor
const canSave =
(
await coreStart?.capabilities.resolveCapabilities(request, {
capabilityPath: 'uptime.*',
})
).uptime.save ?? false;
if (!canSave) {
return response.forbidden();
}
const encryptedSavedObjectsClient = encryptedSavedObjects.getClient();
return await getSyntheticsMonitor({
const monitor = await getSyntheticsMonitor({
monitorId,
encryptedSavedObjectsClient,
savedObjectsClient,
spaceId,
});
return mapSavedObjectToMonitor({ monitor, internal });
} else {
return mapSavedObjectToMonitor({
monitor: await savedObjectsClient.get<EncryptedSyntheticsMonitorAttributes>(
syntheticsMonitorType,
monitorId
),
internal,
});
}
} catch (getErr) {

View file

@ -4,11 +4,11 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { mapSavedObjectToMonitor } from './formatters/saved_object_to_monitor';
import { SyntheticsRestApiRouteFactory } from '../types';
import { SYNTHETICS_API_URLS } from '../../../common/constants';
import { getMonitors, isMonitorsQueryFiltered, QuerySchema } from '../common';
import { syntheticsMonitorType } from '../../../common/types/saved_objects';
import { mapSavedObjectToMonitor } from './helper';
export const getAllSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
method: 'GET',
@ -39,7 +39,11 @@ export const getAllSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () =>
return {
...rest,
monitors: savedObjects.map(mapSavedObjectToMonitor),
monitors: savedObjects.map((monitor) =>
mapSavedObjectToMonitor({
monitor,
})
),
absoluteTotal,
perPage: perPageT,
syncErrors: syntheticsMonitorClient.syntheticsService.syncErrors,

View file

@ -1,97 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { SavedObject } from '@kbn/core/server';
import { mergeWith, omit, omitBy } from 'lodash';
import {
ConfigKey,
EncryptedSyntheticsMonitor,
MonitorFields,
} from '../../../common/runtime_types';
const keysToOmit = [
ConfigKey.URLS,
ConfigKey.SOURCE_INLINE,
ConfigKey.HOSTS,
ConfigKey.CONFIG_HASH,
ConfigKey.JOURNEY_ID,
ConfigKey.FORM_MONITOR_TYPE,
];
type Result = MonitorFields & { url?: string; host?: string; inline_script?: string };
export const transformPublicKeys = (result: Result) => {
if (result[ConfigKey.URLS]) {
result.url = result[ConfigKey.URLS];
}
if (result[ConfigKey.SOURCE_INLINE]) {
result.inline_script = result[ConfigKey.SOURCE_INLINE];
}
if (result[ConfigKey.HOSTS]) {
result.host = result[ConfigKey.HOSTS];
}
if (result[ConfigKey.PARAMS]) {
try {
result[ConfigKey.PARAMS] = JSON.parse(result[ConfigKey.PARAMS] ?? '{}');
} catch (e) {
// ignore
}
}
if (result[ConfigKey.PLAYWRIGHT_OPTIONS]) {
try {
result[ConfigKey.PLAYWRIGHT_OPTIONS] = JSON.parse(
result[ConfigKey.PLAYWRIGHT_OPTIONS] ?? '{}'
);
} catch (e) {
// ignore
}
}
return omit(result, keysToOmit) as Result;
};
export function mapSavedObjectToMonitor(
so: SavedObject<MonitorFields | EncryptedSyntheticsMonitor>
) {
let result = Object.assign(so.attributes, {
created_at: so.created_at,
updated_at: so.updated_at,
}) as Result;
result = transformPublicKeys(result);
// omit undefined value or null value
return omitBy(result, removeMonitorEmptyValues);
}
export function mergeSourceMonitor(
normalizedPreviousMonitor: EncryptedSyntheticsMonitor,
monitor: EncryptedSyntheticsMonitor
) {
return mergeWith({ ...normalizedPreviousMonitor }, monitor, customizer);
}
// Ensure that METADATA is merged deeply, to protect AAD and prevent decryption errors
const customizer = (destVal: any, srcValue: any, key: string) => {
if (key === ConfigKey.ALERT_CONFIG) {
return { ...destVal, ...srcValue };
}
if (key !== ConfigKey.METADATA) {
return srcValue;
}
};
export const removeMonitorEmptyValues = (v: any) => {
// value is falsy
return (
v === undefined ||
v === null ||
// value is empty string
(typeof v === 'string' && v.trim() === '') ||
// is empty array
(Array.isArray(v) && v.length === 0) ||
// object is has no values
(typeof v === 'object' && Object.keys(v).length === 0)
);
};

View file

@ -163,6 +163,7 @@ describe('getNormalizeCommonFields', () => {
timeout: '16',
params: '',
max_attempts: 2,
labels: {},
},
});
}
@ -228,6 +229,7 @@ describe('getNormalizeCommonFields', () => {
timeout: '16',
params: '',
max_attempts: 2,
labels: {},
},
});
});

View file

@ -94,6 +94,7 @@ export const getNormalizeCommonFields = ({
: defaultFields[ConfigKey.PARAMS],
// picking out keys specifically, so users can't add arbitrary fields
[ConfigKey.ALERT_CONFIG]: getAlertConfig(monitor),
[ConfigKey.LABELS]: monitor.fields || defaultFields[ConfigKey.LABELS],
};
return { normalizedFields, errors };
};
@ -459,7 +460,9 @@ export const flattenAndFormatObject = (obj: Record<string, unknown>, prefix = ''
return acc;
}, {});
export const normalizeYamlConfig = (monitor: NormalizedProjectProps['monitor']) => {
export const normalizeYamlConfig = (data: NormalizedProjectProps['monitor']) => {
// we map fields to labels
const { fields: _fields, ...monitor } = data;
const defaultFields = DEFAULT_FIELDS[monitor.type as MonitorTypeEnum];
const supportedKeys = Object.keys(defaultFields);
const flattenedConfig = flattenAndFormatObject(monitor, '', supportedKeys);

View file

@ -124,5 +124,5 @@ export const formatMaxRedirects = (value?: string | number): string => {
const defaultFields = DEFAULT_FIELDS[MonitorTypeEnum.HTTP];
return value ?? defaultFields[ConfigKey.MAX_REDIRECTS];
return value ?? String(defaultFields[ConfigKey.MAX_REDIRECTS]);
};

View file

@ -8,7 +8,7 @@ import expect from '@kbn/expect';
import epct from 'expect';
import moment from 'moment/moment';
import { v4 as uuidv4 } from 'uuid';
import { omit, omitBy } from 'lodash';
import { omit } from 'lodash';
import {
ConfigKey,
MonitorTypeEnum,
@ -23,10 +23,7 @@ import { format as formatUrl } from 'url';
import supertest from 'supertest';
import { getServiceApiKeyPrivileges } from '@kbn/synthetics-plugin/server/synthetics_service/get_api_key';
import { syntheticsMonitorType } from '@kbn/synthetics-plugin/common/types/saved_objects';
import {
removeMonitorEmptyValues,
transformPublicKeys,
} from '@kbn/synthetics-plugin/server/routes/monitor_cruds/helper';
import { transformPublicKeys } from '@kbn/synthetics-plugin/server/routes/monitor_cruds/formatters/saved_object_to_monitor';
import { FtrProviderContext } from '../../ftr_provider_context';
import { getFixtureJson } from './helper/get_fixture_json';
import { SyntheticsMonitorTestService } from './services/synthetics_monitor_test_service';
@ -54,8 +51,10 @@ export const addMonitorAPIHelper = async (supertestAPI: any, monitor: any, statu
return result.body;
};
export const keyToOmitList = ['created_at', 'updated_at', 'id', 'config_id', 'form_monitor_type'];
export const omitMonitorKeys = (monitor: any) => {
return omitBy(transformPublicKeys(monitor), removeMonitorEmptyValues);
return omit(transformPublicKeys(monitor), keyToOmitList);
};
export default function ({ getService }: FtrProviderContext) {
@ -154,10 +153,10 @@ export default function ({ getService }: FtrProviderContext) {
const { body: apiResponse } = await addMonitorAPI(newMonitor);
epct(apiResponse).toEqual(epct.objectContaining({ max_attempts: maxAttempts }));
epct(apiResponse).toEqual(epct.objectContaining({ retest_on_failure: false }));
});
it('can enable retries', async () => {
it('can enable retries with max attempts', async () => {
const maxAttempts = 2;
const newMonitor = {
max_attempts: maxAttempts,
@ -169,7 +168,21 @@ export default function ({ getService }: FtrProviderContext) {
const { body: apiResponse } = await addMonitorAPI(newMonitor);
epct(apiResponse).toEqual(epct.objectContaining({ max_attempts: maxAttempts }));
epct(apiResponse).toEqual(epct.objectContaining({ retest_on_failure: true }));
});
it('can enable retries', async () => {
const newMonitor = {
retest_on_failure: false,
urls: 'https://elastic.co',
name: `Sample name ${uuidv4()}`,
type: 'http',
locations: [localLoc],
};
const { body: apiResponse } = await addMonitorAPI(newMonitor);
epct(apiResponse).toEqual(epct.objectContaining({ retest_on_failure: false }));
});
it('cannot create a invalid monitor without a monitor type', async () => {

View file

@ -26,7 +26,7 @@ import {
INSTALLED_VERSION,
PrivateLocationTestService,
} from './services/private_location_test_service';
import { addMonitorAPIHelper, omitMonitorKeys } from './add_monitor';
import { addMonitorAPIHelper, keyToOmitList, omitMonitorKeys } from './add_monitor';
import { SyntheticsMonitorTestService } from './services/synthetics_monitor_test_service';
export default function ({ getService }: FtrProviderContext) {
@ -211,12 +211,10 @@ export default function ({ getService }: FtrProviderContext) {
const { created_at: createdAt, updated_at: updatedAt } = apiResponse.body;
expect([createdAt, updatedAt].map((d) => moment(d).isValid())).eql([true, true]);
expect(apiResponse.body).eql(
expect(omit(apiResponse.body, keyToOmitList)).eql(
omitMonitorKeys({
...omit(httpMonitorJson, ['urls']),
url: httpMonitorJson.urls,
[ConfigKey.MONITOR_QUERY_ID]: apiResponse.body.id,
[ConfigKey.CONFIG_ID]: apiResponse.body.id,
updated_at: updatedAt,
revision: 2,
})
@ -269,7 +267,7 @@ export default function ({ getService }: FtrProviderContext) {
);
await supertestAPI
.put(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS + '/' + newMonitorId + '?ui=true')
.put(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS + '/' + newMonitorId + '?internal=true')
.set('kbn-xsrf', 'true')
.send(httpMonitorJson)
.expect(200);
@ -370,15 +368,11 @@ export default function ({ getService }: FtrProviderContext) {
const { created_at: createdAt, updated_at: updatedAt } = apiResponse.body;
expect([createdAt, updatedAt].map((d) => moment(d).isValid())).eql([true, true]);
expect(apiResponse.body).eql(
expect(omit(apiResponse.body, keyToOmitList)).eql(
omitMonitorKeys({
...monitor,
[ConfigKey.MONITOR_QUERY_ID]: apiResponse.body.id,
[ConfigKey.CONFIG_ID]: apiResponse.body.id,
[ConfigKey.NAMESPACE]: formatKibanaNamespace(SPACE_ID),
url: apiResponse.body.url,
created_at: createdAt,
updated_at: updatedAt,
})
);
monitorId = apiResponse.body.id;

View file

@ -170,17 +170,20 @@ export default function ({ getService }: FtrProviderContext) {
.expect(200);
const decryptedCreatedMonitor = await monitorTestService.getMonitor(
createdMonitorsResponse.body.monitors[0].config_id
createdMonitorsResponse.body.monitors[0].config_id,
{
internal: true,
}
);
expect(decryptedCreatedMonitor.body).to.eql({
expect(decryptedCreatedMonitor.rawBody).to.eql({
__ui: {
script_source: {
file_name: '',
is_generated_script: false,
},
},
config_id: decryptedCreatedMonitor.body.config_id,
config_id: decryptedCreatedMonitor.rawBody.config_id,
custom_heartbeat_id: `${journeyId}-${project}-default`,
enabled: true,
alert: {
@ -241,6 +244,8 @@ export default function ({ getService }: FtrProviderContext) {
id: `${journeyId}-${project}-default`,
hash: 'ekrjelkjrelkjre',
max_attempts: 2,
updated_at: decryptedCreatedMonitor.rawBody.updated_at,
created_at: decryptedCreatedMonitor.rawBody.created_at,
labels: {},
});
}
@ -341,17 +346,20 @@ export default function ({ getService }: FtrProviderContext) {
.set('kbn-xsrf', 'true')
.expect(200);
const decryptedCreatedMonitor = await monitorTestService.getMonitor(
createdMonitorsResponse.body.monitors[0].config_id
const { rawBody: decryptedCreatedMonitor } = await monitorTestService.getMonitor(
createdMonitorsResponse.body.monitors[0].config_id,
{
internal: true,
}
);
expect(decryptedCreatedMonitor.body).to.eql({
expect(decryptedCreatedMonitor).to.eql({
__ui: {
is_tls_enabled: isTLSEnabled,
},
'check.request.method': 'POST',
'check.response.status': ['200'],
config_id: decryptedCreatedMonitor.body.config_id,
config_id: decryptedCreatedMonitor.config_id,
custom_heartbeat_id: `${journeyId}-${project}-default`,
'check.response.body.negative': [],
'check.response.body.positive': ['${testLocal1}', 'saved'],
@ -364,8 +372,10 @@ export default function ({ getService }: FtrProviderContext) {
type: 'text',
value: '',
},
params:
'{"testLocal1":"testLocalParamsValue","testGlobalParam2":"testGlobalParamOverwrite"}',
params: JSON.stringify({
testLocal1: 'testLocalParamsValue',
testGlobalParam2: 'testGlobalParamOverwrite',
}),
'check.request.headers': {
'Content-Type': 'application/x-www-form-urlencoded',
},
@ -427,6 +437,8 @@ export default function ({ getService }: FtrProviderContext) {
ipv4: true,
max_attempts: 2,
labels: {},
updated_at: decryptedCreatedMonitor.updated_at,
created_at: decryptedCreatedMonitor.created_at,
});
}
} finally {
@ -478,15 +490,18 @@ export default function ({ getService }: FtrProviderContext) {
.set('kbn-xsrf', 'true')
.expect(200);
const decryptedCreatedMonitor = await monitorTestService.getMonitor(
createdMonitorsResponse.body.monitors[0].config_id
const { rawBody: decryptedCreatedMonitor } = await monitorTestService.getMonitor(
createdMonitorsResponse.body.monitors[0].config_id,
{
internal: true,
}
);
expect(decryptedCreatedMonitor.body).to.eql({
expect(decryptedCreatedMonitor).to.eql({
__ui: {
is_tls_enabled: isTLSEnabled,
},
config_id: decryptedCreatedMonitor.body.config_id,
config_id: decryptedCreatedMonitor.config_id,
custom_heartbeat_id: `${journeyId}-${project}-default`,
'check.receive': '',
'check.send': '',
@ -545,6 +560,8 @@ export default function ({ getService }: FtrProviderContext) {
params: '',
max_attempts: 2,
labels: {},
updated_at: decryptedCreatedMonitor.updated_at,
created_at: decryptedCreatedMonitor.created_at,
});
}
} finally {
@ -594,12 +611,15 @@ export default function ({ getService }: FtrProviderContext) {
.set('kbn-xsrf', 'true')
.expect(200);
const decryptedCreatedMonitor = await monitorTestService.getMonitor(
createdMonitorsResponse.body.monitors[0].config_id
const { rawBody: decryptedCreatedMonitor } = await monitorTestService.getMonitor(
createdMonitorsResponse.body.monitors[0].config_id,
{
internal: true,
}
);
expect(decryptedCreatedMonitor.body).to.eql({
config_id: decryptedCreatedMonitor.body.config_id,
expect(decryptedCreatedMonitor).to.eql({
config_id: decryptedCreatedMonitor.config_id,
custom_heartbeat_id: `${journeyId}-${project}-default`,
enabled: true,
alert: {
@ -659,6 +679,8 @@ export default function ({ getService }: FtrProviderContext) {
ipv6: true,
params: '',
max_attempts: 2,
updated_at: decryptedCreatedMonitor.updated_at,
created_at: decryptedCreatedMonitor.created_at,
labels: {},
});
}
@ -1106,8 +1128,7 @@ export default function ({ getService }: FtrProviderContext) {
const decryptedCreatedMonitor = await monitorTestService.getMonitor(
getResponse.body.monitors[0].config_id,
true,
SPACE_ID
{ internal: true, space: SPACE_ID }
);
const { monitors } = getResponse.body;
expect(monitors.length).eql(1);
@ -1144,8 +1165,7 @@ export default function ({ getService }: FtrProviderContext) {
const decryptedUpdatedMonitor = await monitorTestService.getMonitor(
monitorsUpdated[0].config_id,
true,
SPACE_ID
{ internal: true, space: SPACE_ID }
);
expect(decryptedUpdatedMonitor.body[ConfigKey.SOURCE_PROJECT_CONTENT]).eql(updatedSource);
} finally {

View file

@ -6,13 +6,8 @@
*/
import expect from '@kbn/expect';
import { v4 as uuidv4 } from 'uuid';
import { omitBy } from 'lodash';
import { DEFAULT_FIELDS } from '@kbn/synthetics-plugin/common/constants/monitor_defaults';
import {
removeMonitorEmptyValues,
transformPublicKeys,
} from '@kbn/synthetics-plugin/server/routes/monitor_cruds/helper';
import { LOCATION_REQUIRED_ERROR } from '@kbn/synthetics-plugin/server/routes/monitor_cruds/monitor_validation';
import { FtrProviderContext } from '../../ftr_provider_context';
import { addMonitorAPIHelper, omitMonitorKeys } from './add_monitor';
@ -101,7 +96,7 @@ export default function ({ getService }: FtrProviderContext) {
});
describe('HTTP Monitor', () => {
const defaultFields = omitBy(DEFAULT_FIELDS.http, removeMonitorEmptyValues);
const defaultFields = DEFAULT_FIELDS.http;
it('return error empty http', async () => {
const { message, attributes } = await addMonitorAPI(
{
@ -154,8 +149,7 @@ export default function ({ getService }: FtrProviderContext) {
...monitor,
locations: [localLoc],
name,
max_attempts: 2,
retest_on_failure: undefined, // this key is not part of the SO and should not be defined
retest_on_failure: true,
})
);
});
@ -185,7 +179,7 @@ export default function ({ getService }: FtrProviderContext) {
});
describe('TCP Monitor', () => {
const defaultFields = omitBy(DEFAULT_FIELDS.tcp, removeMonitorEmptyValues);
const defaultFields = DEFAULT_FIELDS.tcp;
it('base tcp monitor', async () => {
const monitor = {
@ -207,7 +201,7 @@ export default function ({ getService }: FtrProviderContext) {
});
describe('ICMP Monitor', () => {
const defaultFields = omitBy(DEFAULT_FIELDS.icmp, removeMonitorEmptyValues);
const defaultFields = DEFAULT_FIELDS.icmp;
it('base icmp monitor', async () => {
const monitor = {
@ -229,7 +223,7 @@ export default function ({ getService }: FtrProviderContext) {
});
describe('Browser Monitor', () => {
const defaultFields = omitBy(DEFAULT_FIELDS.browser, removeMonitorEmptyValues);
const defaultFields = DEFAULT_FIELDS.browser;
it('empty browser monitor', async () => {
const monitor = {
@ -259,7 +253,7 @@ export default function ({ getService }: FtrProviderContext) {
};
const { body: result } = await addMonitorAPI(monitor);
expect(transformPublicKeys(result)).eql(
expect(result).eql(
omitMonitorKeys({
...defaultFields,
...monitor,

View file

@ -6,7 +6,7 @@
*/
import moment from 'moment';
import { v4 as uuidv4 } from 'uuid';
import { omit, omitBy } from 'lodash';
import { omit } from 'lodash';
import {
ConfigKey,
EncryptedSyntheticsSavedMonitor,
@ -15,7 +15,6 @@ import {
} from '@kbn/synthetics-plugin/common/runtime_types';
import { SYNTHETICS_API_URLS } from '@kbn/synthetics-plugin/common/constants';
import expect from '@kbn/expect';
import { removeMonitorEmptyValues } from '@kbn/synthetics-plugin/server/routes/monitor_cruds/helper';
import { FtrProviderContext } from '../../ftr_provider_context';
import { getFixtureJson } from './helper/get_fixture_json';
import { omitResponseTimestamps, omitEmptyValues } from './helper/monitor';
@ -40,12 +39,11 @@ export default function ({ getService }: FtrProviderContext) {
let testPolicyId = '';
const saveMonitor = async (monitor: MonitorFields, spaceId?: string) => {
const apiURL = spaceId
? `/s/${spaceId}${SYNTHETICS_API_URLS.SYNTHETICS_MONITORS}`
: SYNTHETICS_API_URLS.SYNTHETICS_MONITORS;
const res = await supertest
.post(
spaceId
? `/s/${spaceId}${SYNTHETICS_API_URLS.SYNTHETICS_MONITORS}`
: SYNTHETICS_API_URLS.SYNTHETICS_MONITORS
)
.post(apiURL + '?internal=true')
.set('kbn-xsrf', 'true')
.send(monitor);
@ -55,21 +53,21 @@ export default function ({ getService }: FtrProviderContext) {
expect([createdAt, updatedAt].map((d) => moment(d).isValid())).eql([true, true]);
return { ...rest, urls: url } as EncryptedSyntheticsSavedMonitor;
return rest as EncryptedSyntheticsSavedMonitor;
};
const editMonitor = async (modifiedMonitor: MonitorFields, monitorId: string) => {
const res = await supertest
.put(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS + '/' + monitorId)
.put(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS + '/' + monitorId + '?internal=true')
.set('kbn-xsrf', 'true')
.send(modifiedMonitor);
expect(res.status).eql(200, JSON.stringify(res.body));
const { url, ...rest } = res.body;
const { created_at: createdAt, updated_at: updatedAt } = res.body;
expect([createdAt, updatedAt].map((d) => moment(d).isValid())).eql([true, true]);
const result = { ...rest, urls: url } as EncryptedSyntheticsSavedMonitor;
return omitBy(omit(result, ['created_at', 'updated_at']), removeMonitorEmptyValues);
return omit(res.body, ['created_at', 'updated_at']);
};
before(async () => {
@ -101,9 +99,6 @@ export default function ({ getService }: FtrProviderContext) {
const savedMonitor = await saveMonitor(newMonitor as MonitorFields);
const monitorId = savedMonitor[ConfigKey.CONFIG_ID];
const { created_at: createdAt, updated_at: updatedAt } = savedMonitor;
expect([createdAt, updatedAt].map((d) => moment(d).isValid())).eql([true, true]);
expect(omitResponseTimestamps(savedMonitor)).eql(
omitEmptyValues({
...newMonitor,
@ -269,6 +264,7 @@ export default function ({ getService }: FtrProviderContext) {
[ConfigKey.CONFIG_ID]: monitorId,
[ConfigKey.MONITOR_QUERY_ID]: monitorId,
name: 'test monitor - 12',
hash: configHash,
})
);
@ -399,7 +395,10 @@ export default function ({ getService }: FtrProviderContext) {
.send(toUpdate)
.expect(200);
const updatedResponse = await monitorTestService.getMonitor(monitorId, true, SPACE_ID);
const updatedResponse = await monitorTestService.getMonitor(monitorId, {
space: SPACE_ID,
internal: true,
});
// ensure monitor was updated
expect(updatedResponse.body.urls).eql(toUpdate.urls);
@ -416,7 +415,10 @@ export default function ({ getService }: FtrProviderContext) {
.send(toUpdate2)
.expect(200);
const updatedResponse2 = await monitorTestService.getMonitor(monitorId, true, SPACE_ID);
const updatedResponse2 = await monitorTestService.getMonitor(monitorId, {
space: SPACE_ID,
internal: true,
});
// ensure monitor was updated
expect(updatedResponse2.body.urls).eql(toUpdate2.urls);

View file

@ -5,10 +5,9 @@
* 2.0.
*/
import expect from '@kbn/expect';
import { omit, omitBy } from 'lodash';
import { omit } from 'lodash';
import { DEFAULT_FIELDS } from '@kbn/synthetics-plugin/common/constants/monitor_defaults';
import { removeMonitorEmptyValues } from '@kbn/synthetics-plugin/server/routes/monitor_cruds/helper';
import { SYNTHETICS_API_URLS } from '@kbn/synthetics-plugin/common/constants';
import moment from 'moment';
import { PrivateLocation } from '@kbn/synthetics-plugin/common/runtime_types';
@ -70,7 +69,7 @@ export default function ({ getService }: FtrProviderContext) {
});
let monitorId = 'test-id';
const defaultFields = omitBy(DEFAULT_FIELDS.http, removeMonitorEmptyValues);
const defaultFields = DEFAULT_FIELDS.http;
it('adds test monitor', async () => {
const monitor = {
type: 'http',

View file

@ -55,5 +55,6 @@
"ssl.supported_protocols": ["TLSv1.1", "TLSv1.2", "TLSv1.3"],
"ssl.verification_mode": "full",
"revision": 1,
"max_attempts": 2
"max_attempts": 2,
"labels": {}
}

View file

@ -15,6 +15,8 @@ import {
import { SYNTHETICS_API_URLS } from '@kbn/synthetics-plugin/common/constants';
import expect from '@kbn/expect';
import { secretKeys } from '@kbn/synthetics-plugin/common/constants/monitor_management';
import { SyntheticsMonitorTestService } from './services/synthetics_monitor_test_service';
import { omitMonitorKeys } from './add_monitor';
import { FtrProviderContext } from '../../ftr_provider_context';
import { getFixtureJson } from './helper/get_fixture_json';
import { LOCAL_LOCATION } from './get_filters';
@ -26,6 +28,7 @@ export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const kibanaServer = getService('kibanaServer');
const retry = getService('retry');
const monitorTestService = new SyntheticsMonitorTestService(getService);
let _monitors: MonitorFields[];
let monitors: MonitorFields[];
@ -189,22 +192,45 @@ export default function ({ getService }: FtrProviderContext) {
monitors.map((mon) => ({ ...mon, name: mon.name + '4' })).map(saveMonitor)
);
const apiResponse = await supertest
.get(
SYNTHETICS_API_URLS.GET_SYNTHETICS_MONITOR.replace('{monitorId}', id1) +
'?decrypted=true'
)
.expect(200);
const apiResponse = await monitorTestService.getMonitor(id1);
expect(apiResponse.body).eql({
...monitors[0],
[ConfigKey.MONITOR_QUERY_ID]: apiResponse.body.id,
[ConfigKey.CONFIG_ID]: apiResponse.body.id,
revision: 1,
locations: [LOCAL_LOCATION],
name: 'Test HTTP Monitor 044',
labels: {},
});
expect(apiResponse.body).eql(
omitMonitorKeys({
...monitors[0],
[ConfigKey.MONITOR_QUERY_ID]: apiResponse.body.id,
[ConfigKey.CONFIG_ID]: apiResponse.body.id,
revision: 1,
locations: [LOCAL_LOCATION],
name: 'Test HTTP Monitor 044',
labels: {},
})
);
});
it('should get by id with ui query param', async () => {
const [{ id: id1 }] = await Promise.all(
monitors.map((mon) => ({ ...mon, name: mon.name + '5' })).map(saveMonitor)
);
const apiResponse = await monitorTestService.getMonitor(id1, { internal: true });
expect(apiResponse.body).eql(
omit(
{
...monitors[0],
form_monitor_type: 'icmp',
revision: 1,
locations: [LOCAL_LOCATION],
name: 'Test HTTP Monitor 045',
hosts: '192.33.22.111:3333',
hash: '',
journey_id: '',
max_attempts: 2,
labels: {},
},
['config_id', 'id', 'form_monitor_type']
)
);
});
it('returns 404 if monitor id is not found', async () => {

View file

@ -5,21 +5,17 @@
* 2.0.
*/
import { omit, omitBy } from 'lodash';
import { removeMonitorEmptyValues } from '@kbn/synthetics-plugin/server/routes/monitor_cruds/helper';
import { omit } from 'lodash';
export function omitResponseTimestamps(monitor: object) {
return omitBy(omit(monitor, ['created_at', 'updated_at']), removeMonitorEmptyValues);
return omit(monitor, ['created_at', 'updated_at']);
}
export function omitEmptyValues(monitor: object) {
const { url, ...rest } = omit(monitor, ['created_at', 'updated_at', 'form_monitor_type']) as any;
const { url, ...rest } = omit(monitor, ['created_at', 'updated_at']) as any;
return omitBy(
{
...rest,
...(url ? { url } : {}),
},
removeMonitorEmptyValues
);
return {
...rest,
...(url ? { url } : {}),
};
}

View file

@ -12,6 +12,8 @@ import { MonitorInspectResponse } from '@kbn/synthetics-plugin/public/apps/synth
import { v4 as uuidv4 } from 'uuid';
import expect from '@kbn/expect';
import { ProjectAPIKeyResponse } from '@kbn/synthetics-plugin/server/routes/monitor_cruds/get_api_key';
import moment from 'moment/moment';
import { omit } from 'lodash';
import { KibanaSupertestProvider } from '@kbn/ftr-common-functional-services';
import { FtrProviderContext } from '../../../ftr_provider_context';
@ -45,14 +47,53 @@ export class SyntheticsMonitorTestService {
return apiKey;
};
async getMonitor(monitorId: string, decrypted: boolean = true, space?: string) {
let url =
SYNTHETICS_API_URLS.GET_SYNTHETICS_MONITOR.replace('{monitorId}', monitorId) +
(decrypted ? '?decrypted=true' : '');
async getMonitor(
monitorId: string,
{
statusCode = 200,
space,
internal,
}: {
statusCode?: number;
space?: string;
internal?: boolean;
} = {}
) {
let url = SYNTHETICS_API_URLS.GET_SYNTHETICS_MONITOR.replace('{monitorId}', monitorId);
if (space) {
url = '/s/' + space + url;
}
return this.supertest.get(url).set('kbn-xsrf', 'true').expect(200);
if (internal) {
url += `?internal=${internal}`;
}
const apiResponse = await this.supertest.get(url).expect(200);
expect(apiResponse.status).eql(statusCode, JSON.stringify(apiResponse.body));
if (statusCode === 200) {
const {
created_at: createdAt,
updated_at: updatedAt,
id,
config_id: configId,
} = apiResponse.body;
expect(id).not.empty();
expect(configId).not.empty();
expect([createdAt, updatedAt].map((d) => moment(d).isValid())).eql([true, true]);
return {
rawBody: apiResponse.body,
body: {
...omit(apiResponse.body, [
'created_at',
'updated_at',
'id',
'config_id',
'form_monitor_type',
]),
},
};
}
return apiResponse.body;
}
async addMonitor(monitor: any) {