Skip to content

Commit e18895b

Browse files
authored
move oss features registration to KP (#66524) (#66794)
* register OSS features with KP SO types only * use Licensing plugin API in features plugin * add plugin tests * filter hidden types out * cleanup tests * rename * add degug logging * add warning for setup contract * fix typo
1 parent ecab323 commit e18895b

File tree

9 files changed

+161
-70
lines changed

9 files changed

+161
-70
lines changed

src/core/server/saved_objects/saved_objects_service.mock.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,4 +107,5 @@ export const savedObjectsServiceMock = {
107107
createInternalStartContract: createInternalStartContractMock,
108108
createStartContract: createStartContractMock,
109109
createMigrationContext: migrationMocks.createContext,
110+
createTypeRegistryMock: typeRegistryMock.create,
110111
};

x-pack/legacy/plugins/xpack_main/index.js

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,7 @@ export const xpackMain = kibana => {
8585

8686
mirrorPluginStatus(server.plugins.elasticsearch, this, 'yellow', 'red');
8787

88-
featuresPlugin.registerLegacyAPI({
89-
xpackInfo: setupXPackMain(server),
90-
savedObjectTypes: server.savedObjects.types,
91-
});
88+
setupXPackMain(server);
9289

9390
// register routes
9491
xpackInfoRoute(server);

x-pack/plugins/endpoint/server/plugin.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ describe('test endpoint plugin', () => {
3434
registerFeature: jest.fn(),
3535
getFeatures: jest.fn(),
3636
getFeaturesUICapabilities: jest.fn(),
37-
registerLegacyAPI: jest.fn(),
3837
};
3938
});
4039

x-pack/plugins/features/kibana.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"id": "features",
33
"version": "8.0.0",
44
"kibanaVersion": "kibana",
5+
"requiredPlugins": ["licensing"],
56
"optionalPlugins": ["visTypeTimelion"],
67
"configPath": ["xpack", "features"],
78
"server": true,

x-pack/plugins/features/server/mocks.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ const createSetup = (): jest.Mocked<PluginSetupContract> => {
1111
getFeatures: jest.fn(),
1212
getFeaturesUICapabilities: jest.fn(),
1313
registerFeature: jest.fn(),
14-
registerLegacyAPI: jest.fn(),
1514
};
1615
};
1716

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
import { coreMock, savedObjectsServiceMock } from 'src/core/server/mocks';
7+
8+
import { Plugin } from './plugin';
9+
const initContext = coreMock.createPluginInitializerContext();
10+
const coreSetup = coreMock.createSetup();
11+
const coreStart = coreMock.createStart();
12+
const typeRegistry = savedObjectsServiceMock.createTypeRegistryMock();
13+
typeRegistry.getAllTypes.mockReturnValue([
14+
{
15+
name: 'foo',
16+
hidden: false,
17+
mappings: { properties: {} },
18+
namespaceType: 'single' as 'single',
19+
},
20+
{
21+
name: 'bar',
22+
hidden: true,
23+
mappings: { properties: {} },
24+
namespaceType: 'agnostic' as 'agnostic',
25+
},
26+
]);
27+
coreStart.savedObjects.getTypeRegistry.mockReturnValue(typeRegistry);
28+
29+
describe('Features Plugin', () => {
30+
it('returns OSS + registered features', async () => {
31+
const plugin = new Plugin(initContext);
32+
const { registerFeature } = await plugin.setup(coreSetup, {});
33+
registerFeature({
34+
id: 'baz',
35+
name: 'baz',
36+
app: [],
37+
privileges: null,
38+
});
39+
40+
const { getFeatures } = await plugin.start(coreStart);
41+
42+
expect(getFeatures().map(f => f.id)).toMatchInlineSnapshot(`
43+
Array [
44+
"baz",
45+
"discover",
46+
"visualize",
47+
"dashboard",
48+
"dev_tools",
49+
"advancedSettings",
50+
"indexPatterns",
51+
"savedObjectsManagement",
52+
]
53+
`);
54+
});
55+
56+
it('returns OSS + registered features with timelion when available', async () => {
57+
const plugin = new Plugin(initContext);
58+
const { registerFeature } = await plugin.setup(coreSetup, {
59+
visTypeTimelion: { uiEnabled: true },
60+
});
61+
registerFeature({
62+
id: 'baz',
63+
name: 'baz',
64+
app: [],
65+
privileges: null,
66+
});
67+
68+
const { getFeatures } = await plugin.start(coreStart);
69+
70+
expect(getFeatures().map(f => f.id)).toMatchInlineSnapshot(`
71+
Array [
72+
"baz",
73+
"discover",
74+
"visualize",
75+
"dashboard",
76+
"dev_tools",
77+
"advancedSettings",
78+
"indexPatterns",
79+
"savedObjectsManagement",
80+
"timelion",
81+
]
82+
`);
83+
});
84+
85+
it('registers not hidden saved objects types', async () => {
86+
const plugin = new Plugin(initContext);
87+
await plugin.setup(coreSetup, {});
88+
const { getFeatures } = await plugin.start(coreStart);
89+
90+
const soTypes =
91+
getFeatures().find(f => f.id === 'savedObjectsManagement')?.privileges?.all.savedObject.all ||
92+
[];
93+
94+
expect(soTypes.includes('foo')).toBe(true);
95+
expect(soTypes.includes('bar')).toBe(false);
96+
});
97+
});

