diff --git a/.changeset/purple-cougars-fry.md b/.changeset/purple-cougars-fry.md new file mode 100644 index 000000000..1b30281b9 --- /dev/null +++ b/.changeset/purple-cougars-fry.md @@ -0,0 +1,8 @@ +--- +'@internal/plugin-dynamic-plugins-info-backend': patch +'backend': patch +--- + +Adds a 'dynamic-plugins-info' backend plugin + +This plugin depends on the `backend-plugin-manager` and lists all the dynamic plugins installed in the dynamic plugins root folder. diff --git a/.dockerignore b/.dockerignore index cab9b430a..fc9c95ddd 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,6 +4,7 @@ **/node_modules plugins !plugins/scalprum-backend +!plugins/dynamic-plugins-info-backend *.local.yaml coverage dist-types diff --git a/docker/Dockerfile b/docker/Dockerfile index 00ad0db86..8ecb5c3ac 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -50,6 +50,7 @@ COPY $EXTERNAL_SOURCE_NESTED/package.json $EXTERNAL_SOURCE_NESTED/yarn.lock ./ COPY $EXTERNAL_SOURCE_NESTED/packages/app/package.json ./packages/app/package.json COPY $EXTERNAL_SOURCE_NESTED/packages/backend/package.json ./packages/backend/package.json COPY $EXTERNAL_SOURCE_NESTED/plugins/scalprum-backend/package.json ./plugins/scalprum-backend/package.json +COPY $EXTERNAL_SOURCE_NESTED/plugins/dynamic-plugins-info-backend/package.json ./plugins/dynamic-plugins-info-backend/package.json RUN $YARN install --frozen-lockfile --network-timeout 600000 @@ -77,10 +78,9 @@ COPY --from=build $CONTAINER_SOURCE/yarn.lock \ $CONTAINER_SOURCE/packages/backend/dist/skeleton.tar.gz \ $CONTAINER_SOURCE/packages/backend/dist/bundle.tar.gz \ ./ - ENV TARBALL_PATH=. RUN tar xzf $TARBALL_PATH/skeleton.tar.gz; tar xzf $TARBALL_PATH/bundle.tar.gz; \ - rm -f $TARBALL_PATH/skeleton.tar.gz $TARBALL_PATH/bundle.tar.gz + rm -f $TARBALL_PATH/skeleton.tar.gz $TARBALL_PATH/bundle.tar.gz # Copy app-config files needed in runtime # Upstream only diff --git a/docker/brew.Dockerfile b/docker/brew.Dockerfile index b3c3ac39d..289572bae 100644 --- a/docker/brew.Dockerfile +++ b/docker/brew.Dockerfile @@ -51,6 +51,7 @@ COPY $EXTERNAL_SOURCE_NESTED/package.json $EXTERNAL_SOURCE_NESTED/yarn.lock ./ COPY $EXTERNAL_SOURCE_NESTED/packages/app/package.json ./packages/app/package.json COPY $EXTERNAL_SOURCE_NESTED/packages/backend/package.json ./packages/backend/package.json COPY $EXTERNAL_SOURCE_NESTED/plugins/scalprum-backend/package.json ./plugins/scalprum-backend/package.json +COPY $EXTERNAL_SOURCE_NESTED/plugins/dynamic-plugins-info-backend/package.json ./plugins/dynamic-plugins-info-backend/package.json # Downstream only - debugging # COPY $REMOTE_SOURCES/ ./ @@ -64,9 +65,9 @@ COPY $EXTERNAL_SOURCE_NESTED/plugins/scalprum-backend/package.json ./plugins/sca # Downstream only - Cachito configuration # see https://docs.engineering.redhat.com/pages/viewpage.action?pageId=228017926#UpstreamSources(Cachito,ContainerFirst)-CachitoIntegrationfornpm COPY $REMOTE_SOURCES/upstream1/cachito.env \ - $REMOTE_SOURCES/upstream1/app/registry-ca.pem \ - $REMOTE_SOURCES/upstream1/app/distgit/containers/rhdh-hub/.npmrc \ - ./ + $REMOTE_SOURCES/upstream1/app/registry-ca.pem \ + $REMOTE_SOURCES/upstream1/app/distgit/containers/rhdh-hub/.npmrc \ + ./ # registry=https://cachito-nexus.engineering.redhat.com/repository/cachito-yarn-914335/ # email=noreply@domain.local # always-auth=true @@ -150,7 +151,7 @@ RUN find dynamic-plugins -type f -not -name 'dist' -delete ENV TARBALL_PATH=./packages/backend/dist RUN tar xzf $TARBALL_PATH/skeleton.tar.gz; tar xzf $TARBALL_PATH/bundle.tar.gz; \ - rm -f $TARBALL_PATH/skeleton.tar.gz $TARBALL_PATH/bundle.tar.gz + rm -f $TARBALL_PATH/skeleton.tar.gz $TARBALL_PATH/bundle.tar.gz # Copy app-config files needed in runtime # Upstream only @@ -177,24 +178,24 @@ WORKDIR $CONTAINER_SOURCE/ # Downstream only - install techdocs dependencies using cachito sources COPY $REMOTE_SOURCES/upstream2 ./upstream2/ RUN microdnf update -y && \ - microdnf install -y python3.11 python3.11-pip python3.11-devel make cmake cpp gcc gcc-c++; \ - ln -s /usr/bin/pip3.11 /usr/bin/pip3; \ - ln -s /usr/bin/pip3.11 /usr/bin/pip; \ - # ls -la $CONTAINER_SOURCE/ $CONTAINER_SOURCE/upstream2/ $CONTAINER_SOURCE/upstream2/app/distgit/containers/rhdh-hub/docker/ || true; \ - cat $CONTAINER_SOURCE/upstream2/cachito.env && \ - # cachito.env contains path to cert: - # export PIP_CERT=/remote-source/upstream2/app/package-index-ca.pem - source $CONTAINER_SOURCE/upstream2/cachito.env && \ - # fix ownership for pip install folder - mkdir -p /opt/app-root/src/.cache/pip && chown -R root:root /opt/app-root && \ - # ls -ld /opt/ /opt/app-root /opt/app-root/src/ /opt/app-root/src/.cache /opt/app-root/src/.cache/pip || true; \ - pushd $CONTAINER_SOURCE/upstream2/app/distgit/containers/rhdh-hub/docker/ >/dev/null && \ - set -xe; \ - python3.11 -V; pip3.11 -V; \ - pip3.11 install --user --no-cache-dir --upgrade pip setuptools pyyaml; \ - pip3.11 install --user --no-cache-dir -r requirements.txt -r requirements-build.txt; \ - popd >/dev/null; \ - microdnf clean all; rm -fr $CONTAINER_SOURCE/upstream2 + microdnf install -y python3.11 python3.11-pip python3.11-devel make cmake cpp gcc gcc-c++; \ + ln -s /usr/bin/pip3.11 /usr/bin/pip3; \ + ln -s /usr/bin/pip3.11 /usr/bin/pip; \ + # ls -la $CONTAINER_SOURCE/ $CONTAINER_SOURCE/upstream2/ $CONTAINER_SOURCE/upstream2/app/distgit/containers/rhdh-hub/docker/ || true; \ + cat $CONTAINER_SOURCE/upstream2/cachito.env && \ + # cachito.env contains path to cert: + # export PIP_CERT=/remote-source/upstream2/app/package-index-ca.pem + source $CONTAINER_SOURCE/upstream2/cachito.env && \ + # fix ownership for pip install folder + mkdir -p /opt/app-root/src/.cache/pip && chown -R root:root /opt/app-root && \ + # ls -ld /opt/ /opt/app-root /opt/app-root/src/ /opt/app-root/src/.cache /opt/app-root/src/.cache/pip || true; \ + pushd $CONTAINER_SOURCE/upstream2/app/distgit/containers/rhdh-hub/docker/ >/dev/null && \ + set -xe; \ + python3.11 -V; pip3.11 -V; \ + pip3.11 install --user --no-cache-dir --upgrade pip setuptools pyyaml; \ + pip3.11 install --user --no-cache-dir -r requirements.txt -r requirements-build.txt; \ + popd >/dev/null; \ + microdnf clean all; rm -fr $CONTAINER_SOURCE/upstream2 # Downstream only - copy from build, not cleanup stage COPY --from=build --chown=1001:1001 $CONTAINER_SOURCE/ ./ diff --git a/packages/backend/package.json b/packages/backend/package.json index bdaed2c0b..470250a67 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -28,6 +28,7 @@ "@backstage/plugin-auth-node": "0.4.0", "@backstage/plugin-catalog-backend": "1.14.0", "@backstage/plugin-catalog-backend-module-openapi": "0.1.23", + "@internal/plugin-dynamic-plugins-info-backend": "0.1.0", "@backstage/plugin-events-backend": "0.2.15", "@backstage/plugin-events-node": "0.2.15", "@backstage/plugin-permission-backend": "0.5.29", @@ -44,6 +45,7 @@ "better-sqlite3": "9.0.0", "express": "4.18.2", "express-prom-bundle": "6.6.0", + "express-promise-router": "4.1.1", "isolated-vm": "4.6.0", "pg": "8.11.3", "prom-client": "15.0.0", diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index ad230632f..0602ea9c2 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -23,6 +23,7 @@ import { DefaultIdentityClient } from '@backstage/plugin-auth-node'; import { DefaultEventBroker } from '@backstage/plugin-events-backend'; import { ServerPermissionClient } from '@backstage/plugin-permission-node'; import { createRouter as scalprumRouter } from '@internal/plugin-scalprum-backend'; +import { createRouter as dynamicPluginsInfoRouter } from '@internal/plugin-dynamic-plugins-info-backend'; import { RequestHandler, Router } from 'express'; import { metricsHandler } from './metrics'; import app from './plugins/app'; @@ -184,13 +185,25 @@ async function main() { const apiRouter = Router(); // Scalprum frontend plugins provider - const scalprumEmv = useHotMemoize(module, () => createEnv('scalprum')); + const scalprumEnv = useHotMemoize(module, () => createEnv('scalprum')); apiRouter.use( '/scalprum', await scalprumRouter({ - logger: scalprumEmv.logger, + logger: scalprumEnv.logger, + pluginManager, + discovery: scalprumEnv.discovery, + }), + ); + + // Dynamic plugins info provider + const dynamicPluginsInfoEnv = useHotMemoize(module, () => + createEnv('dynamic-plugins-info'), + ); + apiRouter.use( + '/dynamic-plugins-info', + await dynamicPluginsInfoRouter({ + logger: dynamicPluginsInfoEnv.logger, pluginManager, - discovery: scalprumEmv.discovery, }), ); diff --git a/plugins/dynamic-plugins-info-backend/.eslintrc.js b/plugins/dynamic-plugins-info-backend/.eslintrc.js new file mode 100644 index 000000000..e2a53a6ad --- /dev/null +++ b/plugins/dynamic-plugins-info-backend/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/dynamic-plugins-info-backend/README.md b/plugins/dynamic-plugins-info-backend/README.md new file mode 100644 index 000000000..1317a9e9a --- /dev/null +++ b/plugins/dynamic-plugins-info-backend/README.md @@ -0,0 +1,12 @@ +# dynamic-plugins-info + +Welcome to the dynamic-plugins-info backend plugin! + +This plugin depends on the `backend-plugin-manager` and lists all the dynamic plugins installed in the dynamic plugins root folder. + +## Getting started + +This plugin has been added to the backend app in this repository, meaning you'll be able to access it by running `yarn +start-backend` in the root directory, and then navigating to [/api/dynamic-plugins-info](http://localhost:7007/api/dynamic-plugins-info). + +To view the list of installed dynamic plugins, navigate to `http://localhost:7007/api/dynamic-plugins-info/loaded-plugins` diff --git a/plugins/dynamic-plugins-info-backend/__fixtures__/data.ts b/plugins/dynamic-plugins-info-backend/__fixtures__/data.ts new file mode 100644 index 000000000..4e996cf30 --- /dev/null +++ b/plugins/dynamic-plugins-info-backend/__fixtures__/data.ts @@ -0,0 +1,163 @@ +// BEGIN-NOSCAN +export const plugins = [ + { + name: 'backstage-plugin-aap-backend-wrapped-dynamic', + version: '1.2.2-dynamic.0', + platform: 'node', + role: 'backend-plugin-module', + installer: { + kind: 'legacy', + }, + }, + { + name: 'backstage-plugin-argo-cd-backend-wrapped-dynamic', + version: '2.11.3-dynamic.0', + platform: 'node', + role: 'backend-plugin-module', + installer: { + kind: 'legacy', + router: { + pluginID: 'argocd', + }, + }, + }, + { + name: 'backstage-plugin-gitlab-backend-wrapped-dynamic', + version: '6.2.0-dynamic.0', + platform: 'node', + role: 'backend-plugin-module', + installer: { + kind: 'legacy', + router: { + pluginID: 'gitlab', + }, + }, + }, + { + name: 'backstage-plugin-keycloak-backend-wrapped-dynamic', + version: '1.5.5-dynamic.0', + platform: 'node', + role: 'backend-plugin-module', + installer: { + kind: 'legacy', + }, + }, + { + name: 'backstage-plugin-ocm-backend-wrapped-dynamic', + version: '3.2.2-dynamic.0', + platform: 'node', + role: 'backend-plugin-module', + installer: { + kind: 'legacy', + router: { + pluginID: 'ocm', + }, + }, + }, + { + name: 'plugin-azure-devops-backend-wrapped-dynamic', + version: '0.4.2-dynamic.0', + platform: 'node', + role: 'backend-plugin-module', + installer: { + kind: 'legacy', + router: { + pluginID: 'azure-devops', + }, + }, + }, + { + name: 'plugin-catalog-backend-module-github-wrapped-dynamic', + version: '0.4.3-dynamic.0', + platform: 'node', + role: 'backend-plugin-module', + installer: { + kind: 'legacy', + }, + }, + { + name: 'plugin-catalog-backend-module-gitlab-wrapped-dynamic', + version: '0.3.2-dynamic.0', + platform: 'node', + role: 'backend-plugin-module', + installer: { + kind: 'legacy', + }, + }, + { + name: 'plugin-jenkins-backend-wrapped-dynamic', + version: '0.2.8-dynamic.0', + platform: 'node', + role: 'backend-plugin', + installer: { + kind: 'legacy', + router: { + pluginID: 'jenkins', + }, + }, + }, + { + name: 'plugin-kubernetes-backend-wrapped-dynamic', + version: '0.12.2-dynamic.0', + platform: 'node', + role: 'backend-plugin-module', + installer: { + kind: 'legacy', + router: { + pluginID: 'kubernetes', + }, + }, + }, + { + name: 'plugin-scaffolder-backend-module-gitlab-wrapped-dynamic', + version: '0.2.8-dynamic.0', + platform: 'node', + role: 'backend-plugin-module', + installer: { + kind: 'legacy', + }, + }, + { + name: 'plugin-sonarqube-backend-wrapped-dynamic', + version: '0.2.7-dynamic.0', + platform: 'node', + role: 'backend-plugin', + installer: { + kind: 'legacy', + router: { + pluginID: 'sonarqube', + }, + }, + }, + { + name: 'plugin-techdocs-backend-wrapped-dynamic', + version: '1.7.2-dynamic.0', + platform: 'node', + role: 'backend-plugin', + installer: { + kind: 'legacy', + router: { + pluginID: 'techdocs', + }, + }, + }, + { + name: 'scaffolder-backend-argocd-wrapped-dynamic', + version: '1.1.17-dynamic.0', + platform: 'node', + role: 'backend-plugin-module', + installer: { + kind: 'legacy', + }, + }, + { + name: 'scaffolder-backend-module-utils-wrapped-dynamic', + version: '1.10.4-dynamic.0', + platform: 'node', + role: 'backend-plugin-module', + installer: { + kind: 'legacy', + }, + }, +]; +// END-NOSCAN diff --git a/plugins/dynamic-plugins-info-backend/__fixtures__/expected_result.ts b/plugins/dynamic-plugins-info-backend/__fixtures__/expected_result.ts new file mode 100644 index 000000000..58f01367b --- /dev/null +++ b/plugins/dynamic-plugins-info-backend/__fixtures__/expected_result.ts @@ -0,0 +1,94 @@ +// BEGIN-NOSCAN +export const expectedList = [ + { + name: 'backstage-plugin-aap-backend-wrapped-dynamic', + version: '1.2.2-dynamic.0', + platform: 'node', + role: 'backend-plugin-module', + }, + { + name: 'backstage-plugin-argo-cd-backend-wrapped-dynamic', + version: '2.11.3-dynamic.0', + platform: 'node', + role: 'backend-plugin-module', + }, + { + name: 'backstage-plugin-gitlab-backend-wrapped-dynamic', + version: '6.2.0-dynamic.0', + platform: 'node', + role: 'backend-plugin-module', + }, + { + name: 'backstage-plugin-keycloak-backend-wrapped-dynamic', + version: '1.5.5-dynamic.0', + platform: 'node', + role: 'backend-plugin-module', + }, + { + name: 'backstage-plugin-ocm-backend-wrapped-dynamic', + version: '3.2.2-dynamic.0', + platform: 'node', + role: 'backend-plugin-module', + }, + { + name: 'plugin-azure-devops-backend-wrapped-dynamic', + version: '0.4.2-dynamic.0', + platform: 'node', + role: 'backend-plugin-module', + }, + { + name: 'plugin-catalog-backend-module-github-wrapped-dynamic', + version: '0.4.3-dynamic.0', + platform: 'node', + role: 'backend-plugin-module', + }, + { + name: 'plugin-catalog-backend-module-gitlab-wrapped-dynamic', + version: '0.3.2-dynamic.0', + platform: 'node', + role: 'backend-plugin-module', + }, + { + name: 'plugin-jenkins-backend-wrapped-dynamic', + version: '0.2.8-dynamic.0', + platform: 'node', + role: 'backend-plugin', + }, + { + name: 'plugin-kubernetes-backend-wrapped-dynamic', + version: '0.12.2-dynamic.0', + platform: 'node', + role: 'backend-plugin-module', + }, + { + name: 'plugin-scaffolder-backend-module-gitlab-wrapped-dynamic', + version: '0.2.8-dynamic.0', + platform: 'node', + role: 'backend-plugin-module', + }, + { + name: 'plugin-sonarqube-backend-wrapped-dynamic', + version: '0.2.7-dynamic.0', + platform: 'node', + role: 'backend-plugin', + }, + { + name: 'plugin-techdocs-backend-wrapped-dynamic', + version: '1.7.2-dynamic.0', + platform: 'node', + role: 'backend-plugin', + }, + { + name: 'scaffolder-backend-argocd-wrapped-dynamic', + version: '1.1.17-dynamic.0', + platform: 'node', + role: 'backend-plugin-module', + }, + { + name: 'scaffolder-backend-module-utils-wrapped-dynamic', + version: '1.10.4-dynamic.0', + platform: 'node', + role: 'backend-plugin-module', + }, +]; +// END-NOSCAN diff --git a/plugins/dynamic-plugins-info-backend/package.json b/plugins/dynamic-plugins-info-backend/package.json new file mode 100644 index 000000000..e184b39c1 --- /dev/null +++ b/plugins/dynamic-plugins-info-backend/package.json @@ -0,0 +1,43 @@ +{ + "name": "@internal/plugin-dynamic-plugins-info-backend", + "version": "0.1.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": true, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "backend-plugin" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/backend-common": "0.19.8", + "@backstage/backend-plugin-manager": "npm:@janus-idp/backend-plugin-manager@0.0.2-janus.5", + "@backstage/config": "1.1.1", + "@types/express": "*", + "express": "4.18.2", + "winston": "3.11.0", + "node-fetch": "2.7.0" + }, + "devDependencies": { + "@backstage/cli": "0.23.1", + "@types/supertest": "2.0.15", + "supertest": "6.3.3", + "msw": "1.3.2" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/dynamic-plugins-info-backend/src/index.ts b/plugins/dynamic-plugins-info-backend/src/index.ts new file mode 100644 index 000000000..47af95cb3 --- /dev/null +++ b/plugins/dynamic-plugins-info-backend/src/index.ts @@ -0,0 +1 @@ +export * from './service/router'; diff --git a/plugins/dynamic-plugins-info-backend/src/service/router.test.ts b/plugins/dynamic-plugins-info-backend/src/service/router.test.ts new file mode 100644 index 000000000..a22bbb09f --- /dev/null +++ b/plugins/dynamic-plugins-info-backend/src/service/router.test.ts @@ -0,0 +1,36 @@ +import { getVoidLogger } from '@backstage/backend-common'; +import express from 'express'; +import request from 'supertest'; +import { plugins } from '../../__fixtures__/data'; +import { expectedList } from '../../__fixtures__/expected_result'; +import { createRouter } from './router'; +import { PluginManager } from '@backstage/backend-plugin-manager'; + +describe('createRouter', () => { + let app: express.Express; + + beforeAll(async () => { + const pluginManager = new (PluginManager as any)(); + pluginManager.plugins = plugins; + + const router = await createRouter({ + logger: getVoidLogger(), + pluginManager, + }); + + app = express(); + app = express().use(router); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('GET /loaded-plugins', () => { + it('returns the list of loaded dynamic plugins', async () => { + const response = await request(app).get('/loaded-plugins'); + expect(response.status).toEqual(200); + expect(response.body).toEqual(expectedList); + }); + }); +}); diff --git a/plugins/dynamic-plugins-info-backend/src/service/router.ts b/plugins/dynamic-plugins-info-backend/src/service/router.ts new file mode 100644 index 000000000..653127493 --- /dev/null +++ b/plugins/dynamic-plugins-info-backend/src/service/router.ts @@ -0,0 +1,36 @@ +import { errorHandler } from '@backstage/backend-common'; +import { + PluginManager, + BaseDynamicPlugin, +} from '@backstage/backend-plugin-manager'; +import express, { Router } from 'express'; +import { Logger } from 'winston'; + +export interface RouterOptions { + logger: Logger; + pluginManager: PluginManager; +} + +export async function createRouter( + options: RouterOptions, +): Promise { + const { pluginManager } = options; + + const router = Router(); + router.use(express.json()); + + const plugins = pluginManager.plugins; + const dynamicPlugins = plugins.map(p => { + // Remove the installer details for the dynamic backend plugins + if (p.platform === 'node') { + const { installer, ...rest } = p; + return rest as BaseDynamicPlugin; + } + return p as BaseDynamicPlugin; + }); + router.get('/loaded-plugins', (_, response) => { + response.send(dynamicPlugins); + }); + router.use(errorHandler()); + return router; +} diff --git a/yarn.lock b/yarn.lock index ec50d425d..87f0b7621 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16130,7 +16130,7 @@ express-prom-bundle@6.6.0: on-finished "^2.3.0" url-value-parser "^2.0.0" -express-promise-router@^4.1.0, express-promise-router@^4.1.1: +express-promise-router@4.1.1, express-promise-router@^4.1.0, express-promise-router@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/express-promise-router/-/express-promise-router-4.1.1.tgz#8fac102060b9bcc868f84d34fbb12fd8fa494291" integrity sha512-Lkvcy/ZGrBhzkl3y7uYBHLMtLI4D6XQ2kiFg9dq7fbktBch5gjqJ0+KovX0cvCAvTJw92raWunRLM/OM+5l4fA==