mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Spaces - migrate default space and enter space view to KP (#66098)
This commit is contained in:
parent
531eb7cadc
commit
dc8dd19543
14 changed files with 601 additions and 70 deletions
|
@ -13,9 +13,6 @@ import { SpacesPluginSetup } from '../../../plugins/spaces/server';
|
|||
// @ts-ignore
|
||||
import { AuditLogger } from '../../server/lib/audit_logger';
|
||||
import { wrapError } from './server/lib/errors';
|
||||
// @ts-ignore
|
||||
import { watchStatusAndLicenseToInitialize } from '../../server/lib/watch_status_and_license_to_initialize';
|
||||
import { initEnterSpaceView } from './server/routes/views';
|
||||
|
||||
export interface LegacySpacesPlugin {
|
||||
getSpaceId: (request: Legacy.Request) => ReturnType<SpacesServiceSetup['getSpaceId']>;
|
||||
|
@ -50,7 +47,7 @@ export const spaces = (kibana: Record<string, any>) =>
|
|||
) {
|
||||
// NOTICE: use of `activeSpace` is deprecated and will not be made available in the New Platform.
|
||||
// Known usages:
|
||||
// - x-pack/legacy/plugins/infra/public/utils/use_kibana_space_id.ts
|
||||
// - x-pack/plugins/infra/public/utils/use_kibana_space_id.ts
|
||||
const spacesPlugin = server.newPlatform.setup.plugins.spaces as SpacesPluginSetup;
|
||||
if (!spacesPlugin) {
|
||||
throw new Error('New Platform XPack Spaces plugin is not available.');
|
||||
|
@ -82,7 +79,7 @@ export const spaces = (kibana: Record<string, any>) =>
|
|||
throw new Error('New Platform XPack Spaces plugin is not available.');
|
||||
}
|
||||
|
||||
const { registerLegacyAPI, createDefaultSpace } = spacesPlugin.__legacyCompat;
|
||||
const { registerLegacyAPI } = spacesPlugin.__legacyCompat;
|
||||
|
||||
registerLegacyAPI({
|
||||
auditLogger: {
|
||||
|
@ -91,12 +88,6 @@ export const spaces = (kibana: Record<string, any>) =>
|
|||
},
|
||||
});
|
||||
|
||||
initEnterSpaceView(server);
|
||||
|
||||
watchStatusAndLicenseToInitialize(server.plugins.xpack_main, this, async () => {
|
||||
await createDefaultSpace();
|
||||
});
|
||||
|
||||
server.expose('getSpaceId', (request: Legacy.Request) =>
|
||||
spacesPlugin.spacesService.getSpaceId(request)
|
||||
);
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Legacy } from 'kibana';
|
||||
import { ENTER_SPACE_PATH } from '../../../../../../plugins/spaces/common/constants';
|
||||
import { wrapError } from '../../lib/errors';
|
||||
|
||||
export function initEnterSpaceView(server: Legacy.Server) {
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: ENTER_SPACE_PATH,
|
||||
async handler(request, h) {
|
||||
try {
|
||||
const uiSettings = request.getUiSettingsService();
|
||||
const defaultRoute = await uiSettings.get<string>('defaultRoute');
|
||||
|
||||
const basePath = server.newPlatform.setup.core.http.basePath.get(request);
|
||||
const url = `${basePath}${defaultRoute}`;
|
||||
|
||||
return h.redirect(url);
|
||||
} catch (e) {
|
||||
server.log(['spaces', 'error'], `Error navigating to space: ${e}`);
|
||||
return wrapError(e);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
13
x-pack/plugins/spaces/common/licensing/index.mock.ts
Normal file
13
x-pack/plugins/spaces/common/licensing/index.mock.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { SpacesLicense } from '.';
|
||||
|
||||
export const licenseMock = {
|
||||
create: (): jest.Mocked<SpacesLicense> => ({
|
||||
isEnabled: jest.fn().mockReturnValue(true),
|
||||
}),
|
||||
};
|
7
x-pack/plugins/spaces/common/licensing/index.ts
Normal file
7
x-pack/plugins/spaces/common/licensing/index.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { SpacesLicenseService, SpacesLicense } from './license_service';
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { of } from 'rxjs';
|
||||
import { licensingMock } from '../../../licensing/public/mocks';
|
||||
import { SpacesLicenseService } from './license_service';
|
||||
import { LICENSE_TYPE, LicenseType } from '../../../licensing/common/types';
|
||||
|
||||
describe('license#isEnabled', function() {
|
||||
it('should indicate that Spaces is disabled when there is no license information', () => {
|
||||
const serviceSetup = new SpacesLicenseService().setup({
|
||||
license$: of(undefined as any),
|
||||
});
|
||||
expect(serviceSetup.license.isEnabled()).toEqual(false);
|
||||
});
|
||||
|
||||
it('should indicate that Spaces is disabled when xpack is unavailable', () => {
|
||||
const rawLicenseMock = licensingMock.createLicenseMock();
|
||||
rawLicenseMock.isAvailable = false;
|
||||
const serviceSetup = new SpacesLicenseService().setup({
|
||||
license$: of(rawLicenseMock),
|
||||
});
|
||||
expect(serviceSetup.license.isEnabled()).toEqual(false);
|
||||
});
|
||||
|
||||
for (const level in LICENSE_TYPE) {
|
||||
if (isNaN(level as any)) {
|
||||
it(`should indicate that Spaces is enabled with a ${level} license`, () => {
|
||||
const rawLicense = licensingMock.createLicense({
|
||||
license: {
|
||||
status: 'active',
|
||||
type: level as LicenseType,
|
||||
},
|
||||
});
|
||||
|
||||
const serviceSetup = new SpacesLicenseService().setup({
|
||||
license$: of(rawLicense),
|
||||
});
|
||||
expect(serviceSetup.license.isEnabled()).toEqual(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
50
x-pack/plugins/spaces/common/licensing/license_service.ts
Normal file
50
x-pack/plugins/spaces/common/licensing/license_service.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
import { ILicense } from '../../../licensing/common/types';
|
||||
|
||||
export interface SpacesLicense {
|
||||
isEnabled(): boolean;
|
||||
}
|
||||
|
||||
interface SetupDeps {
|
||||
license$: Observable<ILicense>;
|
||||
}
|
||||
|
||||
export class SpacesLicenseService {
|
||||
private licenseSubscription?: Subscription;
|
||||
|
||||
public setup({ license$ }: SetupDeps) {
|
||||
let rawLicense: Readonly<ILicense> | undefined;
|
||||
|
||||
this.licenseSubscription = license$.subscribe(nextRawLicense => {
|
||||
rawLicense = nextRawLicense;
|
||||
});
|
||||
|
||||
return {
|
||||
license: Object.freeze({
|
||||
isEnabled: () => this.isSpacesEnabledFromRawLicense(rawLicense),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
public stop() {
|
||||
if (this.licenseSubscription) {
|
||||
this.licenseSubscription.unsubscribe();
|
||||
this.licenseSubscription = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private isSpacesEnabledFromRawLicense(rawLicense: Readonly<ILicense> | undefined) {
|
||||
if (!rawLicense || !rawLicense.isAvailable) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const licenseCheck = rawLicense.check('spaces', 'basic');
|
||||
return licenseCheck.state !== 'unavailable' && licenseCheck.state !== 'invalid';
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import { createDefaultSpace } from './create_default_space';
|
||||
import { SavedObjectsErrorHelpers } from 'src/core/server';
|
||||
import { loggingServiceMock } from '../../../../../src/core/server/mocks';
|
||||
|
||||
interface MockServerSettings {
|
||||
defaultExists?: boolean;
|
||||
|
@ -47,14 +48,16 @@ const createMockDeps = (settings: MockServerSettings = {}) => {
|
|||
});
|
||||
|
||||
return {
|
||||
savedObjects: {
|
||||
createInternalRepository: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
get: mockGet,
|
||||
create: mockCreate,
|
||||
};
|
||||
getSavedObjects: () =>
|
||||
Promise.resolve({
|
||||
createInternalRepository: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
get: mockGet,
|
||||
create: mockCreate,
|
||||
};
|
||||
}),
|
||||
}),
|
||||
},
|
||||
logger: loggingServiceMock.createLogger(),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -65,7 +68,7 @@ test(`it creates the default space when one does not exist`, async () => {
|
|||
|
||||
await createDefaultSpace(deps);
|
||||
|
||||
const repository = deps.savedObjects.createInternalRepository();
|
||||
const repository = (await deps.getSavedObjects()).createInternalRepository();
|
||||
|
||||
expect(repository.get).toHaveBeenCalledTimes(1);
|
||||
expect(repository.create).toHaveBeenCalledTimes(1);
|
||||
|
@ -89,7 +92,7 @@ test(`it does not attempt to recreate the default space if it already exists`, a
|
|||
|
||||
await createDefaultSpace(deps);
|
||||
|
||||
const repository = deps.savedObjects.createInternalRepository();
|
||||
const repository = (await deps.getSavedObjects()).createInternalRepository();
|
||||
|
||||
expect(repository.get).toHaveBeenCalledTimes(1);
|
||||
expect(repository.create).toHaveBeenCalledTimes(0);
|
||||
|
@ -114,7 +117,7 @@ test(`it ignores conflict errors if the default space already exists`, async ()
|
|||
|
||||
await createDefaultSpace(deps);
|
||||
|
||||
const repository = deps.savedObjects.createInternalRepository();
|
||||
const repository = (await deps.getSavedObjects()).createInternalRepository();
|
||||
|
||||
expect(repository.get).toHaveBeenCalledTimes(1);
|
||||
expect(repository.create).toHaveBeenCalledTimes(1);
|
|
@ -5,22 +5,26 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { SavedObjectsServiceStart, SavedObjectsRepository } from 'src/core/server';
|
||||
import { SavedObjectsServiceStart, SavedObjectsRepository, Logger } from 'src/core/server';
|
||||
import { SavedObjectsErrorHelpers } from '../../../../../src/core/server';
|
||||
import { DEFAULT_SPACE_ID } from '../../common/constants';
|
||||
|
||||
interface Deps {
|
||||
savedObjects: Pick<SavedObjectsServiceStart, 'createInternalRepository'>;
|
||||
getSavedObjects: () => Promise<Pick<SavedObjectsServiceStart, 'createInternalRepository'>>;
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
export async function createDefaultSpace({ savedObjects }: Deps) {
|
||||
const { createInternalRepository } = savedObjects;
|
||||
export async function createDefaultSpace({ getSavedObjects, logger }: Deps) {
|
||||
const { createInternalRepository } = await getSavedObjects();
|
||||
|
||||
const savedObjectsRepository = createInternalRepository(['space']);
|
||||
|
||||
logger.debug('Checking for existing default space');
|
||||
|
||||
const defaultSpaceExists = await doesDefaultSpaceExist(savedObjectsRepository);
|
||||
|
||||
if (defaultSpaceExists) {
|
||||
logger.debug('Default space already exists');
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -28,6 +32,7 @@ export async function createDefaultSpace({ savedObjects }: Deps) {
|
|||
id: DEFAULT_SPACE_ID,
|
||||
};
|
||||
|
||||
logger.debug('Creating the default space');
|
||||
try {
|
||||
await savedObjectsRepository.create(
|
||||
'space',
|
||||
|
@ -53,6 +58,8 @@ export async function createDefaultSpace({ savedObjects }: Deps) {
|
|||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
logger.debug('Default space created');
|
||||
}
|
||||
|
||||
async function doesDefaultSpaceExist(savedObjectsRepository: Pick<SavedObjectsRepository, 'get'>) {
|
|
@ -0,0 +1,280 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import * as Rx from 'rxjs';
|
||||
import {
|
||||
DefaultSpaceService,
|
||||
RETRY_SCALE_DURATION,
|
||||
RETRY_DURATION_MAX,
|
||||
} from './default_space_service';
|
||||
import {
|
||||
ServiceStatusLevels,
|
||||
ServiceStatusLevel,
|
||||
CoreStatus,
|
||||
SavedObjectsRepository,
|
||||
SavedObjectsErrorHelpers,
|
||||
} from '../../../../../src/core/server';
|
||||
import { coreMock, loggingServiceMock } from 'src/core/server/mocks';
|
||||
import { licensingMock } from '../../../licensing/server/mocks';
|
||||
import { SpacesLicenseService } from '../../common/licensing';
|
||||
import { ILicense } from '../../../licensing/server';
|
||||
import { nextTick } from 'test_utils/enzyme_helpers';
|
||||
import { first } from 'rxjs/operators';
|
||||
|
||||
const advanceRetry = async (initializeCount: number) => {
|
||||
await Promise.resolve();
|
||||
let duration = initializeCount * RETRY_SCALE_DURATION;
|
||||
if (duration > RETRY_DURATION_MAX) {
|
||||
duration = RETRY_DURATION_MAX;
|
||||
}
|
||||
jest.advanceTimersByTime(duration);
|
||||
};
|
||||
|
||||
interface SetupOpts {
|
||||
elasticsearchStatus: ServiceStatusLevel;
|
||||
savedObjectsStatus: ServiceStatusLevel;
|
||||
license: ILicense;
|
||||
}
|
||||
const setup = ({ elasticsearchStatus, savedObjectsStatus, license }: SetupOpts) => {
|
||||
const core = coreMock.createSetup();
|
||||
const { status } = core;
|
||||
status.core$ = (new Rx.BehaviorSubject({
|
||||
elasticsearch: {
|
||||
level: elasticsearchStatus,
|
||||
summary: '',
|
||||
},
|
||||
savedObjects: {
|
||||
level: savedObjectsStatus,
|
||||
summary: '',
|
||||
},
|
||||
}) as unknown) as Rx.Observable<CoreStatus>;
|
||||
|
||||
const { savedObjects } = coreMock.createStart();
|
||||
const repository = savedObjects.createInternalRepository() as jest.Mocked<SavedObjectsRepository>;
|
||||
// simulate space not found
|
||||
repository.get.mockRejectedValue(SavedObjectsErrorHelpers.createGenericNotFoundError());
|
||||
repository.create.mockReturnValue(Promise.resolve({} as any));
|
||||
|
||||
const license$ = new Rx.BehaviorSubject(license);
|
||||
|
||||
const logger = loggingServiceMock.createLogger();
|
||||
|
||||
const { license: spacesLicense } = new SpacesLicenseService().setup({ license$ });
|
||||
|
||||
const defaultSpaceService = new DefaultSpaceService();
|
||||
const { serviceStatus$ } = defaultSpaceService.setup({
|
||||
coreStatus: status,
|
||||
getSavedObjects: () => Promise.resolve(savedObjects),
|
||||
license$,
|
||||
logger,
|
||||
spacesLicense,
|
||||
});
|
||||
|
||||
return {
|
||||
coreStatus: (status as unknown) as { core$: Rx.BehaviorSubject<CoreStatus> },
|
||||
serviceStatus$,
|
||||
logger,
|
||||
license$,
|
||||
savedObjects,
|
||||
repository,
|
||||
};
|
||||
};
|
||||
|
||||
test(`does not initialize if elasticsearch is unavailable`, async () => {
|
||||
const { repository, serviceStatus$ } = setup({
|
||||
elasticsearchStatus: ServiceStatusLevels.unavailable,
|
||||
savedObjectsStatus: ServiceStatusLevels.available,
|
||||
license: licensingMock.createLicense({
|
||||
license: {
|
||||
status: 'active',
|
||||
type: 'gold',
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(repository.get).not.toHaveBeenCalled();
|
||||
expect(repository.create).not.toHaveBeenCalled();
|
||||
|
||||
const status = await serviceStatus$.pipe(first()).toPromise();
|
||||
expect(status.level).toEqual(ServiceStatusLevels.unavailable);
|
||||
expect(status.summary).toMatchInlineSnapshot(`"required core services are not ready"`);
|
||||
});
|
||||
|
||||
test(`does not initialize if savedObjects is unavailable`, async () => {
|
||||
const { serviceStatus$, repository } = setup({
|
||||
elasticsearchStatus: ServiceStatusLevels.available,
|
||||
savedObjectsStatus: ServiceStatusLevels.unavailable,
|
||||
license: licensingMock.createLicense({
|
||||
license: {
|
||||
status: 'active',
|
||||
type: 'gold',
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(repository.get).not.toHaveBeenCalled();
|
||||
expect(repository.create).not.toHaveBeenCalled();
|
||||
const status = await serviceStatus$.pipe(first()).toPromise();
|
||||
expect(status.level).toEqual(ServiceStatusLevels.unavailable);
|
||||
expect(status.summary).toMatchInlineSnapshot(`"required core services are not ready"`);
|
||||
});
|
||||
|
||||
test(`does not initialize if the license is unavailable`, async () => {
|
||||
const license = licensingMock.createLicense({
|
||||
license: ({ type: ' ', status: ' ' } as unknown) as ILicense,
|
||||
}) as Writable<ILicense>;
|
||||
license.isAvailable = false;
|
||||
|
||||
const { serviceStatus$, repository } = setup({
|
||||
elasticsearchStatus: ServiceStatusLevels.available,
|
||||
savedObjectsStatus: ServiceStatusLevels.available,
|
||||
license,
|
||||
});
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(repository.get).not.toHaveBeenCalled();
|
||||
expect(repository.create).not.toHaveBeenCalled();
|
||||
const status = await serviceStatus$.pipe(first()).toPromise();
|
||||
expect(status.level).toEqual(ServiceStatusLevels.unavailable);
|
||||
expect(status.summary).toMatchInlineSnapshot(`"missing or invalid license"`);
|
||||
});
|
||||
|
||||
test(`initializes once all dependencies are met`, async () => {
|
||||
const { repository, coreStatus, serviceStatus$ } = setup({
|
||||
elasticsearchStatus: ServiceStatusLevels.available,
|
||||
savedObjectsStatus: ServiceStatusLevels.unavailable,
|
||||
license: licensingMock.createLicense({
|
||||
license: {
|
||||
type: 'gold',
|
||||
status: 'active',
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(repository.get).not.toHaveBeenCalled();
|
||||
expect(repository.create).not.toHaveBeenCalled();
|
||||
|
||||
const status = await serviceStatus$.pipe(first()).toPromise();
|
||||
expect(status.level).toEqual(ServiceStatusLevels.unavailable);
|
||||
expect(status.summary).toMatchInlineSnapshot(`"required core services are not ready"`);
|
||||
|
||||
coreStatus.core$.next({
|
||||
elasticsearch: {
|
||||
level: ServiceStatusLevels.available,
|
||||
summary: '',
|
||||
},
|
||||
savedObjects: {
|
||||
level: ServiceStatusLevels.available,
|
||||
summary: '',
|
||||
},
|
||||
});
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(repository.get).toHaveBeenCalled();
|
||||
expect(repository.create).toHaveBeenCalled();
|
||||
|
||||
const nextStatus = await serviceStatus$.pipe(first()).toPromise();
|
||||
expect(nextStatus.level).toEqual(ServiceStatusLevels.available);
|
||||
expect(nextStatus.summary).toMatchInlineSnapshot(`"ready"`);
|
||||
});
|
||||
|
||||
test('maintains unavailable status if default space cannot be created', async () => {
|
||||
const { repository, serviceStatus$ } = setup({
|
||||
elasticsearchStatus: ServiceStatusLevels.available,
|
||||
savedObjectsStatus: ServiceStatusLevels.available,
|
||||
license: licensingMock.createLicense({
|
||||
license: {
|
||||
type: 'gold',
|
||||
status: 'active',
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
repository.create.mockRejectedValue(new Error('something bad happened'));
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(repository.get).toHaveBeenCalled();
|
||||
expect(repository.create).toHaveBeenCalled();
|
||||
|
||||
const status = await serviceStatus$.pipe(first()).toPromise();
|
||||
expect(status.level).toEqual(ServiceStatusLevels.unavailable);
|
||||
expect(status.summary).toMatchInlineSnapshot(
|
||||
`"Error creating default space: something bad happened"`
|
||||
);
|
||||
});
|
||||
|
||||
test('retries operation', async () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
const { repository, serviceStatus$ } = setup({
|
||||
elasticsearchStatus: ServiceStatusLevels.available,
|
||||
savedObjectsStatus: ServiceStatusLevels.available,
|
||||
license: licensingMock.createLicense({
|
||||
license: {
|
||||
type: 'gold',
|
||||
status: 'active',
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
repository.create.mockRejectedValue(new Error('something bad happened'));
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(repository.get).toHaveBeenCalledTimes(1);
|
||||
expect(repository.create).toHaveBeenCalledTimes(1);
|
||||
|
||||
let status = await serviceStatus$.pipe(first()).toPromise();
|
||||
expect(status.level).toEqual(ServiceStatusLevels.unavailable);
|
||||
expect(status.summary).toMatchInlineSnapshot(
|
||||
`"Error creating default space: something bad happened"`
|
||||
);
|
||||
|
||||
await advanceRetry(1);
|
||||
await nextTick();
|
||||
|
||||
expect(repository.get).toHaveBeenCalledTimes(2);
|
||||
expect(repository.create).toHaveBeenCalledTimes(2);
|
||||
|
||||
status = await serviceStatus$.pipe(first()).toPromise();
|
||||
expect(status.level).toEqual(ServiceStatusLevels.unavailable);
|
||||
expect(status.summary).toMatchInlineSnapshot(
|
||||
`"Error creating default space: something bad happened"`
|
||||
);
|
||||
|
||||
repository.create.mockResolvedValue({} as any);
|
||||
|
||||
// retries are scaled back, so this should not cause the repository to be invoked
|
||||
await advanceRetry(1);
|
||||
await nextTick();
|
||||
|
||||
expect(repository.get).toHaveBeenCalledTimes(2);
|
||||
expect(repository.create).toHaveBeenCalledTimes(2);
|
||||
|
||||
status = await serviceStatus$.pipe(first()).toPromise();
|
||||
expect(status.level).toEqual(ServiceStatusLevels.unavailable);
|
||||
expect(status.summary).toMatchInlineSnapshot(
|
||||
`"Error creating default space: something bad happened"`
|
||||
);
|
||||
|
||||
await advanceRetry(1);
|
||||
await nextTick();
|
||||
|
||||
expect(repository.get).toHaveBeenCalledTimes(3);
|
||||
expect(repository.create).toHaveBeenCalledTimes(3);
|
||||
|
||||
status = await serviceStatus$.pipe(first()).toPromise();
|
||||
expect(status.level).toEqual(ServiceStatusLevels.available);
|
||||
expect(status.summary).toMatchInlineSnapshot(`"ready"`);
|
||||
});
|
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { CoreSetup, SavedObjectsServiceStart, Logger, ServiceStatus } from 'src/core/server';
|
||||
import {
|
||||
concat,
|
||||
of,
|
||||
timer,
|
||||
Observable,
|
||||
ObservableInput,
|
||||
combineLatest,
|
||||
defer,
|
||||
Subscription,
|
||||
BehaviorSubject,
|
||||
} from 'rxjs';
|
||||
import { mergeMap, switchMap, catchError, tap } from 'rxjs/operators';
|
||||
import { ServiceStatusLevels } from '../../../../../src/core/server';
|
||||
import { ILicense } from '../../../licensing/server';
|
||||
import { SpacesLicense } from '../../common/licensing';
|
||||
import { createDefaultSpace } from './create_default_space';
|
||||
|
||||
interface Deps {
|
||||
coreStatus: CoreSetup['status'];
|
||||
getSavedObjects: () => Promise<Pick<SavedObjectsServiceStart, 'createInternalRepository'>>;
|
||||
license$: Observable<ILicense>;
|
||||
spacesLicense: SpacesLicense;
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
export const RETRY_SCALE_DURATION = 100;
|
||||
export const RETRY_DURATION_MAX = 10000;
|
||||
|
||||
const calculateDuration = (i: number) => {
|
||||
const duration = i * RETRY_SCALE_DURATION;
|
||||
if (duration > RETRY_DURATION_MAX) {
|
||||
return RETRY_DURATION_MAX;
|
||||
}
|
||||
|
||||
return duration;
|
||||
};
|
||||
|
||||
// we can't use a retryWhen here, because we want to propagate the unavailable status and then retry
|
||||
const propagateUnavailableStatusAndScaleRetry = () => {
|
||||
let i = 0;
|
||||
return (err: Error, caught: ObservableInput<any>) =>
|
||||
concat(
|
||||
of({
|
||||
level: ServiceStatusLevels.unavailable,
|
||||
summary: `Error creating default space: ${err.message}`,
|
||||
}),
|
||||
timer(calculateDuration(++i)).pipe(mergeMap(() => caught))
|
||||
);
|
||||
};
|
||||
|
||||
export class DefaultSpaceService {
|
||||
private initializeSubscription?: Subscription;
|
||||
|
||||
private serviceStatus$?: BehaviorSubject<ServiceStatus>;
|
||||
|
||||
public setup({ coreStatus, getSavedObjects, license$, spacesLicense, logger }: Deps) {
|
||||
const statusLogger = logger.get('status');
|
||||
|
||||
this.serviceStatus$ = new BehaviorSubject({
|
||||
level: ServiceStatusLevels.unavailable,
|
||||
summary: 'not initialized',
|
||||
} as ServiceStatus);
|
||||
|
||||
this.initializeSubscription = combineLatest([coreStatus.core$, license$])
|
||||
.pipe(
|
||||
switchMap(([status]) => {
|
||||
const isElasticsearchReady = status.elasticsearch.level === ServiceStatusLevels.available;
|
||||
const isSavedObjectsReady = status.savedObjects.level === ServiceStatusLevels.available;
|
||||
|
||||
if (!isElasticsearchReady || !isSavedObjectsReady) {
|
||||
return of({
|
||||
level: ServiceStatusLevels.unavailable,
|
||||
summary: 'required core services are not ready',
|
||||
} as ServiceStatus);
|
||||
}
|
||||
|
||||
if (!spacesLicense.isEnabled()) {
|
||||
return of({
|
||||
level: ServiceStatusLevels.unavailable,
|
||||
summary: 'missing or invalid license',
|
||||
} as ServiceStatus);
|
||||
}
|
||||
|
||||
return defer(() =>
|
||||
createDefaultSpace({
|
||||
getSavedObjects,
|
||||
logger,
|
||||
}).then(() => {
|
||||
return {
|
||||
level: ServiceStatusLevels.available,
|
||||
summary: 'ready',
|
||||
};
|
||||
})
|
||||
).pipe(catchError(propagateUnavailableStatusAndScaleRetry()));
|
||||
}),
|
||||
tap<ServiceStatus>(spacesStatus => {
|
||||
// This is temporary for debugging/visibility until we get a proper status service from core.
|
||||
// See issue #41983 for details.
|
||||
statusLogger.debug(`${spacesStatus.level.toString()}: ${spacesStatus.summary}`);
|
||||
this.serviceStatus$!.next(spacesStatus);
|
||||
})
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
return {
|
||||
serviceStatus$: this.serviceStatus$!.asObservable(),
|
||||
};
|
||||
}
|
||||
|
||||
public stop() {
|
||||
if (this.initializeSubscription) {
|
||||
this.initializeSubscription.unsubscribe();
|
||||
}
|
||||
this.initializeSubscription = undefined;
|
||||
|
||||
if (this.serviceStatus$) {
|
||||
this.serviceStatus$.complete();
|
||||
this.serviceStatus$ = undefined;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,4 +4,4 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { initEnterSpaceView } from './enter_space';
|
||||
export { DefaultSpaceService } from './default_space_service';
|
|
@ -24,7 +24,6 @@ describe('Spaces Plugin', () => {
|
|||
expect(spacesSetup).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"__legacyCompat": Object {
|
||||
"createDefaultSpace": [Function],
|
||||
"registerLegacyAPI": [Function],
|
||||
},
|
||||
"spacesService": Object {
|
||||
|
|
|
@ -14,7 +14,6 @@ import {
|
|||
} from '../../features/server';
|
||||
import { SecurityPluginSetup } from '../../security/server';
|
||||
import { LicensingPluginSetup } from '../../licensing/server';
|
||||
import { createDefaultSpace } from './lib/create_default_space';
|
||||
// @ts-ignore
|
||||
import { AuditLogger } from '../../../../server/lib/audit_logger';
|
||||
import { SpacesAuditLogger } from './lib/audit_logger';
|
||||
|
@ -29,6 +28,8 @@ import { initInternalSpacesApi } from './routes/api/internal';
|
|||
import { initSpacesViewsRoutes } from './routes/views';
|
||||
import { setupCapabilities } from './capabilities';
|
||||
import { SpacesSavedObjectsService } from './saved_objects';
|
||||
import { DefaultSpaceService } from './default_space';
|
||||
import { SpacesLicenseService } from '../common/licensing';
|
||||
|
||||
/**
|
||||
* Describes a set of APIs that is available in the legacy platform only and required by this plugin
|
||||
|
@ -56,10 +57,6 @@ export interface SpacesPluginSetup {
|
|||
spacesService: SpacesServiceSetup;
|
||||
__legacyCompat: {
|
||||
registerLegacyAPI: (legacyAPI: LegacyAPI) => void;
|
||||
// TODO: We currently need the legacy plugin to inform this plugin when it is safe to create the default space.
|
||||
// The NP does not have the equivilent ES connection/health/comapt checks that the legacy world does.
|
||||
// See: https://github.com/elastic/kibana/issues/43456
|
||||
createDefaultSpace: () => Promise<void>;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -72,6 +69,10 @@ export class Plugin {
|
|||
|
||||
private readonly log: Logger;
|
||||
|
||||
private readonly spacesLicenseService = new SpacesLicenseService();
|
||||
|
||||
private defaultSpaceService?: DefaultSpaceService;
|
||||
|
||||
private legacyAPI?: LegacyAPI;
|
||||
private readonly getLegacyAPI = () => {
|
||||
if (!this.legacyAPI) {
|
||||
|
@ -115,8 +116,21 @@ export class Plugin {
|
|||
const savedObjectsService = new SpacesSavedObjectsService();
|
||||
savedObjectsService.setup({ core, spacesService });
|
||||
|
||||
const { license } = this.spacesLicenseService.setup({ license$: plugins.licensing.license$ });
|
||||
|
||||
this.defaultSpaceService = new DefaultSpaceService();
|
||||
this.defaultSpaceService.setup({
|
||||
coreStatus: core.status,
|
||||
getSavedObjects: async () => (await core.getStartServices())[0].savedObjects,
|
||||
license$: plugins.licensing.license$,
|
||||
spacesLicense: license,
|
||||
logger: this.log,
|
||||
});
|
||||
|
||||
initSpacesViewsRoutes({
|
||||
httpResources: core.http.resources,
|
||||
basePath: core.http.basePath,
|
||||
logger: this.log,
|
||||
});
|
||||
|
||||
const externalRouter = core.http.createRouter();
|
||||
|
@ -167,15 +181,13 @@ export class Plugin {
|
|||
registerLegacyAPI: (legacyAPI: LegacyAPI) => {
|
||||
this.legacyAPI = legacyAPI;
|
||||
},
|
||||
createDefaultSpace: async () => {
|
||||
const [coreStart] = await core.getStartServices();
|
||||
return await createDefaultSpace({
|
||||
savedObjects: coreStart.savedObjects,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public stop() {}
|
||||
public stop() {
|
||||
if (this.defaultSpaceService) {
|
||||
this.defaultSpaceService.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,10 +4,14 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { HttpResources } from 'src/core/server';
|
||||
import { HttpResources, IBasePath, Logger } from 'src/core/server';
|
||||
import { ENTER_SPACE_PATH } from '../../../common';
|
||||
import { wrapError } from '../../lib/errors';
|
||||
|
||||
export interface ViewRouteDeps {
|
||||
httpResources: HttpResources;
|
||||
basePath: IBasePath;
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
export function initSpacesViewsRoutes(deps: ViewRouteDeps) {
|
||||
|
@ -15,4 +19,25 @@ export function initSpacesViewsRoutes(deps: ViewRouteDeps) {
|
|||
{ path: '/spaces/space_selector', validate: false },
|
||||
(context, request, response) => response.renderCoreApp()
|
||||
);
|
||||
|
||||
deps.httpResources.register(
|
||||
{ path: ENTER_SPACE_PATH, validate: false },
|
||||
async (context, request, response) => {
|
||||
try {
|
||||
const defaultRoute = await context.core.uiSettings.client.get<string>('defaultRoute');
|
||||
|
||||
const basePath = deps.basePath.get(request);
|
||||
const url = `${basePath}${defaultRoute}`;
|
||||
|
||||
return response.redirected({
|
||||
headers: {
|
||||
location: url,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
deps.logger.error(`Error navigating to space: ${e}`);
|
||||
return response.customError(wrapError(e));
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue