From 4b13b6ac035a261f78089f04252efd8be5e598c1 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Fri, 22 May 2026 14:46:51 +0200 Subject: [PATCH 1/2] [Synthetics] Fix monitor health API for monitors in non-default spaces (#270540) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When using Kibana Spaces, the Synthetics monitor health endpoint could incorrectly report monitors as unhealthy — showing errors such as "missing location", "missing agent policy", or "missing package policy" — even when everything was properly configured. This happened because the health check was only looking for monitors, private locations, and Fleet policies in the current space, missing resources that existed in other spaces. These issues are now fixed: the health check correctly resolves monitors, private locations, package policies, and agent policies across all relevant spaces, giving an accurate health status regardless of how resources are distributed across your Kibana Spaces. Closes elastic/kibana#270477. `POST /internal/synthetics/monitors/_health` returned wrong results when monitors lived outside the request's space — `missing_package_policy` errors when called from the monitor's space, and 404s when called from `default`. Two independent space-scoping bugs: 1. **Package policy lookup ignored space.** `getExistingPackagePoliciesMap` called Fleet's `packagePolicyService.getByIDs` with `createInternalRepository()`, which is scoped to the default namespace. Package policies created for monitors in another space were therefore invisible. 2. **Monitor saved-object lookup was space-scoped.** `MonitorConfigRepository.get` used the request-scoped saved-objects client, restricted to the request's space. Calling `_health` from `default` for a monitor that lives elsewhere returned a 404. - **`PackagePolicyService.getByIds`** — accepts a new optional `additionalSpaceIds`, so the wrapper's per-space scoped-client fan-out can broaden beyond `[spaceId, default]`. Existing callers keep their old behavior. - **`MonitorConfigRepository.getAcrossSpaces(id, namespaces, soClient?)`** — new method that resolves a monitor across an arbitrary list of spaces. Uses the multi-space type's per-object `namespaces` array in one bulkGet entry, plus one entry per namespace for the `namespaceType: 'single'` legacy type. Accepts an injected `soClient` so the health API can pass `createInternalRepository()` and bypass the request's space restriction. - **`MonitorIntegrationHealthApi`**: - Computes `allSpaces = { requestSpace, ...allSpacesWithMonitors }` once, up-front. - `fetchMonitors` calls `monitorConfigRepository.getAcrossSpaces` with the internal repository → fixes bug #2. - `getExistingPackagePoliciesMap` uses the `PackagePolicyService` wrapper with `additionalSpaceIds` → fixes bug #1. - [x] `node scripts/jest` on the three affected suites — **77/77 passing** (includes new cross-space coverage and a new `getAcrossSpaces` test block). - [x] `node scripts/type_check --project x-pack/solutions/observability/plugins/synthetics/tsconfig.json` — clean. - [x] `node scripts/eslint` on the changed files — clean. - [ ] Manual: create a monitor with a private location in a non-default space, then call `POST /internal/synthetics/monitors/_health` both from that space and from `default`. Verify each call reports the monitor accurately instead of `missing_package_policy` / 404. - elastic/kibana#270137 — related health API fix (project monitor policy ID + infinite polling). Made with [Cursor](https://cursor.com) --------- Co-authored-by: Cursor Co-authored-by: Miguel Martín Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> (cherry picked from commit 4e2f2e6496e98d8ffa6344a963438c17c74d7940) --- .../monitor_config_repository.test.ts | 95 ++++ .../services/monitor_config_repository.ts | 42 ++ .../monitor_integration_health_api.test.ts | 412 ++++++++++++++---- .../monitor_integration_health_api.ts | 83 ++-- .../package_policy_service.ts | 24 +- 5 files changed, 551 insertions(+), 105 deletions(-) diff --git a/x-pack/solutions/observability/plugins/synthetics/server/services/monitor_config_repository.test.ts b/x-pack/solutions/observability/plugins/synthetics/server/services/monitor_config_repository.test.ts index 88795c1aafc79..002dd9fb6e4ca 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/services/monitor_config_repository.test.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/services/monitor_config_repository.test.ts @@ -85,6 +85,101 @@ describe('MonitorConfigRepository', () => { }); }); + describe('getAcrossSpaces', () => { + it('issues a single multi-space lookup and one legacy lookup per namespace', async () => { + const id = 'test-id'; + const namespaces = ['default', 'space-two']; + const mockMonitor = { + id, + attributes: { name: 'Test Monitor' }, + type: syntheticsMonitorSavedObjectType, + references: [], + }; + soClient.bulkGet.mockResolvedValue({ saved_objects: [mockMonitor] }); + + const result = await repository.getAcrossSpaces(id, namespaces); + + expect(soClient.bulkGet).toHaveBeenCalledWith([ + { type: syntheticsMonitorSavedObjectType, id, namespaces: ['default', 'space-two'] }, + { type: legacySyntheticsMonitorTypeSingle, id, namespaces: ['default'] }, + { type: legacySyntheticsMonitorTypeSingle, id, namespaces: ['space-two'] }, + ]); + expect(result).toBe(mockMonitor); + }); + + it('returns the first saved object that has attributes and no error', async () => { + const id = 'test-id'; + const errored = { + id, + type: syntheticsMonitorSavedObjectType, + attributes: {}, + references: [], + error: { statusCode: 404, error: 'Not Found', message: 'not found' }, + }; + const found = { + id, + type: legacySyntheticsMonitorTypeSingle, + attributes: { name: 'Legacy' }, + references: [], + }; + soClient.bulkGet.mockResolvedValue({ saved_objects: [errored as any, found] }); + + const result = await repository.getAcrossSpaces(id, ['default']); + + expect(result).toBe(found); + }); + + it('throws not-found when no namespace has the monitor', async () => { + const id = 'missing-id'; + soClient.bulkGet.mockResolvedValue({ + saved_objects: [ + { + id, + type: syntheticsMonitorSavedObjectType, + error: { statusCode: 404, error: 'Not Found', message: 'not found' }, + }, + ], + } as any); + + await expect(repository.getAcrossSpaces(id, ['default'])).rejects.toMatchObject({ + output: { statusCode: 404 }, + }); + }); + + it('deduplicates the namespaces array', async () => { + const id = 'dup-id'; + soClient.bulkGet.mockResolvedValue({ + saved_objects: [ + { id, type: syntheticsMonitorSavedObjectType, attributes: {}, references: [] }, + ], + } as any); + + await repository.getAcrossSpaces(id, ['default', 'default', 'space-two']); + + const calledWith = soClient.bulkGet.mock.calls[0][0]; + expect(calledWith).toEqual([ + { type: syntheticsMonitorSavedObjectType, id, namespaces: ['default', 'space-two'] }, + { type: legacySyntheticsMonitorTypeSingle, id, namespaces: ['default'] }, + { type: legacySyntheticsMonitorTypeSingle, id, namespaces: ['space-two'] }, + ]); + }); + + it('uses the supplied saved objects client when provided', async () => { + const id = 'test-id'; + const altClient = savedObjectsClientMock.create(); + altClient.bulkGet.mockResolvedValue({ + saved_objects: [ + { id, type: syntheticsMonitorSavedObjectType, attributes: {}, references: [] }, + ], + } as any); + + await repository.getAcrossSpaces(id, ['default'], altClient); + + expect(altClient.bulkGet).toHaveBeenCalledTimes(1); + expect(soClient.bulkGet).not.toHaveBeenCalled(); + }); + }); + describe('getDecrypted', () => { it('should get and decrypt a monitor by id and space', async () => { const id = 'test-id'; diff --git a/x-pack/solutions/observability/plugins/synthetics/server/services/monitor_config_repository.ts b/x-pack/solutions/observability/plugins/synthetics/server/services/monitor_config_repository.ts index 950e595b70bb3..03934d3196cbb 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/services/monitor_config_repository.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/services/monitor_config_repository.ts @@ -6,8 +6,10 @@ */ import type { + ISavedObjectsRepository, SavedObject, SavedObjectReference, + SavedObjectsBulkGetObject, SavedObjectsClientContract, SavedObjectsFindOptions, SavedObjectsFindResult, @@ -75,6 +77,46 @@ export class MonitorConfigRepository { return resolved; } + /** + * Look up a monitor by id across the supplied spaces. + * + * Required for cross-space callers (e.g. the monitor health API) because + * `get` is bound to the request-scoped saved objects client and therefore + * only ever sees the request's space — see Kibana issue #270477. + * + * The multi-space type (`syntheticsMonitorSavedObjectType`, + * `namespaceType: 'multiple'`) supports a per-object `namespaces` array, so + * a single entry covers all spaces. The legacy type + * (`legacySyntheticsMonitorTypeSingle`, `namespaceType: 'single'`) only + * accepts one namespace per object, so we add one entry per space. + */ + async getAcrossSpaces( + id: string, + namespaces: string[], + soClient: SavedObjectsClientContract | ISavedObjectsRepository = this.soClient + ): Promise> { + const uniqueNamespaces = [...new Set(namespaces)]; + const bulkObjects: SavedObjectsBulkGetObject[] = [ + { type: syntheticsMonitorSavedObjectType, id, namespaces: uniqueNamespaces }, + ...uniqueNamespaces.map((namespace) => ({ + type: legacySyntheticsMonitorTypeSingle, + id, + namespaces: [namespace], + })), + ]; + const { saved_objects: results } = await soClient.bulkGet( + bulkObjects + ); + const resolved = results.find((obj) => obj?.attributes && !obj.error); + if (!resolved) { + throw SavedObjectsErrorHelpers.createGenericNotFoundError( + syntheticsMonitorSavedObjectType, + id + ); + } + return resolved; + } + async getDecrypted( id: string, spaceId: string diff --git a/x-pack/solutions/observability/plugins/synthetics/server/services/monitor_integration_health_api.test.ts b/x-pack/solutions/observability/plugins/synthetics/server/services/monitor_integration_health_api.test.ts index 34933f7d0e9a9..d8e0660efd906 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/services/monitor_integration_health_api.test.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/services/monitor_integration_health_api.test.ts @@ -21,16 +21,20 @@ import { MonitorIntegrationHealthApi } from './monitor_integration_health_api'; jest.mock('../synthetics_service/get_private_locations'); jest.mock('../synthetics_service/private_location/synthetics_private_location'); +jest.mock('../synthetics_service/private_location/package_policy_service'); -import { getPrivateLocations } from '../synthetics_service/get_private_locations'; +import { getPrivateLocationsForNamespaces } from '../synthetics_service/get_private_locations'; import { SyntheticsPrivateLocation } from '../synthetics_service/private_location/synthetics_private_location'; +import { PackagePolicyService } from '../synthetics_service/private_location/package_policy_service'; -const mockedGetPrivateLocations = getPrivateLocations as jest.MockedFunction< - typeof getPrivateLocations ->; +const mockedGetPrivateLocationsForNamespaces = + getPrivateLocationsForNamespaces as jest.MockedFunction; const MockedSyntheticsPrivateLocation = SyntheticsPrivateLocation as jest.MockedClass< typeof SyntheticsPrivateLocation >; +const MockedPackagePolicyService = PackagePolicyService as jest.MockedClass< + typeof PackagePolicyService +>; const SPACE_ID = 'default'; @@ -70,13 +74,29 @@ const createPackagePolicy = (policyId: string, agentPolicyIds: string[]): Packag policy_ids: agentPolicyIds, } as unknown as PackagePolicy); -const buildApi = (overrides: { - monitorConfigRepository?: { get: jest.Mock }; - fleetGetByIDs?: jest.Mock; +interface BuildApiOverrides { + monitorConfigRepository?: { getAcrossSpaces: jest.Mock }; + /** + * Mocks the Synthetics PackagePolicyService wrapper that the health API + * uses to fetch package policies across spaces. + */ + packagePolicyServiceGetByIds?: jest.Mock; fleetAgentPolicyGetByIds?: jest.Mock; fleetGetInstallation?: jest.Mock; -}): MonitorIntegrationHealthApi => { - const fleetGetByIDs = overrides.fleetGetByIDs ?? jest.fn().mockResolvedValue([]); + getUnsafeInternalClient?: jest.Mock; + spaceId?: string; +} + +const buildApi = (overrides: BuildApiOverrides = {}): MonitorIntegrationHealthApi => { + const packagePolicyServiceGetByIds = + overrides.packagePolicyServiceGetByIds ?? jest.fn().mockResolvedValue([]); + + MockedPackagePolicyService.mockImplementation( + () => + ({ + getByIds: packagePolicyServiceGetByIds, + } as any) + ); const fleetAgentPolicyGetByIds = overrides.fleetAgentPolicyGetByIds ?? @@ -91,10 +111,12 @@ const buildApi = (overrides: { coreStart: { savedObjects: { createInternalRepository: jest.fn().mockReturnValue({}), + getUnsafeInternalClient: + overrides.getUnsafeInternalClient ?? + jest.fn().mockReturnValue({ asScopedToNamespace: jest.fn().mockReturnValue({}) }), }, }, fleet: { - packagePolicyService: { getByIDs: fleetGetByIDs }, agentPolicyService: { getByIds: fleetAgentPolicyGetByIds }, packageService: { asInternalUser: { getInstallation: fleetGetInstallation }, @@ -105,14 +127,14 @@ const buildApi = (overrides: { const savedObjectsClient = {} as SavedObjectsClientContract; const monitorConfigRepository = (overrides.monitorConfigRepository ?? { - get: jest.fn(), + getAcrossSpaces: jest.fn(), }) as unknown as MonitorConfigRepository; return new MonitorIntegrationHealthApi( server, savedObjectsClient, monitorConfigRepository, - SPACE_ID + overrides.spaceId ?? SPACE_ID ); }; @@ -158,7 +180,7 @@ describe('MonitorIntegrationHealthApi', () => { } as any) ); - mockedGetPrivateLocations.mockResolvedValue([]); + mockedGetPrivateLocationsForNamespaces.mockResolvedValue([]); }); describe('monitor fetching and partial errors', () => { @@ -169,7 +191,7 @@ describe('MonitorIntegrationHealthApi', () => { ); const api = buildApi({ monitorConfigRepository: { - get: jest.fn().mockRejectedValue(notFoundError), + getAcrossSpaces: jest.fn().mockRejectedValue(notFoundError), }, }); @@ -193,7 +215,7 @@ describe('MonitorIntegrationHealthApi', () => { .mockResolvedValueOnce(successSO) .mockRejectedValueOnce(notFoundError); - const api = buildApi({ monitorConfigRepository: { get: getMock } }); + const api = buildApi({ monitorConfigRepository: { getAcrossSpaces: getMock } }); const result = await api.getHealth(['mon-1', 'mon-2']); @@ -207,7 +229,7 @@ describe('MonitorIntegrationHealthApi', () => { it('provides a default error message when rejection has no message', async () => { const api = buildApi({ monitorConfigRepository: { - get: jest.fn().mockRejectedValue({}), + getAcrossSpaces: jest.fn().mockRejectedValue({}), }, }); @@ -224,7 +246,7 @@ describe('MonitorIntegrationHealthApi', () => { ); const api = buildApi({ monitorConfigRepository: { - get: jest.fn().mockRejectedValue(forbiddenError), + getAcrossSpaces: jest.fn().mockRejectedValue(forbiddenError), }, }); @@ -238,7 +260,7 @@ describe('MonitorIntegrationHealthApi', () => { it('defaults statusCode to 500 for generic errors without output.statusCode', async () => { const api = buildApi({ monitorConfigRepository: { - get: jest.fn().mockRejectedValue(new Error('Something went wrong')), + getAcrossSpaces: jest.fn().mockRejectedValue(new Error('Something went wrong')), }, }); @@ -257,7 +279,7 @@ describe('MonitorIntegrationHealthApi', () => { }); const api = buildApi({ - monitorConfigRepository: { get: jest.fn().mockResolvedValue(so) }, + monitorConfigRepository: { getAcrossSpaces: jest.fn().mockResolvedValue(so) }, }); const result = await api.getHealth(['mon-1']); @@ -281,15 +303,15 @@ describe('MonitorIntegrationHealthApi', () => { locations: [{ id: 'priv-loc-1', label: 'Private Loc 1', isServiceManaged: false }], }); - mockedGetPrivateLocations.mockResolvedValue([privateLoc]); + mockedGetPrivateLocationsForNamespaces.mockResolvedValue([privateLoc]); const expectedPolicyId = 'mon-1-priv-loc-1'; const packagePolicy = createPackagePolicy(expectedPolicyId, ['agent-policy-1']); - const fleetGetByIDs = jest.fn().mockResolvedValue([packagePolicy]); + const packagePolicyServiceGetByIds = jest.fn().mockResolvedValue([packagePolicy]); const api = buildApi({ - monitorConfigRepository: { get: jest.fn().mockResolvedValue(so) }, - fleetGetByIDs, + monitorConfigRepository: { getAcrossSpaces: jest.fn().mockResolvedValue(so) }, + packagePolicyServiceGetByIds, }); const result = await api.getHealth(['mon-1']); @@ -320,12 +342,12 @@ describe('MonitorIntegrationHealthApi', () => { locations: [{ id: 'priv-loc-1', label: 'Private Loc 1', isServiceManaged: false }], }); - mockedGetPrivateLocations.mockResolvedValue([privateLoc]); + mockedGetPrivateLocationsForNamespaces.mockResolvedValue([privateLoc]); - const fleetGetByIDs = jest.fn().mockResolvedValue([]); + const packagePolicyServiceGetByIds = jest.fn().mockResolvedValue([]); const api = buildApi({ - monitorConfigRepository: { get: jest.fn().mockResolvedValue(so) }, - fleetGetByIDs, + monitorConfigRepository: { getAcrossSpaces: jest.fn().mockResolvedValue(so) }, + packagePolicyServiceGetByIds, }); const result = await api.getHealth(['mon-1']); @@ -343,10 +365,10 @@ describe('MonitorIntegrationHealthApi', () => { locations: [{ id: 'gone-loc', label: 'Gone Location', isServiceManaged: false }], }); - mockedGetPrivateLocations.mockResolvedValue([]); + mockedGetPrivateLocationsForNamespaces.mockResolvedValue([]); const api = buildApi({ - monitorConfigRepository: { get: jest.fn().mockResolvedValue(so) }, + monitorConfigRepository: { getAcrossSpaces: jest.fn().mockResolvedValue(so) }, }); const result = await api.getHealth(['mon-1']); @@ -363,10 +385,10 @@ describe('MonitorIntegrationHealthApi', () => { locations: [{ id: 'gone-loc', isServiceManaged: false }], }); - mockedGetPrivateLocations.mockResolvedValue([]); + mockedGetPrivateLocationsForNamespaces.mockResolvedValue([]); const api = buildApi({ - monitorConfigRepository: { get: jest.fn().mockResolvedValue(so) }, + monitorConfigRepository: { getAcrossSpaces: jest.fn().mockResolvedValue(so) }, }); const result = await api.getHealth(['mon-1']); @@ -382,11 +404,11 @@ describe('MonitorIntegrationHealthApi', () => { locations: [{ id: 'priv-loc-1', label: 'Private Loc 1', isServiceManaged: false }], }); - mockedGetPrivateLocations.mockResolvedValue([privateLoc]); + mockedGetPrivateLocationsForNamespaces.mockResolvedValue([privateLoc]); const fleetAgentPolicyGetByIds = jest.fn().mockResolvedValue([]); const api = buildApi({ - monitorConfigRepository: { get: jest.fn().mockResolvedValue(so) }, + monitorConfigRepository: { getAcrossSpaces: jest.fn().mockResolvedValue(so) }, fleetAgentPolicyGetByIds, }); @@ -409,17 +431,17 @@ describe('MonitorIntegrationHealthApi', () => { ], }); - mockedGetPrivateLocations.mockResolvedValue([privateLoc1, privateLoc2]); + mockedGetPrivateLocationsForNamespaces.mockResolvedValue([privateLoc1, privateLoc2]); const expectedPolicyId1 = 'mon-1-loc-1'; - const fleetGetByIDs = jest + const packagePolicyServiceGetByIds = jest .fn() .mockResolvedValue([createPackagePolicy(expectedPolicyId1, ['existing-agent'])]); const fleetAgentPolicyGetByIds = jest.fn().mockResolvedValue([{ id: 'existing-agent' }]); const api = buildApi({ - monitorConfigRepository: { get: jest.fn().mockResolvedValue(so) }, - fleetGetByIDs, + monitorConfigRepository: { getAcrossSpaces: jest.fn().mockResolvedValue(so) }, + packagePolicyServiceGetByIds, fleetAgentPolicyGetByIds, }); @@ -442,15 +464,15 @@ describe('MonitorIntegrationHealthApi', () => { locations: [{ id: 'priv-loc-1', label: 'Private Loc 1', isServiceManaged: false }], }); - mockedGetPrivateLocations.mockResolvedValue([privateLoc]); + mockedGetPrivateLocationsForNamespaces.mockResolvedValue([privateLoc]); const expectedPolicyId = 'mon-1-priv-loc-1'; const packagePolicy = createPackagePolicy(expectedPolicyId, ['agent-policy-1']); - const fleetGetByIDs = jest.fn().mockResolvedValue([packagePolicy]); + const packagePolicyServiceGetByIds = jest.fn().mockResolvedValue([packagePolicy]); const api = buildApi({ - monitorConfigRepository: { get: jest.fn().mockResolvedValue(so) }, - fleetGetByIDs, + monitorConfigRepository: { getAcrossSpaces: jest.fn().mockResolvedValue(so) }, + packagePolicyServiceGetByIds, }); const result = await api.getHealth(['mon-1']); @@ -470,29 +492,29 @@ describe('MonitorIntegrationHealthApi', () => { locations: [{ id: 'priv-loc-1', label: 'Private Loc 1', isServiceManaged: false }], }); - mockedGetPrivateLocations.mockResolvedValue([privateLoc]); + mockedGetPrivateLocationsForNamespaces.mockResolvedValue([privateLoc]); const expectedPolicyId = `${monitorQueryId}-priv-loc-1`; const wrongPolicyId = `so-uuid-priv-loc-1`; const packagePolicy = createPackagePolicy(expectedPolicyId, ['agent-policy-1']); - const fleetGetByIDs = jest.fn().mockResolvedValue([packagePolicy]); + const packagePolicyServiceGetByIds = jest.fn().mockResolvedValue([packagePolicy]); const api = buildApi({ - monitorConfigRepository: { get: jest.fn().mockResolvedValue(so) }, - fleetGetByIDs, + monitorConfigRepository: { getAcrossSpaces: jest.fn().mockResolvedValue(so) }, + packagePolicyServiceGetByIds, }); const result = await api.getHealth(['so-uuid']); - expect(fleetGetByIDs).toHaveBeenCalledWith( - expect.anything(), - expect.arrayContaining([expectedPolicyId]), - expect.anything() + expect(packagePolicyServiceGetByIds).toHaveBeenCalledWith( + expect.objectContaining({ + packagePolicyIds: expect.arrayContaining([expectedPolicyId]), + }) ); - expect(fleetGetByIDs).not.toHaveBeenCalledWith( - expect.anything(), - expect.arrayContaining([wrongPolicyId]), - expect.anything() + expect(packagePolicyServiceGetByIds).not.toHaveBeenCalledWith( + expect.objectContaining({ + packagePolicyIds: expect.arrayContaining([wrongPolicyId]), + }) ); expect(result.monitors[0].privateLocations[0].status).toBe( PrivateLocationHealthStatusValue.Healthy @@ -522,9 +544,9 @@ describe('MonitorIntegrationHealthApi', () => { ], }); - mockedGetPrivateLocations.mockResolvedValue([privateLoc1, privateLoc2]); + mockedGetPrivateLocationsForNamespaces.mockResolvedValue([privateLoc1, privateLoc2]); - const fleetGetByIDs = jest + const packagePolicyServiceGetByIds = jest .fn() .mockResolvedValue([ createPackagePolicy('mon-1-loc-1', ['agent-1']), @@ -534,8 +556,8 @@ describe('MonitorIntegrationHealthApi', () => { const getMock = jest.fn().mockResolvedValueOnce(so1).mockResolvedValueOnce(so2); const api = buildApi({ - monitorConfigRepository: { get: getMock }, - fleetGetByIDs, + monitorConfigRepository: { getAcrossSpaces: getMock }, + packagePolicyServiceGetByIds, }); const result = await api.getHealth(['mon-1', 'mon-2']); @@ -567,15 +589,15 @@ describe('MonitorIntegrationHealthApi', () => { locations: [{ id: 'priv-loc-1', label: 'Private Loc 1', isServiceManaged: false }], }); - mockedGetPrivateLocations.mockResolvedValue([privateLoc]); + mockedGetPrivateLocationsForNamespaces.mockResolvedValue([privateLoc]); const legacyPolicyId = `mon-1-priv-loc-1-${SPACE_ID}`; const packagePolicy = createPackagePolicy(legacyPolicyId, ['agent-policy-1']); - const fleetGetByIDs = jest.fn().mockResolvedValue([packagePolicy]); + const packagePolicyServiceGetByIds = jest.fn().mockResolvedValue([packagePolicy]); const api = buildApi({ - monitorConfigRepository: { get: jest.fn().mockResolvedValue(so) }, - fleetGetByIDs, + monitorConfigRepository: { getAcrossSpaces: jest.fn().mockResolvedValue(so) }, + packagePolicyServiceGetByIds, }); const result = await api.getHealth(['mon-1']); @@ -593,11 +615,11 @@ describe('MonitorIntegrationHealthApi', () => { locations: [{ id: 'priv-loc-1', label: 'Private Loc 1', isServiceManaged: false }], }); - mockedGetPrivateLocations.mockResolvedValue([privateLoc]); + mockedGetPrivateLocationsForNamespaces.mockResolvedValue([privateLoc]); const newPolicyId = 'mon-1-priv-loc-1'; const legacyPolicyId = `mon-1-priv-loc-1-${SPACE_ID}`; - const fleetGetByIDs = jest + const packagePolicyServiceGetByIds = jest .fn() .mockResolvedValue([ createPackagePolicy(newPolicyId, ['agent-policy-1']), @@ -605,8 +627,8 @@ describe('MonitorIntegrationHealthApi', () => { ]); const api = buildApi({ - monitorConfigRepository: { get: jest.fn().mockResolvedValue(so) }, - fleetGetByIDs, + monitorConfigRepository: { getAcrossSpaces: jest.fn().mockResolvedValue(so) }, + packagePolicyServiceGetByIds, }); const result = await api.getHealth(['mon-1']); @@ -623,15 +645,15 @@ describe('MonitorIntegrationHealthApi', () => { locations: [{ id: 'priv-loc-1', label: 'Private Loc 1', isServiceManaged: false }], }); - mockedGetPrivateLocations.mockResolvedValue([privateLoc]); + mockedGetPrivateLocationsForNamespaces.mockResolvedValue([privateLoc]); const legacyPolicyId = `mon-1-priv-loc-1-${SPACE_ID}`; const packagePolicy = createPackagePolicy(legacyPolicyId, ['wrong-agent']); - const fleetGetByIDs = jest.fn().mockResolvedValue([packagePolicy]); + const packagePolicyServiceGetByIds = jest.fn().mockResolvedValue([packagePolicy]); const api = buildApi({ - monitorConfigRepository: { get: jest.fn().mockResolvedValue(so) }, - fleetGetByIDs, + monitorConfigRepository: { getAcrossSpaces: jest.fn().mockResolvedValue(so) }, + packagePolicyServiceGetByIds, }); const result = await api.getHealth(['mon-1']); @@ -643,6 +665,250 @@ describe('MonitorIntegrationHealthApi', () => { }); }); + describe('cross-space lookups (issue #270477)', () => { + it('passes every space that has monitors to monitorConfigRepository.getAcrossSpaces', async () => { + MockedSyntheticsPrivateLocation.mockImplementation( + () => + ({ + getPolicyId: jest.fn( + (config: { origin?: string; id: string }, locId: string) => `${config.id}-${locId}` + ), + getLegacyPolicyIdsForAllSpaces: jest.fn(() => []), + getAllSpacesWithMonitors: jest.fn().mockResolvedValue(['space-two', 'space-three']), + getPolicyIdFormatInfo: jest.fn(() => ({ + hasNewFormatPolicyId: false, + hasAnyLegacyPolicyId: false, + legacyPolicyIds: [], + })), + } as any) + ); + + const so = createMonitorSO('mon-1'); + const getAcrossSpaces = jest.fn().mockResolvedValue(so); + + const api = buildApi({ + monitorConfigRepository: { getAcrossSpaces }, + spaceId: 'default', + }); + + await api.getHealth(['mon-1']); + + expect(getAcrossSpaces).toHaveBeenCalledTimes(1); + const [calledId, calledNamespaces] = getAcrossSpaces.mock.calls[0]; + expect(calledId).toBe('mon-1'); + expect(new Set(calledNamespaces)).toEqual(new Set(['default', 'space-two', 'space-three'])); + }); + + it('passes additional spaces (excluding the request space) to PackagePolicyService.getByIds', async () => { + MockedSyntheticsPrivateLocation.mockImplementation( + () => + ({ + getPolicyId: jest.fn( + (config: { origin?: string; id: string }, locId: string) => `${config.id}-${locId}` + ), + getLegacyPolicyIdsForAllSpaces: jest.fn(() => []), + // Reproduces the bug scenario in #270477: caller is in `default`, + // monitors live in `space-two`. + getAllSpacesWithMonitors: jest.fn().mockResolvedValue(['space-two']), + getPolicyIdFormatInfo: jest.fn( + ( + config: { id: string }, + locId: string, + existingPolicies: Array<{ id: string }> | undefined + ) => { + const newId = `${config.id}-${locId}`; + const hasNewFormatPolicyId = existingPolicies?.some((p) => p.id === newId) ?? false; + return { + hasNewFormatPolicyId, + hasAnyLegacyPolicyId: false, + legacyPolicyIds: [], + }; + } + ), + } as any) + ); + + const privateLoc = createPrivateLocation('priv-loc-1', 'agent-policy-1'); + mockedGetPrivateLocationsForNamespaces.mockResolvedValue([privateLoc]); + + const so = createMonitorSO('mon-1', { + locations: [{ id: 'priv-loc-1', label: 'Private Loc 1', isServiceManaged: false }], + }); + + const packagePolicyServiceGetByIds = jest + .fn() + .mockResolvedValue([createPackagePolicy('mon-1-priv-loc-1', ['agent-policy-1'])]); + + const api = buildApi({ + monitorConfigRepository: { getAcrossSpaces: jest.fn().mockResolvedValue(so) }, + packagePolicyServiceGetByIds, + spaceId: 'default', + }); + + const result = await api.getHealth(['mon-1']); + + expect(packagePolicyServiceGetByIds).toHaveBeenCalledTimes(1); + const [args] = packagePolicyServiceGetByIds.mock.calls[0]; + expect(args.spaceId).toBe('default'); + // The request's space must not be repeated under additionalSpaceIds. + expect(args.additionalSpaceIds).not.toContain('default'); + // Any other space that has monitors must be included so cross-space + // package policies are discoverable. + expect(args.additionalSpaceIds).toEqual(expect.arrayContaining(['space-two'])); + + // The monitor's package policy was created in a different space, but the + // wrapper finds it — so the location is reported as healthy. + expect(result.monitors[0].privateLocations[0].status).toBe( + PrivateLocationHealthStatusValue.Healthy + ); + }); + + it('uses getUnsafeInternalClient with namespace-scoped clients for agent policy lookup', async () => { + const privateLoc = createPrivateLocation('priv-loc-1', 'agent-policy-1'); + mockedGetPrivateLocationsForNamespaces.mockResolvedValue([privateLoc]); + + const so = createMonitorSO('mon-1', { + locations: [{ id: 'priv-loc-1', label: 'Private Loc 1', isServiceManaged: false }], + }); + + const asScopedToNamespace = jest.fn().mockReturnValue({}); + const getUnsafeInternalClient = jest.fn().mockReturnValue({ asScopedToNamespace }); + + const packagePolicy = createPackagePolicy('mon-1-priv-loc-1', ['agent-policy-1']); + + const api = buildApi({ + monitorConfigRepository: { getAcrossSpaces: jest.fn().mockResolvedValue(so) }, + packagePolicyServiceGetByIds: jest.fn().mockResolvedValue([packagePolicy]), + getUnsafeInternalClient, + }); + + await api.getHealth(['mon-1']); + + expect(getUnsafeInternalClient).toHaveBeenCalled(); + expect(asScopedToNamespace).toHaveBeenCalledWith(SPACE_ID); + }); + + it('finds agent policies that only exist in a non-default space', async () => { + // Reproduces the case in which an agent policy has space_ids: ['space-two'] so an internal + // client scoped to 'default' cannot find it. + const CUSTOM_SPACE = 'space-two'; + const privateLoc = createPrivateLocation('priv-loc-1', 'space-agent-policy'); + mockedGetPrivateLocationsForNamespaces.mockResolvedValue([privateLoc]); + + const so = createMonitorSO('mon-1', { + locations: [{ id: 'priv-loc-1', label: 'Private Loc 1', isServiceManaged: false }], + }); + + // Override so getAllSpacesWithMonitors includes CUSTOM_SPACE, causing the health + // API to build spaces = {'default', CUSTOM_SPACE} for agent policy queries. + MockedSyntheticsPrivateLocation.mockImplementationOnce( + () => + ({ + getPolicyId: jest.fn( + (config: { origin?: string; id: string }, locId: string) => `${config.id}-${locId}` + ), + getLegacyPolicyIdsForAllSpaces: jest.fn(() => []), + getAllSpacesWithMonitors: jest.fn().mockResolvedValue([CUSTOM_SPACE]), + getPolicyIdFormatInfo: jest.fn( + ( + config: { id: string }, + locId: string, + existingPolicies: Array<{ id: string }> | undefined + ) => ({ + hasNewFormatPolicyId: + existingPolicies?.some((p) => p.id === `${config.id}-${locId}`) ?? false, + hasAnyLegacyPolicyId: false, + legacyPolicyIds: [], + }) + ), + } as any) + ); + + // Simulate the policy being absent in 'default' but present in CUSTOM_SPACE. + const fleetAgentPolicyGetByIds = jest + .fn() + .mockResolvedValueOnce([]) // default namespace → not found + .mockResolvedValueOnce([{ id: 'space-agent-policy' }]); // CUSTOM_SPACE → found + + const packagePolicy = createPackagePolicy('mon-1-priv-loc-1', ['space-agent-policy']); + + const api = buildApi({ + monitorConfigRepository: { getAcrossSpaces: jest.fn().mockResolvedValue(so) }, + packagePolicyServiceGetByIds: jest.fn().mockResolvedValue([packagePolicy]), + fleetAgentPolicyGetByIds, + }); + + const result = await api.getHealth(['mon-1']); + + // Both spaces were queried (default + CUSTOM_SPACE) + expect(fleetAgentPolicyGetByIds).toHaveBeenCalledTimes(2); + // Policy found in CUSTOM_SPACE → status must not be MissingAgentPolicy + expect(result.monitors[0].privateLocations[0].status).not.toBe( + PrivateLocationHealthStatusValue.MissingAgentPolicy + ); + }); + + it('fetches private locations across all spaces so monitors in non-default spaces are not reported as missing_location (Bug #4)', async () => { + // Reproduces the case in which a private location is created in a custom space. When _health is + // called from 'default', getPrivateLocations would be scoped to 'default' and could + // not find the location → MissingLocation. For this reason, we use getPrivateLocationsForNamespaces + // with allSpaces so every relevant namespace is searched. + const CUSTOM_SPACE = 'space-two'; + const privateLoc = createPrivateLocation('priv-loc-1', 'agent-policy-1'); + + // Override so getAllSpacesWithMonitors returns CUSTOM_SPACE, causing allSpaces + // to include it. The health API then passes allSpaces to getPrivateLocationsForNamespaces. + MockedSyntheticsPrivateLocation.mockImplementationOnce( + () => + ({ + getPolicyId: jest.fn( + (config: { origin?: string; id: string }, locId: string) => `${config.id}-${locId}` + ), + getLegacyPolicyIdsForAllSpaces: jest.fn(() => []), + getAllSpacesWithMonitors: jest.fn().mockResolvedValue([CUSTOM_SPACE]), + getPolicyIdFormatInfo: jest.fn( + ( + config: { id: string }, + locId: string, + existingPolicies: Array<{ id: string }> | undefined + ) => ({ + hasNewFormatPolicyId: + existingPolicies?.some((p) => p.id === `${config.id}-${locId}`) ?? false, + hasAnyLegacyPolicyId: false, + legacyPolicyIds: [], + }) + ), + } as any) + ); + + mockedGetPrivateLocationsForNamespaces.mockResolvedValue([privateLoc]); + + const so = createMonitorSO('mon-1', { + locations: [{ id: 'priv-loc-1', label: 'Private Loc 1', isServiceManaged: false }], + }); + + const packagePolicy = createPackagePolicy('mon-1-priv-loc-1', ['agent-policy-1']); + + const api = buildApi({ + monitorConfigRepository: { getAcrossSpaces: jest.fn().mockResolvedValue(so) }, + packagePolicyServiceGetByIds: jest.fn().mockResolvedValue([packagePolicy]), + }); + + const result = await api.getHealth(['mon-1']); + + // getPrivateLocationsForNamespaces must be called with allSpaces so cross-space + // locations are discoverable — verify both the default and CUSTOM_SPACE are included. + expect(mockedGetPrivateLocationsForNamespaces).toHaveBeenCalledWith( + expect.anything(), + expect.arrayContaining([SPACE_ID, CUSTOM_SPACE]) + ); + // Private location was found → status must not be MissingLocation + expect(result.monitors[0].privateLocations[0].status).not.toBe( + PrivateLocationHealthStatusValue.MissingLocation + ); + }); + }); + describe('healthy status has no reason field', () => { it('omits reason for healthy locations', async () => { const privateLoc = createPrivateLocation('priv-loc-1', 'agent-policy-1'); @@ -650,15 +916,15 @@ describe('MonitorIntegrationHealthApi', () => { locations: [{ id: 'priv-loc-1', label: 'Private Loc 1', isServiceManaged: false }], }); - mockedGetPrivateLocations.mockResolvedValue([privateLoc]); + mockedGetPrivateLocationsForNamespaces.mockResolvedValue([privateLoc]); const expectedPolicyId = 'mon-1-priv-loc-1'; const packagePolicy = createPackagePolicy(expectedPolicyId, ['agent-policy-1']); - const fleetGetByIDs = jest.fn().mockResolvedValue([packagePolicy]); + const packagePolicyServiceGetByIds = jest.fn().mockResolvedValue([packagePolicy]); const api = buildApi({ - monitorConfigRepository: { get: jest.fn().mockResolvedValue(so) }, - fleetGetByIDs, + monitorConfigRepository: { getAcrossSpaces: jest.fn().mockResolvedValue(so) }, + packagePolicyServiceGetByIds, }); const result = await api.getHealth(['mon-1']); diff --git a/x-pack/solutions/observability/plugins/synthetics/server/services/monitor_integration_health_api.ts b/x-pack/solutions/observability/plugins/synthetics/server/services/monitor_integration_health_api.ts index 8c74f26634feb..7d695e7cf9f8e 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/services/monitor_integration_health_api.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/services/monitor_integration_health_api.ts @@ -8,6 +8,7 @@ import type { SavedObject } from '@kbn/core/server'; import type { SavedObjectsClientContract } from '@kbn/core/server'; import type { AgentPolicy, PackagePolicy } from '@kbn/fleet-plugin/common'; +import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common'; import { ConfigKey, PrivateLocationHealthStatusValue, @@ -18,7 +19,8 @@ import { type MonitorsHealthResponse, } from '../../common/runtime_types'; import { SyntheticsPrivateLocation } from '../synthetics_service/private_location/synthetics_private_location'; -import { getPrivateLocations } from '../synthetics_service/get_private_locations'; +import { PackagePolicyService } from '../synthetics_service/private_location/package_policy_service'; +import { getPrivateLocationsForNamespaces } from '../synthetics_service/get_private_locations'; import type { PrivateLocationAttributes } from '../runtime_types/private_locations'; import type { SyntheticsServerSetup } from '../types'; import type { MonitorConfigRepository } from './monitor_config_repository'; @@ -61,30 +63,36 @@ export class MonitorIntegrationHealthApi { } async getHealth(monitorIds: string[]): Promise { - const { foundMonitors, errors } = await this.fetchMonitors(monitorIds); + const privateLocationAPI = new SyntheticsPrivateLocation(this.server); + + // Resolve the union of every space that may host a relevant monitor or + // package policy. Computed up-front so monitor and package-policy lookups + // can both look across spaces — see Kibana issue #270477. + const allSpacesWithMonitors = await privateLocationAPI.getAllSpacesWithMonitors(); + const allSpaces = new Set([this.spaceId, ...allSpacesWithMonitors]); + + const { foundMonitors, errors } = await this.fetchMonitors(monitorIds, allSpaces); if (foundMonitors.length === 0) { return { monitors: [], errors }; } - const allPrivateLocations = await getPrivateLocations(this.savedObjectsClient); + const allPrivateLocations = await getPrivateLocationsForNamespaces(this.savedObjectsClient, [ + ...allSpaces, + ]); const allPrivateLocationsMap = new Map( allPrivateLocations.map((loc) => [loc.id, loc]) ); - const privateLocationAPI = new SyntheticsPrivateLocation(this.server); - - const allSpacesWithMonitors = await privateLocationAPI.getAllSpacesWithMonitors(); - const allSpaces = new Set([this.spaceId, ...allSpacesWithMonitors]); - const referencedAgentPolicyIds = [ ...new Set(allPrivateLocations.map((loc) => loc.agentPolicyId)), ]; const [existingPackagePoliciesMap, existingAgentPoliciesMap] = await Promise.all([ this.getExistingPackagePoliciesMap( - this.getExpectedPackagePolicyIds(foundMonitors, privateLocationAPI, allSpaces) + this.getExpectedPackagePolicyIds(foundMonitors, privateLocationAPI, allSpaces), + allSpaces ), - this.getExistingAgentPoliciesMap(referencedAgentPolicyIds), + this.getExistingAgentPoliciesMap(referencedAgentPolicyIds, allSpaces), ]); const existingPoliciesArray = [...existingPackagePoliciesMap.values()]; @@ -169,12 +177,19 @@ export class MonitorIntegrationHealthApi { return { monitors, errors }; } - private async fetchMonitors(monitorIds: string[]) { + private async fetchMonitors(monitorIds: string[], allSpaces: Set) { const errors: MonitorHealthError[] = []; const foundMonitors: FoundMonitor[] = []; + // Use an internal saved-objects repository (un-scoped) so monitors created + // in any space — not just the request's space — can be resolved. + const internalSoRepository = this.server.coreStart.savedObjects.createInternalRepository(); + const namespaces = [...allSpaces]; + const settledResults = await Promise.allSettled( - monitorIds.map((id) => this.monitorConfigRepository.get(id)) + monitorIds.map((id) => + this.monitorConfigRepository.getAcrossSpaces(id, namespaces, internalSoRepository) + ) ); for (let i = 0; i < monitorIds.length; i++) { @@ -225,32 +240,48 @@ export class MonitorIntegrationHealthApi { return [...ids]; } - private async getExistingPackagePoliciesMap(expectedPackagePolicyIds: string[]) { + private async getExistingPackagePoliciesMap( + expectedPackagePolicyIds: string[], + allSpaces: Set + ) { if (expectedPackagePolicyIds.length === 0) { return new Map(); } - const internalSoClient = this.server.coreStart.savedObjects.createInternalRepository(); - const existingPackagePolicies = await this.server.fleet.packagePolicyService.getByIDs( - internalSoClient, - expectedPackagePolicyIds, - { ignoreMissing: true } - ); + // The Synthetics wrapper builds a namespace-scoped saved-objects client + // per space and de-duplicates the results, so package policies created + // for monitors in any space are visible regardless of the caller's space. + const packagePolicyService = new PackagePolicyService(this.server); + const additionalSpaceIds = [...allSpaces].filter((space) => space !== this.spaceId); + const existingPackagePolicies = await packagePolicyService.getByIds({ + spaceId: this.spaceId, + packagePolicyIds: expectedPackagePolicyIds, + additionalSpaceIds, + }); return new Map((existingPackagePolicies ?? []).map((policy) => [policy.id, policy])); } - private async getExistingAgentPoliciesMap(agentPolicyIds: string[]) { + private async getExistingAgentPoliciesMap(agentPolicyIds: string[], allSpaces: Set) { if (agentPolicyIds.length === 0) { return new Map(); } - const internalSoClient = this.server.coreStart.savedObjects.createInternalRepository(); - const existingAgentPolicies = await this.server.fleet.agentPolicyService.getByIds( - internalSoClient, - agentPolicyIds, - { ignoreMissing: true, withPackagePolicies: false } + // Agent policies can be scoped to a non-default space via space_ids. A plain + // createInternalRepository() defaults to the 'default' namespace and misses + // those policies. Query every relevant space with a namespace-scoped internal + // client (same pattern as PackagePolicyService.getByIds) — see issue #270477. + const spaces = new Set([this.spaceId, DEFAULT_SPACE_ID, ...allSpaces]); + const unsafeClient = this.server.coreStart.savedObjects.getUnsafeInternalClient(); + const results = await Promise.all( + [...spaces].map((space) => + this.server.fleet.agentPolicyService.getByIds( + unsafeClient.asScopedToNamespace(space), + agentPolicyIds, + { ignoreMissing: true, withPackagePolicies: false } + ) + ) ); - return new Map((existingAgentPolicies ?? []).map((policy) => [policy.id, policy])); + return new Map(results.flat().map((policy) => [policy.id, policy])); } private static buildLocationStatus( diff --git a/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/private_location/package_policy_service.ts b/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/private_location/package_policy_service.ts index 7b25b89cc2291..04230cc75504b 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/private_location/package_policy_service.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/private_location/package_policy_service.ts @@ -53,12 +53,24 @@ export class PackagePolicyService { ); } - async getByIds({ spaceId, packagePolicyIds }: { spaceId: string; packagePolicyIds: string[] }) { - // For legacy reasons, we need to get the package policies from both the space and the default space - const clients = - spaceId === DEFAULT_SPACE_ID - ? [this.getSpaceSoClient(DEFAULT_SPACE_ID)] - : [this.getSpaceSoClient(spaceId), this.getSpaceSoClient(DEFAULT_SPACE_ID)]; + async getByIds({ + spaceId, + packagePolicyIds, + additionalSpaceIds, + }: { + spaceId: string; + packagePolicyIds: string[]; + /** + * Extra spaces to look in alongside `spaceId` (and the default space). + * Use this when callers need a cross-space view — e.g. the monitor health + * API, which reports on monitors that may live in any space. + */ + additionalSpaceIds?: string[]; + }) { + // For legacy reasons, we always include the default space in addition to + // the request's space (older package policies were created there). + const spaces = new Set([spaceId, DEFAULT_SPACE_ID, ...(additionalSpaceIds ?? [])]); + const clients = [...spaces].map((space) => this.getSpaceSoClient(space)); const ids = await Promise.all( clients.map((soClient) => From 2eea0c2e4b64429a2e299468732f9a65ba636fcf Mon Sep 17 00:00:00 2001 From: Shahzad Date: Mon, 25 May 2026 13:14:55 +0200 Subject: [PATCH 2/2] fix CI: port getPrivateLocationsForNamespaces to 9.4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #270540's monitor_integration_health_api.{ts,test.ts} import getPrivateLocationsForNamespaces, which was added on main by PR #260642 but does not exist on 9.4 yet — causing 5 type errors in CI. Port the symbol verbatim from main: add the multi-namespace export and rewrite getPrivateLocations as a one-line wrapper. The public signature of getPrivateLocations is preserved, so all existing 9.4 callers are unaffected. Co-authored-by: Cursor --- .../synthetics_service/get_private_locations.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/get_private_locations.ts b/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/get_private_locations.ts index 51105038075d1..b700be040cbc4 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/get_private_locations.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/synthetics_service/get_private_locations.ts @@ -21,10 +21,17 @@ import type { export const getPrivateLocations = async ( client: SavedObjectsClientContract, spaceId?: string +): Promise => { + return getPrivateLocationsForNamespaces(client, spaceId ? [spaceId] : undefined); +}; + +export const getPrivateLocationsForNamespaces = async ( + client: SavedObjectsClientContract, + namespaces?: string[] ): Promise => { try { const [results, legacyLocations] = await Promise.all([ - getNewPrivateLocations(client, spaceId), + getNewPrivateLocations(client, namespaces), getLegacyPrivateLocations(client), ]); @@ -37,11 +44,14 @@ export const getPrivateLocations = async ( } }; -const getNewPrivateLocations = async (client: SavedObjectsClientContract, spaceId?: string) => { +const getNewPrivateLocations = async ( + client: SavedObjectsClientContract, + namespaces?: string[] +) => { const finder = client.createPointInTimeFinder({ type: privateLocationSavedObjectName, perPage: 1000, - ...(spaceId ? { namespaces: [spaceId] } : {}), + ...(namespaces && namespaces.length > 0 ? { namespaces } : {}), }); const results: Array<