diff --git a/x-pack/solutions/observability/plugins/synthetics/common/runtime_types/monitor_management/monitor_types.ts b/x-pack/solutions/observability/plugins/synthetics/common/runtime_types/monitor_management/monitor_types.ts index 6be951534a4d5..3db59141f97c6 100644 --- a/x-pack/solutions/observability/plugins/synthetics/common/runtime_types/monitor_management/monitor_types.ts +++ b/x-pack/solutions/observability/plugins/synthetics/common/runtime_types/monitor_management/monitor_types.ts @@ -345,6 +345,7 @@ export const EncryptedSyntheticsMonitorCodec = t.union([ export const SyntheticsMonitorWithIdCodec = t.intersection([ SyntheticsMonitorCodec, t.interface({ id: t.string, updated_at: t.string, created_at: t.string }), + t.partial({ spaces: t.array(t.string), spaceId: t.string, revision: t.number }), ]); const HeartbeatFieldsCodec = t.intersection([ diff --git a/x-pack/solutions/observability/plugins/synthetics/common/types/synthetics_monitor.ts b/x-pack/solutions/observability/plugins/synthetics/common/types/synthetics_monitor.ts index 6989b54568c76..ae476c2b31581 100644 --- a/x-pack/solutions/observability/plugins/synthetics/common/types/synthetics_monitor.ts +++ b/x-pack/solutions/observability/plugins/synthetics/common/types/synthetics_monitor.ts @@ -20,3 +20,10 @@ export interface AgentPolicyInfo { description?: string; namespace?: string; } + +export interface PackagePolicyLink { + locationId: string; + locationLabel: string; + agentPolicyId: string; + packagePolicyId: string; +} diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/common/components/monitor_inspect.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/common/components/monitor_inspect.tsx index 911f5376fee82..a0c6cc73e680e 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/common/components/monitor_inspect.tsx +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/common/components/monitor_inspect.tsx @@ -10,6 +10,9 @@ import { useFetcher } from '@kbn/observability-shared-plugin/public'; import { i18n } from '@kbn/i18n'; import { + EuiBasicTable, + EuiButtonEmpty, + EuiCallOut, EuiFlyout, EuiButton, EuiCodeBlock, @@ -17,6 +20,7 @@ import { EuiTitle, EuiFlyoutFooter, EuiHorizontalRule, + EuiLink, EuiSpacer, EuiFlyoutBody, EuiToolTip, @@ -24,21 +28,34 @@ import { EuiFlexGroup, EuiFlexItem, } from '@elastic/eui'; +import type { EuiBasicTableColumn } from '@elastic/eui'; import yaml from 'js-yaml'; import { useSyntheticsSettingsContext } from '../../../contexts'; import { LoadingState } from '../../monitors_page/overview/overview/monitor_detail_flyout'; -import type { SyntheticsMonitor } from '../../../../../../common/runtime_types'; +import type { + SyntheticsMonitor, + SyntheticsMonitorWithId, +} from '../../../../../../common/runtime_types'; import { MonitorTypeEnum } from '../../../../../../common/runtime_types'; -import type { MonitorInspectResponse } from '../../../state/monitor_management/api'; -import { inspectMonitorAPI } from '../../../state/monitor_management/api'; +import type { + MonitorInspectResponse, + PackagePolicyLink, +} from '../../../state/monitor_management/api'; +import { + fetchMonitorAPI, + inspectMonitorAPI, + updateMonitorAPI, +} from '../../../state/monitor_management/api'; +import { kibanaService } from '../../../../../utils/kibana_service'; interface InspectorProps { isValid: boolean; monitorFields: SyntheticsMonitor; + isEditFlow?: boolean; } -export const MonitorInspect = ({ isValid, monitorFields }: InspectorProps) => { +export const MonitorInspect = ({ isValid, monitorFields, isEditFlow = false }: InspectorProps) => { const { isDev } = useSyntheticsSettingsContext(); const [hideParams, setHideParams] = useState(() => !isDev); @@ -51,6 +68,7 @@ export const MonitorInspect = ({ isValid, monitorFields }: InspectorProps) => { }; const [isInspecting, setIsInspecting] = useState(false); + const [migrateCount, setMigrateCount] = useState(0); const onButtonClick = () => { setIsInspecting(() => !isInspecting); setIsFlyoutVisible(() => !isFlyoutVisible); @@ -66,7 +84,7 @@ export const MonitorInspect = ({ isValid, monitorFields }: InspectorProps) => { // FIXME: Dario couldn't find a solution for monitorFields // which is not memoized downstream // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isInspecting, hideParams]); + }, [isInspecting, hideParams, migrateCount]); let flyout; @@ -111,6 +129,17 @@ export const MonitorInspect = ({ isValid, monitorFields }: InspectorProps) => { {formatContent(data.result, asJson)} {data.decodedCode && } + {isEditFlow && (data.packagePolicyLinks.length > 0 || data.hasMissingReferences) && ( + <> + + setMigrateCount((c) => c + 1)} + /> + + )} ) : loading && !error ? ( @@ -149,6 +178,124 @@ export const MonitorInspect = ({ isValid, monitorFields }: InspectorProps) => { ); }; +const stripServerFields = ({ + created_at: _ca, + updated_at: _ua, + spaceId: _si, + revision: _rev, + ...savedMonitor +}: SyntheticsMonitorWithId): SyntheticsMonitor => savedMonitor; + +const PackagePolicyLinksTable = ({ + links, + hasMissingReferences, + monitorFields, + onMigrateSuccess, +}: { + links: PackagePolicyLink[]; + hasMissingReferences: boolean; + monitorFields: SyntheticsMonitor; + onMigrateSuccess: () => void; +}) => { + const { basePath } = useSyntheticsSettingsContext(); + const [isMigrating, setIsMigrating] = useState(false); + + const handleMigrate = async () => { + const monitorId = monitorFields.config_id; + if (!monitorId) return; + setIsMigrating(true); + try { + const savedMonitor = stripServerFields(await fetchMonitorAPI({ id: monitorId })); + await updateMonitorAPI({ monitor: savedMonitor, id: monitorId }); + kibanaService.toasts.addSuccess({ + title: MIGRATE_SUCCESS_LABEL, + toastLifeTimeMs: 3000, + }); + onMigrateSuccess(); + } catch (err) { + kibanaService.toasts.addError(err, { title: MIGRATE_FAILURE_LABEL }); + } finally { + setIsMigrating(false); + } + }; + + const columns: Array> = [ + { + field: 'locationLabel', + name: PRIVATE_LOCATION_LABEL, + }, + { + field: 'agentPolicyId', + name: AGENT_POLICY_LABEL, + render: (agentPolicyId: string) => ( + + {agentPolicyId} + + ), + }, + { + field: 'packagePolicyId', + name: PACKAGE_POLICY_LABEL, + render: (packagePolicyId: string, item: PackagePolicyLink) => ( + + {packagePolicyId} + + ), + }, + ]; + + return ( + <> + +

{LINKED_POLICIES_LABEL}

+
+ + {hasMissingReferences && ( + <> + +

{MISSING_REFERENCES_DESCRIPTION}

+ + {MIGRATE_LABEL} + +
+ + + )} + {links.length > 0 && ( + + )} + + ); +}; + // @ts-ignore: Unused variable // tslint:disable-next-line: no-unused-variable const MonitorCode = ({ code }: { code: string }) => ( @@ -219,3 +366,54 @@ const HIDE_PARAMS = i18n.translate('xpack.synthetics.monitorInspect.hideParams', const AS_JSON = i18n.translate('xpack.synthetics.monitorInspect.asJson', { defaultMessage: 'As JSON', }); + +const LINKED_POLICIES_LABEL = i18n.translate( + 'xpack.synthetics.monitorInspect.linkedPoliciesLabel', + { + defaultMessage: 'Linked Fleet policies', + } +); + +const PRIVATE_LOCATION_LABEL = i18n.translate( + 'xpack.synthetics.monitorInspect.privateLocationLabel', + { + defaultMessage: 'Private location', + } +); + +const AGENT_POLICY_LABEL = i18n.translate('xpack.synthetics.monitorInspect.agentPolicyLabel', { + defaultMessage: 'Agent policy', +}); + +const PACKAGE_POLICY_LABEL = i18n.translate('xpack.synthetics.monitorInspect.packagePolicyLabel', { + defaultMessage: 'Package policy', +}); + +const MISSING_REFERENCES_TITLE = i18n.translate( + 'xpack.synthetics.monitorInspect.missingReferencesTitle', + { + defaultMessage: 'Package policy references not found', + } +); + +const MISSING_REFERENCES_DESCRIPTION = i18n.translate( + 'xpack.synthetics.monitorInspect.missingReferencesDescription', + { + defaultMessage: + 'This monitor has package policies that use a legacy ID format. Click Migrate now to update them.', + } +); + +const MIGRATE_LABEL = i18n.translate('xpack.synthetics.monitorInspect.migrateLabel', { + defaultMessage: 'Migrate now', +}); + +const MIGRATE_SUCCESS_LABEL = i18n.translate( + 'xpack.synthetics.monitorInspect.migrateSuccessLabel', + { defaultMessage: 'Monitor policies migrated successfully' } +); + +const MIGRATE_FAILURE_LABEL = i18n.translate( + 'xpack.synthetics.monitorInspect.migrateFailureLabel', + { defaultMessage: 'Failed to migrate monitor policies' } +); diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/steps/index.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/steps/index.tsx index 63744417d4233..47232410c1633 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/steps/index.tsx +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/steps/index.tsx @@ -52,7 +52,11 @@ export const MonitorSteps = ({ )} - + ); }; diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/steps/inspect_monitor_portal.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/steps/inspect_monitor_portal.tsx index 31d0344b7ceb7..c73faf94bcda6 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/steps/inspect_monitor_portal.tsx +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/steps/inspect_monitor_portal.tsx @@ -14,13 +14,15 @@ import { InspectMonitorPortalNode } from '../portals'; export const InspectMonitorPortal = ({ isValid, monitorFields, + isEditFlow = false, }: { isValid: boolean; monitorFields: SyntheticsMonitor; + isEditFlow?: boolean; }) => { return ( - + ); }; diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/monitor_management/api.ts b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/monitor_management/api.ts index 928d222aedf07..60182b45a1495 100644 --- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/monitor_management/api.ts +++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/state/monitor_management/api.ts @@ -15,6 +15,8 @@ import type { SyntheticsMonitorWithId, } from '../../../../../common/runtime_types'; import { INITIAL_REST_VERSION, SYNTHETICS_API_URLS } from '../../../../../common/constants'; +import type { PackagePolicyLink } from '../../../../../common/types'; +export type { PackagePolicyLink }; export type UpsertMonitorResponse = ServiceLocationErrorsResponse | SyntheticsMonitorWithId; @@ -34,18 +36,31 @@ export interface MonitorInspectResponse { privateConfig: PackagePolicy | null; } +export interface InspectMonitorAPIResponse { + result: MonitorInspectResponse; + decodedCode: string; + packagePolicyLinks: PackagePolicyLink[]; + hasMissingReferences: boolean; +} + export const inspectMonitorAPI = async ({ monitor, hideParams, }: { hideParams?: boolean; monitor: SyntheticsMonitor | EncryptedSyntheticsMonitor; -}): Promise<{ result: MonitorInspectResponse; decodedCode: string }> => { +}): Promise => { return await apiService.post(SYNTHETICS_API_URLS.SYNTHETICS_MONITOR_INSPECT, monitor, undefined, { hideParams, }); }; +export const fetchMonitorAPI = async ({ id }: { id: string }): Promise => { + return await apiService.get( + SYNTHETICS_API_URLS.GET_SYNTHETICS_MONITOR.replace('{monitorId}', id) + ); +}; + export const updateMonitorAPI = async ({ monitor, id, diff --git a/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/inspect_monitor.test.ts b/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/inspect_monitor.test.ts new file mode 100644 index 0000000000000..4f104d4e581d7 --- /dev/null +++ b/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/inspect_monitor.test.ts @@ -0,0 +1,204 @@ +/* + * 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 type { PrivateLocationAttributes } from '../../runtime_types/private_locations'; +import { buildPackagePolicyLinks } from './inspect_monitor'; + +const makePrivateLocation = ( + overrides: Partial = {} +): PrivateLocationAttributes => ({ + id: 'loc-1', + label: 'My Private Location', + agentPolicyId: 'agent-policy-1', + isServiceManaged: false, + ...overrides, +}); + +const makeMockRepository = ( + references: Array<{ id: string; name: string; type: string }> = [] +) => ({ + get: jest.fn().mockResolvedValue({ references }), +}); + +const mockGetPolicyId = (configId: string, locationId: string) => `${configId}-${locationId}`; + +describe('buildPackagePolicyLinks', () => { + it('returns empty links when monitorId is undefined (new monitor)', async () => { + const result = await buildPackagePolicyLinks({ + monitorId: undefined, + monitorPrivateLocations: [{ id: 'loc-1', label: 'Loc 1' }], + privateLocations: [makePrivateLocation()], + monitorConfigRepository: makeMockRepository(), + getPolicyId: mockGetPolicyId, + }); + + expect(result).toEqual({ packagePolicyLinks: [], hasMissingReferences: false }); + }); + + it('returns empty links when there are no private locations', async () => { + const result = await buildPackagePolicyLinks({ + monitorId: 'monitor-1', + monitorPrivateLocations: [], + privateLocations: [makePrivateLocation()], + monitorConfigRepository: makeMockRepository(), + getPolicyId: mockGetPolicyId, + }); + + expect(result).toEqual({ packagePolicyLinks: [], hasMissingReferences: false }); + }); + + it('builds links with references present', async () => { + const result = await buildPackagePolicyLinks({ + monitorId: 'monitor-1', + monitorPrivateLocations: [{ id: 'loc-1', label: 'My Location' }], + privateLocations: [makePrivateLocation({ id: 'loc-1', agentPolicyId: 'ap-1' })], + monitorConfigRepository: makeMockRepository([ + { id: 'monitor-1-loc-1', name: 'monitor-1-loc-1', type: 'ingest-package-policies' }, + ]), + getPolicyId: mockGetPolicyId, + }); + + expect(result.hasMissingReferences).toBe(false); + expect(result.packagePolicyLinks).toEqual([ + { + locationId: 'loc-1', + locationLabel: 'My Location', + agentPolicyId: 'ap-1', + packagePolicyId: 'monitor-1-loc-1', + }, + ]); + }); + + it('sets hasMissingReferences and returns no links when references are empty', async () => { + const result = await buildPackagePolicyLinks({ + monitorId: 'monitor-1', + monitorPrivateLocations: [{ id: 'loc-1', label: 'My Location' }], + privateLocations: [makePrivateLocation({ id: 'loc-1', agentPolicyId: 'ap-1' })], + monitorConfigRepository: makeMockRepository([]), + getPolicyId: mockGetPolicyId, + }); + + expect(result.hasMissingReferences).toBe(true); + expect(result.packagePolicyLinks).toHaveLength(0); + }); + + it('handles multiple private locations with partial references', async () => { + const result = await buildPackagePolicyLinks({ + monitorId: 'monitor-1', + monitorPrivateLocations: [ + { id: 'loc-1', label: 'Location 1' }, + { id: 'loc-2', label: 'Location 2' }, + ], + privateLocations: [ + makePrivateLocation({ id: 'loc-1', agentPolicyId: 'ap-1', label: 'Location 1' }), + makePrivateLocation({ id: 'loc-2', agentPolicyId: 'ap-2', label: 'Location 2' }), + ], + monitorConfigRepository: makeMockRepository([ + { id: 'monitor-1-loc-1', name: 'monitor-1-loc-1', type: 'ingest-package-policies' }, + ]), + getPolicyId: mockGetPolicyId, + }); + + expect(result.hasMissingReferences).toBe(true); + // Only loc-1 has a reference; loc-2 is missing so it's excluded from links + expect(result.packagePolicyLinks).toHaveLength(1); + expect(result.packagePolicyLinks[0]).toEqual({ + locationId: 'loc-1', + locationLabel: 'Location 1', + agentPolicyId: 'ap-1', + packagePolicyId: 'monitor-1-loc-1', + }); + }); + + it('skips locations without a matching private location definition', async () => { + const result = await buildPackagePolicyLinks({ + monitorId: 'monitor-1', + monitorPrivateLocations: [ + { id: 'loc-1', label: 'Location 1' }, + { id: 'loc-unknown', label: 'Unknown' }, + ], + privateLocations: [ + makePrivateLocation({ id: 'loc-1', agentPolicyId: 'ap-1', label: 'Location 1' }), + ], + monitorConfigRepository: makeMockRepository([ + { id: 'monitor-1-loc-1', name: 'monitor-1-loc-1', type: 'ingest-package-policies' }, + ]), + getPolicyId: mockGetPolicyId, + }); + + expect(result.packagePolicyLinks).toHaveLength(1); + expect(result.packagePolicyLinks[0].locationId).toBe('loc-1'); + }); + + it('falls back to locationId when label is empty', async () => { + const result = await buildPackagePolicyLinks({ + monitorId: 'monitor-1', + monitorPrivateLocations: [{ id: 'loc-1', label: '' }], + privateLocations: [makePrivateLocation({ id: 'loc-1', agentPolicyId: 'ap-1' })], + monitorConfigRepository: makeMockRepository([ + { id: 'monitor-1-loc-1', name: 'monitor-1-loc-1', type: 'ingest-package-policies' }, + ]), + getPolicyId: mockGetPolicyId, + }); + + expect(result.packagePolicyLinks[0].locationLabel).toBe('loc-1'); + }); + + it('handles monitorConfigRepository.get throwing (new monitor not saved yet)', async () => { + const mockRepo = { + get: jest.fn().mockRejectedValue(new Error('Not found')), + }; + + const result = await buildPackagePolicyLinks({ + monitorId: 'monitor-1', + monitorPrivateLocations: [{ id: 'loc-1', label: 'My Location' }], + privateLocations: [makePrivateLocation({ id: 'loc-1', agentPolicyId: 'ap-1' })], + monitorConfigRepository: mockRepo, + getPolicyId: mockGetPolicyId, + }); + + expect(result.hasMissingReferences).toBe(true); + expect(result.packagePolicyLinks).toHaveLength(0); + }); + + it('handles saved object with no references field', async () => { + const mockRepo = { + get: jest.fn().mockResolvedValue({}), + }; + + const result = await buildPackagePolicyLinks({ + monitorId: 'monitor-1', + monitorPrivateLocations: [{ id: 'loc-1', label: 'My Location' }], + privateLocations: [makePrivateLocation({ id: 'loc-1', agentPolicyId: 'ap-1' })], + monitorConfigRepository: mockRepo, + getPolicyId: mockGetPolicyId, + }); + + expect(result.hasMissingReferences).toBe(true); + expect(result.packagePolicyLinks).toHaveLength(0); + }); + + it('uses the provided getPolicyId function', async () => { + const customGetPolicyId = jest.fn( + (configId: string, locationId: string) => `custom-${configId}--${locationId}` + ); + + const result = await buildPackagePolicyLinks({ + monitorId: 'mon-1', + monitorPrivateLocations: [{ id: 'loc-1', label: 'Loc' }], + privateLocations: [makePrivateLocation({ id: 'loc-1', agentPolicyId: 'ap-1' })], + monitorConfigRepository: makeMockRepository([ + { id: 'custom-mon-1--loc-1', name: 'ref', type: 'ingest-package-policies' }, + ]), + getPolicyId: customGetPolicyId, + }); + + expect(customGetPolicyId).toHaveBeenCalledWith('mon-1', 'loc-1'); + expect(result.packagePolicyLinks[0].packagePolicyId).toBe('custom-mon-1--loc-1'); + expect(result.hasMissingReferences).toBe(false); + }); +}); diff --git a/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/inspect_monitor.ts b/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/inspect_monitor.ts index 9447b2a72a297..3f3325a79c283 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/inspect_monitor.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/inspect_monitor.ts @@ -6,6 +6,7 @@ */ import { v4 as uuidV4 } from 'uuid'; import { schema } from '@kbn/config-schema'; +import type { SavedObjectReference } from '@kbn/core-saved-objects-api-server'; import type { PrivateLocationAttributes } from '../../runtime_types/private_locations'; import type { SyntheticsRestApiRouteFactory } from '../types'; import { unzipFile } from '../../common/unzip_project_code'; @@ -16,6 +17,7 @@ import { DEFAULT_FIELDS } from '../../../common/constants/monitor_defaults'; import { validateMonitor } from './monitor_validation'; import { getPrivateLocationsForMonitor } from './add_monitor/utils'; import { AddEditMonitorAPI } from './add_monitor/add_monitor_api'; +import type { PackagePolicyLink } from '../../../common/types'; export const inspectSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({ method: 'POST', @@ -28,8 +30,15 @@ export const inspectSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () = }), }, handler: async (routeContext): Promise => { - const { savedObjectsClient, server, syntheticsMonitorClient, request, spaceId, response } = - routeContext; + const { + savedObjectsClient, + server, + syntheticsMonitorClient, + request, + spaceId, + response, + monitorConfigRepository, + } = routeContext; // usually id is auto generated, but this is useful for testing const { id, hideParams = true } = request.query; @@ -91,7 +100,28 @@ export const inspectSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () = decodedCode = await unzipFile(hasSourceContent); } - return response.ok({ body: { result, decodedCode: formatCode(decodedCode) } }); + const monitorId = normalizedMonitor.config_id; + const monitorPrivateLocations = normalizedMonitor[ConfigKey.LOCATIONS].filter( + (loc) => !loc.isServiceManaged + ); + + const { packagePolicyLinks, hasMissingReferences } = await buildPackagePolicyLinks({ + monitorId, + monitorPrivateLocations, + privateLocations, + monitorConfigRepository, + getPolicyId: (configId, locationId) => + syntheticsMonitorClient.privateLocationAPI.getPolicyId({ id: configId }, locationId), + }); + + return response.ok({ + body: { + result, + decodedCode: formatCode(decodedCode), + packagePolicyLinks, + hasMissingReferences, + }, + }); } catch (error) { server.logger.error( `Unable to inspect Synthetics monitor ${monitorWithDefaults[ConfigKey.NAME]}`, @@ -106,6 +136,63 @@ export const inspectSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () = }, }); +export const buildPackagePolicyLinks = async ({ + monitorId, + monitorPrivateLocations, + privateLocations, + monitorConfigRepository, + getPolicyId, +}: { + monitorId?: string; + monitorPrivateLocations: Array<{ id: string; label?: string; isServiceManaged?: boolean }>; + privateLocations: PrivateLocationAttributes[]; + monitorConfigRepository: { + get: (id: string) => Promise<{ references?: SavedObjectReference[] }>; + }; + getPolicyId: (configId: string, locationId: string) => string; +}): Promise<{ packagePolicyLinks: PackagePolicyLink[]; hasMissingReferences: boolean }> => { + if (!monitorId || monitorPrivateLocations.length === 0) { + return { packagePolicyLinks: [], hasMissingReferences: false }; + } + + let references: SavedObjectReference[] = []; + try { + const monitorSO = await monitorConfigRepository.get(monitorId); + references = monitorSO.references ?? []; + } catch { + // Monitor doesn't exist yet (new monitor) or can't be fetched + } + + const referenceIdSet = new Set(references.map((ref) => ref.id)); + + const links: PackagePolicyLink[] = []; + let hasMissingReferences = false; + + for (const loc of monitorPrivateLocations) { + const privateLocationDef = privateLocations.find((pl) => pl.id === loc.id); + if (!privateLocationDef) { + continue; + } + + const expectedPolicyId = getPolicyId(monitorId, loc.id); + const hasReference = referenceIdSet.has(expectedPolicyId); + + if (!hasReference) { + hasMissingReferences = true; + continue; + } + + links.push({ + locationId: loc.id, + locationLabel: loc.label || loc.id, + agentPolicyId: privateLocationDef.agentPolicyId, + packagePolicyId: expectedPolicyId, + }); + } + + return { packagePolicyLinks: links, hasMissingReferences }; +}; + const formatCode = (code: string) => { const replacements = [ { pattern: /\(\d*,\s*import_synthetics\d*\.step\)/g, replacement: 'step' }, diff --git a/x-pack/solutions/observability/test/api_integration_deployment_agnostic/apis/synthetics/inspect_monitor.ts b/x-pack/solutions/observability/test/api_integration_deployment_agnostic/apis/synthetics/inspect_monitor.ts index 8f5f12df2d975..c3f24f354190c 100644 --- a/x-pack/solutions/observability/test/api_integration_deployment_agnostic/apis/synthetics/inspect_monitor.ts +++ b/x-pack/solutions/observability/test/api_integration_deployment_agnostic/apis/synthetics/inspect_monitor.ts @@ -74,6 +74,8 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { ); rawExpect(apiResponse).toEqual({ + hasMissingReferences: false, + packagePolicyLinks: [], result: { publicConfigs: [ rawExpect.objectContaining({ @@ -147,6 +149,8 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { ], }); rawExpect(apiResponse).toEqual({ + hasMissingReferences: false, + packagePolicyLinks: [], result: { publicConfigs: [ rawExpect.objectContaining({