Skip to content

[FEATURE] fetch plugin based on version#128

Open
shahrokni wants to merge 1 commit intomainfrom
feat/plugin_versioning_front
Open

[FEATURE] fetch plugin based on version#128
shahrokni wants to merge 1 commit intomainfrom
feat/plugin_versioning_front

Conversation

@shahrokni
Copy link
Copy Markdown
Contributor

@shahrokni shahrokni commented May 4, 2026

perses/perses#1186

⚠️ BACKEND NEEDS TO BE UPDAED. EXPLAINED AT THE END OF DESCRIPTION

Different Plugins Versions and Registries

So far a plugin could be imported by its name only.

const remoteEntryURL = baseURL ? `${baseURL}/${name}/mf-manifest.json` : `/plugins/${name}/mf-manifest.json`;

The backend has recently introduced a feature by which multiple versions of the same plugin could be available.
This means if the consumer of the Plugin Registry provided the version and registry, the exact desired plugin could be loaded in the front!

To achieve the mentioned goal the new URL will carry the version and registry as well. If not provided by the consumer, the front uses the default values introduced by the backend which are latest and perses.dev

 const remoteEntryURL = baseURL
      ? `${baseURL}/${name}/${registry || DEFAULT_PLUGIN_REGISTRY}/${version || DEFAULT_PLUGIN_VERSION}/mf-manifest.json`
      : `/plugins/${name}/${registry || DEFAULT_PLUGIN_REGISTRY}/${version || DEFAULT_PLUGIN_VERSION}/mf-manifest.json`;

/*ESAMPLE FROM MY LOCAL MACHINE BY DEFAULT VALUES*/
/* http://localhost:3000/plugins/TimeSeriesChart/perses.dev/latest/mf-manifest.json */

⚠️ Backend needs to do

Backend should be adjusted to manage the new URL. I checked the backend and understood that at the moment the intercepted GET request is translated to the address of the files on the disk. Please take a look at (ui\endpoint.go)
I believe the following shared code should be adjusted for the new URL that I just explained.

if devEnvironment == nil {
		// In that case, we need to read the requested files from the file system.
		// The First thing to do is to replace the URL path with the local path of the plugin.
		localPath := strings.Replace(req.URL.Path, fmt.Sprintf("%s/plugins/%s", f.apiPrefix, pluginName), loaded.LocalPath, 1)

		// Then we just need to rely on the echo router to serve the file.

		// X-Content-Type-Options: nosniff is an HTTP response header that tells browsers: "Do not try to guess the content type — trust the Content-Type header I sent you."
		// Without it, browsers perform "MIME sniffing". For example, a file served as text/plain could be sniffed as text/html and executed as HTML, which could open the door to XSS attacks.
		// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Content-Type-Options
		c.Response().Header().Set("X-Content-Type-Options", "nosniff")
		return c.File(localPath)
	}

There is also a TODO comment in backend that I believe could to be addressed now

// TODO: These hardcoded values should be replaced once the frontend is able to manage multiple registries and plugin versions.
	// This suppose to update the plugin system first.
	// These hardocded values are matching the default behavior of the plugin system and it helps to keep backward compatibility.
	loaded, isLoaded := f.pluginService.GetLoadedPlugin(pluginName, pluginModel.LatestVersion, pluginModel.DefaultRegistry)
	if !isLoaded || !loaded.Module.Status.IsLoaded {
		logrus.Errorf("unable to find the plugin => %s", pluginName)
		return apiinterface.NotFoundError
	}

Checklist

  • Pull request has a descriptive title and context useful to a reviewer.
  • Pull request title follows the [<catalog_entry>] <commit message> naming convention using one of the
    following catalog_entry values: FEATURE, ENHANCEMENT, BUGFIX, BREAKINGCHANGE, DOC,IGNORE.
  • All commits have DCO signoffs.

UI Changes

  • Changes that impact the UI include screenshots and/or screencasts of the relevant changes.
  • Code follows the UI guidelines.
  • E2E tests are stable and unlikely to be flaky.
    See e2e docs for more details. Common issues include:
    • Is the data inconsistent? You need to mock API requests.
    • Does the time change? You need to use consistent time values or mock time utilities.
    • Does it have loading states? You need to wait for loading to complete.

@shahrokni shahrokni force-pushed the feat/plugin_versioning_front branch 9 times, most recently from 5d1fbee to 4d0516d Compare May 6, 2026 10:37
Signed-off-by: Seyed Mahmoud SHAHROKNI <seyedmahmoud.shahrokni@amadeus.com>

Signed-off-by: Seyed Mahmoud SHAHROKNI <seyedmahmoud.shahrokni@amadeus.com>

Signed-off-by: Seyed Mahmoud SHAHROKNI <seyedmahmoud.shahrokni@amadeus.com>

Signed-off-by: Seyed Mahmoud SHAHROKNI <seyedmahmoud.shahrokni@amadeus.com>

Signed-off-by: Seyed Mahmoud SHAHROKNI <seyedmahmoud.shahrokni@amadeus.com>

Signed-off-by: Seyed Mahmoud SHAHROKNI <seyedmahmoud.shahrokni@amadeus.com>

Signed-off-by: Seyed Mahmoud SHAHROKNI <seyedmahmoud.shahrokni@amadeus.com>

Signed-off-by: Seyed Mahmoud SHAHROKNI <seyedmahmoud.shahrokni@amadeus.com>

