-
Notifications
You must be signed in to change notification settings - Fork 15
[BREAKINGCHANGE] fetch plugin based on version #128
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,7 +23,8 @@ import { | |
| } from '../../model'; | ||
| import { PluginRegistryContext } from '../../runtime'; | ||
| import { useEvent } from '../../utils'; | ||
| import { usePluginIndexes, getTypeAndKindKey } from './plugin-indexes'; | ||
| import { usePluginIndexes, PluginCompoundKey } from './plugin-indexes'; | ||
| import { lookUpDefaultPluginKey } from './getPluginSearchHelper'; | ||
|
|
||
| export interface PluginRegistryProps { | ||
| pluginLoader: PluginLoader; | ||
|
|
@@ -62,28 +63,68 @@ export function PluginRegistry(props: PluginRegistryProps): ReactElement { | |
| }); | ||
|
|
||
| const getPlugin = useCallback( | ||
| async <T extends PluginType>(kind: T, name: string): Promise<PluginImplementation<T>> => { | ||
| // Get the indexes of the installed plugins | ||
| async <T extends PluginType>(compoundKeyObj: PluginCompoundKey<T>): Promise<PluginImplementation<T>> => { | ||
| const pluginIndexes = await getPluginIndexes(); | ||
| let resource: PluginModuleResource | undefined = undefined; | ||
| let pluginModule: Record<string, Plugin<UnknownSpec>> | undefined; | ||
| const { kind, name, version, registry } = compoundKeyObj; | ||
|
|
||
| // Figure out what module the plugin is in by looking in the index | ||
| const typeAndKindKey = getTypeAndKindKey(kind, name); | ||
| const resource = pluginIndexes.pluginResourcesByNameAndKind.get(typeAndKindKey); | ||
| if (resource === undefined) { | ||
| /** | ||
| * By default both version and registry are undefined, | ||
| * If one or both are passed, the registry will check if the plugin with the specific version and registry is available, | ||
| * falling back to the current behavior which is returning the default. | ||
| */ | ||
|
|
||
| if (registry || version) { | ||
| let compoundKey = ''; | ||
| /** | ||
| * This branch tries to look up a resource deterministically, using a compound_key which consists of kind, name, registry, and version | ||
| * Based on the user input, the likely keys are | ||
| * 1- kind:name:registry:version (This is the complete compound key. It is very likely the key is looked up) | ||
| * 2- kind:name::version (This is very likely, because registry is an optional field of the resource object) | ||
| * 3- kind:name:registry: (It is impossible to find any combination, because version is a mandatory field of the resource. So, this will be handled by the fallback) | ||
| * Note: It is likely that the key is not found. However, the search should NOT give up easily! | ||
| * Instead it should continue with the fallback mechanism | ||
| */ | ||
| compoundKey = `${kind}:${name}:${registry}:${version}`; | ||
| resource = pluginIndexes.pluginResourcesByNameKindRegistryVersion.get(compoundKey); | ||
| if (resource) { | ||
| pluginModule = (await loadPluginModule(resource)) as Record<string, Plugin<UnknownSpec>>; | ||
| const plugin = pluginModule?.[`${name}:${registry ?? ''}:${version ?? ''}`]; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe we should also have a function for this name, it might seem trivial but is repeated a couple of times.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Its the compound key. We can use the same function no? |
||
| if (!plugin) { | ||
| throw new Error( | ||
| `The ${name} plugin for kind '${kind}' is missing from the ${resource.metadata.name} plugin module` | ||
| ); | ||
| } | ||
| return plugin as PluginImplementation<T>; | ||
| } | ||
| } | ||
| /** | ||
| * This is the fallback mechanism branch. | ||
| * It performs a minimal search using the mandatory inputs from user and returns the default resource | ||
| * More information can be found in the searchHelper.ts | ||
| */ | ||
| const resourceKey = lookUpDefaultPluginKey( | ||
| Array.from(pluginIndexes.pluginResourcesByNameKindRegistryVersion.keys()), | ||
| { | ||
| kind, | ||
| name, | ||
| } | ||
| ); | ||
|
|
||
| if (!resourceKey) { | ||
| throw new Error(`A ${name} plugin for kind '${kind}' is not installed`); | ||
| } | ||
|
|
||
| // Treat the plugin module as a bunch of named exports that have plugins | ||
| const pluginModule = (await loadPluginModule(resource)) as Record<string, Plugin<UnknownSpec>>; | ||
| resource = pluginIndexes.pluginResourcesByNameKindRegistryVersion.get(resourceKey)!; | ||
| pluginModule = (await loadPluginModule(resource)) as Record<string, Plugin<UnknownSpec>>; | ||
|
|
||
| // We currently assume that plugin modules will have named exports that match the kinds they handle | ||
| const plugin = pluginModule[name]; | ||
| if (plugin === undefined) { | ||
| const plugin = pluginModule?.[resourceKey]; | ||
| if (!plugin) { | ||
| throw new Error( | ||
| `The ${name} plugin for kind '${kind}' is missing from the ${resource.metadata.name} plugin module` | ||
| ); | ||
| } | ||
|
|
||
| return plugin as PluginImplementation<T>; | ||
| }, | ||
| [getPluginIndexes, loadPluginModule] | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| // Copyright The Perses Authors | ||
| // Licensed 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 { lookUpDefaultPluginKey } from './getPluginSearchHelper'; | ||
|
|
||
| describe('getPluginSearchHelper', () => { | ||
| describe('same registers but different versions', () => { | ||
| const keys: string[] = ['Panel:TimeSeriesChart:dev:1.0.0', 'Panel:TimeSeriesChart:dev:2.0.0']; | ||
| it('should take the higher version', () => { | ||
| expect(lookUpDefaultPluginKey(keys, { kind: 'Panel', name: 'TimeSeriesChart' })).toBe( | ||
| 'Panel:TimeSeriesChart:dev:2.0.0' | ||
| ); | ||
| }); | ||
| }); | ||
|
|
||
| describe('with and without registry, versions race', () => { | ||
| const keys: string[] = [ | ||
| 'Panel:TimeSeriesChart:dev:2.0.0', | ||
| 'Panel:TimeSeriesChart::1.0.0', | ||
| 'Panel:TimeSeriesChartX:dev:1.0.0', | ||
| 'Panel:TimeSeriesChartX::2.0.0', | ||
| ]; | ||
|
|
||
| it('should take the higher version', () => { | ||
| expect(lookUpDefaultPluginKey(keys, { kind: 'Panel', name: 'TimeSeriesChart' })).toBe( | ||
| 'Panel:TimeSeriesChart:dev:2.0.0' | ||
| ); | ||
|
|
||
| expect(lookUpDefaultPluginKey(keys, { kind: 'Panel', name: 'TimeSeriesChartX' })).toBe( | ||
| 'Panel:TimeSeriesChartX::2.0.0' | ||
| ); | ||
| }); | ||
| }); | ||
|
|
||
| describe('with and without registry - same versions - check policy', () => { | ||
| const keys: string[] = ['Panel:TimeSeriesChart:dev:2.0.0', 'Panel:TimeSeriesChart::2.0.0']; | ||
| it('should return the one without the registry by default', () => { | ||
| expect(lookUpDefaultPluginKey(keys, { kind: 'Panel', name: 'TimeSeriesChart' })).toBe( | ||
| 'Panel:TimeSeriesChart::2.0.0' | ||
| ); | ||
| }); | ||
|
|
||
| it('should return the one with the registry', () => { | ||
| expect( | ||
| lookUpDefaultPluginKey(keys, { kind: 'Panel', name: 'TimeSeriesChart' }, { registryOverVersion: true }) | ||
| ).toBe('Panel:TimeSeriesChart:dev:2.0.0'); | ||
| }); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| // Copyright The Perses Authors | ||
| // Licensed 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 { gt } from 'semver'; | ||
| import { PluginType } from '../../model'; | ||
| import { PluginCompoundKey } from './plugin-indexes'; | ||
|
|
||
| /** | ||
| * ____ LOOK UP DEFAULT PLUGIN KEYS WITH PLUGIN TYPE (KIND) AND NAME ___ | ||
| * This is the fallback mechanism to look up the default plugin using the plugin type (kind) and the name | ||
| * When the version and registry are not available, the function shortlists all plugins which have the kind and name combination | ||
| * If multiple plugins are nominated, the function will follow a precedence policy | ||
| * ___ PLUGIN LOOKUP PRECEDENCE POLICY ___ | ||
| * The search finds the latest versions available for plugins with and without registry by keeping them in two separate buckets | ||
| * 1- If nothing found, simply return undefined | ||
| * 2- If only one of the buckets have value, there will be no comparison. Return the one with the value | ||
| * 3- If both have the value, check the Precedence Logic input and act accordingly | ||
| * 3.1- If the one WITH the registry has a greater version, return it. | ||
| * 3.2- If the one WITHOUT the registry has a greater version, return it | ||
| * 3.2.1- If we have a draw consider the policy flag | ||
| */ | ||
|
|
||
| export interface PluginLookupPrecedenceLogic { | ||
| registryOverVersion: boolean; | ||
| } | ||
|
|
||
| const PLUGIN_LOOKUP_PRECEDENCE_LOGIC: PluginLookupPrecedenceLogic = { registryOverVersion: false }; | ||
|
|
||
| export const lookUpDefaultPluginKey = <T extends PluginType>( | ||
| pluginModuleResourceMapKeys: string[], | ||
| query: Pick<PluginCompoundKey<T>, 'kind' | 'name'>, | ||
| precedenceLogic: PluginLookupPrecedenceLogic = PLUGIN_LOOKUP_PRECEDENCE_LOGIC | ||
| ): string | undefined => { | ||
| type PluginBucket = { key: string; version: string }; | ||
| const latestFoundVersionWithRegistry: PluginBucket = { key: '', version: '' }; | ||
| const latestFoundVersionWithoutRegistry: PluginBucket = { key: '', version: '' }; | ||
|
|
||
| for (const np of pluginModuleResourceMapKeys) { | ||
| if (!np.startsWith(`${query.kind}:${query.name}:`)) continue; | ||
| const split = np.split(':'); | ||
|
|
||
| /** | ||
| * This is not a valid key. A valid key has 4 sections. The registry is optional. It might be empty or holds a value | ||
| */ | ||
| if (split.length !== 4) { | ||
| console.warn(`An invalid Plugin Resource key detected during default plugin lookup: ${np}`); | ||
| continue; | ||
| } | ||
|
|
||
| const [kind, name, registry, version] = split; | ||
| /** | ||
| * Such a case is representing a wrong key and it is not technically possible (Just to be precautious) | ||
| * A resource MUST have kind, name, and version according to its definition and interface | ||
| * So skip this record | ||
| */ | ||
| if (!kind || !name || !version) { | ||
| console.warn(`An invalid Plugin Resource key detected during default plugin lookup: ${np}`); | ||
| continue; | ||
| } | ||
|
|
||
| if (registry) { | ||
| if (!latestFoundVersionWithRegistry.key || gt(version, latestFoundVersionWithRegistry.version!)) { | ||
| latestFoundVersionWithRegistry.key = np; | ||
| latestFoundVersionWithRegistry.version = version; | ||
| } | ||
| continue; | ||
| } | ||
|
|
||
| if (!latestFoundVersionWithoutRegistry.key || gt(version, latestFoundVersionWithoutRegistry.version!)) { | ||
| latestFoundVersionWithoutRegistry.key = np; | ||
| latestFoundVersionWithoutRegistry.version = version; | ||
| } | ||
| } | ||
|
|
||
| /* Before it proceeds with the precedence logic, it checks whether so far anything has been found, if not return undefined */ | ||
| if ([latestFoundVersionWithRegistry, latestFoundVersionWithoutRegistry].every((p) => !p.key)) { | ||
| return undefined; | ||
| } | ||
|
|
||
| if (latestFoundVersionWithRegistry.key && latestFoundVersionWithoutRegistry.key) { | ||
| const { registryOverVersion } = precedenceLogic; | ||
|
|
||
| // Registry version is strictly higher | ||
| if (gt(latestFoundVersionWithRegistry.version, latestFoundVersionWithoutRegistry.version)) { | ||
| return latestFoundVersionWithRegistry.key; | ||
| } | ||
|
|
||
| // None-registry version is strictly higher | ||
| if (gt(latestFoundVersionWithoutRegistry.version, latestFoundVersionWithRegistry.version)) { | ||
| return latestFoundVersionWithoutRegistry.key; | ||
| } | ||
|
|
||
| // Versions are equal - use the tie-breaker | ||
| return registryOverVersion ? latestFoundVersionWithRegistry.key : latestFoundVersionWithoutRegistry.key; | ||
| } | ||
|
|
||
| return latestFoundVersionWithRegistry.key || latestFoundVersionWithoutRegistry?.key; | ||
| }; |
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.
we should reuse the
getPluginModuleCompoundKeyotherwise some strings with undefined will be generated.