Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose whitelisted config values to client-side plugin #50641

Merged
merged 20 commits into from
Nov 21, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ export interface InjectedMetadataParams {
uiPlugins: Array<{
id: PluginName;
plugin: DiscoveredPlugin;
config?: {
[key: string]: unknown;
};
}>;
capabilities: Capabilities;
legacyMode: boolean;
Expand Down Expand Up @@ -168,6 +171,9 @@ export interface InjectedMetadataSetup {
getPlugins: () => Array<{
id: string;
plugin: DiscoveredPlugin;
config?: {
[key: string]: unknown;
};
}>;
/** Indicates whether or not we are rendering a known legacy app. */
getLegacyMode: () => boolean;
Expand Down
1 change: 1 addition & 0 deletions src/core/server/legacy/legacy_service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ beforeEach(() => {
uiPlugins: {
public: new Map([['plugin-id', {} as DiscoveredPlugin]]),
internal: new Map([['plugin-id', {} as DiscoveredPluginInternal]]),
config: new Map([['plugin-id', null]]),
},
},
},
Expand Down
1 change: 1 addition & 0 deletions src/core/server/plugins/plugins_service.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const createServiceMock = () => {
uiPlugins: {
public: new Map(),
internal: new Map(),
config: new Map(),
},
});
mocked.start.mockResolvedValue({ contracts: new Map() });
Expand Down
52 changes: 44 additions & 8 deletions src/core/server/plugins/plugins_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,33 @@
* under the License.
*/

import { Observable } from 'rxjs';
import { Observable, of } from 'rxjs';
import { filter, first, map, mergeMap, tap, toArray } from 'rxjs/operators';
import { CoreService } from '../../types';
import { CoreContext } from '../core_context';

import { Logger } from '../logging';
import { discover, PluginDiscoveryError, PluginDiscoveryErrorType } from './discovery';
import { PluginWrapper } from './plugin';
import { DiscoveredPlugin, DiscoveredPluginInternal, PluginName } from './types';
import {
DiscoveredPlugin,
DiscoveredPluginInternal,
PluginConfigDescriptor,
PluginName,
} from './types';
import { PluginsConfig, PluginsConfigType } from './plugins_config';
import { PluginsSystem } from './plugins_system';
import { InternalCoreSetup } from '../internal_types';
import { IConfigService } from '../config';
import { pick } from '../../utils';

/** @public */
export interface PluginsServiceSetup {
contracts: Map<PluginName, unknown>;
uiPlugins: {
public: Map<PluginName, DiscoveredPlugin>;
internal: Map<PluginName, DiscoveredPluginInternal>;
config: Map<PluginName, Observable<unknown> | null>;
};
}

Expand All @@ -54,11 +62,14 @@ export interface PluginsServiceStartDeps {} // eslint-disable-line @typescript-e
export class PluginsService implements CoreService<PluginsServiceSetup, PluginsServiceStart> {
private readonly log: Logger;
private readonly pluginsSystem: PluginsSystem;
private readonly configService: IConfigService;
private readonly config$: Observable<PluginsConfig>;
private readonly pluginConfigDescriptors = new Map<PluginName, PluginConfigDescriptor>();

constructor(private readonly coreContext: CoreContext) {
this.log = coreContext.logger.get('plugins-service');
this.pluginsSystem = new PluginsSystem(coreContext);
this.configService = coreContext.configService;
this.config$ = coreContext.configService
.atPath<PluginsConfigType>('plugins')
.pipe(map(rawConfig => new PluginsConfig(rawConfig, coreContext.env)));
Expand All @@ -82,17 +93,20 @@ export class PluginsService implements CoreService<PluginsServiceSetup, PluginsS

const config = await this.config$.pipe(first()).toPromise();

let contracts = new Map<PluginName, unknown>();
if (!config.initialize || this.coreContext.env.isDevClusterMaster) {
this.log.info('Plugin initialization disabled.');
return {
contracts: new Map(),
uiPlugins: this.pluginsSystem.uiPlugins(),
};
} else {
contracts = await this.pluginsSystem.setupPlugins(deps);
}

const uiPlugins = this.pluginsSystem.uiPlugins();
return {
contracts: await this.pluginsSystem.setupPlugins(deps),
uiPlugins: this.pluginsSystem.uiPlugins(),
contracts,
uiPlugins: {
...uiPlugins,
config: this.generateUiPluginsConfigs(uiPlugins.public),
Copy link
Contributor

@mshustov mshustov Nov 18, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optional: The name collision probability is minimal. But to simplify reading, shouldn't we handle it separately?

{ contracts, uiPlugins, uiConfig }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, wasn't sure which one was the best approach. Both works with me, I can change it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a87b7b0 separates configs from plugins

},
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uiPlugins are directly serialized in injected metadatas, so I added the configuration in a distinct property instead of trying to merge in the uiPlugins structs

};
}

Expand All @@ -107,6 +121,27 @@ export class PluginsService implements CoreService<PluginsServiceSetup, PluginsS
await this.pluginsSystem.stopPlugins();
}

private generateUiPluginsConfigs(
uiPlugins: Map<string, DiscoveredPlugin>
): Map<PluginName, Observable<unknown>> {
Comment on lines +122 to +124
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The observable is currently not of use, as we take(1) before serializing to InjectedMetadata, however this is futur-proof for when we'll want to implement dynamic client-side config refresh as we do on server side.

return new Map(
[...uiPlugins].map(([pluginId, plugin]) => {
const configDescriptor = this.pluginConfigDescriptors.get(pluginId);
if (configDescriptor && configDescriptor.exposeToBrowser) {
return [
pluginId,
this.configService
.atPath(plugin.configPath)
.pipe(
map((config: any) => pick(config || {}, configDescriptor.exposeToBrowser || []))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: exposeToBrowser cannot be falsy. daf09e3#diff-725c498a267472754f01cff2baa3901bR130

),
];
}
return [pluginId, of({})];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this necessary with your check here? daf09e3#diff-ce5eebb41b6315c38446e8a68a4ca0aeR242

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was filtering null values after the map call using [...uiPlugins].map([...]).filter, but it seems typescript doesn't properly recognize Observable<unknown> | null filtered with !== null as Observable<unknown>. So the only purpose of this is to avoid having the method return type to Map<PluginName, Observable<unknown> | null>. But this can be changed

})
);
}

private async handleDiscoveryErrors(error$: Observable<PluginDiscoveryError>) {
// At this stage we report only errors that can occur when new platform plugin
// manifest is present, otherwise we can't be sure that the plugin is for the new
Expand Down Expand Up @@ -140,6 +175,7 @@ export class PluginsService implements CoreService<PluginsServiceSetup, PluginsS
mergeMap(async plugin => {
const configDescriptor = plugin.getConfigDescriptor();
if (configDescriptor) {
this.pluginConfigDescriptors.set(plugin.name, configDescriptor);
await this.coreContext.configService.setSchema(
plugin.configPath,
configDescriptor.schema
Expand Down
112 changes: 67 additions & 45 deletions src/legacy/ui/ui_render/ui_render_mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
* under the License.
*/

import { take } from 'rxjs/operators';
import { createHash } from 'crypto';
import { props, reduce as reduceAsync } from 'bluebird';
import Boom from 'boom';
Expand All @@ -42,21 +43,31 @@ export function uiRenderMixin(kbnServer, server, config) {
let defaultInjectedVars = {};
kbnServer.afterPluginsInit(() => {
const { defaultInjectedVarProviders = [] } = kbnServer.uiExports;
defaultInjectedVars = defaultInjectedVarProviders
.reduce((allDefaults, { fn, pluginSpec }) => (
defaultInjectedVars = defaultInjectedVarProviders.reduce(
(allDefaults, { fn, pluginSpec }) =>
mergeVariables(
allDefaults,
fn(kbnServer.server, pluginSpec.readConfigValue(kbnServer.config, []))
)
), {});
),
{}
);
});

// render all views from ./views
server.setupViews(resolve(__dirname, 'views'));

server.exposeStaticDir('/node_modules/@elastic/eui/dist/{path*}', fromRoot('node_modules/@elastic/eui/dist'));
server.exposeStaticDir('/node_modules/@kbn/ui-framework/dist/{path*}', fromRoot('node_modules/@kbn/ui-framework/dist'));
server.exposeStaticDir('/node_modules/@elastic/charts/dist/{path*}', fromRoot('node_modules/@elastic/charts/dist'));
server.exposeStaticDir(
'/node_modules/@elastic/eui/dist/{path*}',
fromRoot('node_modules/@elastic/eui/dist')
);
server.exposeStaticDir(
'/node_modules/@kbn/ui-framework/dist/{path*}',
fromRoot('node_modules/@kbn/ui-framework/dist')
);
server.exposeStaticDir(
'/node_modules/@elastic/charts/dist/{path*}',
fromRoot('node_modules/@elastic/charts/dist')
);

const translationsCache = { translations: null, hash: null };
server.route({
Expand All @@ -80,11 +91,12 @@ export function uiRenderMixin(kbnServer, server, config) {
.digest('hex');
}

return h.response(translationsCache.translations)
return h
.response(translationsCache.translations)
.header('cache-control', 'must-revalidate')
.header('content-type', 'application/json')
.etag(translationsCache.hash);
}
},
});

// register the bootstrap.js route after plugins are initialized so that we can
Expand All @@ -105,42 +117,38 @@ export function uiRenderMixin(kbnServer, server, config) {
const isCore = !app;

const uiSettings = request.getUiSettingsService();
const darkMode = !authEnabled || request.auth.isAuthenticated
? await uiSettings.get('theme:darkMode')
: false;
const darkMode =
!authEnabled || request.auth.isAuthenticated
? await uiSettings.get('theme:darkMode')
: false;

const basePath = config.get('server.basePath');
const regularBundlePath = `${basePath}/bundles`;
const dllBundlePath = `${basePath}/built_assets/dlls`;
const styleSheetPaths = [
`${dllBundlePath}/vendors.style.dll.css`,
...(
darkMode ?
[
`${basePath}/node_modules/@elastic/eui/dist/eui_theme_dark.css`,
`${basePath}/node_modules/@kbn/ui-framework/dist/kui_dark.css`,
`${basePath}/node_modules/@elastic/charts/dist/theme_only_dark.css`,
] : [
`${basePath}/node_modules/@elastic/eui/dist/eui_theme_light.css`,
`${basePath}/node_modules/@kbn/ui-framework/dist/kui_light.css`,
`${basePath}/node_modules/@elastic/charts/dist/theme_only_light.css`,
]
),
...(darkMode
? [
`${basePath}/node_modules/@elastic/eui/dist/eui_theme_dark.css`,
`${basePath}/node_modules/@kbn/ui-framework/dist/kui_dark.css`,
`${basePath}/node_modules/@elastic/charts/dist/theme_only_dark.css`,
]
: [
`${basePath}/node_modules/@elastic/eui/dist/eui_theme_light.css`,
`${basePath}/node_modules/@kbn/ui-framework/dist/kui_light.css`,
`${basePath}/node_modules/@elastic/charts/dist/theme_only_light.css`,
]),
`${regularBundlePath}/${darkMode ? 'dark' : 'light'}_theme.style.css`,
`${regularBundlePath}/commons.style.css`,
...(
!isCore ? [`${regularBundlePath}/${app.getId()}.style.css`] : []
),
...(!isCore ? [`${regularBundlePath}/${app.getId()}.style.css`] : []),
...kbnServer.uiExports.styleSheetPaths
.filter(path => (
path.theme === '*' || path.theme === (darkMode ? 'dark' : 'light')
))
.map(path => (
.filter(path => path.theme === '*' || path.theme === (darkMode ? 'dark' : 'light'))
.map(path =>
path.localPath.endsWith('.scss')
? `${basePath}/built_assets/css/${path.publicPath}`
: `${basePath}/${path.publicPath}`
))
.reverse()
)
.reverse(),
];

const bootstrap = new AppBootstrap({
Expand All @@ -149,17 +157,18 @@ export function uiRenderMixin(kbnServer, server, config) {
regularBundlePath,
dllBundlePath,
styleSheetPaths,
}
},
});

const body = await bootstrap.getJsFile();
const etag = await bootstrap.getJsFileHash();

return h.response(body)
return h
.response(body)
.header('cache-control', 'must-revalidate')
.header('content-type', 'application/javascript')
.etag(etag);
}
},
});
});

Expand All @@ -179,14 +188,14 @@ export function uiRenderMixin(kbnServer, server, config) {
} catch (err) {
throw Boom.boomify(err);
}
}
},
});

async function getUiSettings({ request, includeUserProvidedConfig }) {
const uiSettings = request.getUiSettingsService();
return props({
defaults: uiSettings.getRegistered(),
user: includeUserProvidedConfig && uiSettings.getUserProvided()
user: includeUserProvidedConfig && uiSettings.getUserProvided(),
});
}

Expand All @@ -206,7 +215,12 @@ export function uiRenderMixin(kbnServer, server, config) {
};
}

async function renderApp({ app, h, includeUserProvidedConfig = true, injectedVarsOverrides = {} }) {
async function renderApp({
app,
h,
includeUserProvidedConfig = true,
injectedVarsOverrides = {},
}) {
const request = h.request;
const basePath = request.getBasePath();
const uiSettings = await getUiSettings({ request, includeUserProvidedConfig });
Expand All @@ -215,14 +229,22 @@ export function uiRenderMixin(kbnServer, server, config) {
const legacyMetadata = getLegacyKibanaPayload({
app,
basePath,
uiSettings
uiSettings,
});

// Get the list of new platform plugins.
// Convert the Map into an array of objects so it is JSON serializable and order is preserved.
const uiPlugins = [
...kbnServer.newPlatform.__internals.uiPlugins.public.entries()
].map(([id, plugin]) => ({ id, plugin }));
const uiPluginConfigs = kbnServer.newPlatform.__internals.uiPlugins.config;
const uiPlugins = await Promise.all([
...kbnServer.newPlatform.__internals.uiPlugins.public.entries(),
].map(async ([id, plugin]) => {
const config$ = uiPluginConfigs.get(id);
if (config$) {
return { id, plugin, config: await config$.pipe(take(1)).toPromise() };
} else {
return { id, plugin, config: {} };
}
}));

const response = h.view('ui_app', {
strictCsp: config.get('csp.strict'),
Expand Down Expand Up @@ -250,8 +272,8 @@ export function uiRenderMixin(kbnServer, server, config) {
mergeVariables(
injectedVarsOverrides,
app ? await server.getInjectedUiAppVars(app.getId()) : {},
defaultInjectedVars,
),
defaultInjectedVars
)
),

uiPlugins,
Expand Down