x-pack/plugins/features/server/plugin.ts

Lines changed: 35 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@
66

77
import {
88
CoreSetup,
9+
CoreStart,
10+
SavedObjectsServiceStart,
911
Logger,
1012
PluginInitializerContext,
1113
RecursiveReadonly,
1214
} from '../../../../src/core/server';
1315
import { Capabilities as UICapabilities } from '../../../../src/core/server';
1416
import { deepFreeze } from '../../../../src/core/server';
15-
import { XPackInfo } from '../../../legacy/plugins/xpack_main/server/lib/xpack_info';
1617
import { PluginSetupContract as TimelionSetupContract } from '../../../../src/plugins/vis_type_timelion/server';
1718
import { FeatureRegistry } from './feature_registry';
1819
import { Feature, FeatureConfig } from '../common/feature';
@@ -25,39 +26,26 @@ import { defineRoutes } from './routes';
2526
*/
2627
export interface PluginSetupContract {
2728
registerFeature(feature: FeatureConfig): void;
29+
/*
30+
* Calling this function during setup will crash Kibana.
31+
* Use start contract instead.
32+
* @deprecated
33+
* */
2834
getFeatures(): Feature[];
2935
getFeaturesUICapabilities(): UICapabilities;
30-
registerLegacyAPI: (legacyAPI: LegacyAPI) => void;
3136
}
3237

3338
export interface PluginStartContract {
3439
getFeatures(): Feature[];
3540
}
3641

