Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,16 @@ Install directly from the OCI registry — no need to clone this repo:

```bash
helm install community-plugins-admin oci://quay.io/rh-ai-community-plugins/community-plugins-admin-chart \
--version 0.1.1 \
--namespace community-plugins-admin \
--version 0.1.2 \
--namespace cp-plugins-admin \
--create-namespace
```

Or, if you have a local checkout of the repository:

```bash
helm install community-plugins-admin chart/ \
--namespace community-plugins-admin \
--namespace cp-plugins-admin \
--create-namespace
```

Expand All @@ -83,7 +83,7 @@ config.append({
'tls': False,
'service': {
'name': 'community-plugins-admin',
'namespace': 'community-plugins-admin',
'namespace': 'cp-plugins-admin',
'port': 8080
}
},
Expand All @@ -94,7 +94,7 @@ config.append({
'tls': False,
'service': {
'name': 'community-plugins-admin-bff',
'namespace': 'community-plugins-admin',
'namespace': 'cp-plugins-admin',
'port': 3000
}
}]
Expand Down
85 changes: 80 additions & 5 deletions bff/__tests__/lifecycleService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ const FAKE_METADATA = {
install: { helm: { registry: 'oci://quay.io/charts/my-plugin' } },
remote: { type: 'module-federation', spec: { name: 'myPlugin', scope: 'myPlugin', paths: [{ type: 'route', path: '/my-plugin' }] } },
};
const FAKE_METADATA_WITH_NS = {
...FAKE_METADATA,
install: { namespace: 'cp-my-plugin', helm: { registry: 'oci://quay.io/charts/my-plugin' } },
};

beforeEach(() => {
jest.resetAllMocks();
Expand Down Expand Up @@ -126,7 +130,7 @@ describe('upgradePlugin namespace handling', () => {
'oci://quay.io/charts/my-plugin',
'custom-ns',
'token',
undefined,
{ namespace: 'custom-ns' },
);
});

Expand All @@ -142,7 +146,7 @@ describe('upgradePlugin namespace handling', () => {
'oci://quay.io/charts/my-plugin',
'installed-ns',
'token',
undefined,
{ namespace: 'installed-ns' },
);
});

Expand All @@ -157,7 +161,23 @@ describe('upgradePlugin namespace handling', () => {
'oci://quay.io/charts/my-plugin',
'my-plugin',
'token',
undefined,
{ namespace: 'my-plugin' },
);
});

it('uses metadata namespace when discovery returns null', async () => {
mockDiscoverReleaseNamespace.mockResolvedValue(null);
mockGetPluginMetadata.mockResolvedValue(FAKE_METADATA_WITH_NS as never);

const result = await upgradePlugin('my-plugin', 'token');

expect(result.success).toBe(true);
expect(mockHelmUpgrade).toHaveBeenCalledWith(
'my-plugin',
'oci://quay.io/charts/my-plugin',
'cp-my-plugin',
'token',
{ namespace: 'cp-my-plugin' },
);
});

Expand Down Expand Up @@ -258,7 +278,7 @@ describe('installPlugin', () => {
'oci://quay.io/charts/my-plugin',
'my-plugin',
'token',
undefined,
{ namespace: 'my-plugin' },
);
expect(mockAddPluginToConfig).toHaveBeenCalledWith(
'token',
Expand All @@ -282,7 +302,7 @@ describe('installPlugin', () => {
'oci://quay.io/charts/my-plugin',
'custom-ns',
'token',
undefined,
{ namespace: 'custom-ns' },
);
});

Expand Down Expand Up @@ -361,6 +381,44 @@ describe('installPlugin', () => {
expect(mockHelmUninstall).not.toHaveBeenCalled();
expect(result.steps.find((s) => s.id === 'cleanup')).toBeUndefined();
});

it('uses namespace from plugin metadata when no explicit namespace is provided', async () => {
mockGetPluginMetadata.mockResolvedValue(FAKE_METADATA_WITH_NS as never);

const result = await installPlugin('my-plugin', 'token');

expect(result.success).toBe(true);
expect(mockHelmInstall).toHaveBeenCalledWith(
'my-plugin',
'oci://quay.io/charts/my-plugin',
'cp-my-plugin',
'token',
{ namespace: 'cp-my-plugin' },
);
expect(mockAddPluginToConfig).toHaveBeenCalledWith(
'token',
expect.objectContaining({
backend: expect.objectContaining({
service: { name: 'my-plugin', namespace: 'cp-my-plugin', port: 8080 },
}),
}),
);
});

it('explicit namespace overrides metadata namespace', async () => {
mockGetPluginMetadata.mockResolvedValue(FAKE_METADATA_WITH_NS as never);

const result = await installPlugin('my-plugin', 'token', 'override-ns');

expect(result.success).toBe(true);
expect(mockHelmInstall).toHaveBeenCalledWith(
'my-plugin',
'oci://quay.io/charts/my-plugin',
'override-ns',
'token',
{ namespace: 'override-ns' },
);
});
});

