From df00323062b2bb4cc126f7c30517b25bf6f59613 Mon Sep 17 00:00:00 2001 From: Fernando Giorgetti Date: Mon, 22 Jun 2026 15:16:50 -0300 Subject: [PATCH 1/3] Proxy to skupper-console --- .gitignore | 3 + Containerfile | 3 +- components/console/src/pages/VANs/VANs.jsx | 2 + components/management-controller/package.json | 1 + .../management-controller/src/mc-apiserver.js | 84 ++++++++++++++++++- pnpm-lock.yaml | 52 ++++++++++++ 6 files changed, 143 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 2d7af0aa..3e47433f 100644 --- a/.gitignore +++ b/.gitignore @@ -114,3 +114,6 @@ jsconfig.json # AutoTab config .autotab.d/ + +# Skupper Console local build +skupper-console/ diff --git a/Containerfile b/Containerfile index 13597451..ec17f783 100644 --- a/Containerfile +++ b/Containerfile @@ -80,6 +80,7 @@ WORKDIR /app COPY --from=management-controller-deploy /deployed/management-controller ./ # Copy console as sibling to /app (code expects ../console/dist) COPY --from=management-controller-deploy /deployed/console ./console/dist +COPY skupper-console/ ./skupper-console RUN useradd --uid 10000 runner USER 10000 @@ -113,4 +114,4 @@ RUN useradd --uid 10000 runner USER 10000 EXPOSE 8085 -CMD ["node", "index.js"] \ No newline at end of file +CMD ["node", "index.js"] diff --git a/components/console/src/pages/VANs/VANs.jsx b/components/console/src/pages/VANs/VANs.jsx index 41eb1d69..8a0ac78a 100644 --- a/components/console/src/pages/VANs/VANs.jsx +++ b/components/console/src/pages/VANs/VANs.jsx @@ -199,6 +199,8 @@ const VANs = () => { const handleVANConsole = (van) => { // TODO: Implement VAN Console functionality console.log('Open VAN Console for:', van); + // Open VAN console in a new tab to avoid React Router + window.open(`/console/${van.id}/index.html`, '_blank'); }; const handleDeployClick = async (van) => { diff --git a/components/management-controller/package.json b/components/management-controller/package.json index 86d65079..f1932c2a 100644 --- a/components/management-controller/package.json +++ b/components/management-controller/package.json @@ -23,6 +23,7 @@ "express": "^4.22.1", "express-session": "^1.19.0", "formidable": "^3.5.4", + "http-proxy": "^1.18.1", "jose": "^6.1.3", "js-yaml": "^4.1.1", "morgan": "^1.10.1", diff --git a/components/management-controller/src/mc-apiserver.js b/components/management-controller/src/mc-apiserver.js index c6f4649f..40dc3a0c 100644 --- a/components/management-controller/src/mc-apiserver.js +++ b/components/management-controller/src/mc-apiserver.js @@ -29,6 +29,7 @@ import formidable from 'formidable'; import yaml from 'js-yaml'; import bodyParser from 'body-parser'; import { X509Certificate } from 'node:crypto'; +import httpProxy from 'http-proxy'; import { ClientFromPool, queryWithContext } from './db.js'; import * as resourceTemplates from './resource-templates.js'; import { LoadSecret } from '@skupperx/modules/kube' @@ -41,7 +42,7 @@ import * as common from '@skupperx/modules/common' import { StartWatchServer } from './watch-server.js'; import ViteExpress from 'vite-express'; import { createManagementOidcAuth } from './auth/management-oidc.js'; -import { NotifyTransaction } from './notify.js'; +import { NotifyTransaction, RegisterNotification } from './notify.js'; const __dirname = import.meta.dirname; /** Deployed image: sources live in `/app/src`, console bundle in `/app/console/dist`. Monorepo dev: `components/console` (two levels up from `components/management-controller/src`). */ @@ -51,6 +52,9 @@ const VITE_CONSOLE_ROOT = fs.existsSync( ? path.resolve(__dirname, '../console') : path.resolve(__dirname, '../../console'); +/** Path to skupper-console static files */ +const SKUPPER_CONSOLE_ROOT = '/app/skupper-console'; + const API_PREFIX = '/api/v1alpha1/'; const API_PORT = 8085; const app = express(); @@ -63,6 +67,8 @@ const sessionParser = session({ store: memoryStore, }); +const vanProxy = {}; // { Id: { vanId, backboneName } } + app.use(sessionParser); const link_config_map_yaml = function(name, data) { @@ -624,6 +630,51 @@ export async function Start(is_standalone) { adminApi.Initialize(router, auth); userApi.Initialize(router, auth); + // Proxy requests to the target collector API + const proxy = httpProxy.createProxyServer({ + selfHandleResponse: false, + }); + proxy.on('error', (err, req, res) => { + Log(`Proxy error: ${err.message}`); + res.status(500).send(err.message); + }); + router.all('/console/:vid*/api/v2alpha1*', auth.protect(), (req, res) => { + let vid = req.params.vid; + if (!vanProxy[vid]) { + return res.status(404).send(`Van ${vid} not found`); + } + let targetVan = vanProxy[vid]; + let targetUrl = `http://skupper-console-${targetVan.vanId}.colo-${targetVan.backboneName}:8080`; + Log(`Proxying ${req.url} to ${targetUrl}`); + req.url = req.url.slice(`/console/${vid}`.length); + proxy.web(req, res, { + target: targetUrl, + changeOrigin: true, + }); + }); + + // Serve skupper-console static files under /console/:vanId + router.use('/console/:vanId*', auth.protect(), (req, res, next) => { + let vid = req.params.vanId; + if (!vanProxy[vid]) { + return res.status(404).send(`Van ${vid} not found`); + } + + // Serve static files from skupper-console build directory + express.static(SKUPPER_CONSOLE_ROOT, { + index: 'index.html', + fallthrough: true + })(req, res, (err) => { + if (err) { + return next(err); + } + // If no static file matched, serve index.html for SPA routing + if (!res.headersSent) { + res.sendFile(path.join(SKUPPER_CONSOLE_ROOT, 'index.html')); + } + }); + }); + // route any unauthenticated requests to the login page (catches SPA navigation requests) router.get('*', auth.protect()); @@ -640,4 +691,35 @@ export async function Start(is_standalone) { await ViteExpress.bind(app, server); await StartWatchServer(server, sessionParser, app, router); + RegisterNotification('ApplicationNetworks', onApplicationNetworks, true); +} + +async function onApplicationNetworks(event, id) { + if (!id) { + return; + } + if (event == 'DELETE') { + delete vanProxy[id]; + return; + } + const client = await ClientFromPool("system"); + try { + // Retrieve VanId and Backbone Name + const result = await client.query( + "SELECT A.VanId, B.Name FROM ApplicationNetworks A JOIN Backbones B ON A.Backbone = B.Id WHERE A.Id = $1", + [id] + ); + if (result.rowCount != 1) { + return; + } + const vanId = result.rows[0].vanid; + const backboneName = result.rows[0].name; + vanProxy[id] = { + vanId: vanId, + backboneName: backboneName + }; + } catch (err) { + Log(`Error retrieving VanId and Backbone Name for ApplicationNetwork ${id}: ${err.message}`); + return; + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fdf13edc..1ad0e91f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -132,6 +132,9 @@ importers: formidable: specifier: ^3.5.4 version: 3.5.4 + http-proxy: + specifier: ^1.18.1 + version: 1.18.1 jose: specifier: ^6.1.3 version: 6.1.3 @@ -526,36 +529,42 @@ packages: engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm-musl@2.5.6': resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==} engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [musl] '@parcel/watcher-linux-arm64-glibc@2.5.6': resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm64-musl@2.5.6': resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [musl] '@parcel/watcher-linux-x64-glibc@2.5.6': resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-x64-musl@2.5.6': resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [musl] '@parcel/watcher-win32-arm64@2.5.6': resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==} @@ -614,36 +623,42 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-arm64-musl@1.0.3': resolution: {integrity: sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] '@rolldown/binding-linux-ppc64-gnu@1.0.3': resolution: {integrity: sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-s390x-gnu@1.0.3': resolution: {integrity: sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + libc: [glibc] '@rolldown/binding-linux-x64-gnu@1.0.3': resolution: {integrity: sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-x64-musl@1.0.3': resolution: {integrity: sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] '@rolldown/binding-openharmony-arm64@1.0.3': resolution: {integrity: sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==} @@ -1099,6 +1114,9 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + events-universal@1.0.1: resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} @@ -1156,6 +1174,15 @@ packages: flatted@3.4.2: resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + form-data@4.0.6: resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} engines: {node: '>= 6'} @@ -1242,6 +1269,10 @@ packages: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} + http-proxy@1.18.1: + resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} + engines: {node: '>=8.0.0'} + iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} @@ -1384,24 +1415,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} @@ -1706,6 +1741,9 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -2941,6 +2979,8 @@ snapshots: etag@1.8.1: {} + eventemitter3@4.0.7: {} + events-universal@1.0.1: dependencies: bare-events: 2.8.2 @@ -3045,6 +3085,8 @@ snapshots: flatted@3.4.2: {} + follow-redirects@1.16.0: {} + form-data@4.0.6: dependencies: asynckit: 0.4.0 @@ -3130,6 +3172,14 @@ snapshots: statuses: 2.0.2 toidentifier: 1.0.1 + http-proxy@1.18.1: + dependencies: + eventemitter3: 4.0.7 + follow-redirects: 1.16.0 + requires-port: 1.0.0 + transitivePeerDependencies: + - debug + iconv-lite@0.4.24: dependencies: safer-buffer: 2.1.2 @@ -3500,6 +3550,8 @@ snapshots: readdirp@4.1.2: {} + requires-port@1.0.0: {} + resolve-from@4.0.0: {} rfc4648@1.5.4: {} From fc7790f694e957a1575decbd41dec105ea078404 Mon Sep 17 00:00:00 2001 From: Fernando Giorgetti Date: Tue, 23 Jun 2026 15:14:38 -0300 Subject: [PATCH 2/3] Embedding the skupper-console into the vms-management-controllre --- Containerfile | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/Containerfile b/Containerfile index ec17f783..33ad556e 100644 --- a/Containerfile +++ b/Containerfile @@ -19,10 +19,12 @@ ## under the License. ## +ARG CONSOLE_VERSION=development + # Stage 1: Base image with package manager FROM registry.access.redhat.com/ubi10/ubi-minimal:latest AS base -RUN microdnf -y install nodejs && \ +RUN microdnf -y install nodejs tar gzip && \ microdnf clean all && \ npm install -g corepack && \ corepack enable && \ @@ -34,6 +36,8 @@ WORKDIR /monorepo # Stage 2: Install all dependencies FROM base AS dependencies +ARG CONSOLE_VERSION + # Copy workspace configuration first # These files change less frequently than source code COPY pnpm-workspace.yaml ./ @@ -51,6 +55,12 @@ COPY components/console/package.json ./components/console/ RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \ pnpm install --frozen-lockfile +# Download and install the skupper-console from a released tarball +RUN curl -L -o /tmp/console-embed.tgz --progress-bar \ + "https://github.com/skupperproject/skupper-console/releases/download/${CONSOLE_VERSION}/console-embed.tgz" && \ + mkdir skupper-console && \ + tar -xzf /tmp/console-embed.tgz -C skupper-console/ && rm /tmp/console-embed.tgz + # Stage 3: Build shared packages FROM dependencies AS shared-builder @@ -80,7 +90,8 @@ WORKDIR /app COPY --from=management-controller-deploy /deployed/management-controller ./ # Copy console as sibling to /app (code expects ../console/dist) COPY --from=management-controller-deploy /deployed/console ./console/dist -COPY skupper-console/ ./skupper-console +# Copy skupper-console to /app +COPY --from=dependencies /monorepo/skupper-console ./skupper-console RUN useradd --uid 10000 runner USER 10000 From ced75d4c4eb8cd8831d9db3c70ad8f55efc3288c Mon Sep 17 00:00:00 2001 From: Fernando Giorgetti Date: Thu, 25 Jun 2026 09:35:18 -0300 Subject: [PATCH 3/3] Implemented requested changes --- .gitignore | 3 --- .../management-controller/src/mc-apiserver.js | 18 +++++++++--------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index 3e47433f..2d7af0aa 100644 --- a/.gitignore +++ b/.gitignore @@ -114,6 +114,3 @@ jsconfig.json # AutoTab config .autotab.d/ - -# Skupper Console local build -skupper-console/ diff --git a/components/management-controller/src/mc-apiserver.js b/components/management-controller/src/mc-apiserver.js index 40dc3a0c..afe94294 100644 --- a/components/management-controller/src/mc-apiserver.js +++ b/components/management-controller/src/mc-apiserver.js @@ -709,17 +709,17 @@ async function onApplicationNetworks(event, id) { "SELECT A.VanId, B.Name FROM ApplicationNetworks A JOIN Backbones B ON A.Backbone = B.Id WHERE A.Id = $1", [id] ); - if (result.rowCount != 1) { - return; + if (result.rowCount == 1) { + const vanId = result.rows[0].vanid; + const backboneName = result.rows[0].name; + vanProxy[id] = { + vanId: vanId, + backboneName: backboneName + }; } - const vanId = result.rows[0].vanid; - const backboneName = result.rows[0].name; - vanProxy[id] = { - vanId: vanId, - backboneName: backboneName - }; } catch (err) { Log(`Error retrieving VanId and Backbone Name for ApplicationNetwork ${id}: ${err.message}`); - return; + } finally { + client.release(); } }