From 2613b2fb3efbf07e4cb7f148f55a198a34ff0983 Mon Sep 17 00:00:00 2001 From: Sergey Pronin Date: Tue, 1 Sep 2026 14:40:53 +0300 Subject: [PATCH] feat: cluster-scope plugin API paths and use host-provided basePath Signed-off-by: Sergey Pronin --- backend/main.go | 18 +++++++++--------- package.json | 2 +- src/components/Drawer.ts | 6 +++--- src/components/Row.ts | 5 ++--- src/icons.ts | 15 +++++++-------- src/main.tsx | 7 +++---- src/runtime.ts | 2 ++ 7 files changed, 27 insertions(+), 28 deletions(-) diff --git a/backend/main.go b/backend/main.go index aac5b1a..abb47c1 100644 --- a/backend/main.go +++ b/backend/main.go @@ -191,10 +191,10 @@ func (c *catalogCache) fetchLocked() ([]byte, error) { // raw.githubusercontent.com). The host UI enforces a `default-src 'self'` // CSP that blocks those cross-origin image loads, so the backend rewrites // every absolute icon URL in the catalog/summary responses to a *relative* -// path (e.g. `api/icon/`). The frontend then prepends the plugin's -// runtime mount prefix (`/v1/plugins/`) — derived from the -// SDK-supplied pluginName — so the URL is always correct regardless of the -// release name the chart was installed under. +// path (e.g. `api/icon/`). The frontend then prepends the host-provided +// proxy base (the SDK's `api.basePath`, i.e. +// `/v1/clusters//plugins/`) so the URL is always correct +// regardless of the cluster or the release name the chart was installed under. // // Keys are SHA-256 of the upstream URL: stable, opaque, and content-addressed // by URL (no caller-supplied URL parameter, no SSRF surface). Only URLs that @@ -463,8 +463,8 @@ func makeCatalogHandler(cache *catalogCache, icons *iconProxy) http.HandlerFunc // - GET /v1/clusters/{cluster}/providers — Kubernetes-style list, one item // per installed Provider CR. The catalog name lives at metadata.name, // the chart version at metadata.labels["app.kubernetes.io/version"]. -// - GET /v1/plugins — flat list of host-registered generic plugins. Catalog -// name lives at .name, version at .version. +// - GET /v1/clusters/{cluster}/plugins — flat list of host-registered generic +// plugins. Catalog name lives at .name, version at .version. // // We surface a non-fatal error if either call fails so the UI can still show // partial state. A 404 on either endpoint is treated as "feature absent" and @@ -482,7 +482,7 @@ func fetchInstalled(apiBase, cluster, authHeader string) ([]installedExtension, } else { all = append(all, providers...) } - if plugins, err := fetchPlugins(apiBase, authHeader); err != nil { + if plugins, err := fetchPlugins(apiBase, cluster, authHeader); err != nil { errs = append(errs, "plugins: "+err.Error()) } else { all = append(all, plugins...) @@ -524,8 +524,8 @@ func fetchProviders(apiBase, cluster, authHeader string) ([]installedExtension, return out, nil } -func fetchPlugins(apiBase, authHeader string) ([]installedExtension, error) { - body, err := everestGET(apiBase+"/v1/plugins", authHeader) +func fetchPlugins(apiBase, cluster, authHeader string) ([]installedExtension, error) { + body, err := everestGET(apiBase+"/v1/clusters/"+url.PathEscape(cluster)+"/plugins", authHeader) if err != nil { return nil, err } diff --git a/package.json b/package.json index f38c6de..dba57b4 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "react-dom": "^18.2.0" }, "devDependencies": { - "@openeverest/plugin-sdk": "^0.1.0", + "@openeverest/plugin-sdk": "^0.3.0", "@types/react": "^18.3.28", "@vitejs/plugin-react-swc": "^4.3.3", "typescript": "^5.2.2", diff --git a/src/components/Drawer.ts b/src/components/Drawer.ts index 6bdaecd..19630a1 100644 --- a/src/components/Drawer.ts +++ b/src/components/Drawer.ts @@ -91,8 +91,8 @@ function useAppBarOffset(): number { return offset; } -export function Drawer(props: { entry: CatalogEntry; pluginName: string; onClose: () => void }): any { - const { entry, pluginName, onClose } = props; +export function Drawer(props: { entry: CatalogEntry; onClose: () => void }): any { + const { entry, onClose } = props; const isGated = entry.access === 'gated'; const version = isGated ? null : defaultChannelVersion(entry); const install = isGated ? null : helmInstallCommand(entry); @@ -115,7 +115,7 @@ export function Drawer(props: { entry: CatalogEntry; pluginName: string; onClose 'div', { style: styles.drawerHeader }, h(IconImg, { - src: resolveIconSrc(entry.icon, pluginName), + src: resolveIconSrc(entry.icon), style: { width: 40, height: 40 }, }), h( diff --git a/src/components/Row.ts b/src/components/Row.ts index 94aa6b2..636cb2a 100644 --- a/src/components/Row.ts +++ b/src/components/Row.ts @@ -6,10 +6,9 @@ import type { CatalogEntry } from '../types'; export function Row(props: { entry: CatalogEntry; - pluginName: string; onSelect: (e: CatalogEntry) => void; }): any { - const { entry, pluginName, onSelect } = props; + const { entry, onSelect } = props; const version = defaultChannelVersion(entry); return h( 'tr', @@ -22,7 +21,7 @@ export function Row(props: { 'td', { style: { ...styles.td, ...styles.iconCell } }, h(IconImg, { - src: resolveIconSrc(entry.icon, pluginName), + src: resolveIconSrc(entry.icon), style: styles.iconImg, }), ), diff --git a/src/icons.ts b/src/icons.ts index f5016b1..0b2cf33 100644 --- a/src/icons.ts +++ b/src/icons.ts @@ -1,6 +1,6 @@ // Icon resolution + a self-healing that falls back to an inline // placeholder when an upstream icon fails to load. -import { h } from './runtime'; +import { h, pluginBasePath } from './runtime'; export const ICON_FALLBACK_DATA_URI = "data:image/svg+xml;utf8," + @@ -13,11 +13,10 @@ export const ICON_FALLBACK_DATA_URI = const failedIconSrcs = new Set(); // resolveIconSrc converts whatever the backend put into entry.icon into a -// usable . The backend now emits *relative* paths (e.g. -// 'api/icon/') for proxied icons; we prepend the plugin's runtime -// mount prefix here so the URL is correct regardless of the release name -// the chart was installed under. -export function resolveIconSrc(rawIcon: string | undefined, pluginName: string): string { +// usable . The backend emits *relative* paths (e.g. 'api/icon/') +// for proxied icons; we prepend the host-provided proxy base so the URL routes +// through the OpenEverest proxy for the current cluster. +export function resolveIconSrc(rawIcon: string | undefined): string { if (!rawIcon) return ICON_FALLBACK_DATA_URI; if ( rawIcon.startsWith('data:') || @@ -27,8 +26,8 @@ export function resolveIconSrc(rawIcon: string | undefined, pluginName: string): ) { return rawIcon; } - if (!pluginName) return ICON_FALLBACK_DATA_URI; - return `/v1/plugins/${pluginName}/${rawIcon}`; + if (!pluginBasePath) return ICON_FALLBACK_DATA_URI; + return `${pluginBasePath}/${rawIcon}`; } export function IconImg(props: { src: string; alt?: string; style?: any }): any { diff --git a/src/main.tsx b/src/main.tsx index 213de9e..8763f54 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -15,7 +15,6 @@ import type { PluginRegisterFn, PluginApi, - PluginRouteProps, } from '@openeverest/plugin-sdk'; import { React, h, initRuntime } from './runtime'; @@ -31,7 +30,7 @@ import type { CatalogEntry, FilterState, SummaryResponse } from './types'; // Page // --------------------------------------------------------------------------- -const HubPage = (props: PluginRouteProps): any => { +const HubPage = (): any => { const [data, setData] = React.useState(null); const [installedKeys, setInstalledKeys] = React.useState | null>(null); const [installedError, setInstalledError] = React.useState(null); @@ -195,10 +194,10 @@ const HubPage = (props: PluginRouteProps): any => { h('th', { style: styles.th }, 'Maturity'), ), ), - h('tbody', null, ...filtered.map((entry) => Row({ entry, pluginName: props.pluginName, onSelect: setSelected }))), + h('tbody', null, ...filtered.map((entry) => Row({ entry, onSelect: setSelected }))), ), - selected ? h(Drawer, { entry: selected, pluginName: props.pluginName, onClose: () => setSelected(null) }) : null, + selected ? h(Drawer, { entry: selected, onClose: () => setSelected(null) }) : null, ); }; diff --git a/src/runtime.ts b/src/runtime.ts index f978e16..be12db7 100644 --- a/src/runtime.ts +++ b/src/runtime.ts @@ -7,10 +7,12 @@ import type { PluginApi } from '@openeverest/plugin-sdk'; export let React: PluginApi['React']; export let pluginFetch: PluginApi['fetch']; +export let pluginBasePath: PluginApi['basePath']; export function initRuntime(api: PluginApi): void { React = api.React; pluginFetch = api.fetch.bind(api); + pluginBasePath = api.basePath; } // h is a thin React.createElement wrapper so components can be authored