Signed-off-by: Seyed Mahmoud SHAHROKNI <seyedmahmoud.shahrokni@amadeus.com>

Signed-off-by: Seyed Mahmoud SHAHROKNI <seyedmahmoud.shahrokni@amadeus.com>

Signed-off-by: Seyed Mahmoud SHAHROKNI <seyedmahmoud.shahrokni@amadeus.com>

Signed-off-by: Seyed Mahmoud SHAHROKNI <seyedmahmoud.shahrokni@amadeus.com>

Signed-off-by: Seyed Mahmoud SHAHROKNI <seyedmahmoud.shahrokni@amadeus.com>

Signed-off-by: Seyed Mahmoud SHAHROKNI <seyedmahmoud.shahrokni@amadeus.com>

Signed-off-by: Seyed Mahmoud SHAHROKNI <seyedmahmoud.shahrokni@amadeus.com>

Signed-off-by: Seyed Mahmoud SHAHROKNI <seyedmahmoud.shahrokni@amadeus.com>
@shahrokni shahrokni force-pushed the feat/plugin_versioning_front branch from 4d0516d to d604ac7 Compare May 6, 2026 11:03
@shahrokni shahrokni marked this pull request as ready for review May 6, 2026 12:31
@shahrokni shahrokni requested a review from a team as a code owner May 6, 2026 12:31
@shahrokni shahrokni requested review from Nexucis and jgbernalp May 6, 2026 12:32
metadata: {
name: 'Fake Plugin Module for Tests',
version: '0',
registry: '',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this could be undefined rather than empty.


for (const plugin of resource.spec.plugins) {
const remotePluginModule = await loadPlugin(pluginModuleName, plugin.spec.name, pluginsAssetsPath);
const remotePluginModule = await loadPlugin(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If we are changing the signature, probably is better to use an object


// Index the plugin by type and kind to point at the module that contains it
const key = getTypeAndKindKey(kind, name);
const key = getTypeAndKindKey(kind, name, registry || '', version || '');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

maybe is better that the function can accept undefined values and will resolve the key, rather than adding logic to the parameters. Also it would be better to use an object so is more readable

/*
Question: Why does it register the remote with the module name?!
This way the URL will be based on the module name and not the plugin!
Unless => the backend has a map of plugins with the module key, And when `${moduleName}/${pluginName}` is called the, the plugin can be found with version and registry
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

actually the backend is storing the plugins in two different ways.

As an entry point in the plugin management, the plugins are registered using the plugin module name / plugin module registry. And then with this key, you have the list of version registered.

Then for the schema management per plugins, the key is the actual name of the plugin (not anymore the plugin module name) with the registry. Which give you access of a list of the various version supported for the given schema.

The same logic apply for the migration script.

But if you want to simplify this vision, the key / entrypoint in the plugin registry is the plugin module name + registry.

For example, if you consider the Prometheus plugin, it is actually a module that contains multiple plugins. So Prometheus is the name of the module and (for example) PrometheusTimeseriesQuery is one of the plugin provided by this module.

But for the frontend when getting the js files for the PrometheusTimeseriesQuery plugin, it will actually call the backend with the name of the module Prometheus and not PrometheusTimeseriesQuery because the js files of this particular plugin are mixed up with the rest of others plugin contained in the module. And the backend cannot give you exactly the files for this particular plugin, only for the module and then frontend is finding the one it is looking for.

Is it clearer or on the contrary even more fuzzy ?

1,
'multi-plugin-module',
'plugin1',
undefined,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

an object parameter will avoid this undefined, undefined fields

@Nexucis
Copy link
Copy Markdown
Member

Nexucis commented May 6, 2026

Thank you @shahrokni for updating the frontend regarding the plugin multi version management !

Indeed the backend will need to be updated at some point, but hopefully that should be as easy as to update the regexp

capturingPluginName = regexp.MustCompile(`/plugins/([a-zA-Z0-9_-]+)/?.*`)

to catch the registry and the version.


Conclusion:
If my assumption is correct, I follow the same pattern and add the version and registry to the URL.
In my opinion: it would be better to be moduleName/pluginName/registry/version At least for readability?
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We don't load individual plugins, we load the whole plugin module, which has specific plugin exports internally.

import { PersesPlugin, RemotePluginModule } from './PersesPlugin.types';

export const DEFAULT_PLUGIN_REGISTRY = 'perses.dev';
export const DEFAULT_PLUGIN_VERSION = 'latest';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

IIUC from the backend, the default version is empty for backwards compatibility, which means the latest version.

pluginLoader: { getInstalledPlugins, importPluginModule },
children,
defaultPluginKinds,
pluginPreferences,
Copy link
Copy Markdown
Contributor

@jgbernalp jgbernalp May 6, 2026

Choose a reason for hiding this comment

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

is this needed? the plugin versions are defined inside dashboards, if not defined it will use whatever is available in the backend.

export interface PluginModuleMetadata {
name: string;
version: string;
registry: string;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this will break existing plugins, we probably want this to be optional.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

yeah this can be optional. It is the case in the equivalent go struct

import * as ReactRouterDOM from 'react-router-dom';
import { PersesPlugin, RemotePluginModule } from './PersesPlugin.types';

export const DEFAULT_PLUGIN_REGISTRY = 'perses.dev';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Not sure how this is handled in the backend, but this won't be a valid url path unless escaped. By default this is probably empty to use the core plugins

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

by default if it is empty, then the value is equal to perses.dev

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants