diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index a48ff12e859a..165a67aa1f83 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -276,6 +276,9 @@ export class LegacyService implements CoreService { registerType: setupDeps.core.savedObjects.registerType, getImportExportObjectLimit: setupDeps.core.savedObjects.getImportExportObjectLimit, setRepositoryFactoryProvider: setupDeps.core.savedObjects.setRepositoryFactoryProvider, + setStatus: () => { + throw new Error(`core.savedObjects.setStatus is unsupported in legacy`); + }, }, status: { isStatusPageAnonymous: setupDeps.core.status.isStatusPageAnonymous, diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index 7782fd93041e..ab028e169a71 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -205,6 +205,7 @@ export function createPluginSetupContext( registerType: deps.savedObjects.registerType, getImportExportObjectLimit: deps.savedObjects.getImportExportObjectLimit, setRepositoryFactoryProvider: deps.savedObjects.setRepositoryFactoryProvider, + setStatus: deps.savedObjects.setStatus, }, status: { core$: deps.status.core$, diff --git a/src/core/server/saved_objects/saved_objects_service.mock.ts b/src/core/server/saved_objects/saved_objects_service.mock.ts index 74168c436c3d..257b5048fc4a 100644 --- a/src/core/server/saved_objects/saved_objects_service.mock.ts +++ b/src/core/server/saved_objects/saved_objects_service.mock.ts @@ -80,6 +80,7 @@ const createSetupContractMock = () => { registerType: jest.fn(), getImportExportObjectLimit: jest.fn(), setRepositoryFactoryProvider: jest.fn(), + setStatus: jest.fn(), }; setupContract.getImportExportObjectLimit.mockReturnValue(100); diff --git a/src/core/server/saved_objects/saved_objects_service.test.ts b/src/core/server/saved_objects/saved_objects_service.test.ts index 98d1da393319..75b0d756f0cf 100644 --- a/src/core/server/saved_objects/saved_objects_service.test.ts +++ b/src/core/server/saved_objects/saved_objects_service.test.ts @@ -34,7 +34,8 @@ import { clientProviderInstanceMock, typeRegistryInstanceMock, } from './saved_objects_service.test.mocks'; -import { BehaviorSubject } from 'rxjs'; +import { BehaviorSubject, of } from 'rxjs'; +import { first } from 'rxjs/operators'; import { ByteSizeValue } from '@osd/config-schema'; import { errors as opensearchErrors } from '@opensearch-project/opensearch'; @@ -50,6 +51,7 @@ import { SavedObjectsClientFactoryProvider } from './service/lib'; import { NodesVersionCompatibility } from '../opensearch/version_check/ensure_opensearch_version'; import { SavedObjectsRepository } from './service/lib/repository'; import { SavedObjectRepositoryFactoryProvider } from './service/lib/scoped_client_provider'; +import { ServiceStatusLevels } from '../status'; jest.mock('./service/lib/repository'); @@ -191,6 +193,31 @@ describe('SavedObjectsService', () => { ); }); }); + + describe('#setStatus', () => { + it('throws error if custom status is already set', async () => { + const coreContext = createCoreContext(); + const soService = new SavedObjectsService(coreContext); + const setup = await soService.setup(createSetupDeps()); + + const customStatus1$ = of({ + level: ServiceStatusLevels.available, + summary: 'Saved Object Service is using external storage and it is up', + }); + const customStatus2$ = of({ + level: ServiceStatusLevels.unavailable, + summary: 'Saved Object Service is not connected to external storage and it is down', + }); + + setup.setStatus(customStatus1$); + + expect(() => { + setup.setStatus(customStatus2$); + }).toThrowErrorMatchingInlineSnapshot( + `"custom saved object service status is already set, and can only be set once"` + ); + }); + }); }); describe('#start()', () => { @@ -312,6 +339,15 @@ describe('SavedObjectsService', () => { }).toThrowErrorMatchingInlineSnapshot( '"cannot call `setRepositoryFactoryProvider` after service startup."' ); + + const customStatus$ = of({ + level: ServiceStatusLevels.available, + summary: 'Saved Object Service is using external storage and it is up', + }); + + expect(() => { + setup.setStatus(customStatus$); + }).toThrowErrorMatchingInlineSnapshot('"cannot call `setStatus` after service startup."'); }); describe('#getTypeRegistry', () => { @@ -430,5 +466,39 @@ describe('SavedObjectsService', () => { expect(SavedObjectsRepository.createRepository as jest.Mocked).toHaveBeenCalled(); }); }); + + describe('#savedObjectServiceStatus', () => { + it('Saved objects service status should be custom when set using setStatus', async () => { + const coreContext = createCoreContext({}); + const soService = new SavedObjectsService(coreContext); + const coreSetup = createSetupDeps(); + const setup = await soService.setup(coreSetup); + + const customStatus$ = of({ + level: ServiceStatusLevels.available, + summary: 'Saved Object Service is using external storage and it is up', + }); + setup.setStatus(customStatus$); + const coreStart = createStartDeps(); + await soService.start(coreStart); + expect(await setup.status$.pipe(first()).toPromise()).toMatchObject({ + level: ServiceStatusLevels.available, + summary: 'Saved Object Service is using external storage and it is up', + }); + }); + + it('Saved objects service should be default when custom status is not set', async () => { + const coreContext = createCoreContext({}); + const soService = new SavedObjectsService(coreContext); + const coreSetup = createSetupDeps(); + const setup = await soService.setup(coreSetup); + const coreStart = createStartDeps(); + await soService.start(coreStart); + expect(await setup.status$.pipe(first()).toPromise()).toMatchObject({ + level: ServiceStatusLevels.available, + summary: 'SavedObjects service has completed migrations and is available', + }); + }); + }); }); }); diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts index b6fc21617bcc..43296f340d85 100644 --- a/src/core/server/saved_objects/saved_objects_service.ts +++ b/src/core/server/saved_objects/saved_objects_service.ts @@ -28,8 +28,10 @@ * under the License. */ -import { Subject, Observable } from 'rxjs'; -import { first, filter, take, switchMap } from 'rxjs/operators'; +import { Subject, Observable, BehaviorSubject } from 'rxjs'; +import { first, filter, take, switchMap, map, distinctUntilChanged } from 'rxjs/operators'; +import { isDeepStrictEqual } from 'util'; + import { CoreService } from '../../types'; import { SavedObjectsClient, @@ -62,7 +64,7 @@ import { Logger } from '../logging'; import { SavedObjectTypeRegistry, ISavedObjectTypeRegistry } from './saved_objects_type_registry'; import { SavedObjectsSerializer } from './serialization'; import { registerRoutes } from './routes'; -import { ServiceStatus } from '../status'; +import { ServiceStatus, ServiceStatusLevels } from '../status'; import { calculateStatus$ } from './status'; import { createMigrationOpenSearchClient } from './migrations/core/'; /** @@ -175,6 +177,12 @@ export interface SavedObjectsServiceSetup { setRepositoryFactoryProvider: ( respositoryFactoryProvider: SavedObjectRepositoryFactoryProvider ) => void; + + /** + * Allows a plugin to specify a custom status dependent on its own criteria. + * Completely overrides the default status. + */ + setStatus(status$: Observable>): void; } /** @@ -301,6 +309,11 @@ export class SavedObjectsService private started = false; private respositoryFactoryProvider?: SavedObjectRepositoryFactoryProvider; + private savedObjectServiceCustomStatus$?: Observable>; + private savedObjectServiceStatus$ = new BehaviorSubject>({ + level: ServiceStatusLevels.unavailable, + summary: `waiting`, + }); constructor(private readonly coreContext: CoreContext) { this.logger = coreContext.logger.get('savedobjects-service'); @@ -329,10 +342,7 @@ export class SavedObjectsService }); return { - status$: calculateStatus$( - this.migrator$.pipe(switchMap((migrator) => migrator.getStatus$())), - setupDeps.opensearch.status$ - ), + status$: this.savedObjectServiceStatus$.asObservable(), setClientFactoryProvider: (provider) => { if (this.started) { throw new Error('cannot call `setClientFactoryProvider` after service startup.'); @@ -368,6 +378,17 @@ export class SavedObjectsService } this.respositoryFactoryProvider = repositoryProvider; }, + setStatus: (status$) => { + if (this.started) { + throw new Error('cannot call `setStatus` after service startup.'); + } + if (this.savedObjectServiceCustomStatus$) { + throw new Error( + 'custom saved object service status is already set, and can only be set once' + ); + } + this.savedObjectServiceCustomStatus$ = status$; + }, }; } @@ -381,6 +402,29 @@ export class SavedObjectsService this.logger.debug('Starting SavedObjects service'); + if (this.savedObjectServiceCustomStatus$) { + this.savedObjectServiceCustomStatus$ + .pipe( + map((savedObjectServiceCustomStatus) => { + return savedObjectServiceCustomStatus; + }), + distinctUntilChanged>(isDeepStrictEqual) + ) + .subscribe((value) => this.savedObjectServiceStatus$.next(value)); + } else { + calculateStatus$( + this.migrator$.pipe(switchMap((migrator) => migrator.getStatus$())), + this.setupDeps.opensearch.status$ + ) + .pipe( + map((defaultstatus) => { + return defaultstatus; + }), + distinctUntilChanged>(isDeepStrictEqual) + ) + .subscribe((value) => this.savedObjectServiceStatus$.next(value)); + } + const opensearchDashboardsConfig = await this.coreContext.configService .atPath('opensearchDashboards') .pipe(first()) @@ -492,7 +536,9 @@ export class SavedObjectsService }; } - public async stop() {} + public async stop() { + this.savedObjectServiceStatus$.unsubscribe(); + } private createMigrator( opensearchDashboardsConfig: OpenSearchDashboardsConfigType,