37-
/**
38-
* Describes a set of APIs that are available in the legacy platform only and required by this plugin
39-
* to function properly.
40-
*/
41-
export interface LegacyAPI {
42-
xpackInfo: Pick<XPackInfo, 'license'>;
43-
savedObjectTypes: string[];
44-
}
45-
4642
/**
4743
* Represents Features Plugin instance that will be managed by the Kibana plugin system.
4844
*/
4945
export class Plugin {
5046
private readonly logger: Logger;
51-
5247
private readonly featureRegistry: FeatureRegistry = new FeatureRegistry();
53-
54-
private legacyAPI?: LegacyAPI;
55-
private readonly getLegacyAPI = () => {
56-
if (!this.legacyAPI) {
57-
throw new Error('Legacy API is not registered!');
58-
}
59-
return this.legacyAPI;
60-
};
48+
private isTimelionEnabled: boolean = false;
6149

6250
constructor(private readonly initializerContext: PluginInitializerContext) {
6351
this.logger = this.initializerContext.logger.get();
@@ -67,39 +55,49 @@ export class Plugin {
6755
core: CoreSetup,
6856
{ visTypeTimelion }: { visTypeTimelion?: TimelionSetupContract }
6957
): Promise<RecursiveReadonly<PluginSetupContract>> {
58+
this.isTimelionEnabled = visTypeTimelion !== undefined && visTypeTimelion.uiEnabled;
59+
7060
defineRoutes({
7161
router: core.http.createRouter(),
7262
featureRegistry: this.featureRegistry,
73-
getLegacyAPI: this.getLegacyAPI,
7463
});
7564

7665
return deepFreeze({
7766
registerFeature: this.featureRegistry.register.bind(this.featureRegistry),
7867
getFeatures: this.featureRegistry.getAll.bind(this.featureRegistry),
7968
getFeaturesUICapabilities: () => uiCapabilitiesForFeatures(this.featureRegistry.getAll()),
80-
81-
registerLegacyAPI: (legacyAPI: LegacyAPI) => {
82-
this.legacyAPI = legacyAPI;
83-
84-
// Register OSS features.
85-
for (const feature of buildOSSFeatures({
86-
savedObjectTypes: this.legacyAPI.savedObjectTypes,
87-
includeTimelion: visTypeTimelion !== undefined && visTypeTimelion.uiEnabled,
88-
})) {
89-
this.featureRegistry.register(feature);
90-
}
91-
},
9269
});
9370
}
9471

95-
public start(): RecursiveReadonly<PluginStartContract> {
96-
this.logger.debug('Starting plugin');
72+
public start(core: CoreStart): RecursiveReadonly<PluginStartContract> {
73+
this.registerOssFeatures(core.savedObjects);
74+
9775
return deepFreeze({
9876
getFeatures: this.featureRegistry.getAll.bind(this.featureRegistry),
9977
});
10078
}
10179

102-
public stop() {
103-
this.logger.debug('Stopping plugin');
80+
public stop() {}
81+
82+
private registerOssFeatures(savedObjects: SavedObjectsServiceStart) {
83+
const registry = savedObjects.getTypeRegistry();
84+
const savedObjectTypes = registry
85+
.getAllTypes()
86+
.filter(t => !t.hidden)
87+
.map(t => t.name);
88+
89+
this.logger.debug(
90+
`Registering OSS features with SO types: ${savedObjectTypes.join(', ')}. "includeTimelion": ${
91+
this.isTimelionEnabled
92+
}.`
93+
);
94+
const features = buildOSSFeatures({
95+
savedObjectTypes,
96+
includeTimelion: this.isTimelionEnabled,
97+
});
98+
99+
for (const feature of features) {
100+
this.featureRegistry.register(feature);
101+
}
104102
}
105103
}

x-pack/plugins/features/server/routes/index.test.ts

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,20 @@
77
import { FeatureRegistry } from '../feature_registry';
88
import { defineRoutes } from './index';
99

10-
import { httpServerMock, httpServiceMock } from '../../../../../src/core/server/mocks';
11-
import { XPackInfoLicense } from '../../../../legacy/plugins/xpack_main/server/lib/xpack_info_license';
10+
import { httpServerMock, httpServiceMock, coreMock } from '../../../../../src/core/server/mocks';
11+
import { LicenseType } from '../../../licensing/server/';
12+
import { licensingMock } from '../../../licensing/server/mocks';
1213
import { RequestHandler } from '../../../../../src/core/server';
1314
import { FeatureConfig } from '../../common';
1415

15-
let currentLicenseLevel: string = 'gold';
16+
function createContextMock(licenseType: LicenseType = 'gold') {
17+
return {
18+
core: coreMock.createRequestHandlerContext(),
19+
licensing: {
20+
license: licensingMock.createLicense({ license: { type: licenseType } }),
21+
},
22+
};
23+
}
1624

1725
describe('GET /api/features', () => {
1826
let routeHandler: RequestHandler<any, any, any>;
@@ -53,24 +61,14 @@ describe('GET /api/features', () => {
5361
defineRoutes({
5462
router: routerMock,
5563
featureRegistry,
56-
getLegacyAPI: () => ({
57-
xpackInfo: {
58-
license: {
59-
isOneOf(candidateLicenses: string[]) {
60-
return candidateLicenses.includes(currentLicenseLevel);
61-
},
62-
} as XPackInfoLicense,
63-
},
64-
savedObjectTypes: [],
65-
}),
6664
});
6765

6866
routeHandler = routerMock.get.mock.calls[0][1];
6967
});
7068

7169
it('returns a list of available features, sorted by their configured order', async () => {
7270
const mockResponse = httpServerMock.createResponseFactory();
73-
routeHandler(undefined as any, { query: {} } as any, mockResponse);
71+
routeHandler(createContextMock(), { query: {} } as any, mockResponse);
7472

7573
expect(mockResponse.ok).toHaveBeenCalledTimes(1);
7674
const [call] = mockResponse.ok.mock.calls;
@@ -98,10 +96,8 @@ describe('GET /api/features', () => {
9896
});
9997

10098
it(`by default does not return features that arent allowed by current license`, async () => {
101-
currentLicenseLevel = 'basic';
102-
10399
const mockResponse = httpServerMock.createResponseFactory();
104-
routeHandler(undefined as any, { query: {} } as any, mockResponse);
100+
routeHandler(createContextMock('basic'), { query: {} } as any, mockResponse);
105101

106102
expect(mockResponse.ok).toHaveBeenCalledTimes(1);
107103
const [call] = mockResponse.ok.mock.calls;
@@ -126,10 +122,12 @@ describe('GET /api/features', () => {
126122
});
127123

128124
it(`ignoreValidLicenses=false does not return features that arent allowed by current license`, async () => {
129-
currentLicenseLevel = 'basic';
130-
131125
const mockResponse = httpServerMock.createResponseFactory();
132-
routeHandler(undefined as any, { query: { ignoreValidLicenses: false } } as any, mockResponse);
126+
routeHandler(
127+
createContextMock('basic'),
128+
{ query: { ignoreValidLicenses: false } } as any,
129+
mockResponse
130+
);
133131

134132
expect(mockResponse.ok).toHaveBeenCalledTimes(1);
135133
const [call] = mockResponse.ok.mock.calls;
@@ -154,10 +152,12 @@ describe('GET /api/features', () => {
154152
});
155153

156154
it(`ignoreValidLicenses=true returns features that arent allowed by current license`, async () => {
157-
currentLicenseLevel = 'basic';
158-
159155
const mockResponse = httpServerMock.createResponseFactory();
160-
routeHandler(undefined as any, { query: { ignoreValidLicenses: true } } as any, mockResponse);
156+
routeHandler(
157+
createContextMock('basic'),
158+
{ query: { ignoreValidLicenses: true } } as any,
159+
mockResponse
160+
);
161161

162162
expect(mockResponse.ok).toHaveBeenCalledTimes(1);
163163
const [call] = mockResponse.ok.mock.calls;

x-pack/plugins/features/server/routes/index.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
import { schema } from '@kbn/config-schema';
88
import { IRouter } from '../../../../../src/core/server';
9-
import { LegacyAPI } from '../plugin';
109
import { FeatureRegistry } from '../feature_registry';
1110

1211
/**
@@ -15,10 +14,9 @@ import { FeatureRegistry } from '../feature_registry';
1514
export interface RouteDefinitionParams {
1615
router: IRouter;
1716
featureRegistry: FeatureRegistry;
18-
getLegacyAPI: () => LegacyAPI;
1917
}
2018

21-
export function defineRoutes({ router, featureRegistry, getLegacyAPI }: RouteDefinitionParams) {
19+
export function defineRoutes({ router, featureRegistry }: RouteDefinitionParams) {
2220
router.get(
2321
{
2422
path: '/api/features',
@@ -37,7 +35,8 @@ export function defineRoutes({ router, featureRegistry, getLegacyAPI }: RouteDef
3735
request.query.ignoreValidLicenses ||
3836
!feature.validLicenses ||
3937
!feature.validLicenses.length ||
40-
getLegacyAPI().xpackInfo.license.isOneOf(feature.validLicenses)
38+
(context.licensing!.license.type &&
39+
feature.validLicenses.includes(context.licensing!.license.type))
4140
)
4241
.sort(
4342
(f1, f2) =>

0 commit comments

Comments
 (0)