Skip to content

Commit

Permalink
[Synthetics] Implement private location run once (elastic#162582)
Browse files Browse the repository at this point in the history
## Summary

Implement private location run once mode , user will be able to do run
once or test now for private locations as well.

This implemented a task manager which will clean up temporarily created
package policies created for the purpose of run once and test now mode.


<img width="1718" alt="image"
src="https://github.com/elastic/kibana/assets/3505601/e5ac3a52-516f-48eb-953f-fa573d825a57">
  • Loading branch information
shahzad31 committed Jul 31, 2023
1 parent 2279dce commit 4eac242
Show file tree
Hide file tree
Showing 28 changed files with 373 additions and 198 deletions.
Expand Up @@ -13,14 +13,11 @@ import { v4 as uuidv4 } from 'uuid';
import { useFetcher } from '@kbn/observability-shared-plugin/public';
import { TestNowModeFlyout, TestRun } from '../../test_now_mode/test_now_mode_flyout';
import { format } from './formatter';
import {
Locations,
MonitorFields as MonitorFieldsType,
} from '../../../../../../common/runtime_types';
import { MonitorFields as MonitorFieldsType } from '../../../../../../common/runtime_types';
import { runOnceMonitor } from '../../../state/manual_test_runs/api';

export const RunTestButton = () => {
const { watch, formState, getValues, handleSubmit } = useFormContext();
const { formState, getValues, handleSubmit } = useFormContext();

const [inProgress, setInProgress] = useState(false);
const [testRun, setTestRun] = useState<TestRun>();
Expand Down Expand Up @@ -51,13 +48,7 @@ export const RunTestButton = () => {
}
}, [testRun?.id]);

const locations = watch('locations') as Locations;

const { tooltipContent, isDisabled } = useTooltipContent(
locations,
formState.isValid,
inProgress
);
const { tooltipContent, isDisabled } = useTooltipContent(formState.isValid, inProgress);

return (
<>
Expand Down Expand Up @@ -94,22 +85,12 @@ export const RunTestButton = () => {
);
};

const useTooltipContent = (
locations: Locations,
isValid: boolean,
isTestRunInProgress?: boolean
) => {
const isAnyPublicLocationSelected = locations?.some((loc) => loc.isServiceManaged);
const isOnlyPrivateLocations = (locations?.length ?? 0) > 0 && !isAnyPublicLocationSelected;

let tooltipContent =
isOnlyPrivateLocations || (isValid && !isAnyPublicLocationSelected)
? PRIVATE_AVAILABLE_LABEL
: TEST_NOW_DESCRIPTION;
const useTooltipContent = (isValid: boolean, isTestRunInProgress?: boolean) => {
let tooltipContent = !isValid ? INVALID_DESCRIPTION : TEST_NOW_DESCRIPTION;

tooltipContent = isTestRunInProgress ? TEST_SCHEDULED_LABEL : tooltipContent;

const isDisabled = isTestRunInProgress || !isAnyPublicLocationSelected;
const isDisabled = isTestRunInProgress || !isValid;

return { tooltipContent, isDisabled };
};
Expand All @@ -118,20 +99,17 @@ const TEST_NOW_DESCRIPTION = i18n.translate('xpack.synthetics.testRun.descriptio
defaultMessage: 'Test your monitor and verify the results before saving',
});

const INVALID_DESCRIPTION = i18n.translate('xpack.synthetics.testRun.invalid', {
defaultMessage: 'Monitor has to be valid to run test, please fix above required fields.',
});

export const TEST_SCHEDULED_LABEL = i18n.translate(
'xpack.synthetics.monitorList.testNow.scheduled',
{
defaultMessage: 'Test is already scheduled',
}
);

export const PRIVATE_AVAILABLE_LABEL = i18n.translate(
'xpack.synthetics.app.testNow.available.private',
{
defaultMessage: `You can't manually start tests on a private location.`,
}
);

export const TEST_NOW_ARIA_LABEL = i18n.translate(
'xpack.synthetics.monitorList.testNow.AriaLabel',
{
Expand Down
Expand Up @@ -9,47 +9,27 @@ import { EuiButton, EuiToolTip } from '@elastic/eui';
import React from 'react';
import { i18n } from '@kbn/i18n';
import { useDispatch, useSelector } from 'react-redux';
import {
TEST_NOW_ARIA_LABEL,
TEST_SCHEDULED_LABEL,
PRIVATE_AVAILABLE_LABEL,
} from '../monitor_add_edit/form/run_test_btn';
import { TEST_NOW_ARIA_LABEL, TEST_SCHEDULED_LABEL } from '../monitor_add_edit/form/run_test_btn';
import { useSelectedMonitor } from './hooks/use_selected_monitor';
import {
manualTestMonitorAction,
manualTestRunInProgressSelector,
} from '../../state/manual_test_runs';
import { useGetUrlParams } from '../../hooks/use_url_params';

export const RunTestManually = () => {
const dispatch = useDispatch();

const { monitor } = useSelectedMonitor();

const hasPublicLocation = monitor?.locations.some((loc) => loc.isServiceManaged);

const { locationId } = useGetUrlParams();

const isSelectedLocationPrivate = monitor?.locations.some(
(loc) => loc.isServiceManaged === false && loc.id === locationId
);

const testInProgress = useSelector(manualTestRunInProgressSelector(monitor?.config_id));

const content =
!hasPublicLocation || isSelectedLocationPrivate
? PRIVATE_AVAILABLE_LABEL
: testInProgress
? TEST_SCHEDULED_LABEL
: TEST_NOW_ARIA_LABEL;
const content = testInProgress ? TEST_SCHEDULED_LABEL : TEST_NOW_ARIA_LABEL;

return (
<EuiToolTip content={content} key={content}>
<EuiButton
data-test-subj="syntheticsRunTestManuallyButton"
color="success"
iconType="beaker"
isDisabled={!hasPublicLocation || isSelectedLocationPrivate}
isLoading={!Boolean(monitor) || testInProgress}
onClick={() => {
if (monitor) {
Expand Down
Expand Up @@ -14,13 +14,11 @@ import {
EuiPanel,
EuiLoadingSpinner,
EuiContextMenuPanelItemDescriptor,
EuiToolTip,
} from '@elastic/eui';
import { FETCH_STATUS } from '@kbn/observability-shared-plugin/public';
import { useDispatch, useSelector } from 'react-redux';
import styled from 'styled-components';
import { toggleStatusAlert } from '../../../../../../../common/runtime_types/monitor_management/alert_config';
import { PRIVATE_AVAILABLE_LABEL } from '../../../monitor_add_edit/form/run_test_btn';
import {
manualTestMonitorAction,
manualTestRunInProgressSelector,
Expand Down Expand Up @@ -106,8 +104,6 @@ export function ActionsPopover({
const location = useLocationName({ locationId });
const locationName = location?.label || monitor.location.id;

const isPrivateLocation = !Boolean(location?.isServiceManaged);

const detailUrl = useMonitorDetailLocator({
configId: monitor.configId,
locationId: locationId ?? monitor.location.id,
Expand Down Expand Up @@ -176,15 +172,9 @@ export function ActionsPopover({
},
quickInspectPopoverItem,
{
name: isPrivateLocation ? (
<EuiToolTip content={PRIVATE_AVAILABLE_LABEL}>
<span>{runTestManually}</span>
</EuiToolTip>
) : (
runTestManually
),
name: runTestManually,
icon: 'beaker',
disabled: testInProgress || isPrivateLocation,
disabled: testInProgress,
onClick: () => {
dispatch(manualTestMonitorAction.get({ configId: monitor.configId, name: monitor.name }));
dispatch(setFlyoutConfig(null));
Expand Down
Expand Up @@ -14,20 +14,14 @@ export function useRunOnceErrors({
serviceError,
errors,
locations,
showErrors = true,
}: {
showErrors?: boolean;
testRunId: string;
serviceError?: Error;
errors: ServiceLocationErrors;
locations: Locations;
}) {
const [locationErrors, setLocationErrors] = useState<ServiceLocationErrors>([]);
const [runOnceServiceError, setRunOnceServiceError] = useState<Error | undefined | null>(null);
const publicLocations = useMemo(
() => (locations ?? []).filter((loc) => loc.isServiceManaged),
[locations]
);

useEffect(() => {
setLocationErrors([]);
Expand All @@ -49,12 +43,12 @@ export function useRunOnceErrors({
}, [serviceError]);

const locationsById: Record<string, Locations[number]> = useMemo(
() => (publicLocations as Locations).reduce((acc, cur) => ({ ...acc, [cur.id]: cur }), {}),
[publicLocations]
() => (locations as Locations).reduce((acc, cur) => ({ ...acc, [cur.id]: cur }), {}),
[locations]
);

const expectPings =
publicLocations.length - (locationErrors ?? []).filter(({ locationId }) => !!locationId).length;
locations.length - (locationErrors ?? []).filter(({ locationId }) => !!locationId).length;

const locationErrorReasons = useMemo(() => {
return (locationErrors ?? [])
Expand All @@ -64,7 +58,7 @@ export function useRunOnceErrors({
}, [locationErrors]);
const hasBlockingError =
!!runOnceServiceError ||
(locationErrors?.length && locationErrors?.length === publicLocations.length);
(locationErrors?.length && locationErrors?.length === locations.length);

const errorMessages = useMemo(() => {
if (hasBlockingError) {
Expand Down
Expand Up @@ -15,14 +15,11 @@ import { Locations } from '../../../../../../common/runtime_types';
export function ManualTestRunMode({
manualTestRun,
onDone,
showErrors,
}: {
showErrors: boolean;
manualTestRun: ManualTestRun;
onDone: (testRunId: string) => void;
}) {
const { expectPings } = useRunOnceErrors({
showErrors,
testRunId: manualTestRun.testRunId!,
locations: (manualTestRun.monitor!.locations ?? []) as Locations,
errors: manualTestRun.errors ?? [],
Expand Down
Expand Up @@ -83,7 +83,6 @@ export function TestNowModeFlyoutContainer() {
key={manualTestRun.testRunId}
manualTestRun={manualTestRun}
onDone={onDone}
showErrors={flyoutOpenTestRun?.testRunId !== manualTestRun.testRunId}
/>
))}
{flyout}
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/synthetics/server/plugin.ts
Expand Up @@ -104,6 +104,7 @@ export class Plugin implements PluginType {

if (this.server) {
this.server.coreStart = coreStart;
this.server.pluginsStart = pluginsStart;
this.server.security = pluginsStart.security;
this.server.fleet = pluginsStart.fleet;
this.server.encryptedSavedObjects = pluginsStart.encryptedSavedObjects;
Expand Down
Expand Up @@ -174,7 +174,7 @@ export const syncNewMonitor = async ({
routeContext: RouteContext;
privateLocations: PrivateLocationAttributes[];
}) => {
const { savedObjectsClient, server, syntheticsMonitorClient, request, spaceId } = routeContext;
const { savedObjectsClient, server, syntheticsMonitorClient, spaceId } = routeContext;
const newMonitorId = id ?? uuidV4();

let monitorSavedObject: SavedObject<EncryptedSyntheticsMonitorAttributes> | null = null;
Expand All @@ -193,7 +193,6 @@ export const syncNewMonitor = async ({

const syncErrorsPromise = syntheticsMonitorClient.addMonitors(
[{ monitor: monitorWithNamespace as MonitorFields, id: newMonitorId }],
request,
savedObjectsClient,
privateLocations,
spaceId
Expand Down
Expand Up @@ -64,7 +64,7 @@ export const syncNewMonitorBulk = async ({
privateLocations: PrivateLocationAttributes[];
spaceId: string;
}) => {
const { server, savedObjectsClient, syntheticsMonitorClient, request } = routeContext;
const { server, savedObjectsClient, syntheticsMonitorClient } = routeContext;
let newMonitors: CreatedMonitors | null = null;

const monitorsToCreate = normalizedMonitors.map((monitor) => {
Expand All @@ -88,7 +88,6 @@ export const syncNewMonitorBulk = async ({
}),
syntheticsMonitorClient.addMonitors(
monitorsToCreate,
request,
savedObjectsClient,
privateLocations,
spaceId
Expand Down
Expand Up @@ -47,7 +47,6 @@ export const deleteMonitorBulk = async ({
...normalizedMonitor.attributes,
id: normalizedMonitor.attributes[ConfigKey.MONITOR_QUERY_ID],
})) as SyntheticsMonitorWithId[],
request,
savedObjectsClient,
spaceId
);
Expand Down
Expand Up @@ -68,7 +68,7 @@ export const deleteMonitor = async ({
routeContext: RouteContext;
monitorId: string;
}) => {
const { spaceId, savedObjectsClient, server, syntheticsMonitorClient, request } = routeContext;
const { spaceId, savedObjectsClient, server, syntheticsMonitorClient } = routeContext;
const { logger, telemetry, stackVersion } = server;

const { monitor, monitorWithSecret } = await getMonitorToDelete(
Expand All @@ -92,7 +92,6 @@ export const deleteMonitor = async ({
/* Type cast encrypted saved objects to decrypted saved objects for delete flow only.
* Deletion does not require all monitor fields */
] as SyntheticsMonitorWithId[],
request,
savedObjectsClient,
spaceId
);
Expand Down
Expand Up @@ -26,7 +26,6 @@ export const syncParamsSyntheticsParamsRoute: SyntheticsRestApiRouteFactory = ()
const allPrivateLocations = await getPrivateLocations(savedObjectsClient);

await syntheticsMonitorClient.syncGlobalParams({
request,
spaceId,
allPrivateLocations,
encryptedSavedObjects: server.encryptedSavedObjects,
Expand Down
Expand Up @@ -6,6 +6,8 @@
*/
import { schema } from '@kbn/config-schema';
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
import { PrivateLocationAttributes } from '../../runtime_types/private_locations';
import { getPrivateLocationsForMonitor } from '../monitor_cruds/add_monitor';
import { SyntheticsRestApiRouteFactory } from '../types';
import { MonitorFields } from '../../../common/runtime_types';
import { SYNTHETICS_API_URLS } from '../../../common/constants';
Expand All @@ -20,7 +22,13 @@ export const runOnceSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () =
monitorId: schema.string({ minLength: 1, maxLength: 1024 }),
}),
},
handler: async ({ request, response, server, syntheticsMonitorClient }): Promise<any> => {
handler: async ({
request,
response,
server,
syntheticsMonitorClient,
savedObjectsClient,
}): Promise<any> => {
const monitor = request.body as MonitorFields;
const { monitorId } = request.params;

Expand All @@ -33,19 +41,22 @@ export const runOnceSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () =
return response.badRequest({ body: { message, attributes: { details, ...payload } } });
}

const { syntheticsService } = syntheticsMonitorClient;
const privateLocations: PrivateLocationAttributes[] = await getPrivateLocationsForMonitor(
savedObjectsClient,
validationResult.decodedMonitor
);

const paramsBySpace = await syntheticsService.getSyntheticsParams({ spaceId });

const errors = await syntheticsService.runOnceConfigs({
// making it enabled, even if it's disabled in the UI
monitor: { ...validationResult.decodedMonitor, enabled: true },
configId: monitorId,
heartbeatId: monitorId,
runOnce: true,
testRunId: monitorId,
params: paramsBySpace[spaceId],
});
const [, errors] = await syntheticsMonitorClient.testNowConfigs(
{
monitor: { ...validationResult.decodedMonitor, config_id: monitorId } as MonitorFields,
id: monitorId,
testRunId: monitorId,
},
savedObjectsClient,
privateLocations,
spaceId,
true
);

if (errors) {
return { errors };
Expand Down

0 comments on commit 4eac242

Please sign in to comment.