Spaces - NP updates for usage collection and capabilities (#57693)

* remove kibanaIndex from LegacyAPI

* moving capabilities, adding tests

* moving usage collection

* cleanup

* don't toggle capabilities on unauthenticated routes

* reintroduce exception handling

* pipe dat config

* start addressing PR feedback

* fix CoreSetup's generic type

* fix usage collector tests

* PR review updates

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Larry Gregory 2020-02-28 09:06:48 -05:00 committed by GitHub
parent 3f7abe3c55
commit 91330d2493
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 409 additions and 119 deletions

View file

@ -43,6 +43,7 @@ interface RequestFixtureOptions<P = any, Q = any, B = any> {
method?: RouteMethod;
socket?: Socket;
routeTags?: string[];
routeAuthRequired?: false;
validation?: {
params?: RouteValidationSpec<P>;
query?: RouteValidationSpec<Q>;
@ -59,6 +60,7 @@ function createKibanaRequestMock<P = any, Q = any, B = any>({
method = 'get',
socket = new Socket(),
routeTags,
routeAuthRequired,
validation = {},
}: RequestFixtureOptions<P, Q, B> = {}) {
const queryString = stringify(query, { sort: false });
@ -77,7 +79,9 @@ function createKibanaRequestMock<P = any, Q = any, B = any>({
query: queryString,
search: queryString ? `?${queryString}` : queryString,
},
route: { settings: { tags: routeTags } },
route: {
settings: { tags: routeTags, auth: routeAuthRequired },
},
raw: {
req: { socket },
},

View file

@ -0,0 +1,38 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { loggingServiceMock } from '../../../core/server/mocks';
import { UsageCollectionSetup } from './plugin';
import { CollectorSet } from './collector';
const createSetupContract = () => {
return {
...new CollectorSet({
logger: loggingServiceMock.createLogger(),
maximumWaitTimeForAllCollectorsInS: 1,
}),
registerLegacySavedObjects: jest.fn() as jest.Mocked<
UsageCollectionSetup['registerLegacySavedObjects']
>,
} as UsageCollectionSetup;
};
export const usageCollectionPluginMock = {
createSetupContract,
};

View file

@ -34,19 +34,6 @@ export const spaces = (kibana: Record<string, any>) =>
publicDir: resolve(__dirname, 'public'),
require: ['kibana', 'elasticsearch', 'xpack_main'],
uiCapabilities() {
return {
spaces: {
manage: true,
},
management: {
kibana: {
spaces: true,
},
},
};
},
uiExports: {
styleSheetPaths: resolve(__dirname, 'public/index.scss'),
managementSections: [],
@ -110,14 +97,9 @@ export const spaces = (kibana: Record<string, any>) =>
throw new Error('New Platform XPack Spaces plugin is not available.');
}
const config = server.config();
const { registerLegacyAPI, createDefaultSpace } = spacesPlugin.__legacyCompat;
registerLegacyAPI({
legacyConfig: {
kibanaIndex: config.get('kibana.index'),
},
savedObjects: server.savedObjects,
auditLogger: {
create: (pluginId: string) =>

View file

@ -14,7 +14,7 @@ import { Plugin } from './plugin';
export { uiCapabilitiesRegex } from './feature_schema';
export { Feature, FeatureWithAllOrReadPrivileges, FeatureKibanaPrivileges } from '../common';
export { PluginSetupContract } from './plugin';
export { PluginSetupContract, PluginStartContract } from './plugin';
export const plugin = (initializerContext: PluginInitializerContext) =>
new Plugin(initializerContext);

View file

@ -0,0 +1,27 @@
/*
* 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 { PluginSetupContract, PluginStartContract } from './plugin';
const createSetup = (): jest.Mocked<PluginSetupContract> => {
return {
getFeatures: jest.fn(),
getFeaturesUICapabilities: jest.fn(),
registerFeature: jest.fn(),
registerLegacyAPI: jest.fn(),
};
};
const createStart = (): jest.Mocked<PluginStartContract> => {
return {
getFeatures: jest.fn(),
};
};
export const featuresPluginMock = {
createSetup,
createStart,
};

View file

@ -30,6 +30,10 @@ export interface PluginSetupContract {
registerLegacyAPI: (legacyAPI: LegacyAPI) => void;
}
export interface PluginStartContract {
getFeatures(): Feature[];
}
/**
* Describes a set of APIs that are available in the legacy platform only and required by this plugin
* to function properly.
@ -45,6 +49,8 @@ export interface LegacyAPI {
export class Plugin {
private readonly logger: Logger;
private readonly featureRegistry: FeatureRegistry = new FeatureRegistry();
private legacyAPI?: LegacyAPI;
private readonly getLegacyAPI = () => {
if (!this.legacyAPI) {
@ -61,18 +67,16 @@ export class Plugin {
core: CoreSetup,
{ timelion }: { timelion?: TimelionSetupContract }
): Promise<RecursiveReadonly<PluginSetupContract>> {
const featureRegistry = new FeatureRegistry();
defineRoutes({
router: core.http.createRouter(),
featureRegistry,
featureRegistry: this.featureRegistry,
getLegacyAPI: this.getLegacyAPI,
});
return deepFreeze({
registerFeature: featureRegistry.register.bind(featureRegistry),
getFeatures: featureRegistry.getAll.bind(featureRegistry),
getFeaturesUICapabilities: () => uiCapabilitiesForFeatures(featureRegistry.getAll()),
registerFeature: this.featureRegistry.register.bind(this.featureRegistry),
getFeatures: this.featureRegistry.getAll.bind(this.featureRegistry),
getFeaturesUICapabilities: () => uiCapabilitiesForFeatures(this.featureRegistry.getAll()),
registerLegacyAPI: (legacyAPI: LegacyAPI) => {
this.legacyAPI = legacyAPI;
@ -82,14 +86,17 @@ export class Plugin {
savedObjectTypes: this.legacyAPI.savedObjectTypes,
includeTimelion: timelion !== undefined && timelion.uiEnabled,
})) {
featureRegistry.register(feature);
this.featureRegistry.register(feature);
}
},
});
}
public start() {
public start(): RecursiveReadonly<PluginStartContract> {
this.logger.debug('Starting plugin');
return deepFreeze({
getFeatures: this.featureRegistry.getAll.bind(this.featureRegistry),
});
}
public stop() {

View file

@ -0,0 +1,24 @@
/*
* 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 { capabilitiesProvider } from './capabilities_provider';
describe('Capabilities provider', () => {
it('provides the expected capabilities', () => {
expect(capabilitiesProvider()).toMatchInlineSnapshot(`
Object {
"management": Object {
"kibana": Object {
"spaces": true,
},
},
"spaces": Object {
"manage": true,
},
}
`);
});
});

View file

@ -0,0 +1,16 @@
/*
* 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 const capabilitiesProvider = () => ({
spaces: {
manage: true,
},
management: {
kibana: {
spaces: true,
},
},
});

View file

@ -6,8 +6,12 @@
import { Feature } from '../../../../plugins/features/server';
import { Space } from '../../common/model/space';
import { toggleUICapabilities } from './toggle_ui_capabilities';
import { Capabilities } from 'src/core/public';
import { setupCapabilitiesSwitcher } from './capabilities_switcher';
import { Capabilities, CoreSetup } from 'src/core/server';
import { coreMock, httpServerMock, loggingServiceMock } from 'src/core/server/mocks';
import { featuresPluginMock } from '../../../features/server/mocks';
import { spacesServiceMock } from '../spaces_service/spaces_service.mock';
import { PluginsStart } from '../plugin';
const features: Feature[] = [
{
@ -91,8 +95,33 @@ const buildCapabilities = () =>
},
}) as Capabilities;
describe('toggleUiCapabilities', () => {
it('does not toggle capabilities when the space has no disabled features', () => {
const setup = (space: Space) => {
const coreSetup = coreMock.createSetup();
const featuresStart = featuresPluginMock.createStart();
featuresStart.getFeatures.mockReturnValue(features);
coreSetup.getStartServices.mockResolvedValue([
coreMock.createStart(),
{ features: featuresStart },
]);
const spacesService = spacesServiceMock.createSetupContract();
spacesService.getActiveSpace.mockResolvedValue(space);
const logger = loggingServiceMock.createLogger();
const switcher = setupCapabilitiesSwitcher(
(coreSetup as unknown) as CoreSetup<PluginsStart>,
spacesService,
logger
);
return { switcher, logger, spacesService };
};
describe('capabilitiesSwitcher', () => {
it('does not toggle capabilities when the space has no disabled features', async () => {
const space: Space = {
id: 'space',
name: '',
@ -100,11 +129,54 @@ describe('toggleUiCapabilities', () => {
};
const capabilities = buildCapabilities();
const result = toggleUICapabilities(features, capabilities, space);
const { switcher } = setup(space);
const request = httpServerMock.createKibanaRequest();
const result = await switcher(request, capabilities);
expect(result).toEqual(buildCapabilities());
});
it('ignores unknown disabledFeatures', () => {
it('does not toggle capabilities when the request is not authenticated', async () => {
const space: Space = {
id: 'space',
name: '',
disabledFeatures: ['feature_1', 'feature_2', 'feature_3'],
};
const capabilities = buildCapabilities();
const { switcher } = setup(space);
const request = httpServerMock.createKibanaRequest({ routeAuthRequired: false });
const result = await switcher(request, capabilities);
expect(result).toEqual(buildCapabilities());
});
it('logs a warning, and does not toggle capabilities if an error is encountered', async () => {
const space: Space = {
id: 'space',
name: '',
disabledFeatures: ['feature_1', 'feature_2', 'feature_3'],
};
const capabilities = buildCapabilities();
const { switcher, logger, spacesService } = setup(space);
const request = httpServerMock.createKibanaRequest();
spacesService.getActiveSpace.mockRejectedValue(new Error('Something terrible happened'));
const result = await switcher(request, capabilities);
expect(result).toEqual(buildCapabilities());
expect(logger.warn).toHaveBeenCalledWith(
`Error toggling capabilities for request to /path: Error: Something terrible happened`
);
});
it('ignores unknown disabledFeatures', async () => {
const space: Space = {
id: 'space',
name: '',
@ -112,11 +184,15 @@ describe('toggleUiCapabilities', () => {
};
const capabilities = buildCapabilities();
const result = toggleUICapabilities(features, capabilities, space);
const { switcher } = setup(space);
const request = httpServerMock.createKibanaRequest();
const result = await switcher(request, capabilities);
expect(result).toEqual(buildCapabilities());
});
it('disables the corresponding navLink, catalogue, management sections, and all capability flags for disabled features', () => {
it('disables the corresponding navLink, catalogue, management sections, and all capability flags for disabled features', async () => {
const space: Space = {
id: 'space',
name: '',
@ -124,7 +200,10 @@ describe('toggleUiCapabilities', () => {
};
const capabilities = buildCapabilities();
const result = toggleUICapabilities(features, capabilities, space);
const { switcher } = setup(space);
const request = httpServerMock.createKibanaRequest();
const result = await switcher(request, capabilities);
const expectedCapabilities = buildCapabilities();
@ -137,7 +216,7 @@ describe('toggleUiCapabilities', () => {
expect(result).toEqual(expectedCapabilities);
});
it('can disable everything', () => {
it('can disable everything', async () => {
const space: Space = {
id: 'space',
name: '',
@ -145,7 +224,10 @@ describe('toggleUiCapabilities', () => {
};
const capabilities = buildCapabilities();
const result = toggleUICapabilities(features, capabilities, space);
const { switcher } = setup(space);
const request = httpServerMock.createKibanaRequest();
const result = await switcher(request, capabilities);
const expectedCapabilities = buildCapabilities();

View file

@ -4,15 +4,41 @@
* you may not use this file except in compliance with the Elastic License.
*/
import _ from 'lodash';
import { UICapabilities } from 'ui/capabilities';
import { Capabilities, CapabilitiesSwitcher, CoreSetup, Logger } from 'src/core/server';
import { Feature } from '../../../../plugins/features/server';
import { Space } from '../../common/model/space';
import { SpacesServiceSetup } from '../spaces_service';
import { PluginsStart } from '../plugin';
export function toggleUICapabilities(
features: Feature[],
capabilities: UICapabilities,
activeSpace: Space
) {
export function setupCapabilitiesSwitcher(
core: CoreSetup<PluginsStart>,
spacesService: SpacesServiceSetup,
logger: Logger
): CapabilitiesSwitcher {
return async (request, capabilities) => {
const isAnonymousRequest = !request.route.options.authRequired;
if (isAnonymousRequest) {
return capabilities;
}
try {
const [activeSpace, [, { features }]] = await Promise.all([
spacesService.getActiveSpace(request),
core.getStartServices(),
]);
const registeredFeatures = features.getFeatures();
return toggleCapabilities(registeredFeatures, capabilities, activeSpace);
} catch (e) {
logger.warn(`Error toggling capabilities for request to ${request.url.pathname}: ${e}`);
return capabilities;
}
};
}
function toggleCapabilities(features: Feature[], capabilities: Capabilities, activeSpace: Space) {
const clonedCapabilities = _.cloneDeep(capabilities);
toggleDisabledFeatures(features, clonedCapabilities, activeSpace);
@ -22,7 +48,7 @@ export function toggleUICapabilities(
function toggleDisabledFeatures(
features: Feature[],
capabilities: UICapabilities,
capabilities: Capabilities,
activeSpace: Space
) {
const disabledFeatureKeys = activeSpace.disabledFeatures;

View file

@ -0,0 +1,20 @@
/*
* 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, Logger } from 'src/core/server';
import { capabilitiesProvider } from './capabilities_provider';
import { setupCapabilitiesSwitcher } from './capabilities_switcher';
import { PluginsStart } from '../plugin';
import { SpacesServiceSetup } from '../spaces_service';
export const setupCapabilities = (
core: CoreSetup<PluginsStart>,
spacesService: SpacesServiceSetup,
logger: Logger
) => {
core.capabilities.registerProvider(capabilitiesProvider);
core.capabilities.registerSwitcher(setupCapabilitiesSwitcher(core, spacesService, logger));
};

View file

@ -201,12 +201,10 @@ describe('onPostAuthInterceptor', () => {
// interceptor to parse out the space id and rewrite the request's URL. Rather than duplicating that logic,
// we are including the already tested interceptor here in the test chain.
initSpacesOnRequestInterceptor({
getLegacyAPI: () => legacyAPI,
http: (http as unknown) as CoreSetup['http'],
});
initSpacesOnPostAuthRequestInterceptor({
getLegacyAPI: () => legacyAPI,
http: (http as unknown) as CoreSetup['http'],
log: loggingMock,
features: featuresPlugin,

View file

@ -7,13 +7,12 @@ import { Logger, CoreSetup } from 'src/core/server';
import { Space } from '../../../common/model/space';
import { wrapError } from '../errors';
import { SpacesServiceSetup } from '../../spaces_service/spaces_service';
import { LegacyAPI, PluginsSetup } from '../../plugin';
import { PluginsSetup } from '../../plugin';
import { getSpaceSelectorUrl } from '../get_space_selector_url';
import { DEFAULT_SPACE_ID, ENTER_SPACE_PATH } from '../../../common/constants';
import { addSpaceIdToPath } from '../../../common';
export interface OnPostAuthInterceptorDeps {
getLegacyAPI(): LegacyAPI;
http: CoreSetup['http'];
features: PluginsSetup['features'];
spacesService: SpacesServiceSetup;
@ -22,7 +21,6 @@ export interface OnPostAuthInterceptorDeps {
export function initSpacesOnPostAuthRequestInterceptor({
features,
getLegacyAPI,
spacesService,
log,
http,

View file

@ -16,7 +16,6 @@ import {
} from '../../../../../../src/core/server';
import * as kbnTestServer from '../../../../../../src/test_utils/kbn_server';
import { LegacyAPI } from '../../plugin';
import { elasticsearchServiceMock } from 'src/core/server/mocks';
describe('onRequestInterceptor', () => {
@ -110,10 +109,6 @@ describe('onRequestInterceptor', () => {
elasticsearch.esNodesCompatibility$ = elasticsearchServiceMock.createInternalSetup().esNodesCompatibility$;
initSpacesOnRequestInterceptor({
getLegacyAPI: () =>
({
legacyConfig: {},
} as LegacyAPI),
http: (http as unknown) as CoreSetup['http'],
});

View file

@ -12,14 +12,12 @@ import {
import { format } from 'url';
import { DEFAULT_SPACE_ID } from '../../../common/constants';
import { modifyUrl } from '../utils/url';
import { LegacyAPI } from '../../plugin';
import { getSpaceIdFromPath } from '../../../common';
export interface OnRequestInterceptorDeps {
getLegacyAPI(): LegacyAPI;
http: CoreSetup['http'];
}
export function initSpacesOnRequestInterceptor({ getLegacyAPI, http }: OnRequestInterceptorDeps) {
export function initSpacesOnRequestInterceptor({ http }: OnRequestInterceptorDeps) {
http.registerOnPreAuth(async function spacesOnPreAuthHandler(
request: KibanaRequest,
response: LifecycleResponseFactory,

View file

@ -23,7 +23,6 @@ import { securityMock } from '../../../security/server/mocks';
const log = loggingServiceMock.createLogger();
const legacyAPI: LegacyAPI = {
legacyConfig: {},
savedObjects: {} as SavedObjectsLegacyService,
} as LegacyAPI;

View file

@ -0,0 +1,72 @@
/*
* 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 } from 'src/core/server';
import { coreMock } from 'src/core/server/mocks';
import { featuresPluginMock } from '../../features/server/mocks';
import { licensingMock } from '../../licensing/server/mocks';
import { Plugin, PluginsSetup } from './plugin';
import { usageCollectionPluginMock } from '../../../../src/plugins/usage_collection/server/mocks';
describe('Spaces Plugin', () => {
describe('#setup', () => {
it('can setup with all optional plugins disabled, exposing the expected contract', async () => {
const initializerContext = coreMock.createPluginInitializerContext({});
const core = coreMock.createSetup() as CoreSetup<PluginsSetup>;
const features = featuresPluginMock.createSetup();
const licensing = licensingMock.createSetup();
const plugin = new Plugin(initializerContext);
const spacesSetup = await plugin.setup(core, { features, licensing });
expect(spacesSetup).toMatchInlineSnapshot(`
Object {
"__legacyCompat": Object {
"createDefaultSpace": [Function],
"registerLegacyAPI": [Function],
},
"spacesService": Object {
"getActiveSpace": [Function],
"getBasePath": [Function],
"getSpaceId": [Function],
"isInDefaultSpace": [Function],
"namespaceToSpaceId": [Function],
"scopedClient": [Function],
"spaceIdToNamespace": [Function],
},
}
`);
});
it('registers the capabilities provider and switcher', async () => {
const initializerContext = coreMock.createPluginInitializerContext({});
const core = coreMock.createSetup() as CoreSetup<PluginsSetup>;
const features = featuresPluginMock.createSetup();
const licensing = licensingMock.createSetup();
const plugin = new Plugin(initializerContext);
await plugin.setup(core, { features, licensing });
expect(core.capabilities.registerProvider).toHaveBeenCalledTimes(1);
expect(core.capabilities.registerSwitcher).toHaveBeenCalledTimes(1);
});
it('registers the usage collector', async () => {
const initializerContext = coreMock.createPluginInitializerContext({});
const core = coreMock.createSetup() as CoreSetup<PluginsSetup>;
const features = featuresPluginMock.createSetup();
const licensing = licensingMock.createSetup();
const usageCollection = usageCollectionPluginMock.createSetupContract();
const plugin = new Plugin(initializerContext);
await plugin.setup(core, { features, licensing, usageCollection });
expect(usageCollection.getCollectorByType('spaces')).toBeDefined();
});
});
});

View file

@ -13,7 +13,10 @@ import {
Logger,
PluginInitializerContext,
} from '../../../../src/core/server';
import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
import {
PluginSetupContract as FeaturesPluginSetup,
PluginStartContract as FeaturesPluginStart,
} from '../../features/server';
import { SecurityPluginSetup } from '../../security/server';
import { LicensingPluginSetup } from '../../licensing/server';
import { createDefaultSpace } from './lib/create_default_space';
@ -22,15 +25,15 @@ import { AuditLogger } from '../../../../server/lib/audit_logger';
import { spacesSavedObjectsClientWrapperFactory } from './lib/saved_objects_client/saved_objects_client_wrapper_factory';
import { SpacesAuditLogger } from './lib/audit_logger';
import { createSpacesTutorialContextFactory } from './lib/spaces_tutorial_context_factory';
import { registerSpacesUsageCollector } from './lib/spaces_usage_collector';
import { registerSpacesUsageCollector } from './usage_collection';
import { SpacesService } from './spaces_service';
import { SpacesServiceSetup } from './spaces_service';
import { ConfigType } from './config';
import { toggleUICapabilities } from './lib/toggle_ui_capabilities';
import { initSpacesRequestInterceptors } from './lib/request_interceptors';
import { initExternalSpacesApi } from './routes/api/external';
import { initInternalSpacesApi } from './routes/api/internal';
import { initSpacesViewsRoutes } from './routes/views';
import { setupCapabilities } from './capabilities';
/**
* Describes a set of APIs that is available in the legacy platform only and required by this plugin
@ -41,9 +44,6 @@ export interface LegacyAPI {
auditLogger: {
create: (pluginId: string) => AuditLogger;
};
legacyConfig: {
kibanaIndex: string;
};
}
export interface PluginsSetup {
@ -54,6 +54,10 @@ export interface PluginsSetup {
home?: HomeServerPluginSetup;
}
export interface PluginsStart {
features: FeaturesPluginStart;
}
export interface SpacesPluginSetup {
spacesService: SpacesServiceSetup;
__legacyCompat: {
@ -70,6 +74,8 @@ export class Plugin {
private readonly config$: Observable<ConfigType>;
private readonly kibanaIndexConfig$: Observable<{ kibana: { index: string } }>;
private readonly log: Logger;
private legacyAPI?: LegacyAPI;
@ -92,12 +98,16 @@ export class Plugin {
constructor(initializerContext: PluginInitializerContext) {
this.config$ = initializerContext.config.create<ConfigType>();
this.kibanaIndexConfig$ = initializerContext.config.legacy.globalConfig$;
this.log = initializerContext.logger.get();
}
public async start() {}
public async setup(core: CoreSetup, plugins: PluginsSetup): Promise<SpacesPluginSetup> {
public async setup(
core: CoreSetup<PluginsStart>,
plugins: PluginsSetup
): Promise<SpacesPluginSetup> {
const service = new SpacesService(this.log, this.getLegacyAPI);
const spacesService = await service.setup({
@ -131,20 +141,19 @@ export class Plugin {
initSpacesRequestInterceptors({
http: core.http,
log: this.log,
getLegacyAPI: this.getLegacyAPI,
spacesService,
features: plugins.features,
});
core.capabilities.registerSwitcher(async (request, uiCapabilities) => {
try {
const activeSpace = await spacesService.getActiveSpace(request);
const features = plugins.features.getFeatures();
return toggleUICapabilities(features, uiCapabilities, activeSpace);
} catch (e) {
return uiCapabilities;
}
});
setupCapabilities(core, spacesService, this.log);
if (plugins.usageCollection) {
registerSpacesUsageCollector(plugins.usageCollection, {
kibanaIndexConfig$: this.kibanaIndexConfig$,
features: plugins.features,
licensing: plugins.licensing,
});
}
if (plugins.security) {
plugins.security.registerSpacesService(spacesService);
@ -161,12 +170,7 @@ export class Plugin {
__legacyCompat: {
registerLegacyAPI: (legacyAPI: LegacyAPI) => {
this.legacyAPI = legacyAPI;
this.setupLegacyComponents(
spacesService,
plugins.features,
plugins.licensing,
plugins.usageCollection
);
this.setupLegacyComponents(spacesService);
},
createDefaultSpace: async () => {
return await createDefaultSpace({
@ -180,12 +184,7 @@ export class Plugin {
public stop() {}
private setupLegacyComponents(
spacesService: SpacesServiceSetup,
featuresSetup: FeaturesPluginSetup,
licensingSetup: LicensingPluginSetup,
usageCollectionSetup?: UsageCollectionSetup
) {
private setupLegacyComponents(spacesService: SpacesServiceSetup) {
const legacyAPI = this.getLegacyAPI();
const { addScopedSavedObjectsClientWrapperFactory, types } = legacyAPI.savedObjects;
addScopedSavedObjectsClientWrapperFactory(
@ -193,11 +192,5 @@ export class Plugin {
'spaces',
spacesSavedObjectsClientWrapperFactory(spacesService, types)
);
// Register a function with server to manage the collection of usage stats
registerSpacesUsageCollector(usageCollectionSetup, {
kibanaIndex: legacyAPI.legacyConfig.kibanaIndex,
features: featuresSetup,
licensing: licensingSetup,
});
}
}

View file

@ -100,9 +100,6 @@ export const createLegacyAPI = ({
} as unknown) as jest.Mocked<SavedObjectsLegacyService>;
const legacyAPI: jest.Mocked<LegacyAPI> = {
legacyConfig: {
kibanaIndex: '',
},
auditLogger: {} as any,
savedObjects: savedObjectsService,
};

View file

@ -28,7 +28,6 @@ const mockLogger = loggingServiceMock.createLogger();
const createService = async (serverBasePath: string = '') => {
const legacyAPI = {
legacyConfig: {},
savedObjects: ({
getSavedObjectsRepository: jest.fn().mockReturnValue({
get: jest.fn().mockImplementation((type, id) => {

View 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 { registerSpacesUsageCollector } from './spaces_usage_collector';

View file

@ -9,6 +9,7 @@ import * as Rx from 'rxjs';
import { PluginsSetup } from '../plugin';
import { Feature } from '../../../features/server';
import { ILicense, LicensingPluginSetup } from '../../../licensing/server';
import { pluginInitializerContextConfigMock } from 'src/core/server/mocks';
interface SetupOpts {
license?: Partial<ILicense>;
@ -72,7 +73,7 @@ describe('error handling', () => {
license: { isAvailable: true, type: 'basic' },
});
const { fetch: getSpacesUsage } = getSpacesUsageCollector(usageCollecion as any, {
kibanaIndex: '.kibana',
kibanaIndexConfig$: Rx.of({ kibana: { index: '.kibana' } }),
features,
licensing,
});
@ -85,7 +86,7 @@ describe('error handling', () => {
license: { isAvailable: true, type: 'basic' },
});
const { fetch: getSpacesUsage } = getSpacesUsageCollector(usageCollecion as any, {
kibanaIndex: '.kibana',
kibanaIndexConfig$: Rx.of({ kibana: { index: '.kibana' } }),
features,
licensing,
});
@ -105,11 +106,25 @@ describe('with a basic license', () => {
license: { isAvailable: true, type: 'basic' },
});
const { fetch: getSpacesUsage } = getSpacesUsageCollector(usageCollecion as any, {
kibanaIndex: '.kibana',
kibanaIndexConfig$: pluginInitializerContextConfigMock({}).legacy.globalConfig$,
features,
licensing,
});
usageStats = await getSpacesUsage(defaultCallClusterMock);
expect(defaultCallClusterMock).toHaveBeenCalledWith('search', {
body: {
aggs: {
disabledFeatures: {
terms: { field: 'space.disabledFeatures', include: ['feature1', 'feature2'], size: 2 },
},
},
query: { term: { type: { value: 'space' } } },
size: 0,
track_total_hits: true,
},
index: '.kibana-tests',
});
});
test('sets enabled to true', () => {
@ -139,7 +154,7 @@ describe('with no license', () => {
beforeAll(async () => {
const { features, licensing, usageCollecion } = setup({ license: { isAvailable: false } });
const { fetch: getSpacesUsage } = getSpacesUsageCollector(usageCollecion as any, {
kibanaIndex: '.kibana',
kibanaIndexConfig$: pluginInitializerContextConfigMock({}).legacy.globalConfig$,
features,
licensing,
});
@ -170,7 +185,7 @@ describe('with platinum license', () => {
license: { isAvailable: true, type: 'platinum' },
});
const { fetch: getSpacesUsage } = getSpacesUsageCollector(usageCollecion as any, {
kibanaIndex: '.kibana',
kibanaIndexConfig$: pluginInitializerContextConfigMock({}).legacy.globalConfig$,
features,
licensing,
});

View file

@ -4,11 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { get } from 'lodash';
import { CallAPIOptions } from 'src/core/server';
import { take } from 'rxjs/operators';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
// @ts-ignore
import { Observable } from 'rxjs';
import { KIBANA_STATS_TYPE_MONITORING } from '../../../../legacy/plugins/monitoring/common/constants';
import { KIBANA_SPACES_STATS_TYPE } from '../../common/constants';
import { PluginsSetup } from '../plugin';
@ -85,8 +84,8 @@ async function getSpacesUsage(
const { hits, aggregations } = resp!;
const count = get(hits, 'total.value', 0);
const disabledFeatureBuckets = get(aggregations, 'disabledFeatures.buckets', []);
const count = hits?.total?.value ?? 0;
const disabledFeatureBuckets = aggregations?.disabledFeatures?.buckets ?? [];
const initialCounts = knownFeatureIds.reduce(
(acc, featureId) => ({ ...acc, [featureId]: 0 }),
@ -125,7 +124,7 @@ export interface UsageStats {
}
interface CollectorDeps {
kibanaIndex: string;
kibanaIndexConfig$: Observable<{ kibana: { index: string } }>;
features: PluginsSetup['features'];
licensing: PluginsSetup['licensing'];
}
@ -145,12 +144,9 @@ export function getSpacesUsageCollector(
const license = await deps.licensing.license$.pipe(take(1)).toPromise();
const available = license.isAvailable; // some form of spaces is available for all valid licenses
const usageStats = await getSpacesUsage(
callCluster,
deps.kibanaIndex,
deps.features,
available
);
const kibanaIndex = (await deps.kibanaIndexConfig$.pipe(take(1)).toPromise()).kibana.index;
const usageStats = await getSpacesUsage(callCluster, kibanaIndex, deps.features, available);
return {
available,
@ -178,12 +174,9 @@ export function getSpacesUsageCollector(
}
export function registerSpacesUsageCollector(
usageCollection: UsageCollectionSetup | undefined,
usageCollection: UsageCollectionSetup,
deps: CollectorDeps
) {
if (!usageCollection) {
return;
}
const collector = getSpacesUsageCollector(usageCollection, deps);
usageCollection.registerCollector(collector);
}