-
Notifications
You must be signed in to change notification settings - Fork 8.3k
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
Introduce support for the server-side new platform plugins. #25473
Introduce support for the server-side new platform plugins. #25473
Conversation
Pinging @elastic/kibana-platform |
4d4d008
to
96661d6
Compare
@@ -38,7 +38,7 @@ export abstract class Type<V> { | |||
*/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note: not related change, please ignore, I'll create a dedicated PR to cleanup TS a bit in this package.
src/cli/serve/serve.js
Outdated
@@ -165,7 +165,8 @@ export default function (program) { | |||
pluginDirCollector, | |||
[ | |||
fromRoot('plugins'), | |||
fromRoot('src/core_plugins') | |||
fromRoot('src/core_plugins'), | |||
fromRoot('src/plugins') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note: this change will go in a dedicated PR probably with testbed
-like plugin for the new platform.
const config = await this.config$.pipe(first()).toPromise(); | ||
const handledPaths = this.handledPaths.map(pathToString); | ||
|
||
return config.getFlattenedPaths().filter(path => !isPathHandled(path, handledPaths)); | ||
} | ||
|
||
public async getUsedPaths() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note: need this for the legacy platform so that it doesn't complain about keys that core knows about (new keys maybe, plugin keys as well).
We'll still need to figure out what to do with the new platform plugin config keys that are not used immediately during startup (like in the testbed
plugin in this PR). Maybe we should forbid that and supply config as part of the PluginCore
when we start plugin so that these keys are always marked as used
....
this.plugins = new PluginsModule(configService, logger, env); | ||
this.legacy = new LegacyCompatModule(configService, logger, env); | ||
|
||
const core = { env, configService, logger }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note: I need this "bundle" (+ http
in the future) in many places so I called them KibanaCore
:)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't usually care that much about naming, but I think this one is a little too ambiguous. The notion of "core" is generic enough to be confusing in its own right, and we make that worse by referring to multiple different things core.
Let's have "kibana core" refer to the src/core
module. If you're talking in the context of the server, "kibana core" might alternatively refer to src/core/server
, which is the only module that is relevant to you. If you're talking in the context of the browser, then src/core/public
.
Perhaps something like "base services" would be more appropriate here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, it's confusing, I didn't use services
because of env
that is not a service like configService
, but maybe we can call it a "service" in a broader sense, so base services
works for me - will rename.
* Converts core log level to a one that's known to the legacy platform. | ||
* @param level Log level from the core. | ||
*/ | ||
function getLegacyLogLevel(level: LogLevel) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note: noticed that during testing, good
doesn't color warn
and trace
, so here I pick alternatives supported by good
.
@@ -126,6 +88,21 @@ export async function parseManifest(pluginPath: string, packageInfo: PackageInfo | |||
); | |||
} | |||
|
|||
if ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note: I was testing various ideas with configPath
and it ended up in the manifest itself, so that I can quickly check whether plugin enabled or not without loading a "definition" file. I think it makes sense to keep it here, since it's only used internally by the PluginCore
and I can't think of any reason to keep it inside of the plugin definition in JS/TS... But please tell me if you know any reason to not keep it inside of manifest.
Also maybe we want to forbid using some "reserved" config paths to protect from reading arbitrary config values by plugins. But since it's in manifest and easily auditable it may not be a big deal.
) | ||
); | ||
} | ||
|
||
const expectedKibanaVersion = |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
* plugin path is handled by the core plugin system or not. | ||
* @param pluginPath Path to the plugin. | ||
*/ | ||
export async function hasPluginManifest(pluginPath: string) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note: was the easiest way I could think of...
// If plugin directory contains manifest file, we should skip it since it | ||
// should have been handled by the core plugin system already. | ||
Rx.defer(() => hasPluginManifest(path)).pipe( | ||
mergeMap(hasPluginManifest => hasPluginManifest ? [] : createPackageJsonAtPath(path)), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note: I'd rather inject this "isnewplatformplugin" function through kbnServer.core
, but it's too much hassle to pass it down to here, quite many hops :/
@elastic/kibana-platform would be great if you can give any preliminary feedback on this PR - I've left a couple of questions in places where I'm particularly interested in what you think. Thanks! |
96661d6
to
c381bad
Compare
…ixed plugin search paths, use classes for test plugins, separate init and start core plugin values and more.
"pluginSearchPaths": Array [ | ||
"/test/cwd/src/plugins", | ||
"/test/cwd/plugins", | ||
"/test/cwd/../kibana-extra", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note: don't be confused, ..
is preserved because I mock path.resolve
in tests.
name: 'development' | 'production'; | ||
dev: boolean; | ||
prod: boolean; | ||
} | ||
|
||
/** @internal */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note: I'm adding a bunch of @internal
here and there as I'm exposing core types and noticing types that shouldn't be exposed.
} | ||
: { autoListen: false }, | ||
handledConfigPaths: await this.core.configService.getUsedPaths(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note: I decided to keep this structure for now (kbnServer.core --> { serverOptions, handledConfigPaths, plugins })
to group everything that core provides to the legacy world, but we can later split into core
and plugins
as we were discussing.
@@ -157,7 +163,7 @@ export class LegacyService implements CoreService { | |||
|
|||
private setupProxyListener(server: HapiServer) { | |||
const legacyProxy = new LegacyPlatformProxy( | |||
this.logger.get('legacy', 'proxy'), | |||
this.core.logger.get('legacy-proxy'), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note: while working on this I had to see lots of logs, and noticed that legacy logger alphabetically sorts tags that represent new logger context and it's not something we want.
src/core/server/plugins/plugin.ts
Outdated
this.optionalDependencies = manifest.optionalPlugins; | ||
} | ||
|
||
public init(core: PluginInitializerCore) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note: it was a part of start
initially, but I've moved it into separate function since we need another arguments for the initializer, we still don't support init
lifecycle event for plugins though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you pass PluginInitializerCore
to the constructor like you do with KibanaCore
elsewhere? If so, the logic can go back to being kicked off in start
(maybe through a private initializePlugin
function), and we avoid the ambiguity of adding an init
function to this lightweight wrapper around plugins, especially since we might some day add a real init
lifecycle handler.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I can, the only thing is that we'll have to create PluginInitializerCore
even for disabled plugins, but we likely aren't going to have hundreds of disabled plugins so I maybe don't have to worry about that ...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Btw, since KibanaCore
is BaseServices
now, do you prefer replacing Core
with BaseServices
in PluginInitializerCore
and friends as well for consistency @epixa ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, now it's CoreContext
, PluginInitializerContext
and PluginStartContext
, I'm passing PluginInitializerContext
into constructor as you suggest. Let's see how it goes,
paths: schema.arrayOf(schema.string(), { | ||
defaultValue: [], | ||
}), | ||
scanDirs: schema.maybe(schema.arrayOf(schema.string())), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note: we don't use scanDirs
and paths
in new platform as we agreed, but we still use initialize
and if we don't define scanDirs
and paths
in schema validation will fail (unknown keys under plugins
). There are several solutions to this problem:
- Ideally I'd add another new platform only config option with a similar meaning and decoupled from
plugins.*
used by the legacy platform - requires "patching" of several places where we passplugins.initalize=false
already - add support for
ignoreUnknownKeys
option forschema.object
, a bit too relaxed and dangerous for the config validation - define
scandDirs
andpaths
in the schema - the easiest option I chose here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually there is one more way, we can just pick initialize
in legacy config adapter like we do for other config keys or even transform it into something else that we want to see in new platform.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a downside to that last approach you mentioned? What's your preference at this point?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a downside to that last approach you mentioned?
I can't think of any downside, we do this for server
config and it works pretty well.
What's your preference at this point?
The last one, I forgot about it when I was working on this PR and remembered again while fixing regression with SSL config deprecations :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool. The last one sounds good to me as well.
await this.handleDiscoveryErrors(error$); | ||
await this.handleDiscoveredPlugins(plugin$); | ||
|
||
if (!config.initialize || this.core.env.isDevClusterMaster) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note: hopefully we'll move cluster manager out of the main source code to a separate independent script/package and remove this this.core.env.isDevClusterMaster
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Configurations seems to be handled inconsistently, and I'm not sure whether it's the result of this PR or not.
If I add core.testbed.secret: "woah"
to kibana.yml
, then node scripts/kibana
starts up properly and reflects the non-default config value.
If I add core.testbed.secret: "woah"
to kibana.dev.yml
, then node scripts/kibana
starts up properly but does not reflect the non-default config value.
If I add core.testbed.secret: "woah"
to either kibana.yml
or kibana.dev.yml
, then node scripts/kibana --dev
fails startup with the exception Error: "core.testbed.secret" setting was not applied. Check for spelling errors and ensure that expected plugins are installed.
First two behaviors seem to be correct, regarding third: I guess, it's |
import { createPluginInitializerCore, createPluginStartCore } from './plugins_core'; | ||
|
||
/** @internal */ | ||
export class PluginsSystem { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm OK with this naming choice for the sake of an initial merge, but I think in the end we should change this name. In isolation I think PluginsSystem
is fine, but in the spirit of having the stuff in core
be an example for future development within core and plugins alike, I think a more clear structure is in order for the the plugin module.
Let's look to core itself for inspiration here.
There's a top level service definition for core, we happen to call it Server
, but effectively it is just the same thing as any other service definition. It's a class that takes in some constructor arguments from its initializer, it contains start/stop lifecycle events to kick off and tear down the relevant domain logic, etc.
Simply looking at the top-most directory structure of the core module you get a pretty good idea of its features, responsibilities, etc.
core
config
http
legacy
logging
plugins
index.js // the interface for the core module
The directory structure can't describe how these features and responsibilities work together or how they get exposed, but it at least gives a good indication of what the core module is all about.
There are currently also some oddities in core that I didn't list above like root
and dev
, but I think of those things as technical debt that we chose to take on for the sake of practicality and that we can resolve later. And I left out some name and path details for the sake of explanation.
We should strive to have that same level of clarity from each of the services within core as well.
When I crack open plugin, I'd expect to see a file structure that was equally as expressive as that of core itself. Perhaps something like:
core
plugins
config // config schema validation
discovery // static manifest discovery
loader // importing plugin interface and instantiation
lifecycle // invoke start/stop/init(?) with relevant contracts
index.js // the interface for the core/plugins module
I might not have the right break down there, but hopefully it illustrates my point.
This feedback is really not unique to plugins, so if you'd prefer not to heed it in this PR, I'm fine with that. But let's at least get on the same page about it and then we can apply it to future service design and come back and refactor some of the existing core service designs at a later date.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On further reflection, this PR is already pretty massive, so I don't think overhauling the existing shape of the plugin module in it makes sense. I'd love your feedback so we can get aligned on it going forward.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hehe, I was struggling with finding the right name and just ended up with what we had in new-platform branch (same as I did for Server
and Root
) :) I don't like our Server
, Root
, PluginSystem
and a couple more things, these are confusing and meaningless names/notions IMO.
Up until this point SomethingService
was a central thing for core/something
and SomethingModule
served as a grouping mechanism, mainly because of simplicity of Something
, but I think ModuleSomething
(e.g. core/something/index.ts
in your example) should become a central thing and be an "interface" for Something
that other SomethingElse
from the core can interact with. Details to be discussed though.
When I crack open plugin, I'd expect to see a file structure that was equally as expressive as that of core itself.
On further reflection, this PR is already pretty massive, so I don't think overhauling the existing shape of the plugin module in it makes sense. I'd love your feedback so we can get aligned on it going forward.
I haven't dug that deep yet, but I think that's an interesting point that totally makes sense to me. And I agree that we can handle this in follow-ups more effectively.
return exposedValues; | ||
} | ||
|
||
const sortedPlugins = this.getTopologicallySortedPluginNames(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think topological sorting should happen during discovery instead of as an implementation detail when attempting to kick off the plugin lifecycle. There will be at least one other time when core needs plugins listed in the appropriate topological order: for client-side plugins, we will need to pass a properly sorted array of plugin names to the browser so that the client core can asynchronously load the plugin bundles in effectively the same order that they were loaded on the server, at least as far as dependencies are concerned.
I can't think of a situation where we'd want the results of plugins discovery and not have them already sorted.
What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@epixa have we considered the case when our graph includes plugins without UI (ui: false
in manifest)? So if plugin A (ui: true) depends on plugin B (ui: false) then on the server we should run them in order, but on the client side order doesn't matter it seems and hence graph will look differently.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a really good point. Let's leave this where it is for now.
paths: schema.arrayOf(schema.string(), { | ||
defaultValue: [], | ||
}), | ||
scanDirs: schema.maybe(schema.arrayOf(schema.string())), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a downside to that last approach you mentioned? What's your preference at this point?
src/core/server/plugins/plugin.ts
Outdated
this.optionalDependencies = manifest.optionalPlugins; | ||
} | ||
|
||
public init(core: PluginInitializerCore) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you pass PluginInitializerCore
to the constructor like you do with KibanaCore
elsewhere? If so, the logic can go back to being kicked off in start
(maybe through a private initializePlugin
function), and we avoid the ambiguity of adding an init
function to this lightweight wrapper around plugins, especially since we might some day add a real init
lifecycle handler.
) | ||
); | ||
} | ||
|
||
const expectedKibanaVersion = |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
this.plugins = new PluginsModule(configService, logger, env); | ||
this.legacy = new LegacyCompatModule(configService, logger, env); | ||
|
||
const core = { env, configService, logger }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't usually care that much about naming, but I think this one is a little too ambiguous. The notion of "core" is generic enough to be confusing in its own right, and we make that worse by referring to multiple different things core.
Let's have "kibana core" refer to the src/core
module. If you're talking in the context of the server, "kibana core" might alternatively refer to src/core/server
, which is the only module that is relevant to you. If you're talking in the context of the browser, then src/core/public
.
Perhaps something like "base services" would be more appropriate here?
Ah yes, that makes sense. I'm fine moving forward without this change, but we'll need to address it before folks start to do anything with plugins. |
💚 Build Succeeded |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
I left one comment that might warrant a change, but if you don't agree with it, I think we're good to go as-is.
I didn't test these changes quite as thoroughly as I did in the first round of feedback, but the changes themselves look good and I started up kibana with various dev configurations (base path, no base path, --dev, no --dev). Everything looked appropriate in verbose logs as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changes LGTM
💚 Build Succeeded |
🎊 🎉 🎊 🎉 |
6.x/6.6: 09cd080 |
In this PR I want to introduce the initial support for the server-side new platform plugins including, but not limited to:
new-platform
branch)scanDirs/path
for legacy Kibana only etc.).Here is example of plugin pseudo definition:
src/plugins/testbed/index.ts
Part of #18840