describe('enablePlugin', () => {
Expand All @@ -381,6 +439,23 @@ describe('enablePlugin', () => {
expect(result.steps.every((s) => s.status === 'completed')).toBe(true);
});

it('uses metadata namespace when discovery returns null', async () => {
mockDiscoverReleaseNamespace.mockResolvedValue(null);
mockGetPluginMetadata.mockResolvedValue(FAKE_METADATA_WITH_NS as never);

const result = await enablePlugin('my-plugin', 'token');

expect(result.success).toBe(true);
expect(mockAddPluginToConfig).toHaveBeenCalledWith(
'token',
expect.objectContaining({
backend: expect.objectContaining({
service: { name: 'my-plugin', namespace: 'cp-my-plugin', port: 8080 },
}),
}),
);
});

it('returns failure when plugin is not found in registry', async () => {
mockGetRegistryPlugins.mockResolvedValue([]);

Expand Down
2 changes: 1 addition & 1 deletion bff/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "community-plugins-admin-bff",
"version": "0.1.1",
"version": "0.1.2",
"description": "BFF service for the Community Plugins Admin RHOAI dashboard plugin",
"main": "dist/server.js",
"scripts": {
Expand Down
1 change: 1 addition & 0 deletions bff/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ app.use(express.json({ limit: '100kb' }));

app.get('/api/health', (_req, res) => res.json({ status: 'ok' }));
app.get('/api/config', (_req, res) => res.json({
bffNamespace: process.env.POD_NAMESPACE || 'cp-plugins-admin',
dashboardNamespace: process.env.DASHBOARD_NAMESPACE || 'redhat-ods-applications',
dashboardDeployment: process.env.DASHBOARD_DEPLOYMENT || 'rhods-dashboard',
}));
Expand Down
1 change: 1 addition & 0 deletions bff/src/routes/catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ function buildCatalogPlugin(
if (metadata.install) {
install = {
method: metadata.install.method,
namespace: metadata.install.namespace,
helm: metadata.install.helm
? { chartPath: metadata.install.helm.chart_path, registry: metadata.install.helm.registry }
: undefined,
Expand Down
6 changes: 6 additions & 0 deletions bff/src/routes/lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,17 +75,23 @@ function sendSSE(
'X-Accel-Buffering': 'no',
});

const heartbeat = setInterval(() => {
res.write(': keepalive\n\n');
}, 15_000);

const onProgress: LifecycleProgressCallback = (steps) => {
const data = JSON.stringify({ steps: steps.map(s => ({ ...s })) });
res.write(`event: progress\ndata: ${data}\n\n`);
};

serviceFn(onProgress)
.then((result) => {
clearInterval(heartbeat);
res.write(`event: complete\ndata: ${JSON.stringify(result)}\n\n`);
res.end();
})
.catch(() => {
clearInterval(heartbeat);
const fallback: LifecycleResponse = {
success: false,
message: 'Operation failed',
Expand Down
2 changes: 1 addition & 1 deletion bff/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const PORT = parseInt(process.env.PORT || '3000', 10);
app.listen(PORT, () => {
try {
const baseUrl = getK8sBaseUrl();
console.log(`BFF listening on port ${PORT}`);
console.log(`BFF listening on port ${PORT} (namespace: ${process.env.POD_NAMESPACE ?? 'unknown'})`);
console.log(`K8s API target: ${baseUrl}`);
} catch {
console.error(`BFF listening on port ${PORT}`);
Expand Down
2 changes: 1 addition & 1 deletion bff/src/services/helmService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as os from 'os';
import * as path from 'path';
import { getK8sBaseUrl } from '../utils/k8sClient';

const HELM_TIMEOUT_MS = 120_000;
const HELM_TIMEOUT_MS = 330_000;
const HELM_BIN = process.env.HELM_BIN || 'helm';

export interface HelmRelease {
Expand Down
19 changes: 11 additions & 8 deletions bff/src/services/lifecycleService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ function markFailed(step: LifecycleStep, error: string): void {
async function resolvePluginChart(pluginName: string): Promise<{
chart: string;
repo: string;
namespace?: string;
mfName: string;
hasBff: boolean;
routePath: string;
Expand All @@ -61,12 +62,13 @@ async function resolvePluginChart(pluginName: string): Promise<{
throw new Error(`Plugin "${pluginName}" has no Helm chart configured`);
}

const ns = metadata.install.namespace;
const mfName = metadata.remote?.spec?.name ?? metadata.remote?.spec?.scope ?? kebabToCamelScope(pluginName);
const hasBff = !!metadata.bff_image;
const routeSpec = metadata.remote?.spec?.paths?.find((p) => p.type === 'route');
const routePath = routeSpec?.path ?? `/${pluginName}`;

return { chart, repo: regEntry.repo, mfName, hasBff, routePath };
return { chart, repo: regEntry.repo, namespace: ns, mfName, hasBff, routePath };
}

export async function installPlugin(
Expand All @@ -83,19 +85,20 @@ export async function installPlugin(
];
onProgress?.(steps);

let pluginInfo: Awaited<ReturnType<typeof resolvePluginChart>> | undefined;
try {
markRunning(steps[0]);
onProgress?.(steps);
const pluginInfo = await resolvePluginChart(pluginName);
pluginInfo = await resolvePluginChart(pluginName);
markCompleted(steps[0]);
onProgress?.(steps);

const ns = namespace ?? pluginName;
const ns = namespace ?? pluginInfo.namespace ?? pluginName;
const releaseName = pluginName;

markRunning(steps[1]);
onProgress?.(steps);
await helmInstall(releaseName, pluginInfo.chart, ns, token, values);
await helmInstall(releaseName, pluginInfo.chart, ns, token, { namespace: ns, ...values });
markCompleted(steps[1]);
onProgress?.(steps);

Expand Down Expand Up @@ -132,7 +135,7 @@ export async function installPlugin(

const helmStep = steps.find((s) => s.id === 'helm-install');
if (helmStep && (helmStep.status === 'completed' || helmStep.status === 'failed')) {
const ns = namespace ?? pluginName;
const ns = namespace ?? pluginInfo?.namespace ?? pluginName;
const cleanupStep = createStep('cleanup', 'Rolling back Helm release');
steps.push(cleanupStep);
markRunning(cleanupStep);
Expand Down Expand Up @@ -174,11 +177,11 @@ export async function upgradePlugin(
markCompleted(steps[0]);
onProgress?.(steps);

const ns = namespace ?? (await discoverReleaseNamespace(pluginName, token)) ?? pluginName;
const ns = namespace ?? (await discoverReleaseNamespace(pluginName, token)) ?? pluginInfo.namespace ?? pluginName;

markRunning(steps[1]);
onProgress?.(steps);
await helmUpgrade(pluginName, pluginInfo.chart, ns, token, values);
await helmUpgrade(pluginName, pluginInfo.chart, ns, token, { namespace: ns, ...values });
markCompleted(steps[1]);
onProgress?.(steps);

Expand Down Expand Up @@ -284,7 +287,7 @@ export async function enablePlugin(
markRunning(steps[1]);
onProgress?.(steps);

const ns = (await discoverReleaseNamespace(pluginName, token)) ?? pluginName;
const ns = (await discoverReleaseNamespace(pluginName, token)) ?? pluginInfo.namespace ?? pluginName;
const mfEntry: ModuleFederationEntry = {
name: pluginInfo.mfName,
backend: {
Expand Down
2 changes: 2 additions & 0 deletions bff/src/types/catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export interface PluginImage {

export interface PluginInstall {
method: 'automatic' | 'assisted' | 'manual';
namespace?: string;
helm?: {
chart_path?: string;
registry?: string;
Expand Down Expand Up @@ -99,6 +100,7 @@ export interface CatalogPluginRemote {

export interface CatalogPluginInstall {
method: 'automatic' | 'assisted' | 'manual';
namespace?: string;
helm?: {
chartPath?: string;
registry?: string;
Expand Down
4 changes: 2 additions & 2 deletions chart/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ apiVersion: v2
name: community-plugins-admin-chart
description: A Helm chart for deploying the RHOAI Community Plugins Admin plugin
type: application
version: 0.1.1
appVersion: "0.1.1"
version: 0.1.2
appVersion: "0.1.2"
keywords:
- rhoai
- openshift-ai
Expand Down
7 changes: 7 additions & 0 deletions chart/templates/_helpers.tpl
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
{{/*
Target namespace for all namespaced resources.
*/}}
{{- define "community-plugins-admin.namespace" -}}
{{- .Values.namespace | default .Release.Namespace }}
{{- end }}

{{/*
Expand the name of the chart.
*/}}
Expand Down
2 changes: 1 addition & 1 deletion chart/templates/bff-clusterrolebinding.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@ roleRef:
subjects:
- kind: ServiceAccount
name: {{ $saName }}
namespace: {{ .Release.Namespace }}
namespace: {{ include "community-plugins-admin.namespace" . }}
{{- end }}
5 changes: 5 additions & 0 deletions chart/templates/bff-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "community-plugins-admin.fullname" . }}-bff
namespace: {{ include "community-plugins-admin.namespace" . }}
labels:
{{- include "community-plugins-admin.labels" . | nindent 4 }}
app.kubernetes.io/component: bff
Expand Down Expand Up @@ -48,6 +49,10 @@ spec:
containerPort: {{ .Values.bff.service.targetPort }}
protocol: TCP
env:
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: DASHBOARD_NAMESPACE
value: {{ .Values.bff.dashboardNamespace | quote }}
- name: DASHBOARD_DEPLOYMENT
Expand Down
1 change: 1 addition & 0 deletions chart/templates/bff-service.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ apiVersion: v1
kind: Service
metadata:
name: {{ include "community-plugins-admin.fullname" . }}-bff
namespace: {{ include "community-plugins-admin.namespace" . }}
labels:
{{- include "community-plugins-admin.labels" . | nindent 4 }}
app.kubernetes.io/component: bff
Expand Down
Loading
Loading