From 258abc7f37568153ab98cb2895dd6f4a0acf011e Mon Sep 17 00:00:00 2001 From: Ashu Ghildiyal Date: Mon, 8 Sep 2025 22:43:08 +0530 Subject: [PATCH 01/72] app: Add langchain mcp adapter --- app/package-lock.json | 1823 ++++++++++++++++++++++++++++++++++++++--- app/package.json | 2 + 2 files changed, 1723 insertions(+), 102 deletions(-) diff --git a/app/package-lock.json b/app/package-lock.json index 7f7d034d619..29151351e16 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -8,6 +8,7 @@ "name": "headlamp", "version": "0.36.0", "dependencies": { + "@langchain/mcp-adapters": "^0.6.0", "copyfiles": "^2.4.1", "cross-env": "^7.0.3", "dotenv": "^16.4.5", @@ -1958,6 +1959,13 @@ "hasInstallScript": true, "optional": true }, + "node_modules/@cfworker/json-schema": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@cfworker/json-schema/-/json-schema-4.1.1.tgz", + "integrity": "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==", + "license": "MIT", + "peer": true + }, "node_modules/@develar/schema-utils": { "version": "2.6.5", "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", @@ -3495,6 +3503,76 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@langchain/core": { + "version": "0.3.73", + "resolved": "https://registry.npmjs.org/@langchain/core/-/core-0.3.73.tgz", + "integrity": "sha512-E5dK9/MDH9671yuU3ZoMfSkMC7njtZrOZYrmLGk+2cvGk92yqxv/+MMxwOYoFtPMEWx9T8mTg2omHqcXXaEpGw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@cfworker/json-schema": "^4.0.2", + "ansi-styles": "^5.0.0", + "camelcase": "6", + "decamelize": "1.2.0", + "js-tiktoken": "^1.0.12", + "langsmith": "^0.3.46", + "mustache": "^4.2.0", + "p-queue": "^6.6.2", + "p-retry": "4", + "uuid": "^10.0.0", + "zod": "^3.25.32", + "zod-to-json-schema": "^3.22.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@langchain/core/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@langchain/core/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@langchain/mcp-adapters": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@langchain/mcp-adapters/-/mcp-adapters-0.6.0.tgz", + "integrity": "sha512-NHQNH9NciLhxlCnL/4HDebiYT3UQvpBfF5KPlIi/uSXn8te/bYjPV64gUyAloNNo+fjj4qDvKP1/nHj0r7fKFw==", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.12.1", + "debug": "^4.4.0", + "zod": "^3.24.2" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "extended-eventsource": "^1.x" + }, + "peerDependencies": { + "@langchain/core": "^0.3.66" + } + }, "node_modules/@malept/cross-spawn-promise": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", @@ -3567,6 +3645,29 @@ "node": ">= 10.0.0" } }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.17.5.tgz", + "integrity": "sha512-QakrKIGniGuRVfWBdMsDea/dx1PNE739QJ7gCM41s9q+qaCYTHCdsIBXQVVXry3mfWAiaM9kT22Hyz53Uw8mfg==", + "license": "MIT", + "dependencies": { + "ajv": "^6.12.6", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@mswjs/interceptors": { "version": "0.38.7", "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.38.7.tgz", @@ -3857,6 +3958,13 @@ "@types/node": "*" } }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "license": "MIT", + "peer": true + }, "node_modules/@types/semver": { "version": "7.7.0", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz", @@ -3876,6 +3984,13 @@ "integrity": "sha512-Lja2xYuuf2B3knEsga8ShbOdsfNOtzT73GyJmZyY7eGl2+ajOqrs8yM5ze0fsSoYwvA6bw7/Qr7OZ7PEEmYwWg==", "dev": true }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "license": "MIT", + "peer": true + }, "node_modules/@types/verror": { "version": "1.10.9", "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.9.tgz", @@ -4138,6 +4253,40 @@ "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", "dev": true }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.12.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", @@ -4175,7 +4324,6 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -5031,7 +5179,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, "funding": [ { "type": "github", @@ -5099,6 +5246,26 @@ "bluebird": "^3.5.5" } }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -5382,6 +5549,15 @@ "node": ">= 10.0.0" } }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -5406,7 +5582,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -5416,6 +5591,22 @@ "node": ">= 0.4" } }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -5784,12 +5975,81 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/console-table-printer": { + "version": "2.14.6", + "resolved": "https://registry.npmjs.org/console-table-printer/-/console-table-printer-2.14.6.tgz", + "integrity": "sha512-MCBl5HNVaFuuHW6FGbL/4fB7N/ormCy+tQ+sxTrF6QtSbSNETvPuOVbkJBhzDgYhvjWGrTma4eYJa37ZuoQsPw==", + "license": "MIT", + "peer": true, + "dependencies": { + "simple-wcswidth": "^1.0.1" + } + }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-disposition/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, "node_modules/copyfiles": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/copyfiles/-/copyfiles-2.4.1.tgz", @@ -5901,6 +6161,19 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/crc": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", @@ -6124,11 +6397,12 @@ } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -6139,6 +6413,16 @@ } } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -6287,6 +6571,15 @@ "node": ">=0.4.0" } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -6510,7 +6803,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -6526,6 +6818,12 @@ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, "node_modules/ejs": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", @@ -6812,6 +7110,15 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/encoding-sniffer": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz", @@ -6939,7 +7246,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6949,7 +7255,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -7012,7 +7317,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -7121,6 +7425,12 @@ "node": ">=6" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", @@ -7697,6 +8007,43 @@ "node": ">=0.10.0" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT", + "peer": true + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -7757,6 +8104,91 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/express/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/extended-eventsource": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/extended-eventsource/-/extended-eventsource-1.7.0.tgz", + "integrity": "sha512-s8rtvZuYcKBpzytHb5g95cHbZ1J99WeMnV18oKc5wKoxkHzlzpPc/bNAm7Da2Db0BDw0CAu1z3LpH+7UsyzIpw==", + "license": "MIT", + "optional": true + }, "node_modules/extract-zip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", @@ -7790,8 +8222,7 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-fifo": { "version": "1.3.2", @@ -7817,8 +8248,7 @@ "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, "node_modules/fast-levenshtein": { "version": "2.0.6", @@ -7905,6 +8335,23 @@ "node": ">=8" } }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/find-process": { "version": "1.4.10", "resolved": "https://registry.npmjs.org/find-process/-/find-process-1.4.10.tgz", @@ -8012,6 +8459,24 @@ "node": ">= 6" } }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -8182,7 +8647,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -8216,7 +8680,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -8420,7 +8883,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8506,7 +8968,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8625,6 +9086,31 @@ "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", "dev": true }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -8803,7 +9289,6 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -8931,6 +9416,15 @@ "node": ">= 0.10" } }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-arguments": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", @@ -9265,6 +9759,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -10224,6 +10724,16 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/js-tiktoken": { + "version": "1.0.21", + "resolved": "https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.21.tgz", + "integrity": "sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==", + "license": "MIT", + "peer": true, + "dependencies": { + "base64-js": "^1.5.1" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -10270,8 +10780,7 @@ "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -10347,6 +10856,42 @@ "node": ">=6" } }, + "node_modules/langsmith": { + "version": "0.3.67", + "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.3.67.tgz", + "integrity": "sha512-l4y3RmJ9yWF5a29fLg3eWZQxn6Q6dxTOgLGgQHzPGZHF3NUynn+A+airYIe/Yt4rwjGbuVrABAPsXBkVu/Hi7g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/uuid": "^10.0.0", + "chalk": "^4.1.2", + "console-table-printer": "^2.12.1", + "p-queue": "^6.6.2", + "p-retry": "4", + "semver": "^7.6.3", + "uuid": "^10.0.0" + }, + "peerDependencies": { + "@opentelemetry/api": "*", + "@opentelemetry/exporter-trace-otlp-proto": "*", + "@opentelemetry/sdk-trace-base": "*", + "openai": "*" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@opentelemetry/exporter-trace-otlp-proto": { + "optional": true + }, + "@opentelemetry/sdk-trace-base": { + "optional": true + }, + "openai": { + "optional": true + } + } + }, "node_modules/language-subtag-registry": { "version": "0.3.23", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", @@ -10604,12 +11149,32 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" } }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -10828,9 +11393,20 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "license": "MIT", + "peer": true, + "bin": { + "mustache": "bin/mustache" + } }, "node_modules/natural-compare": { "version": "1.4.0", @@ -10838,6 +11414,15 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", @@ -10955,17 +11540,15 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true, "engines": { "node": ">=0.10.0" } }, "node_modules/object-inspect": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", - "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", - "dev": true, - "peer": true, + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -11085,6 +11668,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -11180,6 +11775,60 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-queue": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", + "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-retry/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "license": "MIT", + "peer": true, + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -11264,6 +11913,15 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -11323,6 +11981,16 @@ "node": "14 || >=16.14" } }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -11364,6 +12032,15 @@ "node": ">= 6" } }, + "node_modules/pkce-challenge": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", @@ -11534,6 +12211,19 @@ "node": ">= 8" } }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -11547,7 +12237,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true, "engines": { "node": ">=6" } @@ -11568,6 +12257,21 @@ } ] }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -11649,6 +12353,46 @@ "rimraf": "bin.js" } }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", + "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.7.0", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/rcedit": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/rcedit/-/rcedit-4.0.1.tgz", @@ -12047,6 +12791,22 @@ "node": ">=8.0" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/rsvp": { "version": "4.8.5", "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", @@ -12130,8 +12890,7 @@ "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/sanitize-filename": { "version": "1.6.3", @@ -12149,9 +12908,10 @@ "dev": true }, "node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -12166,6 +12926,49 @@ "dev": true, "optional": true }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/send/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/send/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/serialize-error": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", @@ -12182,6 +12985,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -12216,6 +13034,12 @@ "node": ">= 0.4" } }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -12376,16 +13200,69 @@ } }, "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", - "dev": true, - "peer": true, + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -12411,6 +13288,13 @@ "node": ">=10" } }, + "node_modules/simple-wcswidth": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/simple-wcswidth/-/simple-wcswidth-1.1.2.tgz", + "integrity": "sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw==", + "license": "MIT", + "peer": true + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -12513,6 +13397,15 @@ "node": ">= 6" } }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/stop-iteration-iterator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", @@ -13013,6 +13906,15 @@ "node": ">=10.13.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/truncate-utf8-bytes": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", @@ -13104,6 +14006,41 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typed-array-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", @@ -13289,6 +14226,15 @@ "node": ">= 4.0.0" } }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/untildify": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", @@ -13331,7 +14277,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, "dependencies": { "punycode": "^2.1.0" } @@ -13347,6 +14292,20 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "peer": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", @@ -13370,6 +14329,15 @@ "node": ">= 10.13.0" } }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/verror": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", @@ -13874,6 +14842,24 @@ "engines": { "node": ">= 6" } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.6", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", + "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } } }, "dependencies": { @@ -15209,6 +16195,12 @@ "integrity": "sha512-iTZ8cVGZ5dglNRyFdSj8U60mHIrC8XNIuOHN/NkM5/dQP4nsmpyqeQTAADLLQgoFCNJD+DiwQCv8dR2cCeWP4g==", "optional": true }, + "@cfworker/json-schema": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@cfworker/json-schema/-/json-schema-4.1.1.tgz", + "integrity": "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==", + "peer": true + }, "@develar/schema-utils": { "version": "2.6.5", "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", @@ -16205,6 +17197,51 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "@langchain/core": { + "version": "0.3.73", + "resolved": "https://registry.npmjs.org/@langchain/core/-/core-0.3.73.tgz", + "integrity": "sha512-E5dK9/MDH9671yuU3ZoMfSkMC7njtZrOZYrmLGk+2cvGk92yqxv/+MMxwOYoFtPMEWx9T8mTg2omHqcXXaEpGw==", + "peer": true, + "requires": { + "@cfworker/json-schema": "^4.0.2", + "ansi-styles": "^5.0.0", + "camelcase": "6", + "decamelize": "1.2.0", + "js-tiktoken": "^1.0.12", + "langsmith": "^0.3.46", + "mustache": "^4.2.0", + "p-queue": "^6.6.2", + "p-retry": "4", + "uuid": "^10.0.0", + "zod": "^3.25.32", + "zod-to-json-schema": "^3.22.3" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "peer": true + }, + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "peer": true + } + } + }, + "@langchain/mcp-adapters": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@langchain/mcp-adapters/-/mcp-adapters-0.6.0.tgz", + "integrity": "sha512-NHQNH9NciLhxlCnL/4HDebiYT3UQvpBfF5KPlIi/uSXn8te/bYjPV64gUyAloNNo+fjj4qDvKP1/nHj0r7fKFw==", + "requires": { + "@modelcontextprotocol/sdk": "^1.12.1", + "debug": "^4.4.0", + "extended-eventsource": "^1.x", + "zod": "^3.24.2" + } + }, "@malept/cross-spawn-promise": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", @@ -16255,6 +17292,25 @@ } } }, + "@modelcontextprotocol/sdk": { + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.17.5.tgz", + "integrity": "sha512-QakrKIGniGuRVfWBdMsDea/dx1PNE739QJ7gCM41s9q+qaCYTHCdsIBXQVVXry3mfWAiaM9kT22Hyz53Uw8mfg==", + "requires": { + "ajv": "^6.12.6", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + } + }, "@mswjs/interceptors": { "version": "0.38.7", "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.38.7.tgz", @@ -16529,6 +17585,12 @@ "@types/node": "*" } }, + "@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "peer": true + }, "@types/semver": { "version": "7.7.0", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz", @@ -16547,6 +17609,12 @@ "integrity": "sha512-Lja2xYuuf2B3knEsga8ShbOdsfNOtzT73GyJmZyY7eGl2+ajOqrs8yM5ze0fsSoYwvA6bw7/Qr7OZ7PEEmYwWg==", "dev": true }, + "@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "peer": true + }, "@types/verror": { "version": "1.10.9", "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.9.tgz", @@ -16716,6 +17784,30 @@ "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", "dev": true }, + "accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "requires": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "dependencies": { + "mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==" + }, + "mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "requires": { + "mime-db": "^1.54.0" + } + } + } + }, "acorn": { "version": "8.12.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", @@ -16742,7 +17834,6 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, "requires": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -17415,8 +18506,7 @@ "base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" }, "binary-extensions": { "version": "2.2.0", @@ -17466,6 +18556,22 @@ "bluebird": "^3.5.5" } }, + "body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "requires": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + } + }, "boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -17678,6 +18784,11 @@ "sax": "^1.2.4" } }, + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" + }, "call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -17696,12 +18807,20 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "requires": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, + "call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "requires": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + } + }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -17971,12 +19090,51 @@ } } }, + "console-table-printer": { + "version": "2.14.6", + "resolved": "https://registry.npmjs.org/console-table-printer/-/console-table-printer-2.14.6.tgz", + "integrity": "sha512-MCBl5HNVaFuuHW6FGbL/4fB7N/ormCy+tQ+sxTrF6QtSbSNETvPuOVbkJBhzDgYhvjWGrTma4eYJa37ZuoQsPw==", + "peer": true, + "requires": { + "simple-wcswidth": "^1.0.1" + } + }, + "content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "requires": { + "safe-buffer": "5.2.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } + } + }, + "content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" + }, "convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==" + }, + "cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==" + }, "copyfiles": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/copyfiles/-/copyfiles-2.4.1.tgz", @@ -18063,6 +19221,15 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, "crc": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", @@ -18216,13 +19383,19 @@ } }, "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "requires": { - "ms": "2.1.2" + "ms": "^2.1.3" } }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "peer": true + }, "decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -18329,6 +19502,11 @@ "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", "dev": true }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, "detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -18495,7 +19673,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "requires": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", @@ -18507,6 +19684,11 @@ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, "ejs": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", @@ -18740,6 +19922,11 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, + "encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" + }, "encoding-sniffer": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz", @@ -18849,14 +20036,12 @@ "es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" }, "es-errors": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" }, "es-get-iterator": { "version": "1.1.3", @@ -18912,7 +20097,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "requires": { "es-errors": "^1.3.0" } @@ -18996,6 +20180,11 @@ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==" }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, "escape-string-regexp": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", @@ -19423,6 +20612,30 @@ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" + }, + "eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "peer": true + }, + "eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "requires": { + "eventsource-parser": "^3.0.1" + } + }, + "eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==" + }, "execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -19467,6 +20680,67 @@ "jest-util": "^29.7.0" } }, + "express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "requires": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "dependencies": { + "mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==" + }, + "mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "requires": { + "mime-db": "^1.54.0" + } + } + } + }, + "express-rate-limit": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "requires": {} + }, + "extended-eventsource": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/extended-eventsource/-/extended-eventsource-1.7.0.tgz", + "integrity": "sha512-s8rtvZuYcKBpzytHb5g95cHbZ1J99WeMnV18oKc5wKoxkHzlzpPc/bNAm7Da2Db0BDw0CAu1z3LpH+7UsyzIpw==", + "optional": true + }, "extract-zip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", @@ -19489,8 +20763,7 @@ "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "fast-fifo": { "version": "1.3.2", @@ -19513,8 +20786,7 @@ "fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, "fast-levenshtein": { "version": "2.0.6", @@ -19594,6 +20866,19 @@ "to-regex-range": "^5.0.1" } }, + "finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "requires": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + } + }, "find-process": { "version": "1.4.10", "resolved": "https://registry.npmjs.org/find-process/-/find-process-1.4.10.tgz", @@ -19677,6 +20962,16 @@ "mime-types": "^2.1.12" } }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" + }, + "fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==" + }, "fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -19809,7 +21104,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "requires": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", @@ -19833,7 +21127,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "requires": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" @@ -19973,8 +21266,7 @@ "gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" }, "graceful-fs": { "version": "4.2.11", @@ -20039,8 +21331,7 @@ "has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" }, "has-tostringtag": { "version": "1.0.2", @@ -20136,6 +21427,25 @@ "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", "dev": true }, + "http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "dependencies": { + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" + } + } + }, "https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -20261,7 +21571,6 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, "requires": { "safer-buffer": ">= 2.1.2 < 3.0.0" } @@ -20343,6 +21652,11 @@ "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==" }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + }, "is-arguments": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", @@ -20557,6 +21871,11 @@ "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", "dev": true }, + "is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" + }, "is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -21272,6 +22591,15 @@ } } }, + "js-tiktoken": { + "version": "1.0.21", + "resolved": "https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.21.tgz", + "integrity": "sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==", + "peer": true, + "requires": { + "base64-js": "^1.5.1" + } + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -21309,8 +22637,7 @@ "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, "json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -21371,6 +22698,21 @@ "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", "dev": true }, + "langsmith": { + "version": "0.3.67", + "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.3.67.tgz", + "integrity": "sha512-l4y3RmJ9yWF5a29fLg3eWZQxn6Q6dxTOgLGgQHzPGZHF3NUynn+A+airYIe/Yt4rwjGbuVrABAPsXBkVu/Hi7g==", + "peer": true, + "requires": { + "@types/uuid": "^10.0.0", + "chalk": "^4.1.2", + "console-table-printer": "^2.12.1", + "p-queue": "^6.6.2", + "p-retry": "4", + "semver": "^7.6.3", + "uuid": "^10.0.0" + } + }, "language-subtag-registry": { "version": "0.3.23", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", @@ -21573,8 +22915,17 @@ "math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" + }, + "media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==" + }, + "merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==" }, "merge-stream": { "version": "2.0.0", @@ -21716,9 +23067,15 @@ "dev": true }, "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "peer": true }, "natural-compare": { "version": "1.4.0", @@ -21726,6 +23083,11 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==" + }, "nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", @@ -21828,15 +23190,12 @@ "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, "object-inspect": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", - "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", - "dev": true, - "peer": true + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==" }, "object-is": { "version": "1.1.6", @@ -21917,6 +23276,14 @@ "es-object-atoms": "^1.0.0" } }, + "on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "requires": { + "ee-first": "1.1.1" + } + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -21987,6 +23354,43 @@ } } }, + "p-queue": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", + "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "peer": true, + "requires": { + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" + } + }, + "p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "peer": true, + "requires": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "dependencies": { + "retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "peer": true + } + } + }, + "p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "peer": true, + "requires": { + "p-finally": "^1.0.0" + } + }, "p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -22047,6 +23451,11 @@ "parse5": "^7.0.0" } }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -22090,6 +23499,11 @@ } } }, + "path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==" + }, "pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -22119,6 +23533,11 @@ "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", "dev": true }, + "pkce-challenge": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==" + }, "pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", @@ -22249,6 +23668,15 @@ "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==" }, + "proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + } + }, "pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -22261,8 +23689,7 @@ "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" }, "pure-rand": { "version": "6.1.0", @@ -22270,6 +23697,14 @@ "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", "dev": true }, + "qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "requires": { + "side-channel": "^1.1.0" + } + }, "queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -22323,6 +23758,32 @@ } } }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", + "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.7.0", + "unpipe": "1.0.0" + }, + "dependencies": { + "iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + } + } + }, "rcedit": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/rcedit/-/rcedit-4.0.1.tgz", @@ -22640,6 +24101,18 @@ "sprintf-js": "^1.1.2" } }, + "router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "requires": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + } + }, "rsvp": { "version": "4.8.5", "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", @@ -22696,8 +24169,7 @@ "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "sanitize-filename": { "version": "1.6.3", @@ -22715,9 +24187,9 @@ "dev": true }, "semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==" + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==" }, "semver-compare": { "version": "1.0.0", @@ -22726,6 +24198,39 @@ "dev": true, "optional": true }, + "send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "requires": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "dependencies": { + "mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==" + }, + "mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "requires": { + "mime-db": "^1.54.0" + } + } + } + }, "serialize-error": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", @@ -22736,6 +24241,17 @@ "type-fest": "^0.13.1" } }, + "serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "requires": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + } + }, "set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -22764,6 +24280,11 @@ "has-property-descriptors": "^1.0.2" } }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -22878,16 +24399,47 @@ } }, "side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", - "dev": true, - "peer": true, + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "requires": { - "call-bind": "^1.0.7", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + } + }, + "side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "requires": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + } + }, + "side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "requires": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + } + }, + "side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "requires": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" } }, "signal-exit": { @@ -22904,6 +24456,12 @@ "semver": "^7.5.3" } }, + "simple-wcswidth": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/simple-wcswidth/-/simple-wcswidth-1.1.2.tgz", + "integrity": "sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw==", + "peer": true + }, "sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -22981,6 +24539,11 @@ "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", "dev": true }, + "statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==" + }, "stop-iteration-iterator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", @@ -23380,6 +24943,11 @@ "streamx": "^2.12.5" } }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" + }, "truncate-utf8-bytes": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", @@ -23450,6 +25018,31 @@ "dev": true, "optional": true }, + "type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "requires": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "dependencies": { + "mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==" + }, + "mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "requires": { + "mime-db": "^1.54.0" + } + } + } + }, "typed-array-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", @@ -23581,6 +25174,11 @@ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" + }, "untildify": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", @@ -23600,7 +25198,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, "requires": { "punycode": "^2.1.0" } @@ -23616,6 +25213,12 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, + "uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "peer": true + }, "v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", @@ -23633,6 +25236,11 @@ "integrity": "sha512-aeVK81SIuT6aMJfNo9Vte8Dw0/FZINGBV8BfCraGtqVxIeLAEhJyoWs8SmvRVmXfGss2PmmOwZCuBPbZR+IYWg==", "dev": true }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" + }, "verror": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", @@ -24019,6 +25627,17 @@ } } } + }, + "zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==" + }, + "zod-to-json-schema": { + "version": "3.24.6", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", + "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", + "requires": {} } } } diff --git a/app/package.json b/app/package.json index 084aba83d76..c510f5eafac 100644 --- a/app/package.json +++ b/app/package.json @@ -118,6 +118,7 @@ "files": [ "electron/main.js", "electron/preload.js", + "electron/mcp-client.js", "electron/i18next.config.js", "electron/i18n-helper.js", "electron/windowSize.js", @@ -171,6 +172,7 @@ "typescript": "5.5.4" }, "dependencies": { + "@langchain/mcp-adapters": "^0.6.0", "copyfiles": "^2.4.1", "cross-env": "^7.0.3", "dotenv": "^16.4.5", From e389f3d9cf67e5c2d6f3391d1be39942a219b597 Mon Sep 17 00:00:00 2001 From: Ashu Ghildiyal Date: Mon, 8 Sep 2025 22:54:15 +0530 Subject: [PATCH 02/72] app: Add mcp client --- app/electron/main.ts | 6 + app/electron/mcp-client.ts | 376 +++++++++++++++++++++++++++++++++++++ app/electron/preload.ts | 11 ++ 3 files changed, 393 insertions(+) create mode 100644 app/electron/mcp-client.ts diff --git a/app/electron/main.ts b/app/electron/main.ts index 68beaba0e01..126b14b8519 100644 --- a/app/electron/main.ts +++ b/app/electron/main.ts @@ -40,6 +40,7 @@ import url from 'url'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; import i18n from './i18next.config'; +import ElectronMCPClient from './mcp-client'; import { addToPath, ArtifactHubHeadlampPkg, @@ -131,6 +132,7 @@ const shouldCheckForUpdates = process.env.HEADLAMP_CHECK_FOR_UPDATES !== 'false' // make it global so that it doesn't get garbage collected let mainWindow: BrowserWindow | null; +let mcpClient: ElectronMCPClient; /** * `Action` is an interface for an action to be performed by the plugin manager. @@ -1463,6 +1465,10 @@ function startElecron() { if (mainWindow) { mainWindow.removeAllListeners('close'); } + // Cleanup MCP client + if (mcpClient) { + mcpClient.cleanup().catch(console.error); + } }); } diff --git a/app/electron/mcp-client.ts b/app/electron/mcp-client.ts new file mode 100644 index 00000000000..01d7c16b21e --- /dev/null +++ b/app/electron/mcp-client.ts @@ -0,0 +1,376 @@ +/* + * Copyright 2025 The Kubernetes Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { MultiServerMCPClient } from '@langchain/mcp-adapters'; +import { app, ipcMain } from 'electron'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; + +interface MCPServer { + name: string; + command: string; + args: string[]; + enabled: boolean; +} + +interface MCPConfig { + enabled: boolean; + servers: MCPServer[]; +} + +class ElectronMCPClient { + private client: MultiServerMCPClient | null = null; + private tools: any[] = []; + private isInitialized = false; + private initializationPromise: Promise | null = null; + + constructor() { + this.setupIpcHandlers(); + } + + /** + * Load MCP server configuration from settings + */ + private loadMCPConfig(): MCPConfig | null { + try { + const settingsPath = path.join(app.getPath('userData'), 'settings.json'); + if (!fs.existsSync(settingsPath)) { + return null; + } + + const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8')); + return settings.mcpConfig || null; + } catch (error) { + console.error('Error loading MCP config:', error); + return null; + } + } + + /** + * Expand environment variables and resolve paths in arguments + */ + private expandArgs(args: string[]): string[] { + return args.map(arg => { + // Replace Windows environment variables like %USERPROFILE% + let expandedArg = arg; + + // Handle %USERPROFILE% + if (expandedArg.includes('%USERPROFILE%')) { + expandedArg = expandedArg.replace(/%USERPROFILE%/g, os.homedir()); + } + + // Handle other common Windows environment variables + if (expandedArg.includes('%APPDATA%')) { + expandedArg = expandedArg.replace(/%APPDATA%/g, process.env.APPDATA || ''); + } + + if (expandedArg.includes('%LOCALAPPDATA%')) { + expandedArg = expandedArg.replace(/%LOCALAPPDATA%/g, process.env.LOCALAPPDATA || ''); + } + + // Convert Windows backslashes to forward slashes for Docker + if (process.platform === 'win32' && expandedArg.includes('\\')) { + expandedArg = expandedArg.replace(/\\/g, '/'); + } + + // Handle Docker volume mount format and ensure proper Windows path format + if (expandedArg.includes('type=bind,src=')) { + const match = expandedArg.match(/type=bind,src=(.+?),dst=(.+)/); + if (match) { + let srcPath = match[1]; + const dstPath = match[2]; + + // Resolve the source path + if (process.platform === 'win32') { + srcPath = path.resolve(srcPath); + // For Docker on Windows, we might need to convert C:\ to /c/ format + if (srcPath.match(/^[A-Za-z]:/)) { + srcPath = + '/' + srcPath.charAt(0).toLowerCase() + srcPath.slice(2).replace(/\\/g, '/'); + } + } + + expandedArg = `type=bind,src=${srcPath},dst=${dstPath}`; + } + } + + return expandedArg; + }); + } + + private async initializeClient(): Promise { + if (this.isInitialized) { + return; + } + + if (this.initializationPromise) { + return this.initializationPromise; + } + + this.initializationPromise = this.doInitialize(); + return this.initializationPromise; + } + + private async doInitialize(): Promise { + try { + console.log('Initializing MCP client in Electron main process...'); + + // Load MCP configuration from settings + const mcpConfig = this.loadMCPConfig(); + + if ( + !mcpConfig || + !mcpConfig.enabled || + !mcpConfig.servers || + mcpConfig.servers.length === 0 + ) { + console.log('MCP is disabled or no servers configured'); + this.isInitialized = true; + return; + } + + // Build MCP servers configuration from settings + const mcpServers: any = {}; + + for (const server of mcpConfig.servers) { + if (!server.enabled || !server.name || !server.command) { + continue; + } + + // Expand environment variables and resolve paths in arguments + const expandedArgs = this.expandArgs(server.args || []); + console.log(`Expanded args for ${server.name}:`, expandedArgs); + + mcpServers[server.name] = { + transport: 'stdio', + command: server.command, + args: expandedArgs, + restart: { + enabled: true, + maxAttempts: 3, + delayMs: 2000, + }, + }; + } + + // If no enabled servers, skip initialization + if (Object.keys(mcpServers).length === 0) { + console.log('No enabled MCP servers found'); + this.isInitialized = true; + return; + } + + console.log('Initializing MCP client with servers:', Object.keys(mcpServers)); + + this.client = new MultiServerMCPClient({ + throwOnLoadError: false, // Don't throw on load error to allow partial initialization + prefixToolNameWithServerName: true, // Prefix to avoid name conflicts + additionalToolNamePrefix: '', + useStandardContentBlocks: true, + mcpServers, + }); + + // Get and cache the tools + this.tools = await this.client.getTools(); + this.isInitialized = true; + console.log('MCP client initialized successfully with', this.tools.length, 'tools'); + } catch (error) { + console.error('Failed to initialize MCP client:', error); + this.client = null; + this.isInitialized = false; + this.initializationPromise = null; + throw error; + } + } + + private setupIpcHandlers(): void { + // Handle MCP tools request + ipcMain.handle('mcp-get-tools', async () => { + try { + await this.initializeClient(); + + if (!this.client || this.tools.length === 0) { + return { success: true, tools: [] }; + } + + // Convert LangChain tools to our format + const toolsInfo = this.tools.map(tool => ({ + name: tool.name, + description: tool.description, + inputSchema: tool.schema, + })); + + console.log('MCP tools retrieved:', toolsInfo.length, 'tools'); + return { success: true, tools: toolsInfo }; + } catch (error) { + console.error('Error getting MCP tools:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + tools: [], + }; + } + }); + + // Handle MCP tool execution + ipcMain.handle('mcp-execute-tool', async (event, { toolName, args, toolCallId }) => { + try { + await this.initializeClient(); + + if (!this.client || this.tools.length === 0) { + throw new Error('MCP client not initialized or no tools available'); + } + + // Find the tool by name + const tool = this.tools.find(t => t.name === toolName); + if (!tool) { + throw new Error(`Tool ${toolName} not found`); + } + + // Execute the tool directly using LangChain's invoke method + const result = await tool.invoke(args); + console.log(`MCP tool ${toolName} executed successfully`); + + return { + success: true, + result, + toolCallId, + }; + } catch (error) { + console.error(`Error executing MCP tool ${toolName}:`, error); + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + toolCallId, + }; + } + }); + + // Handle MCP client status check + ipcMain.handle('mcp-get-status', async () => { + return { + isInitialized: this.isInitialized, + hasClient: this.client !== null, + }; + }); + + // Handle MCP client reset/restart + ipcMain.handle('mcp-reset-client', async () => { + try { + console.log('Resetting MCP client...'); + + if (this.client) { + // If the client has a close/dispose method, call it + if (typeof (this.client as any).close === 'function') { + await (this.client as any).close(); + } + } + + this.client = null; + this.isInitialized = false; + this.initializationPromise = null; + + // Re-initialize + await this.initializeClient(); + + return { success: true }; + } catch (error) { + console.error('Error resetting MCP client:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }; + } + }); + + // Handle MCP configuration updates + ipcMain.handle('mcp-update-config', async (event, mcpConfig: MCPConfig) => { + try { + console.log('Updating MCP configuration...'); + + // Save to settings file + const settingsPath = path.join(app.getPath('userData'), 'settings.json'); + let settings: any = {}; + + if (fs.existsSync(settingsPath)) { + settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8')); + } + + settings.mcpConfig = mcpConfig; + fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf-8'); + + // Reset and reinitialize client with new config + if (this.client) { + if (typeof (this.client as any).close === 'function') { + await (this.client as any).close(); + } + } + this.client = null; + this.isInitialized = false; + this.initializationPromise = null; + + // Re-initialize with new config + await this.initializeClient(); + + return { success: true }; + } catch (error) { + console.error('Error updating MCP configuration:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }; + } + }); + + // Handle getting current MCP configuration + ipcMain.handle('mcp-get-config', async () => { + try { + const mcpConfig = this.loadMCPConfig(); + return { + success: true, + config: mcpConfig || { enabled: false, servers: [] }, + }; + } catch (error) { + console.error('Error getting MCP configuration:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + config: { enabled: false, servers: [] }, + }; + } + }); + } + + /** + * Cleanup method to be called when the app is shutting down + */ + async cleanup(): Promise { + if (this.client) { + try { + await this.client.close(); + } catch (error) { + console.error('Error cleaning up MCP client:', error); + } + } + this.client = null; + this.tools = []; + this.isInitialized = false; + this.initializationPromise = null; + } +} + +export default ElectronMCPClient; diff --git a/app/electron/preload.ts b/app/electron/preload.ts index e65c2c13404..072513a1b11 100644 --- a/app/electron/preload.ts +++ b/app/electron/preload.ts @@ -57,4 +57,15 @@ contextBridge.exposeInMainWorld('desktopApi', { removeListener: (channel: string, func: (...args: unknown[]) => void) => { ipcRenderer.removeListener(channel, func); }, + + // MCP client APIs + mcp: { + getTools: () => ipcRenderer.invoke('mcp-get-tools'), + executeTool: (toolName: string, args: Record, toolCallId?: string) => + ipcRenderer.invoke('mcp-execute-tool', { toolName, args, toolCallId }), + getStatus: () => ipcRenderer.invoke('mcp-get-status'), + resetClient: () => ipcRenderer.invoke('mcp-reset-client'), + getConfig: () => ipcRenderer.invoke('mcp-get-config'), + updateConfig: (config: any) => ipcRenderer.invoke('mcp-update-config', config), + }, }); From d986808dbaf9d959a48999ebed0c8969c2efca52 Mon Sep 17 00:00:00 2001 From: ashu8912 Date: Sun, 21 Sep 2025 20:36:21 +0530 Subject: [PATCH 03/72] app: Allo mcp server to accept env vars --- app/electron/mcp-client.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/electron/mcp-client.ts b/app/electron/mcp-client.ts index 01d7c16b21e..bc76608f616 100644 --- a/app/electron/mcp-client.ts +++ b/app/electron/mcp-client.ts @@ -25,6 +25,7 @@ interface MCPServer { command: string; args: string[]; enabled: boolean; + env?: Record; } interface MCPConfig { @@ -155,10 +156,14 @@ class ElectronMCPClient { const expandedArgs = this.expandArgs(server.args || []); console.log(`Expanded args for ${server.name}:`, expandedArgs); + // Prepare environment variables + const serverEnv = server.env ? { ...process.env, ...server.env } : process.env; + mcpServers[server.name] = { transport: 'stdio', command: server.command, args: expandedArgs, + env: serverEnv, restart: { enabled: true, maxAttempts: 3, @@ -228,6 +233,7 @@ class ElectronMCPClient { // Handle MCP tool execution ipcMain.handle('mcp-execute-tool', async (event, { toolName, args, toolCallId }) => { + console.log('args in mcp-execute-tool:', args); try { await this.initializeClient(); From 2c03c6ccca735e8678f5020df1b52f123b67e1a5 Mon Sep 17 00:00:00 2001 From: ashu8912 Date: Tue, 23 Sep 2025 13:05:05 +0530 Subject: [PATCH 04/72] ai-plugin: fix mcp client initialization --- app/electron/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/electron/main.ts b/app/electron/main.ts index 126b14b8519..50935408a08 100644 --- a/app/electron/main.ts +++ b/app/electron/main.ts @@ -1133,7 +1133,7 @@ function adjustZoom(delta: number) { function startElecron() { console.info('App starting...'); - + mcpClient = new ElectronMCPClient(); let appVersion: string; if (isDev && process.env.HEADLAMP_APP_VERSION) { appVersion = process.env.HEADLAMP_APP_VERSION; From fe6db0f30230974603b17642dabf5dfb4ea56a21 Mon Sep 17 00:00:00 2001 From: ashu8912 Date: Mon, 13 Oct 2025 09:57:09 +0530 Subject: [PATCH 05/72] app: Add confirmation and mcp config update api --- app/electron/main.ts | 7 + app/electron/mcp-client.ts | 428 +++++++++++++++++++++++++++++++++++-- app/electron/mcp-config.ts | 222 +++++++++++++++++++ app/electron/preload.ts | 6 + 4 files changed, 649 insertions(+), 14 deletions(-) create mode 100644 app/electron/mcp-config.ts diff --git a/app/electron/main.ts b/app/electron/main.ts index 50935408a08..f7ab6d32561 100644 --- a/app/electron/main.ts +++ b/app/electron/main.ts @@ -1249,6 +1249,9 @@ function startElecron() { }, }); + // Set the main window reference in the MCP client for dialogs + mcpClient.setMainWindow(mainWindow); + // Load the frontend mainWindow.loadURL(startUrl); @@ -1469,6 +1472,10 @@ function startElecron() { if (mcpClient) { mcpClient.cleanup().catch(console.error); } + // Cleanup MCP client + if (mcpClient) { + mcpClient.cleanup().catch(console.error); + } }); } diff --git a/app/electron/mcp-client.ts b/app/electron/mcp-client.ts index bc76608f616..74147a70c4c 100644 --- a/app/electron/mcp-client.ts +++ b/app/electron/mcp-client.ts @@ -15,10 +15,11 @@ */ import { MultiServerMCPClient } from '@langchain/mcp-adapters'; -import { app, ipcMain } from 'electron'; +import { app, BrowserWindow, dialog, ipcMain } from 'electron'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; +import { MCPConfigManager } from './mcp-config'; interface MCPServer { name: string; @@ -38,11 +39,286 @@ class ElectronMCPClient { private tools: any[] = []; private isInitialized = false; private initializationPromise: Promise | null = null; + private configManager: MCPConfigManager; + private mainWindow: BrowserWindow | null = null; - constructor() { + constructor(mainWindow: BrowserWindow | null = null) { + this.mainWindow = mainWindow; + this.configManager = new MCPConfigManager(); this.setupIpcHandlers(); } + /** + * Set the main window reference for dialogs + */ + setMainWindow(mainWindow: BrowserWindow | null): void { + this.mainWindow = mainWindow; + } + + /** + * Initialize tools configuration for all available tools + */ + private initializeToolsConfiguration(): void { + if (!this.tools || this.tools.length === 0) { + return; + } + + // Group tools by server name (assuming tool names are prefixed with server name) + const toolsByServer: Record = {}; + + for (const tool of this.tools) { + // Extract server name from tool name (format: "serverName__toolName") + const toolName = tool.name; + const parts = toolName.split('__'); + + if (parts.length >= 2) { + const serverName = parts[0]; + const actualToolName = parts.slice(1).join('__'); + + if (!toolsByServer[serverName]) { + toolsByServer[serverName] = []; + } + toolsByServer[serverName].push(actualToolName); + } else { + // Fallback for tools without server prefix + if (!toolsByServer['default']) { + toolsByServer['default'] = []; + } + toolsByServer['default'].push(toolName); + } + } + + // Initialize configuration for each server's tools + for (const [serverName, toolNames] of Object.entries(toolsByServer)) { + this.configManager.initializeToolsConfig(serverName, toolNames); + } + } + + /** + * Show user confirmation dialog for MCP operations + */ + private async showConfirmationDialog( + title: string, + message: string, + operation: string + ): Promise { + if (!this.mainWindow) { + console.warn('No main window available for confirmation dialog, allowing operation'); + return true; + } + + const result = await dialog.showMessageBox(this.mainWindow, { + type: 'question', + buttons: ['Allow', 'Cancel'], + defaultId: 1, + title, + message, + detail: `Operation: ${operation}\n\nDo you want to allow this MCP operation?`, + }); + + return result.response === 0; // 0 is "Allow" + } + + /** + * Show detailed confirmation dialog for tools configuration changes + */ + private async showToolsConfigConfirmationDialog(newConfig: any): Promise { + if (!this.mainWindow) { + console.warn('No main window available for confirmation dialog, allowing operation'); + return true; + } + + const currentConfig = this.configManager.getConfig(); + const changes = this.compareToolsConfigs(currentConfig, newConfig); + + if (changes.length === 0) { + return true; // No changes, allow operation + } + + const changesText = changes.join('\n'); + + const result = await dialog.showMessageBox(this.mainWindow, { + type: 'question', + buttons: ['Apply Changes', 'Cancel'], + defaultId: 1, + title: 'MCP Tools Configuration Changes', + message: 'The following changes will be applied to your MCP tools configuration:', + detail: changesText + '\n\nDo you want to apply these changes?', + }); + + return result.response === 0; // 0 is "Apply Changes" + } + + /** + * Compare two tools configurations and return a list of changes + */ + private compareToolsConfigs(currentConfig: any, newConfig: any): string[] { + const changes: string[] = []; + + // Get all server names from both configs + const allServers = new Set([ + ...Object.keys(currentConfig || {}), + ...Object.keys(newConfig || {}), + ]); + + for (const serverName of allServers) { + const currentServerConfig = currentConfig[serverName] || {}; + const newServerConfig = newConfig[serverName] || {}; + + // Get all tool names from both configs + const allTools = new Set([ + ...Object.keys(currentServerConfig), + ...Object.keys(newServerConfig), + ]); + + for (const toolName of allTools) { + const currentTool = currentServerConfig[toolName]; + const newTool = newServerConfig[toolName]; + + if (!currentTool && newTool) { + // New tool added + const status = newTool.enabled ? 'enabled' : 'disabled'; + changes.push(`+ Add tool "${toolName}" on server "${serverName}" (${status})`); + } else if (currentTool && !newTool) { + // Tool removed + changes.push(`- Remove tool "${toolName}" from server "${serverName}"`); + } else if (currentTool && newTool) { + // Tool modified + if (currentTool.enabled !== newTool.enabled) { + const status = newTool.enabled ? 'enabled' : 'disabled'; + changes.push(`~ Change tool "${toolName}" on server "${serverName}" to ${status}`); + } + } + } + } + + return changes; + } + + /** + * Show detailed configuration change confirmation dialog + */ + private async showConfigChangeDialog( + currentConfig: MCPConfig | null, + newConfig: MCPConfig + ): Promise { + console.log('Current MCP Config:', currentConfig); + console.log('New MCP Config:', newConfig); + if (!this.mainWindow) { + console.warn('No main window available for confirmation dialog, allowing operation'); + return true; + } + + const changes = this.analyzeConfigChanges(currentConfig, newConfig); + + const result = await dialog.showMessageBox(this.mainWindow, { + type: 'question', + buttons: ['Apply Changes', 'Cancel'], + defaultId: 1, + title: 'MCP Configuration Changes', + message: 'The application wants to update the MCP configuration.', + detail: + changes.length > 0 + ? `The following changes will be applied:\n\n${changes.join( + '\n' + )}\n\nDo you want to apply these changes?` + : 'No changes detected in the configuration.\n\nDo you want to proceed anyway?', + }); + + return result.response === 0; // 0 is "Apply Changes" + } + + /** + * Analyze differences between current and new configuration + */ + private analyzeConfigChanges(currentConfig: MCPConfig | null, newConfig: MCPConfig): string[] { + const changes: string[] = []; + + // Check if MCP is being enabled/disabled + const currentEnabled = currentConfig?.enabled ?? false; + const newEnabled = newConfig.enabled ?? false; + + if (currentEnabled !== newEnabled) { + changes.push(`• MCP will be ${newEnabled ? 'ENABLED' : 'DISABLED'}`); + } + + // Get current and new server lists + const currentServers = currentConfig?.servers ?? []; + const newServers = newConfig.servers ?? []; + + // Check for added servers + const currentServerNames = new Set(currentServers.map(s => s.name)); + const newServerNames = new Set(newServers.map(s => s.name)); + + for (const server of newServers) { + if (!currentServerNames.has(server.name)) { + changes.push(`• ADD server: "${server.name}" (${server.command})`); + } + } + + // Check for removed servers + for (const server of currentServers) { + if (!newServerNames.has(server.name)) { + changes.push(`• REMOVE server: "${server.name}"`); + } + } + + // Check for modified servers + for (const newServer of newServers) { + const currentServer = currentServers.find(s => s.name === newServer.name); + if (currentServer) { + const serverChanges: string[] = []; + + // Check enabled status + if (currentServer.enabled !== newServer.enabled) { + serverChanges.push(`${newServer.enabled ? 'enable' : 'disable'}`); + } + + // Check command + if (currentServer.command !== newServer.command) { + serverChanges.push(`change command: "${currentServer.command}" → "${newServer.command}"`); + } + + // Check arguments + const currentArgs = JSON.stringify(currentServer.args || []); + const newArgs = JSON.stringify(newServer.args || []); + if (currentArgs !== newArgs) { + serverChanges.push(`change arguments: ${currentArgs} → ${newArgs}`); + } + + // Check environment variables + const currentEnv = JSON.stringify(currentServer.env || {}); + const newEnv = JSON.stringify(newServer.env || {}); + if (currentEnv !== newEnv) { + serverChanges.push(`change environment variables`); + } + + if (serverChanges.length > 0) { + changes.push(`• MODIFY server "${newServer.name}": ${serverChanges.join(', ')}`); + } + } + } + + return changes; + } + + /** + * Parse tool name to extract server and tool components + */ + private parseToolName(fullToolName: string): { serverName: string; toolName: string } { + const parts = fullToolName.split('__'); + if (parts.length >= 2) { + return { + serverName: parts[0], + toolName: parts.slice(1).join('__'), + }; + } + return { + serverName: 'default', + toolName: fullToolName, + }; + } + /** * Load MCP server configuration from settings */ @@ -191,6 +467,10 @@ class ElectronMCPClient { // Get and cache the tools this.tools = await this.client.getTools(); + + // Initialize configuration for available tools + this.initializeToolsConfiguration(); + this.isInitialized = true; console.log('MCP client initialized successfully with', this.tools.length, 'tools'); } catch (error) { @@ -212,15 +492,20 @@ class ElectronMCPClient { return { success: true, tools: [] }; } - // Convert LangChain tools to our format - const toolsInfo = this.tools.map(tool => ({ - name: tool.name, - description: tool.description, - inputSchema: tool.schema, - })); - - console.log('MCP tools retrieved:', toolsInfo.length, 'tools'); - return { success: true, tools: toolsInfo }; + // Filter tools based on configuration and convert to our format + const enabledToolsInfo = this.tools + .filter(tool => { + const { serverName, toolName } = this.parseToolName(tool.name); + return this.configManager.isToolEnabled(serverName, toolName); + }) + .map(tool => ({ + name: tool.name, + description: tool.description, + inputSchema: tool.schema, + })); + + console.log('MCP tools retrieved:', enabledToolsInfo.length, 'enabled tools'); + return { success: true, tools: enabledToolsInfo }; } catch (error) { console.error('Error getting MCP tools:', error); return { @@ -241,16 +526,29 @@ class ElectronMCPClient { throw new Error('MCP client not initialized or no tools available'); } + // Parse tool name + const { serverName, toolName: actualToolName } = this.parseToolName(toolName); + + // Check if tool is enabled + if (!this.configManager.isToolEnabled(serverName, actualToolName)) { + throw new Error(`Tool ${toolName} is disabled`); + } + // Find the tool by name const tool = this.tools.find(t => t.name === toolName); if (!tool) { throw new Error(`Tool ${toolName} not found`); } + console.log(`Executing MCP tool: ${toolName} with args:`, args); + // Execute the tool directly using LangChain's invoke method const result = await tool.invoke(args); console.log(`MCP tool ${toolName} executed successfully`); + // Record tool usage + this.configManager.recordToolUsage(serverName, actualToolName); + return { success: true, result, @@ -274,9 +572,23 @@ class ElectronMCPClient { }; }); - // Handle MCP client reset/restart + // Handle MCP client reset/restart with user confirmation ipcMain.handle('mcp-reset-client', async () => { try { + // Show confirmation dialog + const userConfirmed = await this.showConfirmationDialog( + 'MCP Client Reset', + 'The application wants to reset the MCP client. This will restart all MCP server connections.', + 'Reset MCP client' + ); + + if (!userConfirmed) { + return { + success: false, + error: 'User cancelled the operation', + }; + } + console.log('Resetting MCP client...'); if (this.client) { @@ -303,10 +615,23 @@ class ElectronMCPClient { } }); - // Handle MCP configuration updates + // Handle MCP configuration updates with detailed user confirmation ipcMain.handle('mcp-update-config', async (event, mcpConfig: MCPConfig) => { try { - console.log('Updating MCP configuration...'); + // Get current configuration for comparison + const currentConfig = this.loadMCPConfig(); + console.log('Requested MCP configuration update:', mcpConfig); + // Show detailed confirmation dialog with changes + const userConfirmed = await this.showConfigChangeDialog(currentConfig, mcpConfig); + + if (!userConfirmed) { + return { + success: false, + error: 'User cancelled the configuration update', + }; + } + + console.log('Updating MCP configuration with user confirmation...'); // Save to settings file const settingsPath = path.join(app.getPath('userData'), 'settings.json'); @@ -332,6 +657,7 @@ class ElectronMCPClient { // Re-initialize with new config await this.initializeClient(); + console.log('MCP configuration updated successfully'); return { success: true }; } catch (error) { console.error('Error updating MCP configuration:', error); @@ -359,6 +685,80 @@ class ElectronMCPClient { }; } }); + + // Handle getting MCP tools configuration + ipcMain.handle('mcp-get-tools-config', async () => { + try { + const toolsConfig = this.configManager.getConfig(); + return { + success: true, + config: toolsConfig, + }; + } catch (error) { + console.error('Error getting MCP tools configuration:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + config: {}, + }; + } + }); + + // Handle updating MCP tools configuration with user confirmation + ipcMain.handle('mcp-update-tools-config', async (event, toolsConfig: any) => { + try { + // Show confirmation dialog with detailed changes + const userConfirmed = await this.showToolsConfigConfirmationDialog(toolsConfig); + + if (!userConfirmed) { + return { + success: false, + error: 'User cancelled the operation', + }; + } + + this.configManager.setConfig(toolsConfig); + return { success: true }; + } catch (error) { + console.error('Error updating MCP tools configuration:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }; + } + }); + + // Handle enabling/disabling specific tools + ipcMain.handle('mcp-set-tool-enabled', async (event, { serverName, toolName, enabled }) => { + try { + this.configManager.setToolEnabled(serverName, toolName, enabled); + return { success: true }; + } catch (error) { + console.error('Error setting tool enabled state:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }; + } + }); + + // Handle getting tool statistics + ipcMain.handle('mcp-get-tool-stats', async (event, { serverName, toolName }) => { + try { + const stats = this.configManager.getToolStats(serverName, toolName); + return { + success: true, + stats, + }; + } catch (error) { + console.error('Error getting tool statistics:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + stats: null, + }; + } + }); } /** diff --git a/app/electron/mcp-config.ts b/app/electron/mcp-config.ts new file mode 100644 index 00000000000..151a9ebeabb --- /dev/null +++ b/app/electron/mcp-config.ts @@ -0,0 +1,222 @@ +/* + * Copyright 2025 The Kubernetes Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { app } from 'electron'; +import * as fs from 'fs'; +import * as path from 'path'; + +export interface MCPToolState { + enabled: boolean; + lastUsed?: Date; + usageCount?: number; +} + +export interface MCPServerToolState { + [toolName: string]: MCPToolState; +} + +export interface MCPToolsConfig { + [serverName: string]: MCPServerToolState; +} + +export class MCPConfigManager { + private configPath: string; + private config: MCPToolsConfig = {}; + + constructor() { + this.configPath = path.join(app.getPath('userData'), 'headlamp-mcp-config.json'); + this.loadConfig(); + } + + /** + * Load MCP tools configuration from file + */ + private loadConfig(): void { + try { + if (fs.existsSync(this.configPath)) { + const configData = fs.readFileSync(this.configPath, 'utf-8'); + this.config = JSON.parse(configData); + console.log('MCP tools configuration loaded successfully'); + } else { + console.log('MCP tools configuration file does not exist, using default empty config'); + this.config = {}; + } + } catch (error) { + console.error('Error loading MCP tools configuration:', error); + this.config = {}; + } + } + + /** + * Save MCP tools configuration to file + */ + private saveConfig(): void { + try { + fs.writeFileSync(this.configPath, JSON.stringify(this.config, null, 2), 'utf-8'); + console.log('MCP tools configuration saved successfully'); + } catch (error) { + console.error('Error saving MCP tools configuration:', error); + } + } + + /** + * Get the enabled state of a specific tool + */ + isToolEnabled(serverName: string, toolName: string): boolean { + const serverConfig = this.config[serverName]; + if (!serverConfig) { + // Default to enabled for new tools + return true; + } + + const toolState = serverConfig[toolName]; + if (!toolState) { + // Default to enabled for new tools + return true; + } + + return toolState.enabled; + } + + /** + * Set the enabled state of a specific tool + */ + setToolEnabled(serverName: string, toolName: string, enabled: boolean): void { + if (!this.config[serverName]) { + this.config[serverName] = {}; + } + + if (!this.config[serverName][toolName]) { + this.config[serverName][toolName] = { + enabled: true, + usageCount: 0, + }; + } + + this.config[serverName][toolName].enabled = enabled; + this.saveConfig(); + } + + /** + * Get all disabled tools for a server + */ + getDisabledTools(serverName: string): string[] { + const serverConfig = this.config[serverName]; + if (!serverConfig) { + return []; + } + + return Object.entries(serverConfig) + .filter(([, toolState]) => !toolState.enabled) + .map(([toolName]) => toolName); + } + + /** + * Get all enabled tools for a server + */ + getEnabledTools(serverName: string): string[] { + const serverConfig = this.config[serverName]; + if (!serverConfig) { + return []; + } + + return Object.entries(serverConfig) + .filter(([, toolState]) => toolState.enabled) + .map(([toolName]) => toolName); + } + + /** + * Update tool usage statistics + */ + recordToolUsage(serverName: string, toolName: string): void { + if (!this.config[serverName]) { + this.config[serverName] = {}; + } + + if (!this.config[serverName][toolName]) { + this.config[serverName][toolName] = { + enabled: true, + usageCount: 0, + }; + } + + const toolState = this.config[serverName][toolName]; + toolState.lastUsed = new Date(); + toolState.usageCount = (toolState.usageCount || 0) + 1; + this.saveConfig(); + } + + /** + * Get the complete configuration + */ + getConfig(): MCPToolsConfig { + return { ...this.config }; + } + + /** + * Set the complete configuration + */ + setConfig(newConfig: MCPToolsConfig): void { + this.config = { ...newConfig }; + this.saveConfig(); + } + + /** + * Reset configuration to empty state + */ + resetConfig(): void { + this.config = {}; + this.saveConfig(); + } + + /** + * Initialize default configuration for available tools + */ + initializeToolsConfig(serverName: string, toolNames: string[]): void { + if (!this.config[serverName]) { + this.config[serverName] = {}; + } + + const serverConfig = this.config[serverName]; + let hasChanges = false; + + for (const toolName of toolNames) { + if (!serverConfig[toolName]) { + serverConfig[toolName] = { + enabled: true, + usageCount: 0, + }; + hasChanges = true; + } + } + + if (hasChanges) { + this.saveConfig(); + } + } + + /** + * Get tool statistics + */ + getToolStats(serverName: string, toolName: string): MCPToolState | null { + const serverConfig = this.config[serverName]; + if (!serverConfig || !serverConfig[toolName]) { + return null; + } + + return { ...serverConfig[toolName] }; + } +} diff --git a/app/electron/preload.ts b/app/electron/preload.ts index 072513a1b11..f6207b4bbe3 100644 --- a/app/electron/preload.ts +++ b/app/electron/preload.ts @@ -67,5 +67,11 @@ contextBridge.exposeInMainWorld('desktopApi', { resetClient: () => ipcRenderer.invoke('mcp-reset-client'), getConfig: () => ipcRenderer.invoke('mcp-get-config'), updateConfig: (config: any) => ipcRenderer.invoke('mcp-update-config', config), + getToolsConfig: () => ipcRenderer.invoke('mcp-get-tools-config'), + updateToolsConfig: (config: any) => ipcRenderer.invoke('mcp-update-tools-config', config), + setToolEnabled: (serverName: string, toolName: string, enabled: boolean) => + ipcRenderer.invoke('mcp-set-tool-enabled', { serverName, toolName, enabled }), + getToolStats: (serverName: string, toolName: string) => + ipcRenderer.invoke('mcp-get-tool-stats', { serverName, toolName }), }, }); From 53f188fe8330aab675031b70120c8cffe751ef2f Mon Sep 17 00:00:00 2001 From: ashu8912 Date: Sat, 18 Oct 2025 14:24:13 +0530 Subject: [PATCH 06/72] app: Fix initialization and schema config --- app/electron/main.ts | 6 + app/electron/mcp-client.ts | 248 ++++++++++++++++++++++++++++--------- app/electron/mcp-config.ts | 54 ++++++-- app/electron/preload.ts | 1 - 4 files changed, 243 insertions(+), 66 deletions(-) diff --git a/app/electron/main.ts b/app/electron/main.ts index f7ab6d32561..79b67c16d3c 100644 --- a/app/electron/main.ts +++ b/app/electron/main.ts @@ -1134,6 +1134,12 @@ function adjustZoom(delta: number) { function startElecron() { console.info('App starting...'); mcpClient = new ElectronMCPClient(); + + // Initialize MCP client + mcpClient.initialize().catch(error => { + console.error('Failed to initialize MCP client on startup:', error); + }); + let appVersion: string; if (isDev && process.env.HEADLAMP_APP_VERSION) { appVersion = process.env.HEADLAMP_APP_VERSION; diff --git a/app/electron/mcp-client.ts b/app/electron/mcp-client.ts index 74147a70c4c..fdbda60a362 100644 --- a/app/electron/mcp-client.ts +++ b/app/electron/mcp-client.ts @@ -60,17 +60,33 @@ class ElectronMCPClient { */ private initializeToolsConfiguration(): void { if (!this.tools || this.tools.length === 0) { + console.log('No tools available for configuration initialization'); return; } - // Group tools by server name (assuming tool names are prefixed with server name) - const toolsByServer: Record = {}; + // Group tools by server name with their schemas + const toolsByServer: Record< + string, + Array<{ + name: string; + inputSchema?: any; + description?: string; + }> + > = {}; for (const tool of this.tools) { + console.log('Initializing tools configuration...', tool); // Extract server name from tool name (format: "serverName__toolName") const toolName = tool.name; const parts = toolName.split('__'); + // Extract schema from the tool (LangChain tools use .schema property) + const toolSchema = (tool as any).schema || tool.inputSchema || null; + console.log('tool schema is ', toolSchema); + console.log( + `Processing tool: ${toolName}, has inputSchema: ${toolSchema}, description: "${tool.description}"` + ); + if (parts.length >= 2) { const serverName = parts[0]; const actualToolName = parts.slice(1).join('__'); @@ -78,19 +94,30 @@ class ElectronMCPClient { if (!toolsByServer[serverName]) { toolsByServer[serverName] = []; } - toolsByServer[serverName].push(actualToolName); + toolsByServer[serverName].push({ + name: actualToolName, + inputSchema: toolSchema, + description: tool.description || '', + }); } else { // Fallback for tools without server prefix if (!toolsByServer['default']) { toolsByServer['default'] = []; } - toolsByServer['default'].push(toolName); + toolsByServer['default'].push({ + name: toolName, + inputSchema: toolSchema, + description: tool.description || '', + }); } } + console.log('Tools grouped by server:', Object.keys(toolsByServer)); + // Initialize configuration for each server's tools - for (const [serverName, toolNames] of Object.entries(toolsByServer)) { - this.configManager.initializeToolsConfig(serverName, toolNames); + for (const [serverName, toolsInfo] of Object.entries(toolsByServer)) { + console.log(`Initializing ${toolsInfo.length} tools for server: ${serverName}`); + this.configManager.initializeToolsConfig(serverName, toolsInfo); } } @@ -129,31 +156,38 @@ class ElectronMCPClient { } const currentConfig = this.configManager.getConfig(); - const changes = this.compareToolsConfigs(currentConfig, newConfig); + const summary = this.createToolsConfigSummary(currentConfig, newConfig); - if (changes.length === 0) { + if (summary.totalChanges === 0) { return true; // No changes, allow operation } - const changesText = changes.join('\n'); - const result = await dialog.showMessageBox(this.mainWindow, { type: 'question', buttons: ['Apply Changes', 'Cancel'], defaultId: 1, title: 'MCP Tools Configuration Changes', - message: 'The following changes will be applied to your MCP tools configuration:', - detail: changesText + '\n\nDo you want to apply these changes?', + message: `${summary.totalChanges} tool configuration change(s) will be applied:`, + detail: summary.summaryText + '\n\nDo you want to apply these changes?', }); return result.response === 0; // 0 is "Apply Changes" } /** - * Compare two tools configurations and return a list of changes + * Create a concise summary of tools configuration changes */ - private compareToolsConfigs(currentConfig: any, newConfig: any): string[] { - const changes: string[] = []; + private createToolsConfigSummary( + currentConfig: any, + newConfig: any + ): { + totalChanges: number; + summaryText: string; + } { + const enabledTools: string[] = []; + const disabledTools: string[] = []; + const addedTools: string[] = []; + const removedTools: string[] = []; // Get all server names from both configs const allServers = new Set([ @@ -174,25 +208,50 @@ class ElectronMCPClient { for (const toolName of allTools) { const currentTool = currentServerConfig[toolName]; const newTool = newServerConfig[toolName]; + const displayName = `${toolName} (${serverName})`; if (!currentTool && newTool) { // New tool added - const status = newTool.enabled ? 'enabled' : 'disabled'; - changes.push(`+ Add tool "${toolName}" on server "${serverName}" (${status})`); + addedTools.push(displayName); + if (newTool.enabled) { + enabledTools.push(displayName); + } else { + disabledTools.push(displayName); + } } else if (currentTool && !newTool) { // Tool removed - changes.push(`- Remove tool "${toolName}" from server "${serverName}"`); + removedTools.push(displayName); } else if (currentTool && newTool) { // Tool modified if (currentTool.enabled !== newTool.enabled) { - const status = newTool.enabled ? 'enabled' : 'disabled'; - changes.push(`~ Change tool "${toolName}" on server "${serverName}" to ${status}`); + if (newTool.enabled) { + enabledTools.push(displayName); + } else { + disabledTools.push(displayName); + } } } } } - return changes; + // Build summary text + const summaryParts: string[] = []; + + if (enabledTools.length > 0) { + summaryParts.push(`✓ ENABLE (${enabledTools.length}): ${enabledTools.join(', ')}`); + } + + if (disabledTools.length > 0) { + summaryParts.push(`✗ DISABLE (${disabledTools.length}): ${disabledTools.join(', ')}`); + } + + const totalChanges = + enabledTools.length + disabledTools.length + addedTools.length + removedTools.length; + + return { + totalChanges, + summaryText: summaryParts.join('\n\n'), + }; } /** @@ -319,6 +378,90 @@ class ElectronMCPClient { }; } + /** + * Validate tool parameters against schema from configuration + */ + private validateToolParameters( + serverName: string, + toolName: string, + args: any + ): { valid: boolean; error?: string } { + const toolState = this.configManager.getToolStats(serverName, toolName); + if (!toolState || !toolState.inputSchema) { + // No schema available, assume valid + return { valid: true }; + } + + try { + const schema = toolState.inputSchema; + + // Basic validation - check required properties + if (schema.required && Array.isArray(schema.required)) { + for (const requiredProp of schema.required) { + if (args[requiredProp] === undefined || args[requiredProp] === null) { + return { + valid: false, + error: `Required parameter '${requiredProp}' is missing`, + }; + } + } + } + + // Check property types if schema properties are defined + if (schema.properties) { + for (const [propName, propSchema] of Object.entries(schema.properties as any)) { + if (args[propName] !== undefined) { + const propType = (propSchema as any).type; + const actualType = typeof args[propName]; + + if (propType === 'string' && actualType !== 'string') { + return { + valid: false, + error: `Parameter '${propName}' should be a string, got ${actualType}`, + }; + } + if (propType === 'number' && actualType !== 'number') { + return { + valid: false, + error: `Parameter '${propName}' should be a number, got ${actualType}`, + }; + } + if (propType === 'boolean' && actualType !== 'boolean') { + return { + valid: false, + error: `Parameter '${propName}' should be a boolean, got ${actualType}`, + }; + } + if (propType === 'array' && !Array.isArray(args[propName])) { + return { + valid: false, + error: `Parameter '${propName}' should be an array, got ${actualType}`, + }; + } + if ( + propType === 'object' && + (actualType !== 'object' || Array.isArray(args[propName]) || args[propName] === null) + ) { + return { + valid: false, + error: `Parameter '${propName}' should be an object, got ${actualType}`, + }; + } + } + } + } + + return { valid: true }; + } catch (error) { + return { + valid: false, + error: `Schema validation error: ${ + error instanceof Error ? error.message : 'Unknown error' + }`, + }; + } + } + /** * Load MCP server configuration from settings */ @@ -390,6 +533,7 @@ class ElectronMCPClient { } private async initializeClient(): Promise { + console.log('initializeClient called'); if (this.isInitialized) { return; } @@ -398,6 +542,7 @@ class ElectronMCPClient { return this.initializationPromise; } + console.log('Starting MCP client initialization...'); this.initializationPromise = this.doInitialize(); return this.initializationPromise; } @@ -467,7 +612,6 @@ class ElectronMCPClient { // Get and cache the tools this.tools = await this.client.getTools(); - // Initialize configuration for available tools this.initializeToolsConfiguration(); @@ -483,39 +627,6 @@ class ElectronMCPClient { } private setupIpcHandlers(): void { - // Handle MCP tools request - ipcMain.handle('mcp-get-tools', async () => { - try { - await this.initializeClient(); - - if (!this.client || this.tools.length === 0) { - return { success: true, tools: [] }; - } - - // Filter tools based on configuration and convert to our format - const enabledToolsInfo = this.tools - .filter(tool => { - const { serverName, toolName } = this.parseToolName(tool.name); - return this.configManager.isToolEnabled(serverName, toolName); - }) - .map(tool => ({ - name: tool.name, - description: tool.description, - inputSchema: tool.schema, - })); - - console.log('MCP tools retrieved:', enabledToolsInfo.length, 'enabled tools'); - return { success: true, tools: enabledToolsInfo }; - } catch (error) { - console.error('Error getting MCP tools:', error); - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - tools: [], - }; - } - }); - // Handle MCP tool execution ipcMain.handle('mcp-execute-tool', async (event, { toolName, args, toolCallId }) => { console.log('args in mcp-execute-tool:', args); @@ -530,8 +641,10 @@ class ElectronMCPClient { const { serverName, toolName: actualToolName } = this.parseToolName(toolName); // Check if tool is enabled - if (!this.configManager.isToolEnabled(serverName, actualToolName)) { - throw new Error(`Tool ${toolName} is disabled`); + const isEnabled = this.configManager.isToolEnabled(serverName, actualToolName); + + if (!isEnabled) { + throw new Error(`Tool ${actualToolName} from server ${serverName} is disabled`); } // Find the tool by name @@ -540,6 +653,12 @@ class ElectronMCPClient { throw new Error(`Tool ${toolName} not found`); } + // Validate parameters against schema from configuration + const validation = this.validateToolParameters(serverName, actualToolName, args); + if (!validation.valid) { + throw new Error(`Parameter validation failed: ${validation.error}`); + } + console.log(`Executing MCP tool: ${toolName} with args:`, args); // Execute the tool directly using LangChain's invoke method @@ -555,7 +674,6 @@ class ElectronMCPClient { toolCallId, }; } catch (error) { - console.error(`Error executing MCP tool ${toolName}:`, error); return { success: false, error: error instanceof Error ? error.message : 'Unknown error', @@ -706,6 +824,7 @@ class ElectronMCPClient { // Handle updating MCP tools configuration with user confirmation ipcMain.handle('mcp-update-tools-config', async (event, toolsConfig: any) => { + console.log('Requested MCP tools configuration update:', toolsConfig); try { // Show confirmation dialog with detailed changes const userConfirmed = await this.showToolsConfigConfirmationDialog(toolsConfig); @@ -761,6 +880,19 @@ class ElectronMCPClient { }); } + /** + * Public method to initialize the MCP client + * This should be called when the app starts + */ + async initialize(): Promise { + try { + await this.initializeClient(); + } catch (error) { + console.error('Failed to initialize MCP client on startup:', error); + // Don't throw error to prevent app startup failure + } + } + /** * Cleanup method to be called when the app is shutting down */ diff --git a/app/electron/mcp-config.ts b/app/electron/mcp-config.ts index 151a9ebeabb..bffb5a18386 100644 --- a/app/electron/mcp-config.ts +++ b/app/electron/mcp-config.ts @@ -22,6 +22,8 @@ export interface MCPToolState { enabled: boolean; lastUsed?: Date; usageCount?: number; + inputSchema?: any; // JSON schema for tool parameters + description?: string; // Tool description from MCP server } export interface MCPServerToolState { @@ -49,13 +51,10 @@ export class MCPConfigManager { if (fs.existsSync(this.configPath)) { const configData = fs.readFileSync(this.configPath, 'utf-8'); this.config = JSON.parse(configData); - console.log('MCP tools configuration loaded successfully'); } else { - console.log('MCP tools configuration file does not exist, using default empty config'); this.config = {}; } } catch (error) { - console.error('Error loading MCP tools configuration:', error); this.config = {}; } } @@ -66,7 +65,6 @@ export class MCPConfigManager { private saveConfig(): void { try { fs.writeFileSync(this.configPath, JSON.stringify(this.config, null, 2), 'utf-8'); - console.log('MCP tools configuration saved successfully'); } catch (error) { console.error('Error saving MCP tools configuration:', error); } @@ -183,9 +181,17 @@ export class MCPConfigManager { } /** - * Initialize default configuration for available tools + * Initialize default configuration for available tools with schemas */ - initializeToolsConfig(serverName: string, toolNames: string[]): void { + initializeToolsConfig( + serverName: string, + toolsInfo: Array<{ + name: string; + inputSchema?: any; + description?: string; + }> + ): void { + console.log('MCP config called ', this.config, serverName, toolsInfo); if (!this.config[serverName]) { this.config[serverName] = {}; } @@ -193,18 +199,52 @@ export class MCPConfigManager { const serverConfig = this.config[serverName]; let hasChanges = false; - for (const toolName of toolNames) { + for (const toolInfo of toolsInfo) { + const toolName = toolInfo.name; + if (!serverConfig[toolName]) { + console.log(`Creating new tool config for: ${toolName}`); serverConfig[toolName] = { enabled: true, usageCount: 0, + inputSchema: toolInfo.inputSchema || null, + description: toolInfo.description || '', }; hasChanges = true; + } else { + console.log(`Updating existing tool config for: ${toolName}`); + // Always update schema and description for existing tools + let toolChanged = false; + + // Update schema if it's different or missing + const currentSchema = JSON.stringify(serverConfig[toolName].inputSchema || null); + const newSchema = JSON.stringify(toolInfo.inputSchema || null); + if (currentSchema !== newSchema) { + console.log(`Updating schema for tool: ${toolName}`); + serverConfig[toolName].inputSchema = toolInfo.inputSchema || null; + toolChanged = true; + } + + // Update description if it's different or missing + const currentDescription = serverConfig[toolName].description || ''; + const newDescription = toolInfo.description || ''; + if (currentDescription !== newDescription) { + console.log(`Updating description for tool: ${toolName}`); + serverConfig[toolName].description = newDescription; + toolChanged = true; + } + + if (toolChanged) { + hasChanges = true; + } } } if (hasChanges) { + console.log(`Saving configuration changes for server: ${serverName}`); this.saveConfig(); + } else { + console.log(`No changes needed for server: ${serverName}`); } } diff --git a/app/electron/preload.ts b/app/electron/preload.ts index f6207b4bbe3..0157265b447 100644 --- a/app/electron/preload.ts +++ b/app/electron/preload.ts @@ -60,7 +60,6 @@ contextBridge.exposeInMainWorld('desktopApi', { // MCP client APIs mcp: { - getTools: () => ipcRenderer.invoke('mcp-get-tools'), executeTool: (toolName: string, args: Record, toolCallId?: string) => ipcRenderer.invoke('mcp-execute-tool', { toolName, args, toolCallId }), getStatus: () => ipcRenderer.invoke('mcp-get-status'), From a72233cfa4d6733bf06deb23263f41d18b374164 Mon Sep 17 00:00:00 2001 From: ashu8912 Date: Sun, 19 Oct 2025 11:51:24 +0530 Subject: [PATCH 07/72] app: Replace mcp config if server config changes --- app/electron/main.ts | 5 +--- app/electron/mcp-client.ts | 17 ++++++------ app/electron/mcp-config.ts | 56 ++++++++++++++++++++++++++++++++------ 3 files changed, 58 insertions(+), 20 deletions(-) diff --git a/app/electron/main.ts b/app/electron/main.ts index 79b67c16d3c..9d2086987e4 100644 --- a/app/electron/main.ts +++ b/app/electron/main.ts @@ -1474,10 +1474,7 @@ function startElecron() { if (mainWindow) { mainWindow.removeAllListeners('close'); } - // Cleanup MCP client - if (mcpClient) { - mcpClient.cleanup().catch(console.error); - } + // Cleanup MCP client if (mcpClient) { mcpClient.cleanup().catch(console.error); diff --git a/app/electron/mcp-client.ts b/app/electron/mcp-client.ts index fdbda60a362..873d3ab81e9 100644 --- a/app/electron/mcp-client.ts +++ b/app/electron/mcp-client.ts @@ -57,10 +57,13 @@ class ElectronMCPClient { /** * Initialize tools configuration for all available tools + * This completely replaces the existing config with current tools */ private initializeToolsConfiguration(): void { if (!this.tools || this.tools.length === 0) { console.log('No tools available for configuration initialization'); + // Clear the config if no tools are available + this.configManager.replaceConfig({}); return; } @@ -75,16 +78,16 @@ class ElectronMCPClient { > = {}; for (const tool of this.tools) { - console.log('Initializing tools configuration...', tool); // Extract server name from tool name (format: "serverName__toolName") const toolName = tool.name; const parts = toolName.split('__'); // Extract schema from the tool (LangChain tools use .schema property) const toolSchema = (tool as any).schema || tool.inputSchema || null; - console.log('tool schema is ', toolSchema); console.log( - `Processing tool: ${toolName}, has inputSchema: ${toolSchema}, description: "${tool.description}"` + `Processing tool: ${toolName}, has inputSchema: ${!!toolSchema}, description: "${ + tool.description + }"` ); if (parts.length >= 2) { @@ -114,11 +117,8 @@ class ElectronMCPClient { console.log('Tools grouped by server:', Object.keys(toolsByServer)); - // Initialize configuration for each server's tools - for (const [serverName, toolsInfo] of Object.entries(toolsByServer)) { - console.log(`Initializing ${toolsInfo.length} tools for server: ${serverName}`); - this.configManager.initializeToolsConfig(serverName, toolsInfo); - } + // Replace the entire configuration with current tools + this.configManager.replaceToolsConfig(toolsByServer); } /** @@ -608,6 +608,7 @@ class ElectronMCPClient { additionalToolNamePrefix: '', useStandardContentBlocks: true, mcpServers, + defaultToolTimeout: 2 * 60 * 1000, // 2 minutes }); // Get and cache the tools diff --git a/app/electron/mcp-config.ts b/app/electron/mcp-config.ts index bffb5a18386..0bc65234b5d 100644 --- a/app/electron/mcp-config.ts +++ b/app/electron/mcp-config.ts @@ -191,7 +191,6 @@ export class MCPConfigManager { description?: string; }> ): void { - console.log('MCP config called ', this.config, serverName, toolsInfo); if (!this.config[serverName]) { this.config[serverName] = {}; } @@ -203,7 +202,6 @@ export class MCPConfigManager { const toolName = toolInfo.name; if (!serverConfig[toolName]) { - console.log(`Creating new tool config for: ${toolName}`); serverConfig[toolName] = { enabled: true, usageCount: 0, @@ -212,7 +210,6 @@ export class MCPConfigManager { }; hasChanges = true; } else { - console.log(`Updating existing tool config for: ${toolName}`); // Always update schema and description for existing tools let toolChanged = false; @@ -220,7 +217,6 @@ export class MCPConfigManager { const currentSchema = JSON.stringify(serverConfig[toolName].inputSchema || null); const newSchema = JSON.stringify(toolInfo.inputSchema || null); if (currentSchema !== newSchema) { - console.log(`Updating schema for tool: ${toolName}`); serverConfig[toolName].inputSchema = toolInfo.inputSchema || null; toolChanged = true; } @@ -229,7 +225,6 @@ export class MCPConfigManager { const currentDescription = serverConfig[toolName].description || ''; const newDescription = toolInfo.description || ''; if (currentDescription !== newDescription) { - console.log(`Updating description for tool: ${toolName}`); serverConfig[toolName].description = newDescription; toolChanged = true; } @@ -241,10 +236,7 @@ export class MCPConfigManager { } if (hasChanges) { - console.log(`Saving configuration changes for server: ${serverName}`); this.saveConfig(); - } else { - console.log(`No changes needed for server: ${serverName}`); } } @@ -259,4 +251,52 @@ export class MCPConfigManager { return { ...serverConfig[toolName] }; } + + /** + * Replace the entire tools configuration with a new set of tools + * This overwrites all existing tools with only the current ones + */ + replaceToolsConfig( + toolsByServer: Record< + string, + Array<{ + name: string; + inputSchema?: any; + description?: string; + }> + > + ): void { + // Create a new config object + const newConfig: MCPToolsConfig = {}; + + for (const [serverName, toolsInfo] of Object.entries(toolsByServer)) { + newConfig[serverName] = {}; + + for (const toolInfo of toolsInfo) { + const toolName = toolInfo.name; + + // Check if this tool existed in the old config to preserve enabled state and usage count + const oldToolState = this.config[serverName]?.[toolName]; + + newConfig[serverName][toolName] = { + enabled: oldToolState?.enabled ?? true, // Preserve enabled state or default to true + usageCount: oldToolState?.usageCount ?? 0, // Preserve usage count or default to 0 + inputSchema: toolInfo.inputSchema || null, + description: toolInfo.description || '', + }; + } + } + + // Replace the entire config + this.config = newConfig; + this.saveConfig(); + } + + /** + * Replace the entire configuration with a new config object + */ + replaceConfig(newConfig: MCPToolsConfig): void { + this.config = newConfig; + this.saveConfig(); + } } From 476f33da804aa2725c8b1794fa4ba33976227225 Mon Sep 17 00:00:00 2001 From: ashu8912 Date: Mon, 27 Oct 2025 13:51:37 +0530 Subject: [PATCH 08/72] app: Handle cluster change --- app/electron/main.ts | 9 ++++ app/electron/mcp-client.ts | 89 +++++++++++++++++++++++++++++++++++++- app/electron/preload.ts | 8 ++++ 3 files changed, 104 insertions(+), 2 deletions(-) diff --git a/app/electron/main.ts b/app/electron/main.ts index 9d2086987e4..dc9de252d9b 100644 --- a/app/electron/main.ts +++ b/app/electron/main.ts @@ -1422,6 +1422,15 @@ function startElecron() { mainWindow?.webContents.send('backend-token', backendToken); }); + // Handle cluster change notifications from frontend + ipcMain.on('cluster-changed', (event: IpcMainEvent, cluster: string | null) => { + if (mcpClient) { + mcpClient.handleClusterChange(cluster).catch(error => { + console.error('Failed to handle cluster change in MCP client:', error); + }); + } + }); + setupRunCmdHandlers(mainWindow, ipcMain); new PluginManagerEventListeners().setupEventHandlers(); diff --git a/app/electron/mcp-client.ts b/app/electron/mcp-client.ts index 873d3ab81e9..a113d1a9f1f 100644 --- a/app/electron/mcp-client.ts +++ b/app/electron/mcp-client.ts @@ -41,6 +41,7 @@ class ElectronMCPClient { private initializationPromise: Promise | null = null; private configManager: MCPConfigManager; private mainWindow: BrowserWindow | null = null; + private currentCluster: string | null = null; constructor(mainWindow: BrowserWindow | null = null) { this.mainWindow = mainWindow; @@ -483,11 +484,18 @@ class ElectronMCPClient { /** * Expand environment variables and resolve paths in arguments */ - private expandArgs(args: string[]): string[] { + private expandArgs(args: string[], cluster: string | null = null): string[] { + const currentCluster = cluster || this.currentCluster || ''; + return args.map(arg => { // Replace Windows environment variables like %USERPROFILE% let expandedArg = arg; + // Handle HEADLAMP_CURRENT_CLUSTER placeholder + if (expandedArg.includes('HEADLAMP_CURRENT_CLUSTER')) { + expandedArg = expandedArg.replace(/HEADLAMP_CURRENT_CLUSTER/g, currentCluster); + } + // Handle %USERPROFILE% if (expandedArg.includes('%USERPROFILE%')) { expandedArg = expandedArg.replace(/%USERPROFILE%/g, os.homedir()); @@ -574,7 +582,7 @@ class ElectronMCPClient { } // Expand environment variables and resolve paths in arguments - const expandedArgs = this.expandArgs(server.args || []); + const expandedArgs = this.expandArgs(server.args || [], this.currentCluster); console.log(`Expanded args for ${server.name}:`, expandedArgs); // Prepare environment variables @@ -879,6 +887,83 @@ class ElectronMCPClient { }; } }); + + // Handle cluster context changes + ipcMain.handle('mcp-cluster-change', async (event, { cluster }) => { + try { + console.log('Received cluster change event:', cluster); + await this.handleClusterChange(cluster); + return { + success: true, + }; + } catch (error) { + console.error('Error handling cluster change:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }; + } + }); + } + + /** + * Check if any server in the config uses HEADLAMP_CURRENT_CLUSTER + */ + private hasClusterDependentServers(mcpConfig: MCPConfig | null): boolean { + if (!mcpConfig || !mcpConfig.servers) { + return false; + } + + return mcpConfig.servers.some( + server => + server.enabled && + server.args && + server.args.some(arg => arg.includes('HEADLAMP_CURRENT_CLUSTER')) + ); + } + + /** + * Handle cluster context change + * This will restart MCP servers if any server uses HEADLAMP_CURRENT_CLUSTER + */ + async handleClusterChange(newCluster: string | null): Promise { + // If cluster hasn't actually changed, do nothing + if (this.currentCluster === newCluster) { + return; + } + + const oldCluster = this.currentCluster; + this.currentCluster = newCluster; + + // Check if we have any cluster-dependent servers + const mcpConfig = this.loadMCPConfig(); + if (!this.hasClusterDependentServers(mcpConfig)) { + console.log('No cluster-dependent MCP servers found, skipping restart'); + return; + } + + try { + // Reset the client + if (this.client) { + if (typeof (this.client as any).close === 'function') { + await (this.client as any).close(); + } + } + + this.client = null; + this.isInitialized = false; + this.initializationPromise = null; + + // Re-initialize with new cluster context + await this.initializeClient(); + + console.log('MCP client restarted successfully for new cluster:', newCluster); + } catch (error) { + console.error('Error restarting MCP client for cluster change:', error); + // Restore previous cluster on error + this.currentCluster = oldCluster; + throw error; + } } /** diff --git a/app/electron/preload.ts b/app/electron/preload.ts index 0157265b447..3ab81137370 100644 --- a/app/electron/preload.ts +++ b/app/electron/preload.ts @@ -30,6 +30,7 @@ contextBridge.exposeInMainWorld('desktopApi', { 'plugin-manager', 'request-backend-token', 'request-plugin-permission-secrets', + 'cluster-changed', ]; if (validChannels.includes(channel)) { ipcRenderer.send(channel, data); @@ -72,5 +73,12 @@ contextBridge.exposeInMainWorld('desktopApi', { ipcRenderer.invoke('mcp-set-tool-enabled', { serverName, toolName, enabled }), getToolStats: (serverName: string, toolName: string) => ipcRenderer.invoke('mcp-get-tool-stats', { serverName, toolName }), + clusterChange: (cluster: string | null) => + ipcRenderer.invoke('mcp-cluster-change', { cluster }), + }, + + // Notify cluster change (for MCP server restart) + notifyClusterChange: (cluster: string | null) => { + ipcRenderer.send('cluster-changed', cluster); }, }); From 216b803e5549b38cdea8dd42fb30705fc53674f1 Mon Sep 17 00:00:00 2001 From: Joaquim Rocha Date: Fri, 17 Oct 2025 14:54:05 +0100 Subject: [PATCH 09/72] Add root-level npm So we can run Headlamp without make, just by using npm. --- package-lock.json | 377 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 85 +++++++++++ 2 files changed, 462 insertions(+) create mode 100644 package-lock.json create mode 100644 package.json diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000000..5ac4ca37c19 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,377 @@ +{ + "name": "headlamp-root", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "headlamp-root", + "version": "0.1.0", + "license": "Apache-2.0", + "devDependencies": { + "concurrently": "^8.2.2" + }, + "engines": { + "node": ">=20.11.1", + "npm": ">=10.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concurrently": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz", + "integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "date-fns": "^2.30.0", + "lodash": "^4.17.21", + "rxjs": "^7.8.1", + "shell-quote": "^1.8.1", + "spawn-command": "0.0.2", + "supports-color": "^8.1.1", + "tree-kill": "^1.2.2", + "yargs": "^17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": "^14.13.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/spawn-command": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", + "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==", + "dev": true + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000000..0e659e46390 --- /dev/null +++ b/package.json @@ -0,0 +1,85 @@ +{ + "name": "headlamp-root", + "version": "0.1.0", + "private": true, + "description": "Headlamp - An easy-to-use and extensible Kubernetes web UI", + "repository": { + "type": "git", + "url": "https://github.com/kubernetes-sigs/headlamp.git" + }, + "license": "Apache-2.0", + "engines": { + "node": ">=20.11.1", + "npm": ">=10.0.0" + }, + "scripts": { + "install:all": "npm run frontend:install && npm run backend:build && npm run app:install", + "install:frontend": "npm run frontend:install", + "install:backend": "npm run backend:build", + "install:app": "npm run app:install", + "build": "npm run backend:build && npm run frontend:build", + "dev": "npm run start", + "start": "concurrently \"npm run backend:start\" \"npm run frontend:start\" --names \"backend,frontend\" --prefix-colors \"blue,green\"", + "test": "npm run backend:test && npm run frontend:test", + "lint": "npm run backend:lint && npm run frontend:lint", + "lint:fix": "npm run backend:lint:fix && npm run frontend:lint:fix", + "clean": "rm -rf frontend/build frontend/node_modules app/node_modules app/dist backend/headlamp-server backend/headlamp-server.exe node_modules", + "backend:build": "cd backend && go build -o ./headlamp-server ./cmd", + "backend:lint": "npm run backend:install:linter && cd backend && ./tools/golangci-lint run", + "backend:lint:fix": "npm run backend:install:linter && cd backend && ./tools/golangci-lint run --fix", + "backend:install:linter": "GOBIN=`pwd`/backend/tools go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.64", + "backend:test": "cd backend && go test -v -p 1 ./...", + "backend:coverage": "cd backend && go test -v -p 1 -coverprofile=coverage.out ./... && go tool cover -func=coverage.out", + "backend:coverage:html": "cd backend && go test -v -p 1 -coverprofile=coverage.out ./... && go tool cover -html=coverage.out", + "backend:format": "cd backend && go fmt ./cmd/ ./pkg/**", + "backend:start": "echo 'Warning: Running with Helm and dynamic-clusters endpoints enabled.' && HEADLAMP_BACKEND_TOKEN=headlamp HEADLAMP_CONFIG_ENABLE_HELM=true HEADLAMP_CONFIG_ENABLE_DYNAMIC_CLUSTERS=true ./backend/headlamp-server -dev -proxy-urls https://artifacthub.io/* -listen-addr=localhost", + "backend:dev": "echo 'Starting Headlamp backend in dev mode with Air...' && cd backend && air", + "backend:start:metrics": "echo 'Running backend with Prometheus metrics enabled' && HEADLAMP_BACKEND_TOKEN=headlamp HEADLAMP_CONFIG_METRICS_ENABLED=true HEADLAMP_CONFIG_ENABLE_HELM=true HEADLAMP_CONFIG_ENABLE_DYNAMIC_CLUSTERS=true ./backend/headlamp-server -dev -proxy-urls https://artifacthub.io/* -listen-addr=localhost", + "backend:start:traces": "echo 'Running backend with distributed tracing enabled' && HEADLAMP_BACKEND_TOKEN=headlamp HEADLAMP_CONFIG_TRACING_ENABLED=true HEADLAMP_CONFIG_ENABLE_HELM=true HEADLAMP_CONFIG_ENABLE_DYNAMIC_CLUSTERS=true ./backend/headlamp-server -dev -proxy-urls https://artifacthub.io/* -listen-addr=localhost", + "frontend:install": "cd frontend && npm install", + "frontend:install:ci": "cd frontend && npm ci", + "frontend:build": "cd frontend && npm run build", + "frontend:build:storybook": "cd frontend && npm run build-storybook", + "frontend:lint": "cd frontend && npm run lint -- --max-warnings 0 && npm run format-check", + "frontend:lint:fix": "cd frontend && npm run lint -- --fix && npm run format", + "frontend:tsc": "cd frontend && npm run tsc", + "frontend:test": "cd frontend && npm test -- --coverage", + "frontend:start": "cd frontend && npm start", + "frontend:storybook": "cd frontend && npm run storybook", + "docs": "cd frontend && npm run build-typedoc", + "i18n": "cd frontend && npm run i18n", + "i18n:check": "cd frontend && npm run i18n -- --fail-on-update", + "app:install": "cd app && npm install", + "app:build": "npm run frontend:build && cd app && node ./scripts/setup-plugins.js && npm run build", + "app:build:dir": "npm run frontend:build && cd app && node ./scripts/setup-plugins.js && npm run build -- --dir", + "app:package": "npm run app:build && cd app && npm run package -- --win --linux --mac", + "app:package:win": "npm run app:build && cd app && npm run package -- --win", + "app:package:win:msi": "npm run app:build && cd app && npm run package-msi", + "app:package:linux": "npm run app:build && cd app && npm run package -- --linux", + "app:package:mac": "npm run app:build && cd app && npm run package -- --mac", + "app:test": "npm run app:test:unit && npm run app:test:e2e", + "app:test:unit": "cd app && npm run test", + "app:test:e2e": "cd app/e2e-tests && npm run test", + "app:tsc": "cd app && npm run tsc", + "app:start": "cd app && node ./scripts/setup-plugins.js && npm run start", + "app:start:client": "cd app && npm run dev-only-app", + "start:with-app": "concurrently \"npm run backend:start\" \"npm run frontend:start\" \"npm run app:start:client\" --names \"backend,frontend,app\" --prefix-colors \"blue,green,yellow\"", + "start:app": "npm run app:start", + "start:backend": "npm run backend:build && npm run backend:start", + "start:frontend": "npm run frontend:start", + "plugins:test": "cd plugins/headlamp-plugin && npm install && ./test-headlamp-plugin.js && ./test-plugins-examples.sh && cd ../pluginctl/src && npm install && node ./plugin-management.e2e.js && cd .. && npx jest src/multi-plugin-management.test.js && npx jest src/plugin-management.test.js && npm run test", + "image:build": "sh -c 'docker buildx build --pull --platform=local -t ghcr.io/headlamp-k8s/headlamp:$(git describe --tags --always --dirty) -f Dockerfile .'" + }, + "devDependencies": { + "concurrently": "^8.2.2" + }, + "keywords": [ + "kubernetes", + "k8s", + "ui", + "web", + "dashboard", + "cluster", + "management" + ] +} From 820a10622cfab12f44a928a21a4e84a938cdc54e Mon Sep 17 00:00:00 2001 From: Joaquim Rocha Date: Fri, 17 Oct 2025 15:34:35 +0100 Subject: [PATCH 10/72] docs,README.md: Update docs to use npm commands instead of make This should make things easier for novice developers. --- app/e2e-tests/README.md | 6 +-- backend/README.md | 4 +- docs/contributing.md | 12 +++--- docs/development/architecture.md | 24 ++++++------ docs/development/backend.md | 16 ++++---- docs/development/frontend.md | 10 ++--- docs/development/i18n/contributing.md | 2 +- docs/development/index.md | 56 +++++++++++++++++---------- docs/installation/base-url.md | 4 +- e2e-tests/README.md | 2 +- 10 files changed, 75 insertions(+), 61 deletions(-) diff --git a/app/e2e-tests/README.md b/app/e2e-tests/README.md index 1fc1da0132c..7cab527a3eb 100644 --- a/app/e2e-tests/README.md +++ b/app/e2e-tests/README.md @@ -41,17 +41,17 @@ To run the tests for the web mode, you will need to have the backend running. Fo `cd headlamp` - run the following command - `make backend` followed by `make run-backend` + `npm run backend:build` followed by `npm run backend:start` ### Frontend To run the tests for the web mode, you will need to have the frontend running. Follow the steps below to run the frontend: - cd into the headlamp directory in a separate terminal - `cd headlamp/frontend` + `cd headlamp` - run the following command - `make frontend` followed by `make run-frontend` + `npm run frontend:build` followed by `npm run frontend:start` ### Running the tests diff --git a/backend/README.md b/backend/README.md index 10aab9e198c..c537e54a90f 100644 --- a/backend/README.md +++ b/backend/README.md @@ -1,8 +1,8 @@ # Quickstart ```bash -make backend -make run-backend +npm run backend:build +npm run backend:start ``` See more detailed [Headlamp backend documentation on the web]( diff --git a/docs/contributing.md b/docs/contributing.md index f2e6c395a7f..d3c345d5eb3 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -55,8 +55,8 @@ Follow these steps when submitting a PR to ensure it meets the project’s stand Run the following commands from your project directory: -- `make frontend-test` - Run the test suite -- `make frontend-lint` - Format your code to match the project +- `npm run frontend:test` - Run the test suite +- `npm run frontend:lint` - Format your code to match the project These steps ensure your code is functional, well-typed, and formatted consistently. @@ -156,11 +156,11 @@ For linting the `backend` and `frontend`, use the following commands (respectively): ```bash -make backend-lint +npm run backend:lint ``` ```bash -make frontend-lint +npm run frontend:lint ``` The linters are also run in the CI system, so any PRs you create will be @@ -194,13 +194,13 @@ an associated story when possible. For running the frontend tests, use the following command: ```bash -make frontend-test +npm run frontend:test ``` The backend uses go's testing and can be run by using the following command: ```bash -make backend-test +npm run backend:test ``` Tests will run as part of the CI after a Pull Request is open. diff --git a/docs/development/architecture.md b/docs/development/architecture.md index 2e63e6a7c54..3862d3c9c0b 100644 --- a/docs/development/architecture.md +++ b/docs/development/architecture.md @@ -128,23 +128,23 @@ The following components are in separate GitHub repos: - **Plugins**: Extensible modules that add custom functionality to the UI. The Headlamp team maintains their plugins in the [headlamp-k8s/plugins repo](https://github.com/headlamp-k8s/plugins). These include plugins for projects like Flux, Backstage and Inspektor Gadget. - **Headlamp Website**: Maintained in the [headlamp-k8s/website repo](https://github.com/headlamp-k8s/website). This contains things like the blog and the documentation. The website can be found at https://headlamp.dev/ -### Makefile task entry point +### npm scripts entry point -The headlamp/ repo [Makefile](https://github.com/kubernetes-sigs/headlamp/blob/main/Makefile) contains targets for building and testing different components. +The headlamp/ repo root [package.json](https://github.com/kubernetes-sigs/headlamp/blob/main/package.json) contains scripts for building and testing different components. Here are some examples: ```shell -make backend -make backend-lint -make backend-test -make run-backend -make frontend -make frontend-lint -make frontend-test -make run-frontend -make app-test -make run-app +npm run backend:build +npm run backend:lint +npm run backend:test +npm run backend:start +npm run frontend:build +npm run frontend:lint +npm run frontend:test +npm run frontend:start +npm run app:test +npm run start:app ``` ### Frontend diff --git a/docs/development/backend.md b/docs/development/backend.md index 0dc214d9375..d4feb0d2387 100644 --- a/docs/development/backend.md +++ b/docs/development/backend.md @@ -18,13 +18,13 @@ redirects the requests to the defined proxies. The backend (Headlamp's server) can be quickly built using: ```bash -make backend +npm run backend:build ``` Once built, it can be run in development mode (insecure / don't use in production) using: ```bash -make run-backend +npm run backend:start ``` ## Lint @@ -32,13 +32,13 @@ make run-backend To lint the backend/ code. ```bash -make backend-lint +npm run backend:lint ``` This command can fix some lint issues. ```bash -make backend-lint-fix +npm run backend:lint:fix ``` ## Format @@ -46,23 +46,23 @@ make backend-lint-fix To format the backend code. ```bash -make backend-format +npm run backend:format ``` ## Test ```bash -make backend-test +npm run backend:test ``` Test coverage with a html report in the browser. ```bash -make backend-coverage-html +npm run backend:coverage:html ``` To just print a simpler coverage report to the console. ```bash -make backend-coverage +npm run backend:coverage ``` diff --git a/docs/development/frontend.md b/docs/development/frontend.md index 190c304d478..6d7b091f52e 100644 --- a/docs/development/frontend.md +++ b/docs/development/frontend.md @@ -15,13 +15,13 @@ The frontend is written in Typescript and React, as well as a few other importan The frontend can be quickly built using: ```bash -make frontend +npm run frontend:build ``` Once built, it can be run in development mode (auto-refresh) using: ```bash -make run-frontend +npm run frontend:start ``` This command leverages the `create-react-app`'s start script that launches @@ -35,7 +35,7 @@ for network request, if you need the devtools for react-query, you can simply se API documentation for TypeScript is done with [typedoc](https://typedoc.org/) and [typedoc-plugin-markdown](https://github.com/tgreyuk/typedoc-plugin-markdown), and is configured in tsconfig.json ```bash -make docs +npm run docs ``` The API output markdown is generated in docs/development/api and is not @@ -49,7 +49,7 @@ Components can be discovered, developed, and tested inside the 'storybook'. From within the [Headlamp](https://github.com/kubernetes-sigs/headlamp/) repo run: ```bash -make storybook +npm run frontend:storybook ``` If you are adding new stories, please wrap your story components with the `TestContext` helper @@ -75,7 +75,7 @@ Any issues found are reported in the developer console. To enable the alert message during development, use the following: ```bash -REACT_APP_SKIP_A11Y=false make run-frontend +REACT_APP_SKIP_A11Y=false npm run frontend:start ``` This shows an alert when an a11y issue is detected. diff --git a/docs/development/i18n/contributing.md b/docs/development/i18n/contributing.md index 0bfa660368f..1c962fe433f 100644 --- a/docs/development/i18n/contributing.md +++ b/docs/development/i18n/contributing.md @@ -91,7 +91,7 @@ Here's an example of using date formatting: Create a folder using the locale code in: `frontend/src/i18n/locales/` -Then run `make i18n`. This command parses the translatable strings in +Then run `npm run i18n`. This command parses the translatable strings in the project and creates the corresponding catalog files. Integrated components may need to be adjusted (MaterialUI/Monaco etc). diff --git a/docs/development/index.md b/docs/development/index.md index 4daa85bd075..ee060c93be1 100644 --- a/docs/development/index.md +++ b/docs/development/index.md @@ -16,46 +16,60 @@ These are the required dependencies to get started. Other dependencies are pulle - [Node.js](https://nodejs.org/en/download/) Latest LTS (20.11.1 at time of writing). Many of us use [nvm](https://github.com/nvm-sh/nvm) for installing multiple versions of Node. - [Go](https://go.dev/doc/install), (1.24 at time of writing) -- [Make](https://www.gnu.org/software/make/) (GNU). Often installed by default. On Windows this can be installed with the "chocolatey" package manager that is installed with node. - [Kubernetes](https://kubernetes.io/), we suggest [minikube](https://minikube.sigs.k8s.io/docs/) as one good K8s installation for testing locally. Other k8s installations are supported (see [platforms](../platforms.md). ## Build the code Headlamp is composed of a `backend` and a `frontend`. -You can build both the `backend` and `frontend` by running. +You can build both the `backend` and `frontend` by running: ```bash -make +npm run build ``` Or individually: ```bash -make backend +npm run backend:build ``` and ```bash -make frontend +npm run frontend:build ``` ## Run the code -The quickest way to get the `backend` and `frontend` running for development is -the following (respectively): +The quickest way to get the `backend` and `frontend` running for development is to run both together: ```bash -make run-backend +npm start +``` + +Or you can run them individually in separate terminal instances: + +```bash +npm run backend:start ``` and in a different terminal instance: ```bash -make run-frontend +npm run frontend:start +``` + +## Generate API documentation + +To generate the TypeScript API documentation: + +```bash +npm run docs ``` +This generates API documentation in `docs/development/api/` using TypeDoc. + ## Build the app You can build the app for Linux, Windows, or Mac. @@ -66,15 +80,15 @@ and the linux app on a linux box. Choose the relevant command: ```bash -make app-linux +npm run app:package:linux ``` ```bash -make app-mac +npm run app:package:mac ``` ```bash -make app-win +npm run app:package:win ``` For Windows, by default it will produce an installer using [NSIS (Nullsoft Scriptable Install System)](https://sourceforge.net/projects/nsis/). @@ -90,24 +104,24 @@ set PATH=%PATH%;C:\Program Files (x86)\WiX Toolset v3.11\bin Then run the following command to generate the `.msi` installer: ```bash -make app-win-msi +npm run app:package:win:msi ``` See the generated app files in app/dist/ . ### Running the app -If you already have **BOTH** the `backend` and `frontend` up and running, the quickest way to +If you already have **BOTH** the `backend` and `frontend` up and running, the quickest way to get the `app` running for development is the following: ```bash -make run-only-app +npm run app:start:client ``` or else you can simply do ```bash -make run-app +npm run start:app ``` which runs everything including the `backend`, `frontend` and `app` in parallel. @@ -137,16 +151,16 @@ source. It will run the `frontend` from a `backend`'s static server, and options can be appended to the main command as arguments. ```bash -make image +npm run image:build ``` ### Custom container base images The Dockerfile takes a build argument for the base image used. You can specify the -base image used using the IMAGE_BASE environment variable with make. +base image used using the IMAGE_BASE environment variable. ```bash -IMAGE_BASE=debian:latest make image +IMAGE_BASE=debian:latest npm run image:build ``` If no IMAGE_BASE is specified, then a default image is used (see Dockerfile for exact default image used). @@ -171,7 +185,7 @@ If you want to make a new container image called `headlamp-k8s/headlamp:developm you can run it like this: ```bash -$ DOCKER_IMAGE_VERSION=development make image +$ DOCKER_IMAGE_VERSION=development npm run image:build ... Successfully tagged headlamp-k8s/headlamp:development @@ -196,7 +210,7 @@ ones made in the local docker environment. ```bash eval $(minikube docker-env) -DOCKER_IMAGE_VERSION=development make image +DOCKER_IMAGE_VERSION=development npm run image:build ``` #### Create a deployment yaml diff --git a/docs/installation/base-url.md b/docs/installation/base-url.md index be9fdf3b992..b84979a48f8 100644 --- a/docs/installation/base-url.md +++ b/docs/installation/base-url.md @@ -22,7 +22,7 @@ If in doubt, host Headlamp on a separate origin (domain or port, don't use the ` ```bash ./backend/headlamp-server -dev -base-url /headlamp -PUBLIC_URL="/headlamp" make run-frontend +PUBLIC_URL="/headlamp" npm run frontend:start ``` Then go to in your browser. @@ -30,7 +30,7 @@ Then go to in your browser. ### Static build mode ```bash -cd frontend && npm install && npm run build && cd .. +npm run frontend:build ./backend/headlamp-server -dev -base-url /headlamp -html-static-dir frontend/build ``` diff --git a/e2e-tests/README.md b/e2e-tests/README.md index e1273ae33d4..6a6a1ec85ad 100644 --- a/e2e-tests/README.md +++ b/e2e-tests/README.md @@ -153,7 +153,7 @@ npx playwright test -g "404 page is present" --headed - Inside `.github/workflows/build-container.yml` is the source line we need, locate the step for building the image and run in your terminal: ``` - DOCKER_IMAGE_VERSION=latest make image + DOCKER_IMAGE_VERSION=latest npm run image:build ``` 2. **Tag and push the Docker image to a registry (e.g., ttl.sh):** From 89f244793834299f094fea9455e8b1c42b72fb73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Dudfield?= Date: Wed, 22 Oct 2025 08:55:43 -0300 Subject: [PATCH 11/72] frontend: components: Fix isDrawerMode fetch when state not there There are common components that require this state that maybe should not depend on it being set. For testing individual components that do not even use the drawer functionality we make it so these components do not fail when the state is not set. --- frontend/src/components/App/Settings/DrawerModeSettings.tsx | 2 +- frontend/src/components/common/Link.tsx | 2 +- frontend/src/components/common/Resource/DetailsDrawer.tsx | 2 +- frontend/src/components/globalSearch/GlobalSearchContent.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/App/Settings/DrawerModeSettings.tsx b/frontend/src/components/App/Settings/DrawerModeSettings.tsx index df7195fb048..459c909843a 100644 --- a/frontend/src/components/App/Settings/DrawerModeSettings.tsx +++ b/frontend/src/components/App/Settings/DrawerModeSettings.tsx @@ -143,7 +143,7 @@ const OptionButton = ({ export default function DrawerModeSettings() { const dispatch = useDispatch(); - const isDrawerEnabled = useTypedSelector(state => state.drawerMode.isDetailDrawerEnabled); + const isDrawerEnabled = useTypedSelector(state => state?.drawerMode?.isDetailDrawerEnabled); return ( diff --git a/frontend/src/components/common/Link.tsx b/frontend/src/components/common/Link.tsx index ac31abfad71..2136a4127c8 100644 --- a/frontend/src/components/common/Link.tsx +++ b/frontend/src/components/common/Link.tsx @@ -142,7 +142,7 @@ function PureLink( } export default function Link(props: React.PropsWithChildren) { - const drawerEnabled = useTypedSelector(state => state.drawerMode.isDetailDrawerEnabled); + const drawerEnabled = useTypedSelector(state => state?.drawerMode?.isDetailDrawerEnabled); const { tooltip, ...propsRest } = props as LinkObjectProps; diff --git a/frontend/src/components/common/Resource/DetailsDrawer.tsx b/frontend/src/components/common/Resource/DetailsDrawer.tsx index af04f977596..34122e8b21f 100644 --- a/frontend/src/components/common/Resource/DetailsDrawer.tsx +++ b/frontend/src/components/common/Resource/DetailsDrawer.tsx @@ -30,7 +30,7 @@ export default function DetailsDrawer() { const dispatch = useDispatch(); const theme = useTheme(); const isSmallScreen = useMediaQuery(theme.breakpoints.down('md')); - const isDetailDrawerEnabled = useTypedSelector(state => state.drawerMode.isDetailDrawerEnabled); + const isDetailDrawerEnabled = useTypedSelector(state => state?.drawerMode?.isDetailDrawerEnabled); function closeDrawer() { dispatch(setSelectedResource(undefined)); diff --git a/frontend/src/components/globalSearch/GlobalSearchContent.tsx b/frontend/src/components/globalSearch/GlobalSearchContent.tsx index 12664bf6350..b80750c4d81 100644 --- a/frontend/src/components/globalSearch/GlobalSearchContent.tsx +++ b/frontend/src/components/globalSearch/GlobalSearchContent.tsx @@ -172,7 +172,7 @@ export function GlobalSearchContent({ const [query, setQuery] = useState(defaultValue ?? ''); const clusters = useClustersConf() ?? {}; const selectedClusters = useSelectedClusters(); - const drawerEnabled = useTypedSelector(state => state.drawerMode.isDetailDrawerEnabled); + const drawerEnabled = useTypedSelector(state => state?.drawerMode?.isDetailDrawerEnabled); const [recent, bump] = useRecent('search-recent-items'); From 404aabfb03031aca3c76c1388e9f79787aeeedd3 Mon Sep 17 00:00:00 2001 From: Evangelos Date: Mon, 20 Oct 2025 09:15:22 -0400 Subject: [PATCH 12/72] backend: serviceproxy: Extract tests for TestGet --- backend/pkg/serviceproxy/connection_test.go | 62 ++++++++++----------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/backend/pkg/serviceproxy/connection_test.go b/backend/pkg/serviceproxy/connection_test.go index 3bdb1e6bcb9..53f4cf153b8 100644 --- a/backend/pkg/serviceproxy/connection_test.go +++ b/backend/pkg/serviceproxy/connection_test.go @@ -41,38 +41,38 @@ func TestNewConnection(t *testing.T) { } } -func TestGet(t *testing.T) { - tests := []struct { - name string - uri string - requestURI string - wantBody []byte - wantErr bool - }{ - { - name: "valid request", - uri: "http://example.com", - requestURI: "/test", - wantBody: []byte("Hello, World!"), - wantErr: false, - }, - { - name: "invalid URI", - uri: " invalid-uri", - requestURI: "/test", - wantBody: nil, - wantErr: true, - }, - { - name: "invalid request URI", - uri: "http://example.com", - requestURI: " invalid-request-uri", - wantBody: nil, - wantErr: true, - }, - } +var getTests = []struct { + name string + uri string + requestURI string + wantBody []byte + wantErr bool +}{ + { + name: "valid request", + uri: "http://example.com", + requestURI: "/test", + wantBody: []byte("Hello, World!"), + wantErr: false, + }, + { + name: "invalid URI", + uri: " invalid-uri", + requestURI: "/test", + wantBody: nil, + wantErr: true, + }, + { + name: "invalid request URI", + uri: "http://example.com", + requestURI: " invalid-request-uri", + wantBody: nil, + wantErr: true, + }, +} - for _, tt := range tests { +func TestGet(t *testing.T) { + for _, tt := range getTests { t.Run(tt.name, func(t *testing.T) { conn := &Connection{URI: tt.uri} From c460e04a9f3baa4fd5d45c870ebcb968c12d5444 Mon Sep 17 00:00:00 2001 From: Evangelos Date: Mon, 20 Oct 2025 07:46:25 -0400 Subject: [PATCH 13/72] backend: serviceproxy: Restrict requests to relative paths This change restricts service proxy requests to relative paths, preventing uncontrolled data from being used in network requests. --- backend/pkg/serviceproxy/connection.go | 4 ++++ backend/pkg/serviceproxy/connection_test.go | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/backend/pkg/serviceproxy/connection.go b/backend/pkg/serviceproxy/connection.go index 834dbe08c4f..292be596119 100644 --- a/backend/pkg/serviceproxy/connection.go +++ b/backend/pkg/serviceproxy/connection.go @@ -35,6 +35,10 @@ func (c *Connection) Get(requestURI string) ([]byte, error) { return nil, fmt.Errorf("invalid request uri: %w", err) } + if rel.IsAbs() { + return nil, fmt.Errorf("request uri must be a relative path") + } + fullURL := base.ResolveReference(rel) body, err := HTTPGet(context.Background(), fullURL.String()) diff --git a/backend/pkg/serviceproxy/connection_test.go b/backend/pkg/serviceproxy/connection_test.go index 53f4cf153b8..71d4490cae6 100644 --- a/backend/pkg/serviceproxy/connection_test.go +++ b/backend/pkg/serviceproxy/connection_test.go @@ -69,6 +69,13 @@ var getTests = []struct { wantBody: nil, wantErr: true, }, + { + name: "absolute request URI rejected", + uri: "http://example.com", + requestURI: "http://malicious.local", + wantBody: nil, + wantErr: true, + }, } func TestGet(t *testing.T) { From f3a7dce26ef227a3ae8ca5cecac36557f6de71d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Dudfield?= Date: Sun, 19 Oct 2025 01:59:25 -0300 Subject: [PATCH 14/72] headlamp-plugin/config/plugins-tsconfig.json: Enable checkJS This makes hovering functions imported from places like headlamp-plugin/lib to show the function definition and documentation. For some reason without that it just says "import type" or so. If you imported from places like lib/plugins/registry where the declarations actually are it also works. It's just places that import from somewhere and export. The docs for checkJs say it enables checking js files, so it might be related to that, because the compiled files inside of headlamp-plugin are .js files and declarations. Docs for checkJs https://www.typescriptlang.org/tsconfig/#checkJs --- plugins/headlamp-plugin/config/plugins-tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/headlamp-plugin/config/plugins-tsconfig.json b/plugins/headlamp-plugin/config/plugins-tsconfig.json index 4bb2963a3d4..99ef927f478 100644 --- a/plugins/headlamp-plugin/config/plugins-tsconfig.json +++ b/plugins/headlamp-plugin/config/plugins-tsconfig.json @@ -7,6 +7,7 @@ "baseUrl": "../../../../", "esModuleInterop": true, "allowJs": true, + "checkJs": true, "types": ["vite/client", "vite-plugin-svgr/client", "vitest/globals", "lodash"], "paths": { "@iconify/react": [ From 2dfa9918950976cf3eed682d8510a434afc44c65 Mon Sep 17 00:00:00 2001 From: Evangelos Skopelitis Date: Tue, 21 Oct 2025 13:53:33 -0400 Subject: [PATCH 15/72] frontend: Bump vite to 6.4.1 --- frontend/package-lock.json | 8 ++++---- frontend/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4a6e7660c7d..cc0b67a2811 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -84,7 +84,7 @@ "simple-eval": "^2.0.0", "spacetime": "^7.4.0", "typescript": "5.6.2", - "vite": "^6.3.6", + "vite": "^6.4.1", "vite-plugin-node-polyfills": "^0.23.0", "vite-plugin-static-copy": "^3.1.2", "vite-plugin-svgr": "^4.3.0" @@ -15400,9 +15400,9 @@ "dev": true }, "node_modules/vite": { - "version": "6.3.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", - "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", diff --git a/frontend/package.json b/frontend/package.json index 4581660e9f5..4b17cad2680 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -85,7 +85,7 @@ "simple-eval": "^2.0.0", "spacetime": "^7.4.0", "typescript": "5.6.2", - "vite": "^6.3.6", + "vite": "^6.4.1", "vite-plugin-node-polyfills": "^0.23.0", "vite-plugin-static-copy": "^3.1.2", "vite-plugin-svgr": "^4.3.0" From ae9a8d86fe0b0b20fcf20f3cb5a12f7d6afbddb2 Mon Sep 17 00:00:00 2001 From: Evangelos Skopelitis Date: Tue, 21 Oct 2025 13:54:14 -0400 Subject: [PATCH 16/72] headlamp-plugin: Bump vite to 6.4.1 --- plugins/headlamp-plugin/package-lock.json | 8 ++++---- plugins/headlamp-plugin/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/headlamp-plugin/package-lock.json b/plugins/headlamp-plugin/package-lock.json index fbfc1cfa892..5125f75f8ef 100644 --- a/plugins/headlamp-plugin/package-lock.json +++ b/plugins/headlamp-plugin/package-lock.json @@ -105,7 +105,7 @@ "ts-loader": "^9.5.2", "typescript": "5.6.2", "validate-npm-package-name": "^3.0.0", - "vite": "^6.3.6", + "vite": "^6.4.1", "vite-plugin-css-injected-by-js": "^3.5.1", "vite-plugin-node-polyfills": "^0.23.0", "vite-plugin-static-copy": "^3.1.2", @@ -15800,9 +15800,9 @@ "license": "MIT" }, "node_modules/vite": { - "version": "6.3.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", - "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", diff --git a/plugins/headlamp-plugin/package.json b/plugins/headlamp-plugin/package.json index 630336ccc4a..c8a0bd7bc5b 100644 --- a/plugins/headlamp-plugin/package.json +++ b/plugins/headlamp-plugin/package.json @@ -110,7 +110,7 @@ "ts-loader": "^9.5.2", "typescript": "5.6.2", "validate-npm-package-name": "^3.0.0", - "vite": "^6.3.6", + "vite": "^6.4.1", "vite-plugin-css-injected-by-js": "^3.5.1", "vite-plugin-node-polyfills": "^0.23.0", "vite-plugin-static-copy": "^3.1.2", From 4c75f441476ce54be496e8cb75f8c37a4c19fc00 Mon Sep 17 00:00:00 2001 From: Evangelos Skopelitis Date: Tue, 21 Oct 2025 14:02:20 -0400 Subject: [PATCH 17/72] headlamp-plugin/template: Bump vite to 6.4.1 --- plugins/headlamp-plugin/template/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/headlamp-plugin/template/package-lock.json b/plugins/headlamp-plugin/template/package-lock.json index 7d5816238b7..8d3967c8f8f 100644 --- a/plugins/headlamp-plugin/template/package-lock.json +++ b/plugins/headlamp-plugin/template/package-lock.json @@ -17048,9 +17048,9 @@ "license": "MIT" }, "node_modules/vite": { - "version": "6.3.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", - "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "dependencies": { "esbuild": "^0.25.0", From d15a8a05da95411ce9c7bdc1e130e2a955370c5b Mon Sep 17 00:00:00 2001 From: Evangelos Skopelitis Date: Tue, 21 Oct 2025 13:54:27 -0400 Subject: [PATCH 18/72] plugins/examples/app-menus: Bump vite to 6.4.1 --- plugins/examples/app-menus/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/examples/app-menus/package-lock.json b/plugins/examples/app-menus/package-lock.json index 7dfb7f112c6..61836b32790 100644 --- a/plugins/examples/app-menus/package-lock.json +++ b/plugins/examples/app-menus/package-lock.json @@ -17187,9 +17187,9 @@ "license": "MIT" }, "node_modules/vite": { - "version": "6.3.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", - "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", "dependencies": { From cf29f4858ab3a2e6664cfa9e4279a120cc893d69 Mon Sep 17 00:00:00 2001 From: Evangelos Skopelitis Date: Tue, 21 Oct 2025 13:54:37 -0400 Subject: [PATCH 19/72] plugins/examples/change-logo: Bump vite to 6.4.1 --- plugins/examples/change-logo/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/examples/change-logo/package-lock.json b/plugins/examples/change-logo/package-lock.json index 2c5004a890e..026ec5186c1 100644 --- a/plugins/examples/change-logo/package-lock.json +++ b/plugins/examples/change-logo/package-lock.json @@ -17187,9 +17187,9 @@ "license": "MIT" }, "node_modules/vite": { - "version": "6.3.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", - "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", "dependencies": { From 84c866a8369d3bd6a7e95a7f8ce631b7fccb35d8 Mon Sep 17 00:00:00 2001 From: Evangelos Skopelitis Date: Tue, 21 Oct 2025 13:54:47 -0400 Subject: [PATCH 20/72] plugins/examples/cluster-chooser: Bump vite to 6.4.1 --- plugins/examples/cluster-chooser/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/examples/cluster-chooser/package-lock.json b/plugins/examples/cluster-chooser/package-lock.json index ecee2c86a8d..a557d5b638a 100644 --- a/plugins/examples/cluster-chooser/package-lock.json +++ b/plugins/examples/cluster-chooser/package-lock.json @@ -17188,9 +17188,9 @@ "license": "MIT" }, "node_modules/vite": { - "version": "6.3.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", - "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", "dependencies": { From 0d1207b44c015963e52162c9c1d9060cdb199cb0 Mon Sep 17 00:00:00 2001 From: Evangelos Skopelitis Date: Tue, 21 Oct 2025 13:54:59 -0400 Subject: [PATCH 21/72] plugins/examples/custom-theme: Bump vite to 6.4.1 --- plugins/examples/custom-theme/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/examples/custom-theme/package-lock.json b/plugins/examples/custom-theme/package-lock.json index a39a143f2eb..d10876ef6d3 100644 --- a/plugins/examples/custom-theme/package-lock.json +++ b/plugins/examples/custom-theme/package-lock.json @@ -17187,9 +17187,9 @@ "license": "MIT" }, "node_modules/vite": { - "version": "6.3.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", - "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", "dependencies": { From 78a62d6325e4ac3f91377ea16fb12dcd21e164c5 Mon Sep 17 00:00:00 2001 From: Evangelos Skopelitis Date: Tue, 21 Oct 2025 13:55:11 -0400 Subject: [PATCH 22/72] plugins/examples/customizing-map: Bump vite to 6.4.1 --- plugins/examples/customizing-map/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/examples/customizing-map/package-lock.json b/plugins/examples/customizing-map/package-lock.json index 2b12052024c..6052e21e976 100644 --- a/plugins/examples/customizing-map/package-lock.json +++ b/plugins/examples/customizing-map/package-lock.json @@ -17187,9 +17187,9 @@ "license": "MIT" }, "node_modules/vite": { - "version": "6.3.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", - "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", "dependencies": { From 8c7537bf395d088e7c1c72afdcb2acc022bf8a33 Mon Sep 17 00:00:00 2001 From: Evangelos Skopelitis Date: Tue, 21 Oct 2025 13:55:19 -0400 Subject: [PATCH 23/72] plugins/examples/details-view: Bump vite to 6.4.1 --- plugins/examples/details-view/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/examples/details-view/package-lock.json b/plugins/examples/details-view/package-lock.json index cded57417a8..5db41748a29 100644 --- a/plugins/examples/details-view/package-lock.json +++ b/plugins/examples/details-view/package-lock.json @@ -17187,9 +17187,9 @@ "license": "MIT" }, "node_modules/vite": { - "version": "6.3.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", - "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", "dependencies": { From 44ffbc392f500d0712b091c1594c868e0527e900 Mon Sep 17 00:00:00 2001 From: Evangelos Skopelitis Date: Tue, 21 Oct 2025 13:55:31 -0400 Subject: [PATCH 24/72] plugins/examples/dynamic-clusters: Bump vite to 6.4.1 --- plugins/examples/dynamic-clusters/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/examples/dynamic-clusters/package-lock.json b/plugins/examples/dynamic-clusters/package-lock.json index 1fe7d7b679f..7da721cfe9d 100644 --- a/plugins/examples/dynamic-clusters/package-lock.json +++ b/plugins/examples/dynamic-clusters/package-lock.json @@ -16301,9 +16301,9 @@ "license": "MIT" }, "node_modules/vite": { - "version": "6.3.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", - "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", "dependencies": { From 729fd37e67679282d8886ffd83335c4b9b007259 Mon Sep 17 00:00:00 2001 From: Evangelos Skopelitis Date: Tue, 21 Oct 2025 13:55:45 -0400 Subject: [PATCH 25/72] plugins/examples/headlamp-events: Bump vite to 6.4.1 --- plugins/examples/headlamp-events/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/examples/headlamp-events/package-lock.json b/plugins/examples/headlamp-events/package-lock.json index 1864316b9d8..57eda579c82 100644 --- a/plugins/examples/headlamp-events/package-lock.json +++ b/plugins/examples/headlamp-events/package-lock.json @@ -17187,9 +17187,9 @@ "license": "MIT" }, "node_modules/vite": { - "version": "6.3.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", - "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", "dependencies": { From 5a45f10b7568ace022f6817ff2abcd474d75d3c2 Mon Sep 17 00:00:00 2001 From: Evangelos Skopelitis Date: Tue, 21 Oct 2025 13:55:54 -0400 Subject: [PATCH 26/72] plugins/examples/pod-counter: Bump vite to 6.4.1 --- plugins/examples/pod-counter/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/examples/pod-counter/package-lock.json b/plugins/examples/pod-counter/package-lock.json index 030aefefd86..e54e0e33fa8 100644 --- a/plugins/examples/pod-counter/package-lock.json +++ b/plugins/examples/pod-counter/package-lock.json @@ -17188,9 +17188,9 @@ "license": "MIT" }, "node_modules/vite": { - "version": "6.3.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", - "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", "dependencies": { From f56e09d56c769a0bcf916b94db4a7e87533cd119 Mon Sep 17 00:00:00 2001 From: Evangelos Skopelitis Date: Tue, 21 Oct 2025 13:56:05 -0400 Subject: [PATCH 27/72] plugins/examples/projects: Bump vite to 6.4.1 --- plugins/examples/projects/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/examples/projects/package-lock.json b/plugins/examples/projects/package-lock.json index d5df86ba0f3..262290a4cc6 100644 --- a/plugins/examples/projects/package-lock.json +++ b/plugins/examples/projects/package-lock.json @@ -16307,9 +16307,9 @@ "license": "MIT" }, "node_modules/vite": { - "version": "6.3.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", - "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", "dependencies": { From a355055961e609ffa8dd7c419960aa1149b00f10 Mon Sep 17 00:00:00 2001 From: Evangelos Skopelitis Date: Tue, 21 Oct 2025 13:56:15 -0400 Subject: [PATCH 28/72] plugins/examples/resource-charts: Bump vite to 6.4.1 --- plugins/examples/resource-charts/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/examples/resource-charts/package-lock.json b/plugins/examples/resource-charts/package-lock.json index ffa663bc672..99be2585119 100644 --- a/plugins/examples/resource-charts/package-lock.json +++ b/plugins/examples/resource-charts/package-lock.json @@ -17187,9 +17187,9 @@ "license": "MIT" }, "node_modules/vite": { - "version": "6.3.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", - "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", "dependencies": { From 17fa7ccc6bc5cf024d6d7c2f67e2bc659e88d3ab Mon Sep 17 00:00:00 2001 From: Evangelos Skopelitis Date: Tue, 21 Oct 2025 13:56:24 -0400 Subject: [PATCH 29/72] plugins/examples/sidebar: Bump vite to 6.4.1 --- plugins/examples/sidebar/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/examples/sidebar/package-lock.json b/plugins/examples/sidebar/package-lock.json index 152969f206f..fbd857574e7 100644 --- a/plugins/examples/sidebar/package-lock.json +++ b/plugins/examples/sidebar/package-lock.json @@ -17187,9 +17187,9 @@ "license": "MIT" }, "node_modules/vite": { - "version": "6.3.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", - "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", "dependencies": { From 241f4a7db68668c516142b24201d2e7bcaefe0f8 Mon Sep 17 00:00:00 2001 From: Evangelos Skopelitis Date: Tue, 21 Oct 2025 13:56:31 -0400 Subject: [PATCH 30/72] plugins/examples/tables: Bump vite to 6.4.1 --- plugins/examples/tables/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/examples/tables/package-lock.json b/plugins/examples/tables/package-lock.json index ff90f109a64..e69d6be01d8 100644 --- a/plugins/examples/tables/package-lock.json +++ b/plugins/examples/tables/package-lock.json @@ -17187,9 +17187,9 @@ "license": "MIT" }, "node_modules/vite": { - "version": "6.3.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", - "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", "dependencies": { From 135c069003d6e72a10fb15ce4a764972c0b5e05e Mon Sep 17 00:00:00 2001 From: Evangelos Skopelitis Date: Tue, 21 Oct 2025 13:56:56 -0400 Subject: [PATCH 31/72] plugins/examples/ui-panels: Bump vite to 6.4.1 --- plugins/examples/ui-panels/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/examples/ui-panels/package-lock.json b/plugins/examples/ui-panels/package-lock.json index 9bd98708dbf..b638418f5a9 100644 --- a/plugins/examples/ui-panels/package-lock.json +++ b/plugins/examples/ui-panels/package-lock.json @@ -17187,9 +17187,9 @@ "license": "MIT" }, "node_modules/vite": { - "version": "6.3.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", - "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", "dependencies": { From 59276ace7cd856612ce2490ee466f5a452086c65 Mon Sep 17 00:00:00 2001 From: Evangelos Date: Mon, 20 Oct 2025 07:29:10 -0400 Subject: [PATCH 32/72] frontend: resourceMap: Display valid dates for events This change fixes the "Invalid Date" on map events and displays them properly in the UI. --- .../resourceMap/KubeObjectGlance/KubeObjectGlance.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/resourceMap/KubeObjectGlance/KubeObjectGlance.tsx b/frontend/src/components/resourceMap/KubeObjectGlance/KubeObjectGlance.tsx index d48b68ae18e..79a1b6a2761 100644 --- a/frontend/src/components/resourceMap/KubeObjectGlance/KubeObjectGlance.tsx +++ b/frontend/src/components/resourceMap/KubeObjectGlance/KubeObjectGlance.tsx @@ -21,7 +21,7 @@ import { useTranslation } from 'react-i18next'; import { KubeObject } from '../../../lib/k8s/cluster'; import Deployment from '../../../lib/k8s/deployment'; import Endpoints from '../../../lib/k8s/endpoints'; -import Event from '../../../lib/k8s/event'; +import Event, { KubeEvent } from '../../../lib/k8s/event'; import HPA from '../../../lib/k8s/hpa'; import Pod from '../../../lib/k8s/pod'; import ReplicaSet from '../../../lib/k8s/replicaSet'; @@ -41,7 +41,9 @@ export const KubeObjectGlance = memo(({ resource }: { resource: KubeObject }) => const { t } = useTranslation(); const [events, setEvents] = useState([]); useEffect(() => { - Event.objectEvents(resource).then(it => setEvents(it)); + Event.objectEvents(resource).then(fetchedEvents => + setEvents(fetchedEvents.map((event: KubeEvent) => new Event(event))) + ); }, []); const kind = resource.kind; From 7db4e559374074167feedea75021813b04bc4e22 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Oct 2025 18:57:58 +0000 Subject: [PATCH 33/72] build(deps): bump playwright and @playwright/test in /e2e-tests Bumps [playwright](https://github.com/microsoft/playwright) to 1.56.1 and updates ancestor dependency [@playwright/test](https://github.com/microsoft/playwright). These dependencies need to be updated together. Updates `playwright` from 1.42.1 to 1.56.1 - [Release notes](https://github.com/microsoft/playwright/releases) - [Commits](https://github.com/microsoft/playwright/compare/v1.42.1...v1.56.1) Updates `@playwright/test` from 1.42.1 to 1.56.1 - [Release notes](https://github.com/microsoft/playwright/releases) - [Commits](https://github.com/microsoft/playwright/compare/v1.42.1...v1.56.1) --- updated-dependencies: - dependency-name: playwright dependency-version: 1.56.1 dependency-type: indirect - dependency-name: "@playwright/test" dependency-version: 1.56.1 dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- e2e-tests/package-lock.json | 50 ++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/e2e-tests/package-lock.json b/e2e-tests/package-lock.json index f1ca54ab23a..dbec72cb2f3 100644 --- a/e2e-tests/package-lock.json +++ b/e2e-tests/package-lock.json @@ -30,18 +30,18 @@ } }, "node_modules/@playwright/test": { - "version": "1.42.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.42.1.tgz", - "integrity": "sha512-Gq9rmS54mjBL/7/MvBaNOBwbfnh7beHvS6oS4srqXFcQHpQCV1+c8JXWE8VLPyRDhgS3H8x8A7hztqI9VnwrAQ==", + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz", + "integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==", "dev": true, "dependencies": { - "playwright": "1.42.1" + "playwright": "1.56.1" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/@types/node": { @@ -77,32 +77,32 @@ } }, "node_modules/playwright": { - "version": "1.42.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.42.1.tgz", - "integrity": "sha512-PgwB03s2DZBcNRoW+1w9E+VkLBxweib6KTXM0M3tkiT4jVxKSi6PmVJ591J+0u10LUrgxB7dLRbiJqO5s2QPMg==", + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", + "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==", "dev": true, "dependencies": { - "playwright-core": "1.42.1" + "playwright-core": "1.56.1" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=16" + "node": ">=18" }, "optionalDependencies": { "fsevents": "2.3.2" } }, "node_modules/playwright-core": { - "version": "1.42.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.42.1.tgz", - "integrity": "sha512-mxz6zclokgrke9p1vtdy/COWBH+eOZgYUVVU34C73M+4j4HLlQJHtfcqiqqxpP0o8HhMkflvfbquLX5dg6wlfA==", + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz", + "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==", "bin": { "playwright-core": "cli.js" }, "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/undici-types": { @@ -130,12 +130,12 @@ } }, "@playwright/test": { - "version": "1.42.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.42.1.tgz", - "integrity": "sha512-Gq9rmS54mjBL/7/MvBaNOBwbfnh7beHvS6oS4srqXFcQHpQCV1+c8JXWE8VLPyRDhgS3H8x8A7hztqI9VnwrAQ==", + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz", + "integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==", "dev": true, "requires": { - "playwright": "1.42.1" + "playwright": "1.56.1" } }, "@types/node": { @@ -160,19 +160,19 @@ "optional": true }, "playwright": { - "version": "1.42.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.42.1.tgz", - "integrity": "sha512-PgwB03s2DZBcNRoW+1w9E+VkLBxweib6KTXM0M3tkiT4jVxKSi6PmVJ591J+0u10LUrgxB7dLRbiJqO5s2QPMg==", + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", + "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==", "dev": true, "requires": { "fsevents": "2.3.2", - "playwright-core": "1.42.1" + "playwright-core": "1.56.1" } }, "playwright-core": { - "version": "1.42.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.42.1.tgz", - "integrity": "sha512-mxz6zclokgrke9p1vtdy/COWBH+eOZgYUVVU34C73M+4j4HLlQJHtfcqiqqxpP0o8HhMkflvfbquLX5dg6wlfA==" + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz", + "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==" }, "undici-types": { "version": "5.26.5", From 74057052003aa889faddd651f931986862b0c470 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 Oct 2025 20:58:38 +0000 Subject: [PATCH 34/72] build(deps): bump playwright and @playwright/test in /app/e2e-tests Bumps [playwright](https://github.com/microsoft/playwright) to 1.56.1 and updates ancestor dependency [@playwright/test](https://github.com/microsoft/playwright). These dependencies need to be updated together. Updates `playwright` from 1.48.1 to 1.56.1 - [Release notes](https://github.com/microsoft/playwright/releases) - [Commits](https://github.com/microsoft/playwright/compare/v1.48.1...v1.56.1) Updates `@playwright/test` from 1.48.1 to 1.56.1 - [Release notes](https://github.com/microsoft/playwright/releases) - [Commits](https://github.com/microsoft/playwright/compare/v1.48.1...v1.56.1) --- updated-dependencies: - dependency-name: playwright dependency-version: 1.56.1 dependency-type: indirect - dependency-name: "@playwright/test" dependency-version: 1.56.1 dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- app/e2e-tests/package-lock.json | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/app/e2e-tests/package-lock.json b/app/e2e-tests/package-lock.json index 8ee2f7b8a9c..2733c2ad3bf 100644 --- a/app/e2e-tests/package-lock.json +++ b/app/e2e-tests/package-lock.json @@ -14,13 +14,13 @@ } }, "node_modules/@playwright/test": { - "version": "1.48.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.1.tgz", - "integrity": "sha512-s9RtWoxkOLmRJdw3oFvhFbs9OJS0BzrLUc8Hf6l2UdCNd1rqeEyD4BhCJkvzeEoD1FsK4mirsWwGerhVmYKtZg==", + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz", + "integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.48.1" + "playwright": "1.56.1" }, "bin": { "playwright": "cli.js" @@ -55,13 +55,13 @@ } }, "node_modules/playwright": { - "version": "1.48.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.1.tgz", - "integrity": "sha512-j8CiHW/V6HxmbntOfyB4+T/uk08tBy6ph0MpBXwuoofkSnLmlfdYNNkFTYD6ofzzlSqLA1fwH4vwvVFvJgLN0w==", + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", + "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.48.1" + "playwright-core": "1.56.1" }, "bin": { "playwright": "cli.js" @@ -74,9 +74,9 @@ } }, "node_modules/playwright-core": { - "version": "1.48.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.1.tgz", - "integrity": "sha512-Yw/t4VAFX/bBr1OzwCuOMZkY1Cnb4z/doAFSwf4huqAGWmf9eMNjmK7NiOljCdLmxeRYcGPPmcDgU0zOlzP0YA==", + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz", + "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==", "dev": true, "license": "Apache-2.0", "bin": { From 5cd99bb0c57aa06e2245639d9b21b44c2bcd52aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Dudfield?= Date: Fri, 24 Oct 2025 15:02:48 -0300 Subject: [PATCH 35/72] .github/build-container.yml: Cleanup disk space to avoid running out Sometimes this job is running out of disk space. --- .github/workflows/build-container.yml | 37 +++++++++++++++++---------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/.github/workflows/build-container.yml b/.github/workflows/build-container.yml index 91a386a27ce..0265125c6e5 100644 --- a/.github/workflows/build-container.yml +++ b/.github/workflows/build-container.yml @@ -51,20 +51,21 @@ jobs: - name: Try the cluster! run: kubectl get pods -A - name: Restore image-cache Folder - id: cache-image-restore + id: cache-image-restore2 uses: actions/cache/restore@0c907a75c2c80ebcb7f088228285e798b750cf8f # v4.2.1 with: path: ~/image-cache # cache the container image. All the paths this PR depends on except the e2e-tests folder for the key. key: ${{ runner.os }}-image-${{ hashFiles('backend/pkg/**', 'backend/cmd/**', 'backend/go.*', 'frontend/src/**', 'frontend/package.json', 'frontend/package-lock.json', 'Makefile', '.github/workflows/build-container.yml', 'Dockerfile', 'Dockerfile.plugins') }} - name: Restore Cached Docker Images - if: steps.cache-image-restore.outputs.cache-hit == 'true' + if: steps.cache-image-restore2.outputs.cache-hit == 'true' run: | export SHELL=/bin/bash - docker load -i ~/image-cache/headlamp-plugins-test.tar - docker load -i ~/image-cache/headlamp.tar + mkdir -p ~/image-cache + gzip -dc ~/image-cache/headlamp-plugins-test.tar.gz | docker load + gzip -dc ~/image-cache/headlamp.tar.gz | docker load - name: Make a .plugins folder for testing later - if: steps.cache-image-restore.outputs.cache-hit != 'true' + if: steps.cache-image-restore2.outputs.cache-hit != 'true' run: | echo "Extract pod-counter plugin into .plugins folder, which will be copied into image later by 'make image'." cd plugins/examples/pod-counter @@ -77,12 +78,12 @@ jobs: cd ../../ ls -laR .plugins - name: Remove unnecessary files - if: steps.cache-image-restore.outputs.cache-hit != 'true' + if: steps.cache-image-restore2.outputs.cache-hit != 'true' run: | sudo rm -rf /usr/share/dotnet sudo rm -rf "$AGENT_TOOLSDIRECTORY" - name: Build image - if: steps.cache-image-restore.outputs.cache-hit != 'true' + if: steps.cache-image-restore2.outputs.cache-hit != 'true' run: | export SHELL=/bin/bash DOCKER_IMAGE_VERSION=latest make image @@ -95,7 +96,7 @@ jobs: kind load docker-image ghcr.io/headlamp-k8s/headlamp-plugins-test:latest --name test kind load docker-image ghcr.io/headlamp-k8s/headlamp:latest --name test - name: Test .plugins folder - if: steps.cache-image-restore.outputs.cache-hit != 'true' + if: steps.cache-image-restore2.outputs.cache-hit != 'true' run: | export SHELL=/bin/bash echo "----------------------------" @@ -186,20 +187,30 @@ jobs: else echo "Playwright tests passed successfully" fi + # Clear disk space by removing unnecessary files, apt files and uninstall some playwright dependencies + - name: Clear Disk Space + if: steps.cache-image-restore2.outputs.cache-hit != 'true' + run: | + export SHELL=/bin/bash + sudo rm -rf /var/lib/apt/lists/* + sudo apt-get remove -y libgbm-dev libxcb-dri3-0 libxcb-dri2-0 libxcb-xfixes0 libxcb-shape0 libxcb-shm0 libxcb-render0 libxcb-glx0 || true + sudo rm -rf e2e-tests/node_modules/ + kind delete cluster --name test + kind delete cluster --name test2 - name: Save Docker Images to Tar files in image-cache Folder - if: steps.cache-image-restore.outputs.cache-hit != 'true' + if: steps.cache-image-restore2.outputs.cache-hit != 'true' run: | export SHELL=/bin/bash mkdir -p ~/image-cache - docker save -o ~/image-cache/headlamp-plugins-test.tar ghcr.io/headlamp-k8s/headlamp-plugins-test - docker save -o ~/image-cache/headlamp.tar ghcr.io/headlamp-k8s/headlamp + docker save ghcr.io/headlamp-k8s/headlamp-plugins-test | gzip -9 > ~/image-cache/headlamp-plugins-test.tar.gz + docker save ghcr.io/headlamp-k8s/headlamp | gzip -9 > ~/image-cache/headlamp.tar.gz - name: Cache image-cache Folder - if: steps.cache-image-restore.outputs.cache-hit != 'true' + if: steps.cache-image-restore2.outputs.cache-hit != 'true' id: cache-image-save uses: actions/cache/save@0c907a75c2c80ebcb7f088228285e798b750cf8f # v4.2.1 with: path: ~/image-cache - key: ${{ steps.cache-image-restore.outputs.cache-primary-key }} + key: ${{ steps.cache-image-restore2.outputs.cache-primary-key }} - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 if: always() with: From d108c1ae241f2e2876d54f9a9f6e5e617565ecb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Dudfield?= Date: Thu, 23 Oct 2025 09:14:58 -0300 Subject: [PATCH 36/72] headlamp-plugin: Add missing registerProjectDeleteButton export This is needed for the type checker. --- plugins/headlamp-plugin/src/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/headlamp-plugin/src/index.ts b/plugins/headlamp-plugin/src/index.ts index a9ded73edbd..f04e8e56e0c 100644 --- a/plugins/headlamp-plugin/src/index.ts +++ b/plugins/headlamp-plugin/src/index.ts @@ -66,6 +66,7 @@ import Registry, { registerMapSource, registerOverviewChartsProcessor, registerPluginSettings, + registerProjectDeleteButton, registerProjectDetailsTab, registerProjectOverviewSection, registerResourceTableColumnsProcessor, @@ -129,6 +130,7 @@ export { registerProjectDetailsTab, registerProjectOverviewSection, registerClusterStatus, + registerProjectDeleteButton, }; export type { From cc491ede2966f10442b6ebc27df8b2def511aecf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Dudfield?= Date: Thu, 23 Oct 2025 09:33:05 -0300 Subject: [PATCH 37/72] plugins/examples/projects: Fix for using id instead of name field The name attribute does not exist on ProjectDefinition. --- plugins/examples/projects/src/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/examples/projects/src/index.tsx b/plugins/examples/projects/src/index.tsx index cc56d107ce9..1fea4800ca8 100644 --- a/plugins/examples/projects/src/index.tsx +++ b/plugins/examples/projects/src/index.tsx @@ -57,7 +57,7 @@ registerProjectDetailsTab({ id: 'my-tab', label: 'Metrics', icon: 'mdi:chart-line', - component: ({ project }) =>
Metrics for project {project.name}
, + component: ({ project }) =>
Metrics for project {project.id}
, }); // Example of overriding a default tab - Replace the Access tab with custom content @@ -156,7 +156,7 @@ registerProjectDetailsTab({ registerProjectOverviewSection({ id: 'resource-usage', - component: ({ project }) =>
Custom resource usage for project {project.name}
, + component: ({ project }) =>
Custom resource usage for project {project.id}
, }); registerProjectDeleteButton({ From ef1147c5c57be5e2a164bf0f9270fbc767506a58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Dudfield?= Date: Thu, 23 Oct 2025 09:25:49 -0300 Subject: [PATCH 38/72] frontend: KubeMetadata: Add version of metadata for creation When creating KubeMetadata uid and creationTimestamp fields are optional. Overload apply type signature to take versions without these fields as well. --- frontend/src/lib/k8s/KubeMetadata.ts | 22 ++++++++++++++++++++++ frontend/src/lib/k8s/KubeObject.ts | 11 ++++++++++- frontend/src/lib/k8s/api/v1/apply.ts | 10 +++++++++- 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/frontend/src/lib/k8s/KubeMetadata.ts b/frontend/src/lib/k8s/KubeMetadata.ts index 0087c20a797..cbe2cdc59bd 100644 --- a/frontend/src/lib/k8s/KubeMetadata.ts +++ b/frontend/src/lib/k8s/KubeMetadata.ts @@ -149,3 +149,25 @@ export interface KubeMetadata { uid: string; apiVersion?: any; } + +/** + * KubeMetadataCreate is a version of KubeMetadata for creating objects where uid, creationTimestamp, etc. are optional + */ +export interface KubeMetadataCreate extends Omit { + /** + * UID is the unique in time and space value for this object. It is typically generated by + * the server on successful creation of a resource and is not allowed to change on PUT + * operations. Populated by the system. Read-only. + * + * @see {@link https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids | UIDs docs} for more details. + * + * This is typically generated by the server on successful creation of a resource. + */ + uid?: string; + /** + * An RFC 3339 date of the date and time an object was created + * + * This is optional when creating an object; the server will set it for you. + */ + creationTimestamp?: string; +} diff --git a/frontend/src/lib/k8s/KubeObject.ts b/frontend/src/lib/k8s/KubeObject.ts index 61595f926d0..04f2b1a5c8d 100644 --- a/frontend/src/lib/k8s/KubeObject.ts +++ b/frontend/src/lib/k8s/KubeObject.ts @@ -32,7 +32,7 @@ import type { ApiError } from './api/v2/ApiError'; import { useKubeObject } from './api/v2/hooks'; import { makeListRequests, useKubeObjectList } from './api/v2/useKubeObjectList'; import type { KubeEvent } from './event'; -import type { KubeMetadata } from './KubeMetadata'; +import type { KubeMetadata, KubeMetadataCreate } from './KubeMetadata'; function getAllowedNamespaces(cluster: string | null = getCluster()): string[] { if (!cluster) { @@ -688,6 +688,15 @@ export interface KubeObjectInterface { key?: any; [otherProps: string]: any; } + +/** + * KubeObjectInterfaceCreate is a version of KubeObjectInterface for creating objects + * where uid, creationTimestamp, etc. are optional + */ +export interface KubeObjectInterfaceCreate extends Omit { + metadata: KubeMetadataCreate; +} + export interface ApiListOptions extends QueryParameters { /** * The clusters to list objects from. By default uses the current clusters being viewed. diff --git a/frontend/src/lib/k8s/api/v1/apply.ts b/frontend/src/lib/k8s/api/v1/apply.ts index 63b1e02fae7..b91f3576425 100644 --- a/frontend/src/lib/k8s/api/v1/apply.ts +++ b/frontend/src/lib/k8s/api/v1/apply.ts @@ -16,7 +16,7 @@ import _ from 'lodash'; import { getCluster } from '../../../cluster'; -import type { KubeObjectInterface } from '../../KubeObject'; +import type { KubeObjectInterface, KubeObjectInterfaceCreate } from '../../KubeObject'; import type { ApiError } from '../v2/ApiError'; import { getClusterDefaultNamespace } from './clusterApi'; import { resourceDefToApiFactory } from './factories'; @@ -26,11 +26,19 @@ import { resourceDefToApiFactory } from './factories'; * * Tries to POST, and if there's a conflict it does a PUT to the api endpoint. * + * Overloads: + * - When called with a KubeObjectInterfaceCreate body, the parameter type is the create type. + * - Otherwise it accepts a full KubeObjectInterface. + * * @param body - The kubernetes object body to apply. * @param clusterName - The cluster to apply the body to. By default uses the current cluster (URL defined). * * @returns The response from the kubernetes API server. */ +export async function apply( + body: T, + clusterName?: string +): Promise; export async function apply( body: T, clusterName?: string From 0496d48568f506d69a3b811c86d59ae635d9998a Mon Sep 17 00:00:00 2001 From: Clement Aymard Date: Fri, 24 Oct 2025 16:23:40 +0200 Subject: [PATCH 39/72] charts: fix loadBalancerSourceRanges formatting This fixes the loadBalancerSourceRanges format after helm templating. --- charts/headlamp/templates/service.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/headlamp/templates/service.yaml b/charts/headlamp/templates/service.yaml index 1a80c333d1b..16a1dd82ec5 100644 --- a/charts/headlamp/templates/service.yaml +++ b/charts/headlamp/templates/service.yaml @@ -18,7 +18,7 @@ spec: externalTrafficPolicy: {{ .Values.service.externalTrafficPolicy | quote }} {{- end }} {{ if (and (eq .Values.service.type "LoadBalancer") (not (empty .Values.service.loadBalancerSourceRanges))) }} - loadBalancerSourceRanges: {{ .Values.service.loadBalancerSourceRanges }} + loadBalancerSourceRanges: {{ .Values.service.loadBalancerSourceRanges | toYaml | nindent 2 }} {{ end }} {{- if (and (eq .Values.service.type "LoadBalancer") (not (empty .Values.service.loadBalancerIP))) }} loadBalancerIP: {{ .Values.service.loadBalancerIP }} From 953ec8763649f3a88ae3aac5f2c5870b9c56b323 Mon Sep 17 00:00:00 2001 From: Harsh Date: Tue, 21 Oct 2025 14:46:07 +0530 Subject: [PATCH 40/72] frontend: Add storybook stories for Layout component with various states --- .../src/components/App/Layout.stories.tsx | 282 ++++++++++++++++++ ...odeShellSettings.Default.stories.storyshot | 4 +- .../Layout.Default.stories.storyshot | 113 +++++++ .../Layout.ErrorState.stories.storyshot | 113 +++++++ .../Layout.MultiCluster.stories.storyshot | 113 +++++++ .../Layout.WithClusterRoute.stories.storyshot | 113 +++++++ frontend/src/storybook.test.tsx | 2 +- 7 files changed, 737 insertions(+), 3 deletions(-) create mode 100644 frontend/src/components/App/Layout.stories.tsx create mode 100644 frontend/src/components/App/__snapshots__/Layout.Default.stories.storyshot create mode 100644 frontend/src/components/App/__snapshots__/Layout.ErrorState.stories.storyshot create mode 100644 frontend/src/components/App/__snapshots__/Layout.MultiCluster.stories.storyshot create mode 100644 frontend/src/components/App/__snapshots__/Layout.WithClusterRoute.stories.storyshot diff --git a/frontend/src/components/App/Layout.stories.tsx b/frontend/src/components/App/Layout.stories.tsx new file mode 100644 index 00000000000..c453eacb22a --- /dev/null +++ b/frontend/src/components/App/Layout.stories.tsx @@ -0,0 +1,282 @@ +/* + * Copyright 2025 The Kubernetes Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Meta, StoryFn } from '@storybook/react'; +import { delay, http, HttpResponse } from 'msw'; +import { Provider } from 'react-redux'; +import { MemoryRouter } from 'react-router-dom'; +import store from '../../redux/stores/store'; +import { TestContext } from '../../test'; +import Layout from './Layout'; + +export default { + title: 'App/Layout', + component: Layout, + parameters: { + layout: 'fullscreen', + docs: { + description: { + component: + 'The main layout component for Headlamp. It includes the top bar, sidebar, main content area, and handles cluster configuration and routing. This is the primary layout wrapper for the application.', + }, + }, + msw: { + handlers: [ + // Mock cluster config + http.get('http://localhost:4466/config', () => + HttpResponse.json({ + clusters: { + minikube: { + name: 'minikube', + meta_data: { namespace: 'default' }, + }, + production: { + name: 'production', + meta_data: { namespace: 'default' }, + }, + }, + }) + ), + // Mock plugins + http.get('http://localhost:4466/plugins', () => HttpResponse.json([])), + // Mock cluster version + http.get('http://localhost:4466/version', () => + HttpResponse.json({ + major: '1', + minor: '28', + gitVersion: 'v1.28.0', + }) + ), + // Mock events + http.get('http://localhost:4466/*/api/v1/events', () => + HttpResponse.json({ + kind: 'EventList', + items: [], + }) + ), + // Mock namespaces + http.get('http://localhost:4466/*/api/v1/namespaces', () => + HttpResponse.json({ + kind: 'NamespaceList', + items: [ + { + metadata: { name: 'default', uid: '1' }, + spec: {}, + status: { phase: 'Active' }, + }, + ], + }) + ), + // Mock CRDs + http.get( + 'http://localhost:4466/apis/apiextensions.k8s.io/v1/customresourcedefinitions', + () => + HttpResponse.json({ + kind: 'List', + items: [], + metadata: {}, + }) + ), + http.get( + 'http://localhost:4466/apis/apiextensions.k8s.io/v1beta1/customresourcedefinitions', + () => + HttpResponse.json({ + kind: 'List', + items: [], + metadata: {}, + }) + ), + ], + }, + }, + decorators: [ + Story => ( + + + + + + + + ), + ], +} as Meta; + +const Template: StoryFn = () => ; + +export const Default = Template.bind({}); +Default.parameters = { + docs: { + description: { + story: 'The default layout with sidebar, topbar, and main content area.', + }, + }, +}; + +export const WithClusterRoute = Template.bind({}); +WithClusterRoute.decorators = [ + Story => ( + + + + + + + + ), +]; +WithClusterRoute.parameters = { + docs: { + description: { + story: 'Layout when viewing a specific cluster route (e.g., /c/minikube/pods).', + }, + }, +}; + +export const LoadingState = Template.bind({}); +LoadingState.parameters = { + docs: { + description: { + story: 'Layout showing loading state while fetching cluster configuration.', + }, + }, + storyshots: { + disable: true, + }, + msw: { + handlers: [ + // Delay config response to show loading for 5 seconds + http.get('http://localhost:4466/config', async () => { + await delay(5000); + return HttpResponse.json({ + clusters: { + minikube: { + name: 'minikube', + meta_data: { namespace: 'default' }, + }, + }, + }); + }), + http.get('http://localhost:4466/plugins', () => HttpResponse.json([])), + http.get('http://localhost:4466/apis/apiextensions.k8s.io/v1/customresourcedefinitions', () => + HttpResponse.json({ + kind: 'List', + items: [], + metadata: {}, + }) + ), + http.get( + 'http://localhost:4466/apis/apiextensions.k8s.io/v1beta1/customresourcedefinitions', + () => + HttpResponse.json({ + kind: 'List', + items: [], + metadata: {}, + }) + ), + ], + }, +}; + +export const ErrorState = Template.bind({}); +ErrorState.parameters = { + docs: { + description: { + story: 'Layout showing error state when cluster configuration fails to load.', + }, + }, + msw: { + handlers: [ + http.get('http://localhost:4466/config', () => HttpResponse.error()), + http.get('http://localhost:4466/plugins', () => HttpResponse.json([])), + http.get('http://localhost:4466/apis/apiextensions.k8s.io/v1/customresourcedefinitions', () => + HttpResponse.json({ + kind: 'List', + items: [], + metadata: {}, + }) + ), + http.get( + 'http://localhost:4466/apis/apiextensions.k8s.io/v1beta1/customresourcedefinitions', + () => + HttpResponse.json({ + kind: 'List', + items: [], + metadata: {}, + }) + ), + ], + }, +}; + +export const MultiCluster = Template.bind({}); +MultiCluster.decorators = [ + Story => ( + + + + + + + + ), +]; +MultiCluster.parameters = { + docs: { + description: { + story: 'Layout when viewing multiple clusters simultaneously.', + }, + }, + msw: { + handlers: [ + http.get('http://localhost:4466/config', () => + HttpResponse.json({ + clusters: { + minikube: { + name: 'minikube', + meta_data: { namespace: 'default' }, + }, + production: { + name: 'production', + meta_data: { namespace: 'default' }, + }, + staging: { + name: 'staging', + meta_data: { namespace: 'default' }, + }, + }, + }) + ), + http.get('http://localhost:4466/plugins', () => HttpResponse.json([])), + http.get('http://localhost:4466/apis/apiextensions.k8s.io/v1/customresourcedefinitions', () => + HttpResponse.json({ + kind: 'List', + items: [], + metadata: {}, + }) + ), + http.get( + 'http://localhost:4466/apis/apiextensions.k8s.io/v1beta1/customresourcedefinitions', + () => + HttpResponse.json({ + kind: 'List', + items: [], + metadata: {}, + }) + ), + ], + }, +}; diff --git a/frontend/src/components/App/Settings/__snapshots__/NodeShellSettings.Default.stories.storyshot b/frontend/src/components/App/Settings/__snapshots__/NodeShellSettings.Default.stories.storyshot index 17f9761c20e..2fcc03fe65f 100644 --- a/frontend/src/components/App/Settings/__snapshots__/NodeShellSettings.Default.stories.storyshot +++ b/frontend/src/components/App/Settings/__snapshots__/NodeShellSettings.Default.stories.storyshot @@ -81,7 +81,7 @@ class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-colorPrimary MuiInputBase-formControl MuiInputBase-sizeSmall MuiInputBase-adornedEnd css-14f1a7d-MuiInputBase-root-MuiOutlinedInput-root" > +
+ + Skip to main content + +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+ \ No newline at end of file diff --git a/frontend/src/components/App/__snapshots__/Layout.ErrorState.stories.storyshot b/frontend/src/components/App/__snapshots__/Layout.ErrorState.stories.storyshot new file mode 100644 index 00000000000..06c2eda5829 --- /dev/null +++ b/frontend/src/components/App/__snapshots__/Layout.ErrorState.stories.storyshot @@ -0,0 +1,113 @@ + +
+ + Skip to main content + +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+ \ No newline at end of file diff --git a/frontend/src/components/App/__snapshots__/Layout.MultiCluster.stories.storyshot b/frontend/src/components/App/__snapshots__/Layout.MultiCluster.stories.storyshot new file mode 100644 index 00000000000..06c2eda5829 --- /dev/null +++ b/frontend/src/components/App/__snapshots__/Layout.MultiCluster.stories.storyshot @@ -0,0 +1,113 @@ + +
+ + Skip to main content + +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+ \ No newline at end of file diff --git a/frontend/src/components/App/__snapshots__/Layout.WithClusterRoute.stories.storyshot b/frontend/src/components/App/__snapshots__/Layout.WithClusterRoute.stories.storyshot new file mode 100644 index 00000000000..06c2eda5829 --- /dev/null +++ b/frontend/src/components/App/__snapshots__/Layout.WithClusterRoute.stories.storyshot @@ -0,0 +1,113 @@ + +
+ + Skip to main content + +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+ \ No newline at end of file diff --git a/frontend/src/storybook.test.tsx b/frontend/src/storybook.test.tsx index 5d3084a8bc6..437e5608c29 100644 --- a/frontend/src/storybook.test.tsx +++ b/frontend/src/storybook.test.tsx @@ -89,7 +89,7 @@ window.matchMedia = () => ({ * Recursively walks the tree and replaces any usage of useId */ function replaceUseId(node: any) { - const attributesToReplace = ['id', 'for', 'aria-described', 'aria-labelledby', 'aria-controls']; + const attributesToReplace = ['id', 'for', 'aria-describedby', 'aria-labelledby', 'aria-controls']; if (node.nodeType === Node.ELEMENT_NODE) { for (const attr of node.attributes) { if (attributesToReplace.includes(attr.name)) { From bf919c154819360046fc1d6ce8af3020dc86dd6d Mon Sep 17 00:00:00 2001 From: Evangelos Skopelitis Date: Tue, 30 Sep 2025 09:59:58 -0400 Subject: [PATCH 41/72] backend: server.go: Extract helpers for createHeadlampConfig --- backend/cmd/server.go | 72 ++++++++++++++++++++++++------------------- 1 file changed, 41 insertions(+), 31 deletions(-) diff --git a/backend/cmd/server.go b/backend/cmd/server.go index 72739ff7c39..6b17926d750 100644 --- a/backend/cmd/server.go +++ b/backend/cmd/server.go @@ -61,32 +61,52 @@ func main() { StartHeadlampServer(headlampConfig) } +// buildHeadlampCFG maps the parsed config into the struct the backend uses. +func buildHeadlampCFG(conf *config.Config, kubeConfigStore kubeconfig.ContextStore) *headlampconfig.HeadlampCFG { + return &headlampconfig.HeadlampCFG{ + UseInCluster: conf.InCluster, + KubeConfigPath: conf.KubeConfigPath, + SkippedKubeContexts: conf.SkippedKubeContexts, + ListenAddr: conf.ListenAddr, + CacheEnabled: conf.CacheEnabled, + Port: conf.Port, + DevMode: conf.DevMode, + StaticDir: conf.StaticDir, + Insecure: conf.InsecureSsl, + PluginDir: conf.PluginsDir, + EnableHelm: conf.EnableHelm, + EnableDynamicClusters: conf.EnableDynamicClusters, + WatchPluginsChanges: conf.WatchPluginsChanges, + KubeConfigStore: kubeConfigStore, + BaseURL: conf.BaseURL, + ProxyURLs: strings.Split(conf.ProxyURLs, ","), + TLSCertPath: conf.TLSCertPath, + TLSKeyPath: conf.TLSKeyPath, + } +} + +// buildTelemetryConfig collects only the telemetry fields and passes them along. +func buildTelemetryConfig(conf *config.Config) config.Config { + return config.Config{ + ServiceName: conf.ServiceName, + ServiceVersion: conf.ServiceVersion, + TracingEnabled: conf.TracingEnabled, + MetricsEnabled: conf.MetricsEnabled, + JaegerEndpoint: conf.JaegerEndpoint, + OTLPEndpoint: conf.OTLPEndpoint, + UseOTLPHTTP: conf.UseOTLPHTTP, + StdoutTraceEnabled: conf.StdoutTraceEnabled, + SamplingRate: conf.SamplingRate, + } +} + func createHeadlampConfig(conf *config.Config) *HeadlampConfig { cache := cache.New[interface{}]() kubeConfigStore := kubeconfig.NewContextStore() multiplexer := NewMultiplexer(kubeConfigStore) headlampConfig := &HeadlampConfig{ - HeadlampCFG: &headlampconfig.HeadlampCFG{ - UseInCluster: conf.InCluster, - KubeConfigPath: conf.KubeConfigPath, - SkippedKubeContexts: conf.SkippedKubeContexts, - ListenAddr: conf.ListenAddr, - CacheEnabled: conf.CacheEnabled, - Port: conf.Port, - DevMode: conf.DevMode, - StaticDir: conf.StaticDir, - Insecure: conf.InsecureSsl, - PluginDir: conf.PluginsDir, - EnableHelm: conf.EnableHelm, - EnableDynamicClusters: conf.EnableDynamicClusters, - WatchPluginsChanges: conf.WatchPluginsChanges, - KubeConfigStore: kubeConfigStore, - BaseURL: conf.BaseURL, - ProxyURLs: strings.Split(conf.ProxyURLs, ","), - TLSCertPath: conf.TLSCertPath, - TLSKeyPath: conf.TLSKeyPath, - }, + HeadlampCFG: buildHeadlampCFG(conf, kubeConfigStore), oidcClientID: conf.OidcClientID, oidcValidatorClientID: conf.OidcValidatorClientID, oidcClientSecret: conf.OidcClientSecret, @@ -98,17 +118,7 @@ func createHeadlampConfig(conf *config.Config) *HeadlampConfig { oidcUseAccessToken: conf.OidcUseAccessToken, cache: cache, multiplexer: multiplexer, - telemetryConfig: config.Config{ - ServiceName: conf.ServiceName, - ServiceVersion: conf.ServiceVersion, - TracingEnabled: conf.TracingEnabled, - MetricsEnabled: conf.MetricsEnabled, - JaegerEndpoint: conf.JaegerEndpoint, - OTLPEndpoint: conf.OTLPEndpoint, - UseOTLPHTTP: conf.UseOTLPHTTP, - StdoutTraceEnabled: conf.StdoutTraceEnabled, - SamplingRate: conf.SamplingRate, - }, + telemetryConfig: buildTelemetryConfig(conf), } if conf.OidcCAFile != "" { From d64ce284b2ece3c80d273eaae6bb5d1d033b7e57 Mon Sep 17 00:00:00 2001 From: Evangelos Skopelitis Date: Tue, 30 Sep 2025 10:00:27 -0400 Subject: [PATCH 42/72] backend: config_test.go: Extract tests for TestParseWithEnv --- backend/pkg/config/config_test.go | 74 +++++++++++++++---------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/backend/pkg/config/config_test.go b/backend/pkg/config/config_test.go index 9059482a23a..6cb38ba99f3 100644 --- a/backend/pkg/config/config_test.go +++ b/backend/pkg/config/config_test.go @@ -63,47 +63,47 @@ func TestParseBasic(t *testing.T) { } } -func TestParseWithEnv(t *testing.T) { - tests := []struct { - name string - args []string - env map[string]string - verify func(*testing.T, *config.Config) - }{ - { - name: "from_env", - args: []string{"go run ./cmd", "-in-cluster"}, - env: map[string]string{ - "HEADLAMP_CONFIG_OIDC_CLIENT_SECRET": "superSecretBotsStayAwayPlease", - }, - verify: func(t *testing.T, conf *config.Config) { - assert.Equal(t, "superSecretBotsStayAwayPlease", conf.OidcClientSecret) - }, +var ParseWithEnvTests = []struct { + name string + args []string + env map[string]string + verify func(*testing.T, *config.Config) +}{ + { + name: "from_env", + args: []string{"go run ./cmd", "-in-cluster"}, + env: map[string]string{ + "HEADLAMP_CONFIG_OIDC_CLIENT_SECRET": "superSecretBotsStayAwayPlease", }, - { - name: "both_args_and_env", - args: []string{"go run ./cmd", "--port=9876"}, - env: map[string]string{ - "HEADLAMP_CONFIG_PORT": "1234", - }, - verify: func(t *testing.T, conf *config.Config) { - assert.NotEqual(t, uint(1234), conf.Port) - assert.Equal(t, uint(9876), conf.Port) - }, + verify: func(t *testing.T, conf *config.Config) { + assert.Equal(t, "superSecretBotsStayAwayPlease", conf.OidcClientSecret) }, - { - name: "kubeconfig_from_default_env", - args: []string{"go run ./cmd"}, - env: map[string]string{ - "KUBECONFIG": "~/.kube/test_config.yaml", - }, - verify: func(t *testing.T, conf *config.Config) { - assert.Equal(t, "~/.kube/test_config.yaml", conf.KubeConfigPath) - }, + }, + { + name: "both_args_and_env", + args: []string{"go run ./cmd", "--port=9876"}, + env: map[string]string{ + "HEADLAMP_CONFIG_PORT": "1234", }, - } + verify: func(t *testing.T, conf *config.Config) { + assert.NotEqual(t, uint(1234), conf.Port) + assert.Equal(t, uint(9876), conf.Port) + }, + }, + { + name: "kubeconfig_from_default_env", + args: []string{"go run ./cmd"}, + env: map[string]string{ + "KUBECONFIG": "~/.kube/test_config.yaml", + }, + verify: func(t *testing.T, conf *config.Config) { + assert.Equal(t, "~/.kube/test_config.yaml", conf.KubeConfigPath) + }, + }, +} - for _, tt := range tests { +func TestParseWithEnv(t *testing.T) { + for _, tt := range ParseWithEnvTests { t.Run(tt.name, func(t *testing.T) { for key, value := range tt.env { os.Setenv(key, value) From b505f20d00cf8c03bc57371fec5711791a14dd53 Mon Sep 17 00:00:00 2001 From: Evangelos Date: Sun, 19 Oct 2025 17:50:31 -0400 Subject: [PATCH 43/72] backend: config.go: Extract helpers for flagset --- backend/pkg/config/config.go | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/backend/pkg/config/config.go b/backend/pkg/config/config.go index 76c3c8f00d6..4545ae9a17e 100644 --- a/backend/pkg/config/config.go +++ b/backend/pkg/config/config.go @@ -320,6 +320,15 @@ func DefaultHeadlampKubeConfigFile() (string, error) { func flagset() *flag.FlagSet { f := flag.NewFlagSet("config", flag.ContinueOnError) + addGeneralFlags(f) + addOIDCFlags(f) + addTelemetryFlags(f) + addTLSFlags(f) + + return f +} + +func addGeneralFlags(f *flag.FlagSet) { f.Bool("version", false, "Print version information and exit") f.Bool("in-cluster", false, "Set when running from a k8s cluster") f.Bool("dev", false, "Allow connections from other origins") @@ -337,19 +346,23 @@ func flagset() *flag.FlagSet { f.String("listen-addr", "", "Address to listen on; default is empty, which means listening to any address") f.Uint("port", defaultPort, "Port to listen from") f.String("proxy-urls", "", "Allow proxy requests to specified URLs") + f.Bool("enable-helm", false, "Enable Helm operations") +} +func addOIDCFlags(f *flag.FlagSet) { f.String("oidc-client-id", "", "ClientID for OIDC") f.String("oidc-client-secret", "", "ClientSecret for OIDC") f.String("oidc-validator-client-id", "", "Override ClientID for OIDC during validation") f.String("oidc-idp-issuer-url", "", "Identity provider issuer URL for OIDC") f.String("oidc-callback-url", "", "Callback URL for OIDC") f.String("oidc-validator-idp-issuer-url", "", "Override Identity provider issuer URL for OIDC during validation") - f.String("oidc-scopes", "profile,email", - "A comma separated list of scopes needed from the OIDC provider") + f.String("oidc-scopes", "profile,email", "A comma separated list of scopes needed from the OIDC provider") f.Bool("oidc-skip-tls-verify", false, "Skip TLS verification for OIDC") f.String("oidc-ca-file", "", "CA file for OIDC") f.Bool("oidc-use-access-token", false, "Setup oidc to pass through the access_token instead of the default id_token") - // Telemetry flags. +} + +func addTelemetryFlags(f *flag.FlagSet) { f.String("service-name", "headlamp", "Service name for telemetry") f.String("service-version", "0.30.0", "Service version for telemetry") f.Bool("tracing-enabled", false, "Enable distributed tracing") @@ -358,12 +371,12 @@ func flagset() *flag.FlagSet { f.Bool("use-otlp-http", false, "Use HTTP instead of gRPC for OTLP export") f.Bool("stdout-trace-enabled", false, "Enable tracing output to stdout") f.Float64("sampling-rate", 1.0, "Sampling rate for traces") +} + +func addTLSFlags(f *flag.FlagSet) { // TLS flags f.String("tls-cert-path", "", "Certificate for serving TLS") f.String("tls-key-path", "", "Key for serving TLS") - f.Bool("enable-helm", false, "Enable Helm operations") - - return f } // Gets the default plugins-dir depending on platform. From 6406233997ebfc9c9d7e572bd0b586fb7199282d Mon Sep 17 00:00:00 2001 From: Evangelos Skopelitis Date: Tue, 30 Sep 2025 10:04:01 -0400 Subject: [PATCH 44/72] backend: Add /clusters/clusterName/me endpoint --- backend/cmd/headlamp.go | 15 +++ backend/cmd/server.go | 3 + backend/go.mod | 1 + backend/go.sum | 1 + backend/pkg/auth/auth.go | 202 ++++++++++++++++++++++++++++++ backend/pkg/auth/auth_test.go | 132 +++++++++++++++++++ backend/pkg/config/config.go | 45 +++++++ backend/pkg/config/config_test.go | 18 +++ 8 files changed, 417 insertions(+) diff --git a/backend/cmd/headlamp.go b/backend/cmd/headlamp.go index 17d47378099..db1e5a3ab80 100644 --- a/backend/cmd/headlamp.go +++ b/backend/cmd/headlamp.go @@ -84,6 +84,12 @@ type HeadlampConfig struct { telemetryConfig cfg.Config oidcScopes []string telemetryHandler *telemetry.RequestHandler + // meUsernamePaths lists the JMESPath expressions tried for the username in /clusters/{cluster}/me. + meUsernamePaths string + // meEmailPaths lists the JMESPath expressions tried for the email in /clusters/{cluster}/me. + meEmailPaths string + // meGroupsPaths lists the JMESPath expressions tried for the groups in /clusters/{cluster}/me. + meGroupsPaths string } const DrainNodeCacheTTL = 20 // seconds @@ -477,6 +483,15 @@ func createHeadlampHandler(config *HeadlampConfig) http.Handler { portforward.GetPortForwardByID(config.cache, w, r) }).Methods("GET") + // Expose user info so the frontend can show the current user in the top bar using the per-cluster auth cookie. + r.HandleFunc("/clusters/{clusterName}/me", + auth.HandleMe(auth.MeHandlerOptions{ + UsernamePaths: config.meUsernamePaths, + EmailPaths: config.meEmailPaths, + GroupsPaths: config.meGroupsPaths, + }), + ).Methods("GET") + config.handleClusterRequests(r) r.HandleFunc("/externalproxy", func(w http.ResponseWriter, r *http.Request) { diff --git a/backend/cmd/server.go b/backend/cmd/server.go index 6b17926d750..2eff74822da 100644 --- a/backend/cmd/server.go +++ b/backend/cmd/server.go @@ -116,6 +116,9 @@ func createHeadlampConfig(conf *config.Config) *HeadlampConfig { oidcScopes: strings.Split(conf.OidcScopes, ","), oidcSkipTLSVerify: conf.OidcSkipTLSVerify, oidcUseAccessToken: conf.OidcUseAccessToken, + meUsernamePaths: conf.MeUsernamePath, + meEmailPaths: conf.MeEmailPath, + meGroupsPaths: conf.MeGroupsPath, cache: cache, multiplexer: multiplexer, telemetryConfig: buildTelemetryConfig(conf), diff --git a/backend/go.mod b/backend/go.mod index 6486d0eed2d..22d05b7a4b2 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -30,6 +30,7 @@ require ( require ( github.com/coreos/go-oidc/v3 v3.11.0 github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 + github.com/jmespath/go-jmespath v0.4.0 github.com/prometheus/client_golang v1.22.0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 go.opentelemetry.io/otel v1.35.0 diff --git a/backend/go.sum b/backend/go.sum index 3e04a1f03fc..854525104bd 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -290,6 +290,7 @@ github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= diff --git a/backend/pkg/auth/auth.go b/backend/pkg/auth/auth.go index d8da4c63da3..0c449e247de 100644 --- a/backend/pkg/auth/auth.go +++ b/backend/pkg/auth/auth.go @@ -30,7 +30,10 @@ import ( "time" "github.com/coreos/go-oidc/v3/oidc" + "github.com/gorilla/mux" + "github.com/jmespath/go-jmespath" "github.com/kubernetes-sigs/headlamp/backend/pkg/cache" + cfg "github.com/kubernetes-sigs/headlamp/backend/pkg/config" "github.com/kubernetes-sigs/headlamp/backend/pkg/kubeconfig" "github.com/kubernetes-sigs/headlamp/backend/pkg/logger" "golang.org/x/oauth2" @@ -271,3 +274,202 @@ func RefreshAndCacheNewToken(ctx context.Context, oidcAuthConfig *kubeconfig.Oid return newToken, nil } + +type MeHandlerOptions struct { + // UsernamePaths is a list of JMESPath expressions to resolve the username claim. + UsernamePaths string + // EmailPaths is a list of JMESPath expressions to resolve the email claim. + EmailPaths string + // GroupsPaths is a list of JMESPath expressions to resolve group memberships. + GroupsPaths string +} + +// HandleMe returns a handler that reads the per-cluster auth cookie and responds with user info. +func HandleMe(opts MeHandlerOptions) http.HandlerFunc { + usernamePaths, emailPaths, groupsPaths := cfg.ApplyMeDefaults( + opts.UsernamePaths, + opts.EmailPaths, + opts.GroupsPaths, + ) + compiledUsernamePaths := compileJMESPaths(usernamePaths) + compiledEmailPaths := compileJMESPaths(emailPaths) + compiledGroupsPaths := compileJMESPaths(groupsPaths) + + return func(w http.ResponseWriter, r *http.Request) { + clusterName := mux.Vars(r)["clusterName"] + if clusterName == "" { + writeMeJSON(w, http.StatusBadRequest, map[string]interface{}{"message": "cluster not specified"}) + return + } + + token, err := GetTokenFromCookie(r, clusterName) + if err != nil || token == "" { + writeMeJSON(w, http.StatusUnauthorized, map[string]interface{}{"message": "unauthorized"}) + return + } + + claims, status, errMsg := parseClaimsFromToken(token) + if status != 0 { + writeMeJSON(w, status, map[string]interface{}{"message": errMsg}) + return + } + + if expiry, err := GetExpiryUnixTimeUTC(claims); err != nil || time.Now().After(expiry) { + writeMeJSON(w, http.StatusUnauthorized, map[string]interface{}{"message": "token expired"}) + return + } + + username := stringValueFromJMESPaths(claims, compiledUsernamePaths) + email := stringValueFromJMESPaths(claims, compiledEmailPaths) + groups := stringSliceFromJMESPaths(claims, compiledGroupsPaths) + + writeMeResponse(w, username, email, groups) + } +} + +// parseClaimsFromToken extracts the JWT claims from a token. +func parseClaimsFromToken(token string) (map[string]interface{}, int, string) { + parts := strings.SplitN(token, ".", 3) + if len(parts) != 3 || parts[1] == "" { + return nil, http.StatusUnauthorized, "invalid token" + } + + claims, err := DecodeBase64JSON(parts[1]) + if err != nil { + return nil, http.StatusUnauthorized, "invalid token claims" + } + + return claims, 0, "" +} + +// writeMeResponse serializes the identity payload with the standard cache-busting headers. +func writeMeResponse(w http.ResponseWriter, username, email string, groups []string) { + writeMeJSON(w, http.StatusOK, map[string]interface{}{ + "username": username, + "email": email, + "groups": groups, + }) +} + +// writeMeJSON sets the standard cache-control headers used by /me responses and writes the JSON payload. +func writeMeJSON(w http.ResponseWriter, status int, payload map[string]interface{}) { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, private") + w.Header().Set("Pragma", "no-cache") + w.Header().Set("Expires", "0") + w.Header().Set("Vary", "Cookie") + w.Header().Del("ETag") + + w.WriteHeader(status) + + if err := json.NewEncoder(w).Encode(payload); err != nil { + logger.Log(logger.LevelError, nil, err, "failed to encode me response") + } +} + +// stringValueFromJMESPaths iterates pre-compiled JMESPath expressions and returns the first string result. +func stringValueFromJMESPaths(payload map[string]interface{}, paths []*jmespath.JMESPath) string { + for _, expr := range paths { + res, err := expr.Search(payload) + if err != nil || res == nil { + continue + } + + switch v := res.(type) { + case string: + if v != "" { + return v + } + case fmt.Stringer: + vs := v.String() + if vs != "" { + return vs + } + case float64: + return fmt.Sprintf("%v", v) + case int64: + return fmt.Sprintf("%v", v) + case map[string]interface{}: + if encoded, ok := marshalToString(v); ok && encoded != "" { + return encoded + } + } + } + + return "" +} + +// stringSliceFromJMESPaths iterates pre-compiled JMESPath expressions and returns the first []string result. +func stringSliceFromJMESPaths(payload map[string]interface{}, paths []*jmespath.JMESPath) []string { + for _, expr := range paths { + res, err := expr.Search(payload) + if err != nil || res == nil { + continue + } + + switch v := res.(type) { + case []interface{}: + out := make([]string, 0, len(v)) + + for _, it := range v { + switch s := it.(type) { + case string: + out = append(out, s) + case float64: + out = append(out, fmt.Sprintf("%v", s)) + case int64: + out = append(out, fmt.Sprintf("%v", s)) + default: + if encoded, ok := marshalToString(it); ok && encoded != "" { + out = append(out, encoded) + } + } + } + + return out + case []string: + return v + } + } + + return []string{} +} + +// compileJMESPaths parses and compiles a list of JMESPath expressions once. +func compileJMESPaths(pathCSV string) []*jmespath.JMESPath { + if strings.TrimSpace(pathCSV) == "" { + return []*jmespath.JMESPath{} + } + + rawPaths := strings.Split(pathCSV, ",") + compiled := make([]*jmespath.JMESPath, 0, len(rawPaths)) + + for _, raw := range rawPaths { + raw = strings.TrimSpace(raw) + if raw == "" { + continue + } + + expr, err := jmespath.Compile(raw) + if err != nil { + logger.Log(logger.LevelWarn, map[string]string{"jmespath": raw}, err, + "failed to compile JMESPath expression, skipping") + continue + } + + compiled = append(compiled, expr) + } + + return compiled +} + +// marshalToString encodes the provided value as JSON and logs failures. +func marshalToString(val interface{}) (string, bool) { + b, err := json.Marshal(val) + if err != nil { + logger.Log(logger.LevelWarn, nil, err, "failed to marshal value to JSON string") + return "", false + } + + return string(b), true +} diff --git a/backend/pkg/auth/auth_test.go b/backend/pkg/auth/auth_test.go index 85e0e856199..e5742d01097 100644 --- a/backend/pkg/auth/auth_test.go +++ b/backend/pkg/auth/auth_test.go @@ -23,6 +23,7 @@ import ( "encoding/json" "encoding/pem" "errors" + "fmt" "net/http" "net/http/httptest" "os" @@ -31,6 +32,7 @@ import ( "testing" "time" + "github.com/gorilla/mux" "github.com/kubernetes-sigs/headlamp/backend/pkg/auth" "github.com/kubernetes-sigs/headlamp/backend/pkg/cache" "github.com/kubernetes-sigs/headlamp/backend/pkg/kubeconfig" @@ -852,3 +854,133 @@ func TestConfigureTLSContext_CACert(t *testing.T) { require.NoError(t, err) assert.True(t, caCertParsed.IsCA, "Generated certificate should be a CA certificate") } + +func makeTestToken(t *testing.T, claims map[string]interface{}) string { + // helper to build unsigned JWT-like string for tests + header := map[string]string{"alg": "none", "typ": "JWT"} + headerJSON, err := json.Marshal(header) + require.NoError(t, err) + claimsJSON, err := json.Marshal(claims) + require.NoError(t, err) + + return fmt.Sprintf("%s.%s.signature", + base64.RawURLEncoding.EncodeToString(headerJSON), + base64.RawURLEncoding.EncodeToString(claimsJSON), + ) +} + +func TestHandleMe_Success(t *testing.T) { + t.Parallel() + + expiry := time.Now().Add(time.Hour).Unix() + claims := map[string]interface{}{ + "preferred_username": "alice", + "email": "alice@example.com", + "groups": []string{"dev", "ops"}, + "exp": float64(expiry), + } + + token := makeTestToken(t, claims) + + req := httptest.NewRequest(http.MethodGet, "/clusters/test/me", nil) + req = mux.SetURLVars(req, map[string]string{"clusterName": "test"}) + req.AddCookie(&http.Cookie{ + Name: fmt.Sprintf("headlamp-auth-%s.0", auth.SanitizeClusterName("test")), + Value: token, + }) + + rr := httptest.NewRecorder() + + handler := auth.HandleMe(auth.MeHandlerOptions{ + UsernamePaths: "preferred_username", + EmailPaths: "email", + GroupsPaths: "groups", + }) + + handler(rr, req) + + require.Equal(t, http.StatusOK, rr.Code) + + var got struct { + Username string `json:"username"` + Email string `json:"email"` + Groups []string `json:"groups"` + } + + require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &got)) + + assert.Equal(t, "alice", got.Username) + assert.Equal(t, "alice@example.com", got.Email) + assert.Equal(t, []string{"dev", "ops"}, got.Groups) + assert.Equal(t, "application/json", rr.Header().Get("Content-Type")) + assert.Equal(t, "no-store, no-cache, must-revalidate, private", rr.Header().Get("Cache-Control")) + assert.Equal(t, "Cookie", rr.Header().Get("Vary")) +} + +func TestHandleMe_ExpiredToken(t *testing.T) { + t.Parallel() + + expiry := time.Now().Add(-time.Hour).Unix() + claims := map[string]interface{}{ + "preferred_username": "alice", + "email": "alice@example.com", + "groups": []string{"dev", "ops"}, + "exp": float64(expiry), + } + + token := makeTestToken(t, claims) + + req := httptest.NewRequest(http.MethodGet, "/clusters/test/me", nil) + req = mux.SetURLVars(req, map[string]string{"clusterName": "test"}) + req.AddCookie(&http.Cookie{ + Name: fmt.Sprintf("headlamp-auth-%s.0", auth.SanitizeClusterName("test")), + Value: token, + }) + + rr := httptest.NewRecorder() + + handler := auth.HandleMe(auth.MeHandlerOptions{ + UsernamePaths: "preferred_username", + EmailPaths: "email", + GroupsPaths: "groups", + }) + + handler(rr, req) + + require.Equal(t, http.StatusUnauthorized, rr.Code) + + var got struct { + Message string `json:"message"` + } + + require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &got)) + assert.Equal(t, "token expired", got.Message) + assert.Equal(t, "application/json", rr.Header().Get("Content-Type")) + assert.Equal(t, "no-store, no-cache, must-revalidate, private", rr.Header().Get("Cache-Control")) + assert.Equal(t, "Cookie", rr.Header().Get("Vary")) +} + +func TestHandleMe_MissingCookie(t *testing.T) { + t.Parallel() + + req := httptest.NewRequest(http.MethodGet, "/clusters/test/me", nil) + req = mux.SetURLVars(req, map[string]string{"clusterName": "test"}) + + rr := httptest.NewRecorder() + + handler := auth.HandleMe(auth.MeHandlerOptions{}) + + handler(rr, req) + + require.Equal(t, http.StatusUnauthorized, rr.Code) + + var got struct { + Message string `json:"message"` + } + + require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &got)) + assert.Equal(t, "unauthorized", got.Message) + assert.Equal(t, "application/json", rr.Header().Get("Content-Type")) + assert.Equal(t, "no-store, no-cache, must-revalidate, private", rr.Header().Get("Cache-Control")) + assert.Equal(t, "Cookie", rr.Header().Get("Vary")) +} diff --git a/backend/pkg/config/config.go b/backend/pkg/config/config.go index 4545ae9a17e..550633fdb63 100644 --- a/backend/pkg/config/config.go +++ b/backend/pkg/config/config.go @@ -20,6 +20,12 @@ import ( const defaultPort = 4466 +const ( + DefaultMeUsernamePath = "preferred_username,upn,username,name" + DefaultMeEmailPath = "email" + DefaultMeGroupsPath = "groups,realm_access.roles" +) + type Config struct { Version bool `koanf:"version"` InCluster bool `koanf:"in-cluster"` @@ -47,6 +53,9 @@ type Config struct { OidcUseAccessToken bool `koanf:"oidc-use-access-token"` OidcSkipTLSVerify bool `koanf:"oidc-skip-tls-verify"` OidcCAFile string `koanf:"oidc-ca-file"` + MeUsernamePath string `koanf:"me-username-path"` + MeEmailPath string `koanf:"me-email-path"` + MeGroupsPath string `koanf:"me-groups-path"` // telemetry configs ServiceName string `koanf:"service-name"` ServiceVersion *string `koanf:"service-version"` @@ -219,6 +228,35 @@ func setKubeConfigPath(config *Config) { } } +// ApplyMeDefaults trims and applies defaults to the JMESPath expressions used for the /me endpoint. +func ApplyMeDefaults(usernamePath, emailPath, groupsPath string) (string, string, string) { + username := strings.TrimSpace(usernamePath) + if username == "" { + username = DefaultMeUsernamePath + } + + email := strings.TrimSpace(emailPath) + if email == "" { + email = DefaultMeEmailPath + } + + groups := strings.TrimSpace(groupsPath) + if groups == "" { + groups = DefaultMeGroupsPath + } + + return username, email, groups +} + +// setMeDefaults ensures the /clusters/{clusterName}/me claim paths fall back to defaults when unset. +func setMeDefaults(config *Config) { + config.MeUsernamePath, config.MeEmailPath, config.MeGroupsPath = ApplyMeDefaults( + config.MeUsernamePath, + config.MeEmailPath, + config.MeGroupsPath, + ) +} + // Parse Loads the config from flags and env. // env vars should start with HEADLAMP_CONFIG_ and use _ as separator // If a value is set both in flags and env then flag takes priority. @@ -267,6 +305,7 @@ func Parse(args []string) (*Config, error) { // 7. Post-process: patch plugin flag and kubeconfig path. patchWatchPluginsChanges(&config, explicitFlags) setKubeConfigPath(&config) + setMeDefaults(&config) // 8. Validate parsed config. if err := config.Validate(); err != nil { @@ -360,6 +399,12 @@ func addOIDCFlags(f *flag.FlagSet) { f.Bool("oidc-skip-tls-verify", false, "Skip TLS verification for OIDC") f.String("oidc-ca-file", "", "CA file for OIDC") f.Bool("oidc-use-access-token", false, "Setup oidc to pass through the access_token instead of the default id_token") + f.String("me-username-path", DefaultMeUsernamePath, + "Comma separated JMESPath expressions used to read username from the JWT payload") + f.String("me-email-path", DefaultMeEmailPath, + "Comma separated JMESPath expressions used to read email from the JWT payload") + f.String("me-groups-path", DefaultMeGroupsPath, + "Comma separated JMESPath expressions used to read groups from the JWT payload") } func addTelemetryFlags(f *flag.FlagSet) { diff --git a/backend/pkg/config/config_test.go b/backend/pkg/config/config_test.go index 6cb38ba99f3..44c6ac307a1 100644 --- a/backend/pkg/config/config_test.go +++ b/backend/pkg/config/config_test.go @@ -41,6 +41,9 @@ func TestParseBasic(t *testing.T) { assert.Equal(t, "", conf.ListenAddr) assert.Equal(t, uint(4466), conf.Port) assert.Equal(t, "profile,email", conf.OidcScopes) + assert.Equal(t, config.DefaultMeUsernamePath, conf.MeUsernamePath) + assert.Equal(t, config.DefaultMeEmailPath, conf.MeEmailPath) + assert.Equal(t, config.DefaultMeGroupsPath, conf.MeGroupsPath) }, }, { @@ -48,6 +51,7 @@ func TestParseBasic(t *testing.T) { args: []string{"go run ./cmd", "--port=3456"}, verify: func(t *testing.T, conf *config.Config) { assert.Equal(t, uint(3456), conf.Port) + assert.Equal(t, config.DefaultMeUsernamePath, conf.MeUsernamePath) }, }, } @@ -90,6 +94,20 @@ var ParseWithEnvTests = []struct { assert.Equal(t, uint(9876), conf.Port) }, }, + { + name: "me_paths", + args: []string{"go run ./cmd"}, + env: map[string]string{ + "HEADLAMP_CONFIG_ME_USERNAME_PATH": "user.name", + "HEADLAMP_CONFIG_ME_EMAIL_PATH": "user.email", + "HEADLAMP_CONFIG_ME_GROUPS_PATH": "user.groups", + }, + verify: func(t *testing.T, conf *config.Config) { + assert.Equal(t, "user.name", conf.MeUsernamePath) + assert.Equal(t, "user.email", conf.MeEmailPath) + assert.Equal(t, "user.groups", conf.MeGroupsPath) + }, + }, { name: "kubeconfig_from_default_env", args: []string{"go run ./cmd"}, From 5dee244034cd92a8752453d108dff24875ffa7e0 Mon Sep 17 00:00:00 2001 From: Evangelos Skopelitis Date: Tue, 30 Sep 2025 10:04:51 -0400 Subject: [PATCH 45/72] frontend: TopBar: Display user info from endpoint in user menu --- .../src/components/App/TopBar.stories.tsx | 39 ++++- frontend/src/components/App/TopBar.tsx | 57 +++++++- .../TopBar.UndefinedData.stories.storyshot | 137 ++++++++++++++++++ .../TopBar.WithEmailOnly.stories.storyshot | 137 ++++++++++++++++++ .../TopBar.WithUserInfo.stories.storyshot | 137 ++++++++++++++++++ frontend/src/components/authchooser/index.tsx | 2 + frontend/src/lib/auth.ts | 11 +- frontend/vite.config.ts | 8 + 8 files changed, 525 insertions(+), 3 deletions(-) create mode 100644 frontend/src/components/App/__snapshots__/TopBar.UndefinedData.stories.storyshot create mode 100644 frontend/src/components/App/__snapshots__/TopBar.WithEmailOnly.stories.storyshot create mode 100644 frontend/src/components/App/__snapshots__/TopBar.WithUserInfo.stories.storyshot diff --git a/frontend/src/components/App/TopBar.stories.tsx b/frontend/src/components/App/TopBar.stories.tsx index 8eb8b016fca..8c0e644ebf7 100644 --- a/frontend/src/components/App/TopBar.stories.tsx +++ b/frontend/src/components/App/TopBar.stories.tsx @@ -16,6 +16,7 @@ import { configureStore } from '@reduxjs/toolkit'; import { Meta, StoryFn } from '@storybook/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { get } from 'lodash'; import { PropsWithChildren } from 'react'; import { Provider } from 'react-redux'; @@ -42,6 +43,8 @@ const store = configureStore({ }, }); +const queryClient = new QueryClient(); + export default { title: 'TopBar', component: PureTopBar, @@ -51,7 +54,9 @@ export default { return ( - + + + ); @@ -119,3 +124,35 @@ TwoCluster.args = { cluster: 'ak8s-desktop', clusters: { 'ak8s-desktop': '', 'ak8s-desktop2': '' }, }; + +export const WithUserInfo = PureTemplate.bind({}); +WithUserInfo.args = { + appBarActions: [], + logout: () => {}, + cluster: 'ak8s-desktop', + clusters: { 'ak8s-desktop': '' }, + userInfo: { + username: 'Ada Lovelace', + email: 'ada@example.com', + }, +}; + +export const WithEmailOnly = PureTemplate.bind({}); +WithEmailOnly.args = { + appBarActions: [], + logout: () => {}, + cluster: 'ak8s-desktop', + clusters: { 'ak8s-desktop': '' }, + userInfo: { + email: 'grace@example.com', + }, +}; + +export const UndefinedData = PureTemplate.bind({}); +UndefinedData.args = { + appBarActions: [], + logout: () => {}, + cluster: 'ak8s-desktop', + clusters: { 'ak8s-desktop': '' }, + userInfo: undefined, +}; diff --git a/frontend/src/components/App/TopBar.tsx b/frontend/src/components/App/TopBar.tsx index 39075f1e6f8..77aeccb4cb2 100644 --- a/frontend/src/components/App/TopBar.tsx +++ b/frontend/src/components/App/TopBar.tsx @@ -25,6 +25,7 @@ import MenuItem from '@mui/material/MenuItem'; import { useTheme } from '@mui/material/styles'; import Toolbar from '@mui/material/Toolbar'; import useMediaQuery from '@mui/material/useMediaQuery'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; import { has } from 'lodash'; import React, { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -33,6 +34,7 @@ import { useHistory } from 'react-router-dom'; import { getProductName, getVersion } from '../../helpers/getProductInfo'; import { logout } from '../../lib/auth'; import { useCluster, useClustersConf } from '../../lib/k8s'; +import { clusterRequest } from '../../lib/k8s/api/v1/clusterRequests'; import { createRouteURL } from '../../lib/router/createRouteURL'; import { AppBarAction, @@ -86,13 +88,38 @@ export default function TopBar({}: TopBarProps) { const cluster = useCluster(); const history = useHistory(); const { appBarActions, appBarActionsProcessors } = useAppBarActionsProcessed(); + const queryClient = useQueryClient(); + const clusterName = cluster ?? undefined; + const { data: me } = useQuery<{ username?: string; email?: string } | null>({ + queryKey: ['clusterMe', clusterName], + queryFn: async () => { + if (!clusterName) { + return null; + } + + try { + const res = await clusterRequest('/me', { + cluster: clusterName, + autoLogoutOnAuthError: false, + }); + + return res ? { username: res.username, email: res.email } : null; + } catch { + return null; + } + }, + enabled: Boolean(clusterName), + staleTime: 0, + refetchOnMount: 'always', + }); const logoutCallback = useCallback(async () => { if (!!cluster) { await logout(cluster); + queryClient.removeQueries({ queryKey: ['clusterMe', cluster], exact: true }); } history.push('/'); - }, [cluster]); + }, [cluster, history, queryClient]); const handletoggleOpen = useCallback(() => { // For medium view we default to closed if they have not made a selection. @@ -118,6 +145,7 @@ export default function TopBar({}: TopBarProps) { onToggleOpen={handletoggleOpen} cluster={cluster || undefined} clusters={clustersConfig || undefined} + userInfo={me || undefined} /> ); } @@ -134,6 +162,7 @@ export interface PureTopBarProps { cluster?: string; isSidebarOpen?: boolean; isSidebarOpenUserSelected?: boolean; + userInfo?: { username?: string; email?: string }; /** Called when sidebar toggles between open and closed. */ onToggleOpen: () => void; @@ -210,6 +239,7 @@ export const PureTopBar = memo( isSidebarOpen, isSidebarOpenUserSelected, onToggleOpen, + userInfo, }: PureTopBarProps) => { const { t } = useTranslation(); const theme = useTheme(); @@ -243,6 +273,11 @@ export const PureTopBar = memo( setMobileMoreAnchorEl(event.currentTarget); }; const userMenuId = 'primary-user-menu'; + const userDisplayName = userInfo?.username || userInfo?.email || ''; + const userSecondaryInfo = + userInfo?.username && userInfo?.email && userInfo.username !== userInfo.email + ? userInfo.email + : undefined; const renderUserMenu = !!isClusterContext && ( + {userInfo && ( + + + + + + + )} { diff --git a/frontend/src/components/App/__snapshots__/TopBar.UndefinedData.stories.storyshot b/frontend/src/components/App/__snapshots__/TopBar.UndefinedData.stories.storyshot new file mode 100644 index 00000000000..0f1b47e9aa5 --- /dev/null +++ b/frontend/src/components/App/__snapshots__/TopBar.UndefinedData.stories.storyshot @@ -0,0 +1,137 @@ + +
+ +
+ \ No newline at end of file diff --git a/frontend/src/components/App/__snapshots__/TopBar.WithEmailOnly.stories.storyshot b/frontend/src/components/App/__snapshots__/TopBar.WithEmailOnly.stories.storyshot new file mode 100644 index 00000000000..0f1b47e9aa5 --- /dev/null +++ b/frontend/src/components/App/__snapshots__/TopBar.WithEmailOnly.stories.storyshot @@ -0,0 +1,137 @@ + +
+ +
+ \ No newline at end of file diff --git a/frontend/src/components/App/__snapshots__/TopBar.WithUserInfo.stories.storyshot b/frontend/src/components/App/__snapshots__/TopBar.WithUserInfo.stories.storyshot new file mode 100644 index 00000000000..0f1b47e9aa5 --- /dev/null +++ b/frontend/src/components/App/__snapshots__/TopBar.WithUserInfo.stories.storyshot @@ -0,0 +1,137 @@ + +
+ +
+ \ No newline at end of file diff --git a/frontend/src/components/authchooser/index.tsx b/frontend/src/components/authchooser/index.tsx index 10118664661..9698b3822d5 100644 --- a/frontend/src/components/authchooser/index.tsx +++ b/frontend/src/components/authchooser/index.tsx @@ -26,6 +26,7 @@ import { getAppUrl } from '../../helpers/getAppUrl'; import { getCluster, getClusterPrefixedPath } from '../../lib/cluster'; import { useClustersConf } from '../../lib/k8s'; import { testAuth } from '../../lib/k8s/api/v1/clusterApi'; +import { queryClient } from '../../lib/queryClient'; import { createRouteURL } from '../../lib/router/createRouteURL'; import { getRoute } from '../../lib/router/getRoute'; import { getRoutePath } from '../../lib/router/getRoutePath'; @@ -207,6 +208,7 @@ function AuthChooser({ children }: AuthChooserProps) { clusterAuthType={clusterAuthType} handleTryAgain={runTestAuthAgain} handleOidcAuth={() => { + queryClient.invalidateQueries({ queryKey: ['clusterMe', clusterName], exact: true }); history.replace({ pathname: generatePath(getClusterPrefixedPath(), { cluster: clusterName as string, diff --git a/frontend/src/lib/auth.ts b/frontend/src/lib/auth.ts index 3b4563f620a..3a943198a5f 100644 --- a/frontend/src/lib/auth.ts +++ b/frontend/src/lib/auth.ts @@ -111,7 +111,15 @@ export function setToken(cluster: string, token: string | null) { return Promise.resolve(setTokenMethodToUse(cluster, token)); } - return setCookieToken(cluster, token); + return setCookieToken(cluster, token).then(result => { + if (token) { + queryClient.invalidateQueries({ queryKey: ['clusterMe', cluster], exact: true }); + } else { + queryClient.removeQueries({ queryKey: ['clusterMe', cluster], exact: true }); + } + + return result; + }); } /** @@ -123,6 +131,7 @@ export function setToken(cluster: string, token: string | null) { export async function logout(cluster: string) { return setToken(cluster, null).then(() => { queryClient.removeQueries({ queryKey: ['auth'], exact: false }); + queryClient.removeQueries({ queryKey: ['clusterMe', cluster], exact: true }); }); } diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index e9234138229..455d1fcfdcf 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -29,6 +29,14 @@ export default defineConfig({ server: { port: 3000, proxy: { + '/api': { + target: 'http://localhost:4466', + changeOrigin: true, + }, + '/clusters': { + target: 'http://localhost:4466', + changeOrigin: true, + }, '/plugins': { target: 'http://localhost:4466', changeOrigin: true, From 4a14f9d7f1fdf7d6a77bf60509c0f71652776113 Mon Sep 17 00:00:00 2001 From: kairos Date: Sun, 27 Jul 2025 23:09:46 +0300 Subject: [PATCH 46/72] helm: Add pkce support to the helm and readme docs --- backend/pkg/config/config.go | 2 + charts/headlamp/README.md | 1 + charts/headlamp/templates/deployment.yaml | 18 +++ charts/headlamp/templates/secret.yaml | 3 + .../tests/expected_templates/oidc-pkce.yaml | 137 ++++++++++++++++++ .../headlamp/tests/test_cases/oidc-pkce.yaml | 15 ++ charts/headlamp/values.schema.json | 4 + charts/headlamp/values.yaml | 2 + 8 files changed, 182 insertions(+) create mode 100644 charts/headlamp/tests/expected_templates/oidc-pkce.yaml create mode 100644 charts/headlamp/tests/test_cases/oidc-pkce.yaml diff --git a/backend/pkg/config/config.go b/backend/pkg/config/config.go index 550633fdb63..92f58583e38 100644 --- a/backend/pkg/config/config.go +++ b/backend/pkg/config/config.go @@ -399,6 +399,7 @@ func addOIDCFlags(f *flag.FlagSet) { f.Bool("oidc-skip-tls-verify", false, "Skip TLS verification for OIDC") f.String("oidc-ca-file", "", "CA file for OIDC") f.Bool("oidc-use-access-token", false, "Setup oidc to pass through the access_token instead of the default id_token") + f.Bool("oidc-use-pkce", false, "Use PKCE (Proof Key for Code Exchange) for enhanced security in OIDC flow") f.String("me-username-path", DefaultMeUsernamePath, "Comma separated JMESPath expressions used to read username from the JWT payload") f.String("me-email-path", DefaultMeEmailPath, @@ -408,6 +409,7 @@ func addOIDCFlags(f *flag.FlagSet) { } func addTelemetryFlags(f *flag.FlagSet) { + // Telemetry flags. f.String("service-name", "headlamp", "Service name for telemetry") f.String("service-version", "0.30.0", "Service version for telemetry") f.Bool("tracing-enabled", false, "Enable distributed tracing") diff --git a/charts/headlamp/README.md b/charts/headlamp/README.md index 26b2fba7efe..babd8ebd26b 100644 --- a/charts/headlamp/README.md +++ b/charts/headlamp/README.md @@ -85,6 +85,7 @@ $ helm install my-headlamp headlamp/headlamp \ | config.oidc.clientSecret | string | `""` | OIDC client secret | | config.oidc.issuerURL | string | `""` | OIDC issuer URL | | config.oidc.scopes | string | `""` | OIDC scopes to be used | +| config.oidc.usePKCE | bool | `false` | Use PKCE (Proof Key for Code Exchange) for enhanced security in OIDC flow | | config.oidc.secret.create | bool | `true` | Create OIDC secret using provided values | | config.oidc.secret.name | string | `"oidc"` | Name of the OIDC secret | | config.oidc.externalSecret.enabled | bool | `false` | Enable using external secret for OIDC | diff --git a/charts/headlamp/templates/deployment.yaml b/charts/headlamp/templates/deployment.yaml index 772dce8fb42..94042e1ee33 100644 --- a/charts/headlamp/templates/deployment.yaml +++ b/charts/headlamp/templates/deployment.yaml @@ -8,6 +8,7 @@ {{- $callbackURL := "" }} {{- $validatorClientID := "" }} {{- $validatorIssuerURL := "" }} +{{- $usePKCE := "" }} {{- $useAccessToken := "" }} # This block of code is used to extract the values from the env. @@ -37,6 +38,9 @@ {{- if eq .name "OIDC_USE_ACCESS_TOKEN" }} {{- $useAccessToken = .value | toString }} {{- end }} + {{- if eq .name "OIDC_USE_PKCE" }} + {{- $usePKCE = .value | toString }} + {{- end }} {{- end }} apiVersion: apps/v1 @@ -159,6 +163,13 @@ spec: name: {{ $oidc.secret.name }} key: useAccessToken {{- end }} + {{- if $oidc.usePKCE }} + - name: OIDC_USE_PKCE + valueFrom: + secretKeyRef: + name: {{ $oidc.secret.name }} + key: usePKCE + {{- end }} {{- else }} {{- if $oidc.clientID }} - name: OIDC_CLIENT_ID @@ -192,6 +203,10 @@ spec: - name: OIDC_USE_ACCESS_TOKEN value: {{ $oidc.useAccessToken }} {{- end }} + {{- if $oidc.usePKCE }} + - name: OIDC_USE_PKCE + value: {{ $oidc.usePKCE }} + {{- end }} {{- end }} {{- if .Values.env }} {{- toYaml .Values.env | nindent 12 }} @@ -245,6 +260,9 @@ spec: # Check if useAccessToken is non false either from env or oidc.config - "-oidc-use-access-token=$(OIDC_USE_ACCESS_TOKEN)" {{- end }} + {{- if or (eq ($oidc.usePKCE | toString) "true") (eq $usePKCE "true") }} + - "-oidc-use-pkce=$(OIDC_USE_PKCE)" + {{- end }} {{- else }} - "-oidc-client-id=$(OIDC_CLIENT_ID)" - "-oidc-client-secret=$(OIDC_CLIENT_SECRET)" diff --git a/charts/headlamp/templates/secret.yaml b/charts/headlamp/templates/secret.yaml index 1af93d96fed..afda05afa01 100644 --- a/charts/headlamp/templates/secret.yaml +++ b/charts/headlamp/templates/secret.yaml @@ -31,5 +31,8 @@ data: {{- with .useAccessToken }} useAccessToken: {{ . | toString | b64enc | quote }} {{- end }} +{{- with .usePKCE }} + usePKCE: {{ . | toString | b64enc | quote }} +{{- end }} {{- end }} {{- end }} diff --git a/charts/headlamp/tests/expected_templates/oidc-pkce.yaml b/charts/headlamp/tests/expected_templates/oidc-pkce.yaml new file mode 100644 index 00000000000..5eef06a23b0 --- /dev/null +++ b/charts/headlamp/tests/expected_templates/oidc-pkce.yaml @@ -0,0 +1,137 @@ +--- +# Source: headlamp/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.36.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/managed-by: Helm +--- +# Source: headlamp/templates/clusterrolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: headlamp-admin + labels: + helm.sh/chart: headlamp-0.36.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/managed-by: Helm +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: +- kind: ServiceAccount + name: headlamp + namespace: default +--- +# Source: headlamp/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.36.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/managed-by: Helm +spec: + type: ClusterIP + + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp +--- +# Source: headlamp/templates/deployment.yaml +# This block of code is used to extract the values from the env. +# This is done to check if the values are non-empty and if they are, they are used in the deployment.yaml. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.36.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/managed-by: Helm +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + template: + metadata: + labels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + spec: + serviceAccountName: headlamp + automountServiceAccountToken: true + securityContext: + {} + containers: + - name: headlamp + securityContext: + privileged: false + runAsGroup: 101 + runAsNonRoot: true + runAsUser: 100 + image: "ghcr.io/headlamp-k8s/headlamp:v0.36.0" + imagePullPolicy: IfNotPresent + + env: + - name: OIDC_CLIENT_ID + value: testClientId + - name: OIDC_CLIENT_SECRET + value: testClientSecret + - name: OIDC_ISSUER_URL + value: testIssuerURL + - name: OIDC_SCOPES + value: testScope + - name: OIDC_USE_PKCE + value: true + args: + - "-in-cluster" + - "-plugins-dir=/headlamp/plugins" + # Check if externalSecret is disabled + # Check if clientID is non empty either from env or oidc.config + - "-oidc-client-id=$(OIDC_CLIENT_ID)" + # Check if clientSecret is non empty either from env or oidc.config + - "-oidc-client-secret=$(OIDC_CLIENT_SECRET)" + # Check if issuerURL is non empty either from env or oidc.config + - "-oidc-idp-issuer-url=$(OIDC_ISSUER_URL)" + # Check if scopes are non empty either from env or oidc.config + - "-oidc-scopes=$(OIDC_SCOPES)" + - "-oidc-use-pkce=$(OIDC_USE_PKCE)" + ports: + - name: http + containerPort: 4466 + protocol: TCP + livenessProbe: + httpGet: + path: "/" + port: http + readinessProbe: + httpGet: + path: "/" + port: http + resources: + {} diff --git a/charts/headlamp/tests/test_cases/oidc-pkce.yaml b/charts/headlamp/tests/test_cases/oidc-pkce.yaml new file mode 100644 index 00000000000..f8a5ed5ec48 --- /dev/null +++ b/charts/headlamp/tests/test_cases/oidc-pkce.yaml @@ -0,0 +1,15 @@ +# This is a test case for the direct OIDC configuration in the Headlamp deployment. +# The oidc.secret.create field is false to avoid creating a secret for OIDC. +# The oidc.clientID field is a string that specifies the client ID for OIDC. +# The oidc.clientSecret field is a string that specifies the client secret for OIDC. +# The oidc.issuerURL field is a string that specifies the issuer URL for OIDC. +# The oidc.scopes field is a string that specifies the scopes for OIDC. +config: + oidc: + secret: + create: false + clientID: "testClientId" + clientSecret: "testClientSecret" + issuerURL: "testIssuerURL" + scopes: "testScope" + usePKCE: true diff --git a/charts/headlamp/values.schema.json b/charts/headlamp/values.schema.json index c3282d045d5..e6e86b531b9 100644 --- a/charts/headlamp/values.schema.json +++ b/charts/headlamp/values.schema.json @@ -207,6 +207,10 @@ "type": "string", "description": "Scopes of the OIDC provider" }, + "usePKCE": { + "type": "boolean", + "description": "Use PKCE (Proof Key for Code Exchange) for enhanced security in OIDC flow" + }, "externalSecret": { "type": "object", "description": "External secret to use for OIDC configuration", diff --git a/charts/headlamp/values.yaml b/charts/headlamp/values.yaml index 4f1281dbe7d..b44b47a63cb 100644 --- a/charts/headlamp/values.yaml +++ b/charts/headlamp/values.yaml @@ -78,6 +78,8 @@ config: validatorIssuerURL: "" # -- Use 'access_token' instead of 'id_token' when authenticating using OIDC useAccessToken: false + # -- Use PKCE (Proof Key for Code Exchange) for enhanced security in OIDC flow + usePKCE: false # Option 3: # @param config.oidc - External OIDC secret configuration From 39906e49414360bfafcdebdc834bd7c9a98c0885 Mon Sep 17 00:00:00 2001 From: kairos Date: Sat, 26 Jul 2025 12:32:49 +0300 Subject: [PATCH 47/72] backend: Add PKCE support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use the golang oauth2 PKCE helper functions. Clean up OIDC auth config state when done with using it. Use unique state variable instead of cluster name. Use a mutex to protect the oauthRequestMap from races. Co-authored-by: René Dudfield --- backend/cmd/headlamp.go | 199 +++++++++++++++++---------- backend/cmd/server.go | 1 + backend/pkg/config/config.go | 1 + backend/pkg/kubeconfig/kubeconfig.go | 3 +- 4 files changed, 131 insertions(+), 73 deletions(-) diff --git a/backend/cmd/headlamp.go b/backend/cmd/headlamp.go index db1e5a3ab80..47390061567 100644 --- a/backend/cmd/headlamp.go +++ b/backend/cmd/headlamp.go @@ -20,6 +20,7 @@ import ( "bytes" "compress/gzip" "context" + "crypto/rand" "crypto/tls" "encoding/base64" "encoding/json" @@ -34,6 +35,7 @@ import ( "path/filepath" "runtime" "strings" + "sync" "syscall" "time" @@ -79,6 +81,7 @@ type HeadlampConfig struct { oidcUseAccessToken bool oidcSkipTLSVerify bool oidcCACert string + oidcUsePKCE bool cache cache.Cache[interface{}] multiplexer *Multiplexer telemetryConfig cfg.Config @@ -117,9 +120,11 @@ type clientConfig struct { } type OauthConfig struct { - Config *oauth2.Config - Verifier *oidc.IDTokenVerifier - Ctx context.Context + Config *oauth2.Config + Verifier *oidc.IDTokenVerifier + Ctx context.Context + CodeVerifier string // PKCE code verifier + Cluster string // cluster context name this is associated with } // returns True if a file exists. @@ -615,7 +620,10 @@ func createHeadlampHandler(config *HeadlampConfig) http.Handler { config.addClusterSetupRoute(r) - oauthRequestMap := make(map[string]*OauthConfig) + var ( + oauthRequestMap = make(map[string]*OauthConfig) + oauthMu sync.Mutex + ) r.HandleFunc("/oidc", func(w http.ResponseWriter, r *http.Request) { ctx := context.Background() @@ -680,12 +688,38 @@ func createHeadlampHandler(config *HeadlampConfig) http.Handler { RedirectURL: getOidcCallbackURL(r, config), Scopes: append([]string{oidc.ScopeOpenID}, oidcAuthConfig.Scopes...), } - /* we encode the cluster to base64 and set it as state so that when getting redirected - by oidc we can use this state value to get cluster name - */ - state := base64.StdEncoding.EncodeToString([]byte(cluster)) - oauthRequestMap[state] = &OauthConfig{Config: oauthConfig, Verifier: verifier, Ctx: ctx} - http.Redirect(w, r, oauthConfig.AuthCodeURL(state), http.StatusFound) + + // state should be unique per request, cryptographically secure random, url safe + state := func() string { + b := make([]byte, 32) + if _, err := rand.Read(b); err != nil { + panic(err) + } + return base64.RawURLEncoding.EncodeToString(b) + }() + + entry := &OauthConfig{ + Config: oauthConfig, + Verifier: verifier, + Ctx: ctx, + Cluster: cluster, + } + + var authURL string + + if config.oidcUsePKCE { + entry.CodeVerifier = oauth2.GenerateVerifier() + authURL = oauthConfig.AuthCodeURL(state, oauth2.S256ChallengeOption(entry.CodeVerifier)) + } else { + authURL = oauthConfig.AuthCodeURL(state) + } + + // Store the request config keyed by state for callback handling + oauthMu.Lock() + oauthRequestMap[state] = entry + oauthMu.Unlock() + + http.Redirect(w, r, authURL, http.StatusFound) }).Queries("cluster", "{cluster}") r.HandleFunc("/drain-node", config.handleNodeDrain).Methods("POST") @@ -695,93 +729,114 @@ func createHeadlampHandler(config *HeadlampConfig) http.Handler { r.HandleFunc("/oidc-callback", func(w http.ResponseWriter, r *http.Request) { state := r.URL.Query().Get("state") - decodedState, err := base64.StdEncoding.DecodeString(state) - if err != nil { - logger.Log(logger.LevelError, nil, err, "failed to decode state") - http.Error(w, "wrong state set, invalid request "+err.Error(), http.StatusBadRequest) + if state == "" { + logger.Log(logger.LevelError, nil, err, "invalid request state is empty") + http.Error(w, "invalid request state is empty", http.StatusBadRequest) return } - if state == "" { - logger.Log(logger.LevelError, nil, err, "invalid request state is empty") - http.Error(w, "invalid request state is empty", http.StatusBadRequest) + oauthMu.Lock() + + oauthConfig, ok := oauthRequestMap[state] + if ok { + // We have a copy of the oauthConfig, we can delete the map entry now + delete(oauthRequestMap, state) + } + oauthMu.Unlock() + + if !ok { + http.Error(w, "invalid request", http.StatusBadRequest) return } - //nolint:nestif - if oauthConfig, ok := oauthRequestMap[state]; ok { - oauth2Token, err := oauthConfig.Config.Exchange(oauthConfig.Ctx, r.URL.Query().Get("code")) - if err != nil { - logger.Log(logger.LevelError, nil, err, "failed to exchange token") - http.Error(w, "Failed to exchange token: "+err.Error(), http.StatusInternalServerError) + var oauth2Token *oauth2.Token - return - } + var err error - tokenType := "id_token" - if config.oidcUseAccessToken { - tokenType = "access_token" - } + // Exchange authorization code for token, with or without PKCE + if config.oidcUsePKCE && oauthConfig.CodeVerifier != "" { + // Use PKCE code verifier for token exchange + oauth2Token, err = oauthConfig.Config.Exchange( + oauthConfig.Ctx, + r.URL.Query().Get("code"), + oauth2.SetAuthURLParam("code_verifier", oauthConfig.CodeVerifier), + ) + } else { + // Standard token exchange without PKCE + oauth2Token, err = oauthConfig.Config.Exchange( + oauthConfig.Ctx, + r.URL.Query().Get("code"), + ) + } - rawUserToken, ok := oauth2Token.Extra(tokenType).(string) - if !ok { - logger.Log(logger.LevelError, nil, err, fmt.Sprintf("no %s field in oauth2 token", tokenType)) - http.Error(w, fmt.Sprintf("No %s field in oauth2 token.", tokenType), http.StatusInternalServerError) + if err != nil { + logger.Log(logger.LevelError, nil, err, "failed to exchange token") + http.Error(w, "Failed to exchange token: "+err.Error(), http.StatusInternalServerError) - return - } + return + } - if err := config.cache.Set(context.Background(), - fmt.Sprintf("oidc-token-%s", rawUserToken), oauth2Token.RefreshToken); err != nil { - logger.Log(logger.LevelError, nil, err, "failed to cache refresh token") - http.Error(w, "Failed to cache refresh token: "+err.Error(), http.StatusInternalServerError) + tokenType := "id_token" + if config.oidcUseAccessToken { + tokenType = "access_token" + } - return - } + rawUserToken, ok := oauth2Token.Extra(tokenType).(string) + if !ok { + logger.Log(logger.LevelError, nil, err, fmt.Sprintf("no %s field in oauth2 token", tokenType)) + http.Error(w, fmt.Sprintf("No %s field in oauth2 token.", tokenType), http.StatusInternalServerError) - idToken, err := oauthConfig.Verifier.Verify(oauthConfig.Ctx, rawUserToken) - if err != nil { - logger.Log(logger.LevelError, nil, err, "failed to verify ID Token") - http.Error(w, "Failed to verify ID Token: "+err.Error(), http.StatusInternalServerError) + return + } - return - } + if err := config.cache.Set(context.Background(), + fmt.Sprintf("oidc-token-%s", rawUserToken), oauth2Token.RefreshToken); err != nil { + logger.Log(logger.LevelError, nil, err, "failed to cache refresh token") + http.Error(w, "Failed to cache refresh token: "+err.Error(), http.StatusInternalServerError) - resp := struct { - OAuth2Token *oauth2.Token - IDTokenClaims *json.RawMessage // ID Token payload is just JSON. - }{oauth2Token, new(json.RawMessage)} + return + } - if err := idToken.Claims(&resp.IDTokenClaims); err != nil { - logger.Log(logger.LevelError, nil, err, "failed to get id token claims") - http.Error(w, err.Error(), http.StatusInternalServerError) + idToken, err := oauthConfig.Verifier.Verify(oauthConfig.Ctx, rawUserToken) + if err != nil { + logger.Log(logger.LevelError, nil, err, "failed to verify ID Token") + http.Error(w, "Failed to verify ID Token: "+err.Error(), http.StatusInternalServerError) - return - } + return + } - var redirectURL string - if config.DevMode { - redirectURL = "http://localhost:3000/" - } else { - redirectURL = "/" - } + resp := struct { + OAuth2Token *oauth2.Token + IDTokenClaims *json.RawMessage // ID Token payload is just JSON. + }{oauth2Token, new(json.RawMessage)} - baseURL := strings.Trim(config.BaseURL, "/") - if baseURL != "" { - redirectURL += baseURL + "/" - } + if err := idToken.Claims(&resp.IDTokenClaims); err != nil { + logger.Log(logger.LevelError, nil, err, "failed to get id token claims") + http.Error(w, err.Error(), http.StatusInternalServerError) - // Set auth cookie - auth.SetTokenCookie(w, r, string(decodedState), rawUserToken, config.BaseURL) + return + } - redirectURL += fmt.Sprintf("auth?cluster=%1s", decodedState) - http.Redirect(w, r, redirectURL, http.StatusSeeOther) + var redirectURL string + if config.DevMode { + redirectURL = "http://localhost:3000/" } else { - http.Error(w, "invalid request", http.StatusBadRequest) - return + redirectURL = "/" + } + + baseURL := strings.Trim(config.BaseURL, "/") + if baseURL != "" { + redirectURL += baseURL + "/" } + + // Set auth cookie + auth.SetTokenCookie(w, r, oauthConfig.Cluster, rawUserToken, config.BaseURL) + + redirectURL += fmt.Sprintf("auth?cluster=%1s", oauthConfig.Cluster) + + http.Redirect(w, r, redirectURL, http.StatusSeeOther) }) // Serve the frontend if needed diff --git a/backend/cmd/server.go b/backend/cmd/server.go index 2eff74822da..b1865175a68 100644 --- a/backend/cmd/server.go +++ b/backend/cmd/server.go @@ -116,6 +116,7 @@ func createHeadlampConfig(conf *config.Config) *HeadlampConfig { oidcScopes: strings.Split(conf.OidcScopes, ","), oidcSkipTLSVerify: conf.OidcSkipTLSVerify, oidcUseAccessToken: conf.OidcUseAccessToken, + oidcUsePKCE: conf.OidcUsePKCE, meUsernamePaths: conf.MeUsernamePath, meEmailPaths: conf.MeEmailPath, meGroupsPaths: conf.MeGroupsPath, diff --git a/backend/pkg/config/config.go b/backend/pkg/config/config.go index 92f58583e38..467ef014d6b 100644 --- a/backend/pkg/config/config.go +++ b/backend/pkg/config/config.go @@ -56,6 +56,7 @@ type Config struct { MeUsernamePath string `koanf:"me-username-path"` MeEmailPath string `koanf:"me-email-path"` MeGroupsPath string `koanf:"me-groups-path"` + OidcUsePKCE bool `koanf:"oidc-use-pkce"` // telemetry configs ServiceName string `koanf:"service-name"` ServiceVersion *string `koanf:"service-version"` diff --git a/backend/pkg/kubeconfig/kubeconfig.go b/backend/pkg/kubeconfig/kubeconfig.go index 1960db1e799..4dff110abf1 100644 --- a/backend/pkg/kubeconfig/kubeconfig.go +++ b/backend/pkg/kubeconfig/kubeconfig.go @@ -975,7 +975,8 @@ func GetInClusterContext(oidcIssuerURL string, var oidcConf *OidcConfig - if oidcClientID != "" && oidcClientSecret != "" && oidcIssuerURL != "" && oidcScopes != "" { + if oidcClientID != "" && oidcIssuerURL != "" && oidcScopes != "" { + // client secret is optional for in-cluster OIDC configuration oidcConf = &OidcConfig{ ClientID: oidcClientID, ClientSecret: oidcClientSecret, From 1574266a5547227f1bddbc793736baaf7e5acf2b Mon Sep 17 00:00:00 2001 From: Vincent T Date: Fri, 24 Oct 2025 16:17:36 -0400 Subject: [PATCH 48/72] docs: Add projects to learn section --- docs/learn/projects.md | 79 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 docs/learn/projects.md diff --git a/docs/learn/projects.md b/docs/learn/projects.md new file mode 100644 index 00000000000..72321600814 --- /dev/null +++ b/docs/learn/projects.md @@ -0,0 +1,79 @@ +--- +title: Projects +sidebar_position: 5 +--- + +# Projects + +The Projects feature in Headlamp allows you to group and manage Kubernetes resources across multiple clusters. A project combines one or more clusters under a single namespace, providing a unified view and management interface. This is particularly useful for teams working on multi-cluster deployments, as it enables easier resource organization and access control. + +By default, Headlamp provides a cluster-centric view of your Kubernetes resources. However, with Projects, you can shift to a namespace-centric perspective that spans multiple clusters. This allows you to see all resources associated with a specific project in one place, regardless of which cluster they reside in. + +## Creating a Project + +### Option 1: Create a Project in the UI + +1. From the home dashboard, open **Projects**. +2. Click **Create Project**. +3. Fill in the required details: + + - **Project Name** — A unique name for your project. + - **Cluster(s)** — Select one or more clusters to include. + - **Namespace** — Choose or create the namespace associated with this project. + +4. Click **Create** to finalize setup. + +Once created, the project appears in your list. +Selecting it opens a detailed view that includes tabs for **Overview**, **Resources**, **Access**, and **Map**. + +--- + +### Option 2: Create a Project from YAML + +You can also add resources to a project by importing a YAML configuration file. This associates existing clusters and resources into the project’s namespace. +Resources defined in your YAML file are added to the **project’s namespace** automatically. + +1. In the **Create Project** dialog, select **From YAML**. +2. Fill in the required details: + + - **Project Name** — A unique name for your project. + - **Cluster(s)** — Select a cluster to add resources to. + +3. Choose one of the following options: + - **Upload File** – Import a YAML file from your computer. + - **Use URL** – Paste a link to a hosted YAML file. +4. Click **Create** once the configuration loads. + +Once created, your projects will be listed in the "Projects" section. + +## Working with Projects + +After creating a project, you can explore it using the available tabs in the Project details view. + +### Overview Tab + +![Project Overview](https://github.com/user-attachments/assets/a03ed234-e734-47e2-86d6-2bf11bf71963) + +Provides a high-level summary of the project — similar to a namespace view, but extended across multiple clusters. +Here you can see general information like labels, annotations, and linked clusters. + +### Resources Tab + +![Project Resources](https://github.com/user-attachments/assets/fbca87df-34ad-423f-995c-3c04d72ac5b9) + +Lists most of the same resources you’d see in a cluster view, scoped to your project namespace. +Some resource types may not appear (like Pods in certain cluster configurations). +This tab aggregates resources from all clusters associated with the project. + +### Access Tab + +![Project Access](https://github.com/user-attachments/assets/c0e56948-6fdd-4a4e-b678-7cb5418cb9a3) + +Displays and manages access controls for the project, similar to how you’d manage permissions within a namespace. + +### Map Tab + +![Project Map](https://github.com/user-attachments/assets/87341cfd-3978-4555-b34b-020e4666c789) + +Shows a visual map of the resources within your project. +This view uses the same resource map as in cluster mode, but filters results to only display items belonging to your project namespace. From aeb4ea66594ab8d4795a33f88ca14a42bafdcf28 Mon Sep 17 00:00:00 2001 From: Harsh Date: Sun, 26 Oct 2025 23:29:05 +0530 Subject: [PATCH 49/72] frontend: Enhance ErrorComponent with GitHub issue reporting and error stack handling --- .../RouteSwitcher.Default.stories.storyshot | 12 +- .../common/ErrorPage/ErrorPage.stories.tsx | 14 ++ .../components/common/ErrorPage/ErrorPage.tsx | 75 ++++++++++- ...ErrorPage.WithErrorStack.stories.storyshot | 121 ++++++++++++++++++ frontend/src/i18n/locales/de/translation.json | 4 + frontend/src/i18n/locales/en/translation.json | 4 + frontend/src/i18n/locales/es/translation.json | 4 + frontend/src/i18n/locales/fr/translation.json | 4 + frontend/src/i18n/locales/hi/translation.json | 4 + frontend/src/i18n/locales/it/translation.json | 4 + frontend/src/i18n/locales/ja/translation.json | 4 + frontend/src/i18n/locales/ko/translation.json | 4 + frontend/src/i18n/locales/pt/translation.json | 4 + frontend/src/i18n/locales/ta/translation.json | 4 + .../src/i18n/locales/zh-tw/translation.json | 4 + frontend/src/i18n/locales/zh/translation.json | 4 + 16 files changed, 267 insertions(+), 3 deletions(-) create mode 100644 frontend/src/components/common/ErrorPage/__snapshots__/ErrorPage.WithErrorStack.stories.storyshot diff --git a/frontend/src/components/App/__snapshots__/RouteSwitcher.Default.stories.storyshot b/frontend/src/components/App/__snapshots__/RouteSwitcher.Default.stories.storyshot index 8f65798cdfe..89d76cce9da 100644 --- a/frontend/src/components/App/__snapshots__/RouteSwitcher.Default.stories.storyshot +++ b/frontend/src/components/App/__snapshots__/RouteSwitcher.Default.stories.storyshot @@ -68,7 +68,7 @@ class="MuiAccordionDetails-root css-15v22id-MuiAccordionDetails-root" >
+
Not sure what to do!,
 };
+
+export const WithErrorStack = Template.bind({});
+WithErrorStack.args = {
+  error: (() => {
+    const error = new Error('Unexpected error occurred');
+    error.stack = `Error: Unexpected error occurred
+    at ComponentName (http://localhost:3000/static/js/main.chunk.js:1234:56)
+    at div
+    at ErrorBoundary (http://localhost:3000/static/js/main.chunk.js:5678:90)
+    at App (http://localhost:3000/static/js/main.chunk.js:9012:34)
+    at Router (http://localhost:3000/static/js/main.chunk.js:3456:78)`;
+    return error;
+  })(),
+};
diff --git a/frontend/src/components/common/ErrorPage/ErrorPage.tsx b/frontend/src/components/common/ErrorPage/ErrorPage.tsx
index 862fbe432f4..4d552d14284 100644
--- a/frontend/src/components/common/ErrorPage/ErrorPage.tsx
+++ b/frontend/src/components/common/ErrorPage/ErrorPage.tsx
@@ -24,9 +24,11 @@ import Grid from '@mui/material/Grid';
 import Link from '@mui/material/Link';
 import Typography from '@mui/material/Typography';
 import { styled } from '@mui/system';
+import { useSnackbar } from 'notistack';
 import React from 'react';
 import { Trans, useTranslation } from 'react-i18next';
 import headlampBrokenImage from '../../../assets/headlamp-broken.svg';
+import { getVersion } from '../../../helpers/getProductInfo';
 
 const WidthImg = styled('img')({
   width: '100%',
@@ -46,8 +48,60 @@ export interface ErrorComponentProps {
   error?: Error;
 }
 
+const MAX_STACK_LENGTH = 1000;
+const MAX_TITLE_LENGTH = 100;
+const GITHUB_REPO_URL = 'https://github.com/kubernetes-sigs/headlamp';
+
+function handleOpenGitHubIssue(
+  error: Error,
+  t: (key: string) => string,
+  enqueueSnackbar: (
+    message: string,
+    options?: { variant?: 'success' | 'error' | 'warning' | 'info' }
+  ) => void
+) {
+  const version = getVersion();
+
+  // Truncate stack trace to prevent URL length limits
+  const truncatedStack =
+    error.stack && error.stack.length > MAX_STACK_LENGTH
+      ? error.stack.substring(0, MAX_STACK_LENGTH) + '\n... (truncated)'
+      : error.stack || 'No stack trace available';
+
+  // Sanitize error message in issue title
+  const sanitizedMessage = (error.message || 'Application Error')
+    .replace(/[\r\n]+/g, ' ') // Replace newlines with spaces
+    .substring(0, MAX_TITLE_LENGTH); // Limit title length
+
+  const issueTitle = encodeURIComponent(`Crash Report: ${sanitizedMessage}`);
+  const issueBody = encodeURIComponent(
+    `## Crash Summary\n${error.message || 'An error occurred in the application'}\n\n` +
+      `## Error Stack\n\`\`\`\n${truncatedStack}\n\`\`\`\n\n` +
+      `## Headlamp Version\n${version.VERSION || 'Unknown'}\n\n` +
+      `## Git Commit\n${version.GIT_VERSION || 'Unknown'}\n\n` +
+      `## System Information\n` +
+      `- User Agent: ${navigator.userAgent}\n` +
+      `- Platform: ${navigator.platform}\n` +
+      `- Language: ${navigator.language}\n\n` +
+      `## Additional Context\n`
+  );
+  const githubUrl = `${GITHUB_REPO_URL}/issues/new?title=${issueTitle}&body=${issueBody}&labels=kind/bug`;
+
+  // Handle popup blocker - window.open returns null if blocked
+  const newWindow = window.open(githubUrl, '_blank');
+  if (!newWindow) {
+    enqueueSnackbar(
+      t(
+        'translation|Unable to open GitHub. Please check your popup blocker settings or copy the error details manually.'
+      ),
+      { variant: 'warning' }
+    );
+  }
+}
+
 export default function ErrorComponent(props: ErrorComponentProps) {
   const { t } = useTranslation();
+  const { enqueueSnackbar } = useSnackbar();
   const {
     title = t('Uh-oh! Something went wrong.'),
     message = '',
@@ -102,14 +156,31 @@ export default function ErrorComponent(props: ErrorComponentProps) {
                 {t('translation|Error Details')}
               
               
-                
+                
                   
+                  
                 
                 
+  
+
+
+ +

+ Uh-oh! Something went wrong. +

+

+ Head back + + home + + . +

+
+
+
+
+ +
+
+
+
+ \ No newline at end of file diff --git a/frontend/src/i18n/locales/de/translation.json b/frontend/src/i18n/locales/de/translation.json index 883cc30b051..467b7407e85 100644 --- a/frontend/src/i18n/locales/de/translation.json +++ b/frontend/src/i18n/locales/de/translation.json @@ -204,9 +204,13 @@ "Yes": "Ja", "Create {{ name }}": "", "Toggle fullscreen": "Vollbild ein/aus", + "Unable to open GitHub. Please check your popup blocker settings or copy the error details manually.": "", "Head back <1>home.": "Head back <1>home.", "Error Details": "", + "Copied to clipboard": "", + "Failed to copy to clipboard": "", "Copy": "", + "Open Issue on GitHub": "", "Find": "Finden", "Download": "Herunterladen", "No results": "Keine Ergebnisse", diff --git a/frontend/src/i18n/locales/en/translation.json b/frontend/src/i18n/locales/en/translation.json index 22267aa91aa..f4d74a92f93 100644 --- a/frontend/src/i18n/locales/en/translation.json +++ b/frontend/src/i18n/locales/en/translation.json @@ -204,9 +204,13 @@ "Yes": "Yes", "Create {{ name }}": "Create {{ name }}", "Toggle fullscreen": "Toggle fullscreen", + "Unable to open GitHub. Please check your popup blocker settings or copy the error details manually.": "Unable to open GitHub. Please check your popup blocker settings or copy the error details manually.", "Head back <1>home.": "Head back <1>home.", "Error Details": "Error Details", + "Copied to clipboard": "Copied to clipboard", + "Failed to copy to clipboard": "Failed to copy to clipboard", "Copy": "Copy", + "Open Issue on GitHub": "Open Issue on GitHub", "Find": "Find", "Download": "Download", "No results": "No results", diff --git a/frontend/src/i18n/locales/es/translation.json b/frontend/src/i18n/locales/es/translation.json index 9d4f4be1ff0..c041c7497b0 100644 --- a/frontend/src/i18n/locales/es/translation.json +++ b/frontend/src/i18n/locales/es/translation.json @@ -204,9 +204,13 @@ "Yes": "Sí", "Create {{ name }}": "Crear {{ name }}", "Toggle fullscreen": "Alternar pantalla completa", + "Unable to open GitHub. Please check your popup blocker settings or copy the error details manually.": "", "Head back <1>home.": "Head back <1>home.", "Error Details": "Detalles del error", + "Copied to clipboard": "", + "Failed to copy to clipboard": "", "Copy": "Copiar", + "Open Issue on GitHub": "", "Find": "Buscar", "Download": "Descargar", "No results": "Sin resultados", diff --git a/frontend/src/i18n/locales/fr/translation.json b/frontend/src/i18n/locales/fr/translation.json index da139cc98b0..ce1e5659faa 100644 --- a/frontend/src/i18n/locales/fr/translation.json +++ b/frontend/src/i18n/locales/fr/translation.json @@ -204,9 +204,13 @@ "Yes": "Oui", "Create {{ name }}": "", "Toggle fullscreen": "Basculer en mode plein écran", + "Unable to open GitHub. Please check your popup blocker settings or copy the error details manually.": "", "Head back <1>home.": "Head back <1>home.", "Error Details": "", + "Copied to clipboard": "", + "Failed to copy to clipboard": "", "Copy": "", + "Open Issue on GitHub": "", "Find": "Trouver", "Download": "Télécharger", "No results": "Aucun résultat", diff --git a/frontend/src/i18n/locales/hi/translation.json b/frontend/src/i18n/locales/hi/translation.json index ee715680abb..de2e588a03d 100644 --- a/frontend/src/i18n/locales/hi/translation.json +++ b/frontend/src/i18n/locales/hi/translation.json @@ -204,9 +204,13 @@ "Yes": "हाँ", "Create {{ name }}": "{{ name }} बनाएँ", "Toggle fullscreen": "फुलस्क्रीन टॉगल करें", + "Unable to open GitHub. Please check your popup blocker settings or copy the error details manually.": "", "Head back <1>home.": "<1>होम पर वापस जाएँ।", "Error Details": "त्रुटि विवरण", + "Copied to clipboard": "", + "Failed to copy to clipboard": "", "Copy": "कॉपी करें", + "Open Issue on GitHub": "", "Find": "खोजें", "Download": "डाउनलोड करें", "No results": "कोई परिणाम नहीं", diff --git a/frontend/src/i18n/locales/it/translation.json b/frontend/src/i18n/locales/it/translation.json index ef96891e90e..689ad62d483 100644 --- a/frontend/src/i18n/locales/it/translation.json +++ b/frontend/src/i18n/locales/it/translation.json @@ -204,9 +204,13 @@ "Yes": "Sì", "Create {{ name }}": "Crea {{ name }}", "Toggle fullscreen": "Schermo intero", + "Unable to open GitHub. Please check your popup blocker settings or copy the error details manually.": "", "Head back <1>home.": "Torna alla <1>home.", "Error Details": "Dettagli errore", + "Copied to clipboard": "", + "Failed to copy to clipboard": "", "Copy": "Copia", + "Open Issue on GitHub": "", "Find": "Trova", "Download": "Scarica", "No results": "Nessun risultato", diff --git a/frontend/src/i18n/locales/ja/translation.json b/frontend/src/i18n/locales/ja/translation.json index 6e03f84afab..da97046e3ac 100644 --- a/frontend/src/i18n/locales/ja/translation.json +++ b/frontend/src/i18n/locales/ja/translation.json @@ -204,9 +204,13 @@ "Yes": "はい", "Create {{ name }}": "{{ name }} の作成", "Toggle fullscreen": "全画面表示の切り替え", + "Unable to open GitHub. Please check your popup blocker settings or copy the error details manually.": "", "Head back <1>home.": "<1>ホームに戻る。", "Error Details": "エラーの詳細", + "Copied to clipboard": "", + "Failed to copy to clipboard": "", "Copy": "コピー", + "Open Issue on GitHub": "", "Find": "検索", "Download": "ダウンロード", "No results": "結果なし", diff --git a/frontend/src/i18n/locales/ko/translation.json b/frontend/src/i18n/locales/ko/translation.json index 402db6aa813..a171301be2a 100644 --- a/frontend/src/i18n/locales/ko/translation.json +++ b/frontend/src/i18n/locales/ko/translation.json @@ -204,9 +204,13 @@ "Yes": "예", "Create {{ name }}": "{{ name }} 생성", "Toggle fullscreen": "전체 화면 전환", + "Unable to open GitHub. Please check your popup blocker settings or copy the error details manually.": "", "Head back <1>home.": "<1>홈으로 돌아가기", "Error Details": "오류 세부 정보", + "Copied to clipboard": "", + "Failed to copy to clipboard": "", "Copy": "복사", + "Open Issue on GitHub": "", "Find": "찾기", "Download": "다운로드", "No results": "결과 없음", diff --git a/frontend/src/i18n/locales/pt/translation.json b/frontend/src/i18n/locales/pt/translation.json index 390018b8f69..11b45ee5206 100644 --- a/frontend/src/i18n/locales/pt/translation.json +++ b/frontend/src/i18n/locales/pt/translation.json @@ -204,9 +204,13 @@ "Yes": "Sim", "Create {{ name }}": "Criar {{ name }}", "Toggle fullscreen": "Alternar ecrã inteiro", + "Unable to open GitHub. Please check your popup blocker settings or copy the error details manually.": "", "Head back <1>home.": "Voltar ao <1>início.", "Error Details": "Detalhes do erro", + "Copied to clipboard": "", + "Failed to copy to clipboard": "", "Copy": "Copiar", + "Open Issue on GitHub": "", "Find": "Pesquisar", "Download": "Descarregar", "No results": "Sem resultados", diff --git a/frontend/src/i18n/locales/ta/translation.json b/frontend/src/i18n/locales/ta/translation.json index 551ea0a21f6..08999a61753 100644 --- a/frontend/src/i18n/locales/ta/translation.json +++ b/frontend/src/i18n/locales/ta/translation.json @@ -204,9 +204,13 @@ "Yes": "ஆம்", "Create {{ name }}": "{{ name }} உருவாக்கு", "Toggle fullscreen": "முழுத்திரை நிலையை மாற்று", + "Unable to open GitHub. Please check your popup blocker settings or copy the error details manually.": "", "Head back <1>home.": "<1>முகப்புக்கு திரும்பிச் செல்லவும்.", "Error Details": "பிழை விவரங்கள்", + "Copied to clipboard": "", + "Failed to copy to clipboard": "", "Copy": "நகலெடு", + "Open Issue on GitHub": "", "Find": "கண்டுபிடி", "Download": "பதிவிறக்கு", "No results": "முடிவுகள் இல்லை", diff --git a/frontend/src/i18n/locales/zh-tw/translation.json b/frontend/src/i18n/locales/zh-tw/translation.json index c738fa5656e..030d14f1803 100644 --- a/frontend/src/i18n/locales/zh-tw/translation.json +++ b/frontend/src/i18n/locales/zh-tw/translation.json @@ -204,9 +204,13 @@ "Yes": "是", "Create {{ name }}": "新增 {{ name }}", "Toggle fullscreen": "切換全螢幕", + "Unable to open GitHub. Please check your popup blocker settings or copy the error details manually.": "", "Head back <1>home.": "返回<1>首頁。", "Error Details": "錯誤詳情", + "Copied to clipboard": "", + "Failed to copy to clipboard": "", "Copy": "複製", + "Open Issue on GitHub": "", "Find": "查詢", "Download": "下載", "No results": "無結果", diff --git a/frontend/src/i18n/locales/zh/translation.json b/frontend/src/i18n/locales/zh/translation.json index 4ecf42d736b..dcd8e8cbfcc 100644 --- a/frontend/src/i18n/locales/zh/translation.json +++ b/frontend/src/i18n/locales/zh/translation.json @@ -204,9 +204,13 @@ "Yes": "是", "Create {{ name }}": "新增 {{ name }}", "Toggle fullscreen": "切换全屏幕", + "Unable to open GitHub. Please check your popup blocker settings or copy the error details manually.": "", "Head back <1>home.": "返回<1>首页。", "Error Details": "错误详情", + "Copied to clipboard": "", + "Failed to copy to clipboard": "", "Copy": "复制", + "Open Issue on GitHub": "", "Find": "查询", "Download": "下载", "No results": "暂无结果", From 97e890b20b6bdef5ff2db947a82a6a5befdb4e5d Mon Sep 17 00:00:00 2001 From: Faakhir30 Date: Sat, 4 Oct 2025 23:52:39 +0500 Subject: [PATCH 50/72] frontend: Settings: Refactor cluster rename functionality. Signed-off-by: Faakhir30 --- .../App/Settings/ClusterNameEditor.tsx | 255 ++++++++++++++++++ .../App/Settings/SettingsCluster.tsx | 227 +--------------- frontend/src/components/App/Settings/util.tsx | 12 + frontend/src/i18n/locales/de/translation.json | 18 +- frontend/src/i18n/locales/en/translation.json | 18 +- frontend/src/i18n/locales/es/translation.json | 18 +- frontend/src/i18n/locales/fr/translation.json | 18 +- frontend/src/i18n/locales/hi/translation.json | 18 +- frontend/src/i18n/locales/it/translation.json | 18 +- frontend/src/i18n/locales/ja/translation.json | 18 +- frontend/src/i18n/locales/ko/translation.json | 18 +- frontend/src/i18n/locales/pt/translation.json | 18 +- frontend/src/i18n/locales/ta/translation.json | 18 +- .../src/i18n/locales/zh-tw/translation.json | 18 +- frontend/src/i18n/locales/zh/translation.json | 18 +- 15 files changed, 383 insertions(+), 327 deletions(-) create mode 100644 frontend/src/components/App/Settings/ClusterNameEditor.tsx diff --git a/frontend/src/components/App/Settings/ClusterNameEditor.tsx b/frontend/src/components/App/Settings/ClusterNameEditor.tsx new file mode 100644 index 00000000000..45302bf9160 --- /dev/null +++ b/frontend/src/components/App/Settings/ClusterNameEditor.tsx @@ -0,0 +1,255 @@ +/* + * Copyright 2025 The Kubernetes Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Box, TextField, Typography } from '@mui/material'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useDispatch } from 'react-redux'; +import { useHistory } from 'react-router-dom'; +import { ClusterSettings } from '../../../helpers/clusterSettings'; +import { parseKubeConfig, renameCluster } from '../../../lib/k8s/api/v1/clusterApi'; +import { Cluster } from '../../../lib/k8s/cluster'; +import { setConfig, setStatelessConfig } from '../../../redux/configSlice'; +import { findKubeconfigByClusterName } from '../../../stateless/findKubeconfigByClusterName'; +import { updateStatelessClusterKubeconfig } from '../../../stateless/updateStatelessClusterKubeconfig'; +import { ConfirmButton, ConfirmDialog, NameValueTable } from '../../common'; +import { isValidClusterNameFormat } from './util'; + +interface ClusterNameEditorProps { + cluster: string; + clusterConf: { + [clusterName: string]: Cluster; + } | null; + clusterSettings: ClusterSettings | null; + setClusterSettings: React.Dispatch>; +} + +export function ClusterNameEditor({ + cluster, + clusterConf, + clusterSettings, + setClusterSettings, +}: ClusterNameEditorProps) { + const { t } = useTranslation(['translation']); + const [customNameInUse, setCustomNameInUse] = React.useState(false); + const [clusterErrorDialogOpen, setClusterErrorDialogOpen] = React.useState(false); + const [clusterErrorDialogMessage, setClusterErrorDialogMessage] = React.useState(''); + const [newClusterName, setNewClusterName] = React.useState(cluster || ''); + + const dispatch = useDispatch(); + const history = useHistory(); + + React.useEffect(() => { + if (clusterSettings?.currentName !== cluster) { + setNewClusterName(clusterSettings?.currentName || ''); + } + }, [cluster, clusterSettings]); + + const clusterInfo = (clusterConf && clusterConf[cluster || '']) || null; + const source = clusterInfo?.meta_data?.source; + const originalName = clusterInfo?.meta_data?.originalName; + const displayName = originalName || (clusterInfo ? clusterInfo.name : ''); + + /** Note: display original name is currently only supported for non dynamic clusters from kubeconfig sources. */ + const clusterID = clusterInfo?.meta_data?.clusterID || ''; + + const invalidClusterNameMessage = t( + "translation|Cluster name must contain only lowercase alphanumeric characters or '-', and must start and end with an alphanumeric character." + ); + + /** + * This function is part of a double check, this is meant to check all the cluster names currently in use as display names + * Note: if the metadata is not available or does not load, another check is done in the backend to ensure the name is unique in its own config + * + * @param name The name to check. + * @returns bool of if the name is in use. + */ + function checkNameInUse(name: string) { + if (!clusterConf) { + return false; + } + /** These are the display names of the clusters, renamed clusters have their display name as the custom name */ + const clusterNames = Object.values(clusterConf).map(cluster => cluster.name); + + /** The original name of the cluster is the name used in the kubeconfig file. */ + const originalNames = Object.values(clusterConf) + .map(cluster => cluster.meta_data?.originalName) + .filter(originalName => originalName !== undefined); + + const allNames = [...clusterNames, ...originalNames]; + + const nameInUse = allNames.includes(name); + + setCustomNameInUse(nameInUse); + } + + function ClusterErrorDialog() { + return ( + { + setClusterErrorDialogOpen(false); + }} + handleClose={() => { + setClusterErrorDialogOpen(false); + }} + hideCancelButton + open={clusterErrorDialogOpen} + title={t('translation|Error')} + description={clusterErrorDialogMessage} + confirmLabel={t('translation|Okay')} + > + ); + } + // Display the original name of the cluster if it was loaded from a kubeconfig file. + function ClusterName() { + const currentName = clusterInfo?.name; + const originalName = clusterInfo?.meta_data?.originalName; + const source = clusterInfo?.meta_data?.source; + // Note: display original name is currently only supported for non dynamic clusters from kubeconfig sources. + const displayOriginalName = source === 'kubeconfig' && originalName; + + return ( + <> + {clusterErrorDialogOpen && } + {t('translation|Name')} + {displayOriginalName && currentName !== displayOriginalName && ( + + {t('translation|Original name: {{ displayName }}', { + displayName: displayName, + })} + + )} + + ); + } + + function storeNewClusterName(name: string) { + let actualName = name; + if (name === cluster) { + actualName = ''; + setNewClusterName(actualName); + } + + setClusterSettings((settings: ClusterSettings | null) => { + const newSettings = { ...(settings || {}) }; + if (isValidClusterNameFormat(name)) { + newSettings.currentName = actualName; + } + return newSettings; + }); + } + + const handleUpdateClusterName = (source: string) => { + try { + renameCluster(cluster || '', newClusterName, source, clusterID) + .then(async config => { + if (cluster) { + const kubeconfig = await findKubeconfigByClusterName(cluster, clusterID); + if (kubeconfig !== null) { + await updateStatelessClusterKubeconfig(kubeconfig, newClusterName, cluster); + // Make another request for updated kubeconfig + const updatedKubeconfig = await findKubeconfigByClusterName(cluster, clusterID); + if (updatedKubeconfig !== null) { + parseKubeConfig({ kubeconfig: updatedKubeconfig }) + .then((config: any) => { + storeNewClusterName(newClusterName); + dispatch(setStatelessConfig(config)); + }) + .catch((err: Error) => { + console.error('Error updating cluster name:', err.message); + }); + } + } else { + dispatch(setConfig(config)); + } + } + history.push('/'); + window.location.reload(); + }) + .catch((err: Error) => { + console.error('Error updating cluster name:', err.message); + setClusterErrorDialogMessage(err.message); + setClusterErrorDialogOpen(true); + }); + } catch (error) { + console.error('Error updating cluster name:', error); + } + }; + const isValidCurrentName = isValidClusterNameFormat(newClusterName); + + return ( + , + value: ( + { + let value = event.target.value; + value = value.replace(' ', ''); + setNewClusterName(value); + checkNameInUse(value); + }} + value={newClusterName} + placeholder={cluster} + error={!isValidCurrentName || customNameInUse} + helperText={ + + {!isValidCurrentName && invalidClusterNameMessage} + {customNameInUse && + t( + 'translation|This custom name is already in use, please choose a different name.' + )} + {isValidCurrentName && + !customNameInUse && + t('translation|The current name of the cluster. You can define a custom name')} + + } + InputProps={{ + endAdornment: ( + + { + if (isValidCurrentName) { + handleUpdateClusterName(source); + } + }} + confirmTitle={t('translation|Change name')} + confirmDescription={t( + 'translation|Are you sure you want to change the name for "{{ clusterName }}"?', + { clusterName: displayName } + )} + disabled={!newClusterName || !isValidCurrentName || customNameInUse} + > + {t('translation|Apply')} + + + ), + onKeyPress: event => { + if (event.key === 'Enter' && isValidCurrentName) { + handleUpdateClusterName(source); + } + }, + autoComplete: 'off', + sx: { maxWidth: 250 }, + }} + /> + ), + }, + ]} + /> + ); +} diff --git a/frontend/src/components/App/Settings/SettingsCluster.tsx b/frontend/src/components/App/Settings/SettingsCluster.tsx index 207c7611d6a..538bb8e5747 100644 --- a/frontend/src/components/App/Settings/SettingsCluster.tsx +++ b/frontend/src/components/App/Settings/SettingsCluster.tsx @@ -32,33 +32,19 @@ import { } from '../../../helpers/clusterSettings'; import { isElectron } from '../../../helpers/isElectron'; import { useCluster, useClustersConf } from '../../../lib/k8s'; -import { deleteCluster, parseKubeConfig, renameCluster } from '../../../lib/k8s/api/v1/clusterApi'; -import { setConfig, setStatelessConfig } from '../../../redux/configSlice'; -import { findKubeconfigByClusterName } from '../../../stateless/findKubeconfigByClusterName'; -import { updateStatelessClusterKubeconfig } from '../../../stateless/updateStatelessClusterKubeconfig'; +import { deleteCluster } from '../../../lib/k8s/api/v1/clusterApi'; +import { setConfig } from '../../../redux/configSlice'; import ConfirmButton from '../../common/ConfirmButton'; -import ConfirmDialog from '../../common/ConfirmDialog'; import Empty from '../../common/EmptyContent'; import Link from '../../common/Link'; import Loader from '../../common/Loader'; import NameValueTable from '../../common/NameValueTable'; import SectionBox from '../../common/SectionBox'; +import { ClusterNameEditor } from './ClusterNameEditor'; import ClusterSelector from './ClusterSelector'; import NodeShellSettings from './NodeShellSettings'; import { isValidNamespaceFormat } from './util'; -function isValidClusterNameFormat(name: string) { - // We allow empty isValidClusterNameFormat just because that's the default value in our case. - if (!name) { - return true; - } - - // Validates that the namespace is a valid DNS-1123 label and returns a boolean. - // https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-label-names - const regex = new RegExp('^[a-z0-9]([-a-z0-9]*[a-z0-9])?$'); - return regex.test(name); -} - export default function SettingsCluster() { const clusterConf = useClustersConf(); const clusters = Object.values(clusterConf || {}).map(cluster => cluster.name); @@ -69,10 +55,6 @@ export default function SettingsCluster() { const [clusterSettings, setClusterSettings] = React.useState(null); const [cluster, setCluster] = React.useState(useCluster() || ''); const clusterFromURLRef = React.useRef(''); - const [newClusterName, setNewClusterName] = React.useState(cluster || ''); - const [clusterErrorDialogOpen, setClusterErrorDialogOpen] = React.useState(false); - const [clusterErrorDialogMessage, setClusterErrorDialogMessage] = React.useState(''); - const [customNameInUse, setCustomNameInUse] = React.useState(false); const theme = useTheme(); @@ -80,77 +62,6 @@ export default function SettingsCluster() { const dispatch = useDispatch(); const location = useLocation(); - const clusterInfo = (clusterConf && clusterConf[cluster || '']) || null; - const originalName = clusterInfo?.meta_data?.originalName; - const displayName = originalName || (clusterInfo ? clusterInfo.name : ''); - const source = clusterInfo?.meta_data?.source; - /** Note: display original name is currently only supported for non dynamic clusters from kubeconfig sources. */ - const clusterID = clusterInfo?.meta_data?.clusterID || ''; - - /** - * This function is part of a double check, this is meant to check all the cluster names currently in use as display names - * Note: if the metadata is not available or does not load, another check is done in the backend to ensure the name is unique in its own config - * - * @param name The name to check. - * @returns bool of if the name is in use. - */ - function checkNameInUse(name: string) { - if (!clusterConf) { - return false; - } - - /** These are the display names of the clusters, renamed clusters have their display name as the custom name */ - const clusterNames = Object.values(clusterConf).map(cluster => cluster.name); - - /** The original name of the cluster is the name used in the kubeconfig file. */ - const originalNames = Object.values(clusterConf) - .map(cluster => cluster.meta_data?.originalName) - .filter(originalName => originalName !== undefined); - - const allNames = [...clusterNames, ...originalNames]; - - const nameInUse = allNames.includes(name); - - setCustomNameInUse(nameInUse); - } - - const handleUpdateClusterName = (source: string) => { - try { - renameCluster(cluster || '', newClusterName, source, clusterID) - .then(async config => { - if (cluster) { - const kubeconfig = await findKubeconfigByClusterName(cluster, clusterID); - if (kubeconfig !== null) { - await updateStatelessClusterKubeconfig(kubeconfig, newClusterName, cluster); - // Make another request for updated kubeconfig - const updatedKubeconfig = await findKubeconfigByClusterName(cluster, clusterID); - if (updatedKubeconfig !== null) { - parseKubeConfig({ kubeconfig: updatedKubeconfig }) - .then((config: any) => { - storeNewClusterName(newClusterName); - dispatch(setStatelessConfig(config)); - }) - .catch((err: Error) => { - console.error('Error updating cluster name:', err.message); - }); - } - } else { - dispatch(setConfig(config)); - } - } - history.push('/'); - window.location.reload(); - }) - .catch((err: Error) => { - console.error('Error updating cluster name:', err.message); - setClusterErrorDialogMessage(err.message); - setClusterErrorDialogOpen(true); - }); - } catch (error) { - console.error('Error updating cluster name:', error); - } - }; - const removeCluster = () => { deleteCluster(cluster || '') .then(config => { @@ -191,10 +102,6 @@ export default function SettingsCluster() { setUserDefaultNamespace(clusterSettings?.defaultNamespace || ''); } - if (clusterSettings?.currentName !== cluster) { - setNewClusterName(clusterSettings?.currentName || ''); - } - // Avoid re-initializing settings as {} just because the cluster is not yet set. if (clusterSettings !== null) { storeClusterSettings(cluster || '', clusterSettings); @@ -266,33 +173,12 @@ export default function SettingsCluster() { }); } - function storeNewClusterName(name: string) { - let actualName = name; - if (name === cluster) { - actualName = ''; - setNewClusterName(actualName); - } - - setClusterSettings((settings: ClusterSettings | null) => { - const newSettings = { ...(settings || {}) }; - if (isValidClusterNameFormat(name)) { - newSettings.currentName = actualName; - } - return newSettings; - }); - } - const isValidDefaultNamespace = isValidNamespaceFormat(userDefaultNamespace); - const isValidCurrentName = isValidClusterNameFormat(newClusterName); const isValidNewAllowedNamespace = isValidNamespaceFormat(newAllowedNamespace); const invalidNamespaceMessage = t( "translation|Namespaces must contain only lowercase alphanumeric characters or '-', and must start and end with an alphanumeric character." ); - const invalidClusterNameMessage = t( - "translation|Cluster name must contain only lowercase alphanumeric characters or '-', and must start and end with an alphanumeric character." - ); - // If we don't have yet a cluster name from the URL, we are still loading. if (!clusterFromURLRef.current) { return ; @@ -331,47 +217,6 @@ export default function SettingsCluster() { ); } - function ClusterErrorDialog() { - return ( - { - setClusterErrorDialogOpen(false); - }} - handleClose={() => { - setClusterErrorDialogOpen(false); - }} - hideCancelButton - open={clusterErrorDialogOpen} - title={t('translation|Error')} - description={clusterErrorDialogMessage} - confirmLabel={t('translation|Okay')} - > - ); - } - - // Display the original name of the cluster if it was loaded from a kubeconfig file. - function ClusterName() { - const currentName = clusterInfo?.name; - const originalName = clusterInfo?.meta_data?.originalName; - const source = clusterInfo?.meta_data?.source; - // Note: display original name is currently only supported for non dynamic clusters from kubeconfig sources. - const displayOriginalName = source === 'kubeconfig' && originalName; - - return ( - <> - {clusterErrorDialogOpen && } - {t('translation|Name')} - {displayOriginalName && currentName !== displayOriginalName && ( - - {t('translation|Original name: {{ displayName }}', { - displayName: displayName, - })} - - )} - - ); - } - const defaultNamespaceLabelID = 'default-namespace-label'; const allowedNamespaceLabelID = 'allowed-namespace-label'; @@ -389,67 +234,11 @@ export default function SettingsCluster() { {isElectron() && ( - , - value: ( - { - let value = event.target.value; - value = value.replace(' ', ''); - setNewClusterName(value); - checkNameInUse(value); - }} - value={newClusterName} - placeholder={cluster} - error={!isValidCurrentName || customNameInUse} - helperText={ - - {!isValidCurrentName && invalidClusterNameMessage} - {customNameInUse && - t( - 'translation|This custom name is already in use, please choose a different name.' - )} - {isValidCurrentName && - !customNameInUse && - t( - 'translation|The current name of the cluster. You can define a custom name.' - )} - - } - InputProps={{ - endAdornment: ( - - { - if (isValidCurrentName) { - handleUpdateClusterName(source); - } - }} - confirmTitle={t('translation|Change name')} - confirmDescription={t( - 'translation|Are you sure you want to change the name for "{{ clusterName }}"?', - { clusterName: displayName } - )} - disabled={!newClusterName || !isValidCurrentName || customNameInUse} - > - {t('translation|Apply')} - - - ), - onKeyPress: event => { - if (event.key === 'Enter' && isValidCurrentName) { - handleUpdateClusterName(source); - } - }, - autoComplete: 'off', - sx: { maxWidth: 250 }, - }} - /> - ), - }, - ]} + )} Date: Sat, 4 Oct 2025 23:52:52 +0500 Subject: [PATCH 51/72] frontend: Settings: Add stories for rename cluster component. Signed-off-by: Faakhir30 --- .../Settings/ClusterNameEditor.stories.tsx | 78 +++++++++++++++++++ ...lusterNameEditor.Default.stories.storyshot | 62 +++++++++++++++ ...meEditor.WithInvalidName.stories.storyshot | 62 +++++++++++++++ ...erNameEditor.WithNewName.stories.storyshot | 64 +++++++++++++++ 4 files changed, 266 insertions(+) create mode 100644 frontend/src/components/App/Settings/ClusterNameEditor.stories.tsx create mode 100644 frontend/src/components/App/Settings/__snapshots__/ClusterNameEditor.Default.stories.storyshot create mode 100644 frontend/src/components/App/Settings/__snapshots__/ClusterNameEditor.WithInvalidName.stories.storyshot create mode 100644 frontend/src/components/App/Settings/__snapshots__/ClusterNameEditor.WithNewName.stories.storyshot diff --git a/frontend/src/components/App/Settings/ClusterNameEditor.stories.tsx b/frontend/src/components/App/Settings/ClusterNameEditor.stories.tsx new file mode 100644 index 00000000000..3befad451c6 --- /dev/null +++ b/frontend/src/components/App/Settings/ClusterNameEditor.stories.tsx @@ -0,0 +1,78 @@ +/* + * Copyright 2025 The Kubernetes Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { configureStore } from '@reduxjs/toolkit'; +import { Meta, StoryFn } from '@storybook/react'; +import { Provider } from 'react-redux'; +import { ClusterNameEditor } from './ClusterNameEditor'; + +const getMockState = () => ({ + plugins: { loaded: true }, + theme: { + name: 'light', + logo: null, + palette: { + navbar: { + background: '#fff', + }, + }, + }, +}); + +const meta: Meta = { + title: 'Settings/ClusterNameEditor', + component: ClusterNameEditor, + parameters: { + layout: 'centered', + }, +}; + +export default meta; +const Template: StoryFn = args => { + const store = configureStore({ + reducer: (state = getMockState()) => state, + preloadedState: getMockState(), + }); + + return ( + + + + ); +}; + +export const Default = Template.bind({}); +Default.args = { + cluster: 'my-cluster', + clusterSettings: null, + setClusterSettings: () => {}, +}; + +export const WithInvalidName = Template.bind({}); +WithInvalidName.args = { + ...Default.args, + clusterSettings: { + currentName: 'Invalid Cluster Name', + }, +}; + +export const WithNewName = Template.bind({}); +WithNewName.args = { + ...Default.args, + clusterSettings: { + currentName: 'new-cluster-name', + }, +}; diff --git a/frontend/src/components/App/Settings/__snapshots__/ClusterNameEditor.Default.stories.storyshot b/frontend/src/components/App/Settings/__snapshots__/ClusterNameEditor.Default.stories.storyshot new file mode 100644 index 00000000000..3d35963d9f4 --- /dev/null +++ b/frontend/src/components/App/Settings/__snapshots__/ClusterNameEditor.Default.stories.storyshot @@ -0,0 +1,62 @@ + +
+
+
+

+ Name +

+
+
+
+
+ +
+ +
+
+
+

+

+ The current name of the cluster. You can define a custom name +

+

+
+
+
+
+ \ No newline at end of file diff --git a/frontend/src/components/App/Settings/__snapshots__/ClusterNameEditor.WithInvalidName.stories.storyshot b/frontend/src/components/App/Settings/__snapshots__/ClusterNameEditor.WithInvalidName.stories.storyshot new file mode 100644 index 00000000000..2625f4c2e52 --- /dev/null +++ b/frontend/src/components/App/Settings/__snapshots__/ClusterNameEditor.WithInvalidName.stories.storyshot @@ -0,0 +1,62 @@ + +
+
+
+

+ Name +

+
+
+
+
+ +
+ +
+
+
+

+

+ Cluster name must contain only lowercase alphanumeric characters or '-', and must start and end with an alphanumeric character. +

+

+
+
+
+
+ \ No newline at end of file diff --git a/frontend/src/components/App/Settings/__snapshots__/ClusterNameEditor.WithNewName.stories.storyshot b/frontend/src/components/App/Settings/__snapshots__/ClusterNameEditor.WithNewName.stories.storyshot new file mode 100644 index 00000000000..6bbba097c2a --- /dev/null +++ b/frontend/src/components/App/Settings/__snapshots__/ClusterNameEditor.WithNewName.stories.storyshot @@ -0,0 +1,64 @@ + +
+
+
+

+ Name +

+
+
+
+
+ +
+ +
+
+
+

+

+ The current name of the cluster. You can define a custom name +

+

+
+
+
+
+ \ No newline at end of file From 08f4d4696c58414786213c402b4880be98681efe Mon Sep 17 00:00:00 2001 From: Faakhir30 Date: Sat, 4 Oct 2025 23:53:12 +0500 Subject: [PATCH 52/72] app: tests: Add e2e test for cluster renaming. Signed-off-by: Faakhir30 --- app/e2e-tests/tests/clusterRename.spec.ts | 109 ++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 app/e2e-tests/tests/clusterRename.spec.ts diff --git a/app/e2e-tests/tests/clusterRename.spec.ts b/app/e2e-tests/tests/clusterRename.spec.ts new file mode 100644 index 00000000000..f4f442d9206 --- /dev/null +++ b/app/e2e-tests/tests/clusterRename.spec.ts @@ -0,0 +1,109 @@ +/* + * Copyright 2025 The Kubernetes Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect, test } from '@playwright/test'; +import path from 'path'; +import { _electron, Page } from 'playwright'; +import { HeadlampPage } from './headlampPage'; + +// Electron setup +const electronExecutable = process.platform === 'win32' ? 'electron.cmd' : 'electron'; +const electronPath = path.resolve(__dirname, `../../node_modules/.bin/${electronExecutable}`); +const appPath = path.resolve(__dirname, '../../'); +let electronApp; +let electronPage: Page; + +// Test configuration +const TEST_CONFIG = { + originalName: 'minikube', + newName: 'test-cluster', + cancelledName: 'cancelled-cluster', + invalidName: 'Invalid Cluster!', +}; + +// Helper functions +async function navigateToSettings(page: Page) { + await page.waitForLoadState('load'); + await page.getByRole('button', { name: 'Settings' }).click(); + await page.waitForLoadState('load'); +} + +async function verifyClusterName(page: Page, expectedName: string) { + await page.getByRole('button', { name: 'Settings' }).click(); + await page.locator('a[href="#/settings/cluster"]').click(); + // Check the cluster name in the cluster selector combobox + await expect(page.locator(`input[placeholder="${expectedName}"]`)).toBeVisible(); +} + +async function renameCluster( + page: Page, + fromName: string, + toName: string, + confirm: boolean = true +) { + await page.fill(`input[placeholder="${fromName}"]`, toName); + await page.getByRole('button', { name: 'Apply' }).click(); + await page.getByRole('button', { name: confirm ? 'Yes' : 'No' }).click(); + await page.waitForLoadState('load'); + await page.locator(`a[href="#/c/${toName}/"]`).click(); +} + +// Setup +test.beforeAll(async () => { + electronApp = await _electron.launch({ + cwd: appPath, + executablePath: electronPath, + args: ['.'], + env: { + ...process.env, + NODE_ENV: 'development', + ELECTRON_DEV: 'true', + }, + }); + + electronPage = await electronApp.firstWindow(); +}); + +test.beforeEach(async ({ page }) => { + page.close(); +}); + +// Tests +test.describe('Cluster rename functionality', () => { + test.beforeEach(() => { + test.skip(process.env.PLAYWRIGHT_TEST_MODE !== 'app', 'These tests only run in app mode'); + }); + + test('should rename cluster and verify changes', async ({ page: browserPage }) => { + const page = process.env.PLAYWRIGHT_TEST_MODE === 'app' ? electronPage : browserPage; + const headlampPage = new HeadlampPage(page); + await headlampPage.authenticate(); + + await navigateToSettings(page); + await expect(page.locator('h2')).toContainText('Cluster Settings'); + + // Test invalid inputs + await page.fill('input[placeholder="minikube"]', TEST_CONFIG.invalidName); + await expect(page.getByRole('button', { name: 'Apply' })).toBeDisabled(); + + await page.fill('input[placeholder="minikube"]', ''); + await expect(page.getByRole('button', { name: 'Apply' })).toBeDisabled(); + + // Test successful rename + await renameCluster(page, TEST_CONFIG.originalName, TEST_CONFIG.newName); + await verifyClusterName(page, TEST_CONFIG.newName); + }); +}); From b0e6b422932f26f67e86d2c28f10116dbb3b1416 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Dudfield?= Date: Mon, 27 Oct 2025 11:01:26 -0300 Subject: [PATCH 53/72] app: app-build-manifest: Bump app-catalog to 0.7.0 and plugin-catalog to 0.4.2 --- app/app-build-manifest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/app-build-manifest.json b/app/app-build-manifest.json index bbb87ce0e42..5d80194b27e 100644 --- a/app/app-build-manifest.json +++ b/app/app-build-manifest.json @@ -3,11 +3,11 @@ "plugins": [ { "name": "app-catalog", - "archive": "https://github.com/headlamp-k8s/plugins/releases/download/app-catalog-0.6.3/app-catalog-0.6.3.tar.gz" + "archive": "https://github.com/headlamp-k8s/plugins/releases/download/app-catalog-0.7.0/app-catalog-0.7.0.tar.gz" }, { "name": "plugin-catalog", - "archive": "https://github.com/headlamp-k8s/plugins/releases/download/plugin-catalog-0.4.1/headlamp-k8s-plugin-catalog-0.4.1.tar.gz" + "archive": "https://github.com/headlamp-k8s/plugins/releases/download/plugin-catalog-0.4.2/headlamp-k8s-plugin-catalog-0.4.2.tar.gz" }, { "name": "prometheus", From 260c739e993ab34d07606eda8b0bd38725c73b20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Dudfield?= Date: Tue, 28 Oct 2025 09:58:19 -0300 Subject: [PATCH 54/72] frontend: KubeObject: ScaleButton: Add isScalable attribute KubeObject classes that have this object as true will make the ScaleButton show up for them. --- frontend/src/components/common/Resource/ScaleButton.tsx | 4 ++-- frontend/src/components/project/ProjectResourcesTab.tsx | 6 +++--- frontend/src/lib/k8s/KubeObject.ts | 9 ++++++++- frontend/src/lib/k8s/deployment.ts | 1 + frontend/src/lib/k8s/replicaSet.ts | 1 + frontend/src/lib/k8s/statefulSet.ts | 1 + 6 files changed, 16 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/common/Resource/ScaleButton.tsx b/frontend/src/components/common/Resource/ScaleButton.tsx index f81b55e2743..ffe594a4392 100644 --- a/frontend/src/components/common/Resource/ScaleButton.tsx +++ b/frontend/src/components/common/Resource/ScaleButton.tsx @@ -44,7 +44,7 @@ import { LightTooltip } from '../Tooltip'; import AuthVisible from './AuthVisible'; interface ScaleButtonProps { - item: Deployment | StatefulSet | ReplicaSet; + item: (Deployment | StatefulSet | ReplicaSet) & { _class?: () => { isScalable?: boolean } }; buttonStyle?: ButtonStyle; options?: CallbackActionOptions; } @@ -91,7 +91,7 @@ export default function ScaleButton(props: ScaleButtonProps) { setOpenDialog(false); } - if (!item || !['Deployment', 'StatefulSet', 'ReplicaSet'].includes(item.kind)) { + if (!item || !item?.isScalable) { return null; } diff --git a/frontend/src/components/project/ProjectResourcesTab.tsx b/frontend/src/components/project/ProjectResourcesTab.tsx index 9a46cc36797..c6d4ca5bdf4 100644 --- a/frontend/src/components/project/ProjectResourcesTab.tsx +++ b/frontend/src/components/project/ProjectResourcesTab.tsx @@ -234,7 +234,7 @@ export function ProjectResourcesTab({ gridTemplate: 'min-content', accessorFn: resource => { const kind = resource.kind; - if (['Deployment', 'StatefulSet', 'ReplicaSet'].includes(kind)) { + if (resource.isScalable) { const res = resource as Deployment | StatefulSet | ReplicaSet; return `Replicas: ${res.status?.readyReplicas || res.status?.availableReplicas || 0}/${ res.spec?.replicas || 0 @@ -256,7 +256,7 @@ export function ProjectResourcesTab({ Cell: ({ row }) => { const resource = row.original; const kind = resource.kind; - if (['Deployment', 'StatefulSet', 'ReplicaSet'].includes(kind)) { + if (resource.isScalable) { const res = resource as Deployment | StatefulSet | ReplicaSet; return ( @@ -315,7 +315,7 @@ export function ProjectResourcesTab({ Cell: ({ row }) => { const resource = row.original; const kind = resource.kind; - const isScalable = ['Deployment', 'StatefulSet', 'ReplicaSet'].includes(kind); + const isScalable = resource.isScalable; const isPod = kind === 'Pod'; return ( diff --git a/frontend/src/lib/k8s/KubeObject.ts b/frontend/src/lib/k8s/KubeObject.ts index 04f2b1a5c8d..8b5dcf5b994 100644 --- a/frontend/src/lib/k8s/KubeObject.ts +++ b/frontend/src/lib/k8s/KubeObject.ts @@ -61,6 +61,9 @@ export class KubeObject { /** Whether the object is namespaced. */ static readonly isNamespaced: boolean; + /** Whether the object is scalable, and should have a ScaleButton */ + static readonly isScalable: boolean; + static _internalApiEndpoint?: ReturnType; static get apiEndpoint() { @@ -72,7 +75,7 @@ export class KubeObject { // Create factory arguments per API version, usually just one const factoryArgumentsArray = versions.map(apiVersion => { const [group, version] = apiVersion.includes('/') ? apiVersion.split('/') : ['', apiVersion]; - const includeScaleApi = ['Deployment', 'ReplicaSet', 'StatefulSet'].includes(this.kind); + const includeScaleApi = this.isScalable; return [group, version, this.apiName, includeScaleApi]; }); @@ -188,6 +191,10 @@ export class KubeObject { return this._class().isNamespaced; } + get isScalable() { + return this._class().isScalable; + } + getEditableObject() { const fieldsToRemove = this._class().readOnlyFields; const code = this.jsonData ? cloneDeep(this.jsonData) : {}; diff --git a/frontend/src/lib/k8s/deployment.ts b/frontend/src/lib/k8s/deployment.ts index 8f3c060f4fe..efdda884e2e 100644 --- a/frontend/src/lib/k8s/deployment.ts +++ b/frontend/src/lib/k8s/deployment.ts @@ -43,6 +43,7 @@ class Deployment extends KubeObject { static apiName = 'deployments'; static apiVersion = 'apps/v1'; static isNamespaced = true; + static isScalable = true; get spec() { return this.getValue('spec'); diff --git a/frontend/src/lib/k8s/replicaSet.ts b/frontend/src/lib/k8s/replicaSet.ts index 266b46088fc..cecb5ad5879 100644 --- a/frontend/src/lib/k8s/replicaSet.ts +++ b/frontend/src/lib/k8s/replicaSet.ts @@ -46,6 +46,7 @@ class ReplicaSet extends KubeObject { static apiName = 'replicasets'; static apiVersion = 'apps/v1'; static isNamespaced = true; + static isScalable = true; get spec(): KubeReplicaSet['spec'] { return this.jsonData.spec; diff --git a/frontend/src/lib/k8s/statefulSet.ts b/frontend/src/lib/k8s/statefulSet.ts index 6e5b89eef7c..988f56fce6a 100644 --- a/frontend/src/lib/k8s/statefulSet.ts +++ b/frontend/src/lib/k8s/statefulSet.ts @@ -45,6 +45,7 @@ class StatefulSet extends KubeObject { static apiName = 'statefulsets'; static apiVersion = 'apps/v1'; static isNamespaced = true; + static isScalable = true; get spec() { return this.jsonData.spec; From 231f1989a255e28af89aa80b9ff9f07e4b8aaffd Mon Sep 17 00:00:00 2001 From: Evangelos Skopelitis Date: Mon, 27 Oct 2025 13:40:06 -0400 Subject: [PATCH 55/72] backend: auth: Fetch user info from header --- backend/pkg/auth/auth.go | 14 ++++++++++-- backend/pkg/auth/auth_test.go | 41 +++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/backend/pkg/auth/auth.go b/backend/pkg/auth/auth.go index 0c449e247de..5a1246e13f6 100644 --- a/backend/pkg/auth/auth.go +++ b/backend/pkg/auth/auth.go @@ -302,8 +302,18 @@ func HandleMe(opts MeHandlerOptions) http.HandlerFunc { return } - token, err := GetTokenFromCookie(r, clusterName) - if err != nil || token == "" { + requestCluster, token := ParseClusterAndToken(r) + + if requestCluster == "" { + requestCluster = clusterName + } + + if requestCluster != clusterName { + writeMeJSON(w, http.StatusBadRequest, map[string]interface{}{"message": "cluster mismatch"}) + return + } + + if token == "" { writeMeJSON(w, http.StatusUnauthorized, map[string]interface{}{"message": "unauthorized"}) return } diff --git a/backend/pkg/auth/auth_test.go b/backend/pkg/auth/auth_test.go index e5742d01097..f165863e05b 100644 --- a/backend/pkg/auth/auth_test.go +++ b/backend/pkg/auth/auth_test.go @@ -917,6 +917,47 @@ func TestHandleMe_Success(t *testing.T) { assert.Equal(t, "Cookie", rr.Header().Get("Vary")) } +func TestHandleMe_HeaderToken(t *testing.T) { + t.Parallel() + + expiry := time.Now().Add(time.Hour).Unix() + claims := map[string]interface{}{ + "preferred_username": "alice", + "email": "alice@example.com", + "groups": []string{"dev", "ops"}, + "exp": float64(expiry), + } + + token := makeTestToken(t, claims) + + req := httptest.NewRequest(http.MethodGet, "/clusters/test/me", nil) + req = mux.SetURLVars(req, map[string]string{"clusterName": "test"}) + req.Header.Set("Authorization", "Bearer "+token) + + rr := httptest.NewRecorder() + + handler := auth.HandleMe(auth.MeHandlerOptions{ + UsernamePaths: "preferred_username", + EmailPaths: "email", + GroupsPaths: "groups", + }) + + handler(rr, req) + + require.Equal(t, http.StatusOK, rr.Code) + + var got struct { + Username string `json:"username"` + Email string `json:"email"` + Groups []string `json:"groups"` + } + + require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &got)) + assert.Equal(t, "alice", got.Username) + assert.Equal(t, "alice@example.com", got.Email) + assert.Equal(t, []string{"dev", "ops"}, got.Groups) +} + func TestHandleMe_ExpiredToken(t *testing.T) { t.Parallel() From ff3dd587affed02692088d8470e5c5673cd66df3 Mon Sep 17 00:00:00 2001 From: Joaquim Rocha Date: Wed, 29 Oct 2025 13:40:43 +0000 Subject: [PATCH 56/72] frontend: Improve empty state for the cluster list view --- .../src/components/App/Home/ClusterTable.tsx | 40 ++++++++++++++++++- frontend/src/i18n/locales/de/translation.json | 2 + frontend/src/i18n/locales/en/translation.json | 2 + frontend/src/i18n/locales/es/translation.json | 2 + frontend/src/i18n/locales/fr/translation.json | 2 + frontend/src/i18n/locales/hi/translation.json | 2 + frontend/src/i18n/locales/it/translation.json | 2 + frontend/src/i18n/locales/ja/translation.json | 2 + frontend/src/i18n/locales/ko/translation.json | 2 + frontend/src/i18n/locales/pt/translation.json | 2 + frontend/src/i18n/locales/ta/translation.json | 2 + .../src/i18n/locales/zh-tw/translation.json | 2 + frontend/src/i18n/locales/zh/translation.json | 2 + 13 files changed, 63 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/App/Home/ClusterTable.tsx b/frontend/src/components/App/Home/ClusterTable.tsx index 20ed93d73a1..52e7436a7b4 100644 --- a/frontend/src/components/App/Home/ClusterTable.tsx +++ b/frontend/src/components/App/Home/ClusterTable.tsx @@ -22,10 +22,12 @@ import Typography from '@mui/material/Typography'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { generatePath, useHistory } from 'react-router-dom'; +import { isElectron } from '../../../helpers/isElectron'; import { formatClusterPathParam } from '../../../lib/cluster'; import { useClustersConf, useClustersVersion } from '../../../lib/k8s'; import { ApiError } from '../../../lib/k8s/api/v2/ApiError'; import { Cluster } from '../../../lib/k8s/cluster'; +import { createRouteURL } from '../../../lib/router/createRouteURL'; import { getClusterPrefixedPath } from '../../../lib/util'; import { useTypedSelector } from '../../../redux/hooks'; import { Loader } from '../../common'; @@ -145,6 +147,42 @@ export default function ClusterTable({ return ; } + const clustersList = Object.values(customNameClusters); + if (clustersList.length === 0) { + return ( + + + + {t('No clusters found')} + + + {t('Add a cluster to get started.')} + + {isElectron() && ( + + )} + + ); + } + return ( { diff --git a/frontend/src/i18n/locales/de/translation.json b/frontend/src/i18n/locales/de/translation.json index 06f2025179f..93b350d8ea3 100644 --- a/frontend/src/i18n/locales/de/translation.json +++ b/frontend/src/i18n/locales/de/translation.json @@ -61,6 +61,8 @@ "In-cluster": "", "View Clusters": "", "Loading...": "", + "No clusters found": "", + "Add a cluster to get started.": "", "Origin": "", "Status": "Status", "Warnings": "", diff --git a/frontend/src/i18n/locales/en/translation.json b/frontend/src/i18n/locales/en/translation.json index 726ebde53a0..54eef6df29c 100644 --- a/frontend/src/i18n/locales/en/translation.json +++ b/frontend/src/i18n/locales/en/translation.json @@ -61,6 +61,8 @@ "In-cluster": "In-cluster", "View Clusters": "View Clusters", "Loading...": "Loading...", + "No clusters found": "No clusters found", + "Add a cluster to get started.": "Add a cluster to get started.", "Origin": "Origin", "Status": "Status", "Warnings": "Warnings", diff --git a/frontend/src/i18n/locales/es/translation.json b/frontend/src/i18n/locales/es/translation.json index c8c8641c939..806fd9f7eff 100644 --- a/frontend/src/i18n/locales/es/translation.json +++ b/frontend/src/i18n/locales/es/translation.json @@ -61,6 +61,8 @@ "In-cluster": "En-cluster", "View Clusters": "", "Loading...": "", + "No clusters found": "", + "Add a cluster to get started.": "", "Origin": "Origen", "Status": "Estado", "Warnings": "Avisos", diff --git a/frontend/src/i18n/locales/fr/translation.json b/frontend/src/i18n/locales/fr/translation.json index 800e0f36aa5..0aa679ef591 100644 --- a/frontend/src/i18n/locales/fr/translation.json +++ b/frontend/src/i18n/locales/fr/translation.json @@ -61,6 +61,8 @@ "In-cluster": "", "View Clusters": "", "Loading...": "", + "No clusters found": "", + "Add a cluster to get started.": "", "Origin": "", "Status": "Statut", "Warnings": "", diff --git a/frontend/src/i18n/locales/hi/translation.json b/frontend/src/i18n/locales/hi/translation.json index e38494b2416..390ac025dfe 100644 --- a/frontend/src/i18n/locales/hi/translation.json +++ b/frontend/src/i18n/locales/hi/translation.json @@ -61,6 +61,8 @@ "In-cluster": "क्लस्टर में", "View Clusters": "क्लस्टर देखें", "Loading...": "", + "No clusters found": "", + "Add a cluster to get started.": "", "Origin": "उत्पत्ति", "Status": "स्थिति", "Warnings": "चेतावनियाँ", diff --git a/frontend/src/i18n/locales/it/translation.json b/frontend/src/i18n/locales/it/translation.json index c08cdf3e4b1..5d92af2b0c9 100644 --- a/frontend/src/i18n/locales/it/translation.json +++ b/frontend/src/i18n/locales/it/translation.json @@ -61,6 +61,8 @@ "In-cluster": "In-cluster", "View Clusters": "", "Loading...": "", + "No clusters found": "", + "Add a cluster to get started.": "", "Origin": "Origine", "Status": "Stato", "Warnings": "Avvertenze", diff --git a/frontend/src/i18n/locales/ja/translation.json b/frontend/src/i18n/locales/ja/translation.json index 3a7f2d55de0..b493d11915c 100644 --- a/frontend/src/i18n/locales/ja/translation.json +++ b/frontend/src/i18n/locales/ja/translation.json @@ -61,6 +61,8 @@ "In-cluster": "クラスター内", "View Clusters": "", "Loading...": "", + "No clusters found": "", + "Add a cluster to get started.": "", "Origin": "元", "Status": "ステータス", "Warnings": "警告", diff --git a/frontend/src/i18n/locales/ko/translation.json b/frontend/src/i18n/locales/ko/translation.json index 63e17aa4116..155f470ecd8 100644 --- a/frontend/src/i18n/locales/ko/translation.json +++ b/frontend/src/i18n/locales/ko/translation.json @@ -61,6 +61,8 @@ "In-cluster": "클러스터 내부", "View Clusters": "클러스터 보기", "Loading...": "", + "No clusters found": "", + "Add a cluster to get started.": "", "Origin": "출처", "Status": "상태", "Warnings": "경고", diff --git a/frontend/src/i18n/locales/pt/translation.json b/frontend/src/i18n/locales/pt/translation.json index ae032fbf8f0..d58727edf78 100644 --- a/frontend/src/i18n/locales/pt/translation.json +++ b/frontend/src/i18n/locales/pt/translation.json @@ -61,6 +61,8 @@ "In-cluster": "No cluster", "View Clusters": "", "Loading...": "", + "No clusters found": "", + "Add a cluster to get started.": "", "Origin": "Origem", "Status": "Estado", "Warnings": "Avisos", diff --git a/frontend/src/i18n/locales/ta/translation.json b/frontend/src/i18n/locales/ta/translation.json index 02f4efcc275..12e1eef37af 100644 --- a/frontend/src/i18n/locales/ta/translation.json +++ b/frontend/src/i18n/locales/ta/translation.json @@ -61,6 +61,8 @@ "In-cluster": "க்ளஸ்டருக்குள்", "View Clusters": "க்ளஸ்டர்களைப் பார்", "Loading...": "", + "No clusters found": "", + "Add a cluster to get started.": "", "Origin": "மூலம்", "Status": "நிலை", "Warnings": "எச்சரிக்கைகள்", diff --git a/frontend/src/i18n/locales/zh-tw/translation.json b/frontend/src/i18n/locales/zh-tw/translation.json index 07591ba8fc4..b2ffd523904 100644 --- a/frontend/src/i18n/locales/zh-tw/translation.json +++ b/frontend/src/i18n/locales/zh-tw/translation.json @@ -61,6 +61,8 @@ "In-cluster": "叢集內", "View Clusters": "", "Loading...": "", + "No clusters found": "", + "Add a cluster to get started.": "", "Origin": "來源", "Status": "狀態", "Warnings": "警告", diff --git a/frontend/src/i18n/locales/zh/translation.json b/frontend/src/i18n/locales/zh/translation.json index 792037f3ef0..876c7ebffe7 100644 --- a/frontend/src/i18n/locales/zh/translation.json +++ b/frontend/src/i18n/locales/zh/translation.json @@ -61,6 +61,8 @@ "In-cluster": "集群內", "View Clusters": "", "Loading...": "", + "No clusters found": "", + "Add a cluster to get started.": "", "Origin": "来源", "Status": "状态", "Warnings": "警告", From ae10b47bfaf1d01e22341f2a4796fa84d09b0782 Mon Sep 17 00:00:00 2001 From: Joaquim Rocha Date: Wed, 29 Oct 2025 16:44:51 +0000 Subject: [PATCH 57/72] app,frontend: Allow open the About dialog from the menu item --- app/electron/main.ts | 10 +++++++--- app/electron/preload.ts | 1 + frontend/src/components/App/AppContainer.tsx | 7 +++++++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/app/electron/main.ts b/app/electron/main.ts index dc9de252d9b..4132b29b9de 100644 --- a/app/electron/main.ts +++ b/app/electron/main.ts @@ -959,8 +959,7 @@ function getDefaultAppMenu(): AppMenu[] { }, { label: i18n.t('About'), - id: 'original-about', - url: 'https://github.com/kubernetes-sigs/headlamp', + id: 'original-about-help', }, ], }, @@ -1054,7 +1053,12 @@ function menusToTemplate(mainWindow: BrowserWindow | null, menusFromPlugins: App return; } - if (!!url) { + // Handle the "About" menu item from the Help menu specially + if (appMenu.id === 'original-about-help') { + menu.click = () => { + mainWindow?.webContents.send('open-about-dialog'); + }; + } else if (!!url) { menu.click = async () => { // Open external links in the external browser. if (!!mainWindow && !url.startsWith('http')) { diff --git a/app/electron/preload.ts b/app/electron/preload.ts index 3ab81137370..c6e7b9545fa 100644 --- a/app/electron/preload.ts +++ b/app/electron/preload.ts @@ -48,6 +48,7 @@ contextBridge.exposeInMainWorld('desktopApi', { 'plugin-manager', 'backend-token', 'plugin-permission-secrets', + 'open-about-dialog', ]; if (validChannels.includes(channel)) { // Deliberately strip event as it includes `sender` diff --git a/frontend/src/components/App/AppContainer.tsx b/frontend/src/components/App/AppContainer.tsx index 914eadd783c..05c26054aaf 100644 --- a/frontend/src/components/App/AppContainer.tsx +++ b/frontend/src/components/App/AppContainer.tsx @@ -22,6 +22,8 @@ import { getBaseUrl } from '../../helpers/getBaseUrl'; import { setBackendToken } from '../../helpers/getHeadlampAPIHeaders'; import { isElectron } from '../../helpers/isElectron'; import Plugins from '../../plugin/Plugins'; +import store from '../../redux/stores/store'; +import { uiSlice } from '../../redux/uiSlice'; import ReleaseNotes from '../common/ReleaseNotes/ReleaseNotes'; import { MonacoEditorLoaderInitializer } from '../monaco/MonacoEditorLoaderInitializer'; import Layout from './Layout'; @@ -32,6 +34,11 @@ window.desktopApi?.receive('backend-token', (token: string) => { setBackendToken(token); }); +// Listen for the open-about-dialog event from the Electron app menu +window.desktopApi?.receive('open-about-dialog', () => { + store.dispatch(uiSlice.actions.setVersionDialogOpen(true)); +}); + /** * Validates if a redirect path is safe to use * @param redirectPath - The path to validate From b9fc006811fc7e4708c610edfcb22268f207960a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Dudfield?= Date: Mon, 27 Oct 2025 18:31:41 -0300 Subject: [PATCH 58/72] charts/headlamp: Fix for pluginctl without global install Because there is no permission to do that now. So we run it without installing it. Also make sure the config and cache folders use somewhere writable. The ConfigMap was rendering with extra newlines. At the top, and after plugin.yml, which was causing an error when validating the configuration inside the plugin manager code. --- charts/headlamp/templates/deployment.yaml | 10 ++++++---- charts/headlamp/templates/plugin-configmap.yaml | 5 ++--- .../tests/expected_templates/security-context.yaml | 10 ++++++---- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/charts/headlamp/templates/deployment.yaml b/charts/headlamp/templates/deployment.yaml index 94042e1ee33..4ac500edf6e 100644 --- a/charts/headlamp/templates/deployment.yaml +++ b/charts/headlamp/templates/deployment.yaml @@ -327,13 +327,15 @@ spec: {{- end }} args: - | - echo "Installing headlamp-plugin globally..." - npm install -g @headlamp-k8s/pluginctl@{{ .Values.pluginsManager.version }} - echo "Installed headlamp-plugin successfully." if [ -f "/config/plugin.yml" ]; then echo "Installing plugins from config..." cat /config/plugin.yml - pluginctl install --config /config/plugin.yml --folderName {{ .Values.config.pluginsDir }} --watch + # Use a writable cache directory + export NPM_CONFIG_CACHE=/tmp/npm-cache + # Use a writable config directory + export NPM_CONFIG_USERCONFIG=/tmp/npm-userconfig + mkdir -p /tmp/npm-cache /tmp/npm-userconfig + npx --yes @headlamp-k8s/pluginctl@{{ .Values.pluginsManager.version }} install --config /config/plugin.yml --folderName {{ .Values.config.pluginsDir }} --watch fi volumeMounts: - name: plugins-dir diff --git a/charts/headlamp/templates/plugin-configmap.yaml b/charts/headlamp/templates/plugin-configmap.yaml index 5d5619fb4d1..43bf007bde7 100644 --- a/charts/headlamp/templates/plugin-configmap.yaml +++ b/charts/headlamp/templates/plugin-configmap.yaml @@ -1,4 +1,4 @@ -{{- if .Values.pluginsManager.enabled }} +{{- if .Values.pluginsManager.enabled -}} apiVersion: v1 kind: ConfigMap metadata: @@ -7,6 +7,5 @@ metadata: labels: {{- include "headlamp.labels" . | nindent 4 }} data: - plugin.yml: | - {{ .Values.pluginsManager.configContent | nindent 4 }} + plugin.yml: |{{ .Values.pluginsManager.configContent | nindent 4 }} {{- end }} diff --git a/charts/headlamp/tests/expected_templates/security-context.yaml b/charts/headlamp/tests/expected_templates/security-context.yaml index 18830ca82c0..b3dae60534d 100644 --- a/charts/headlamp/tests/expected_templates/security-context.yaml +++ b/charts/headlamp/tests/expected_templates/security-context.yaml @@ -159,13 +159,15 @@ spec: command: ["/bin/sh", "-c"] args: - | - echo "Installing headlamp-plugin globally..." - npm install -g @headlamp-k8s/pluginctl@1.0.0 - echo "Installed headlamp-plugin successfully." if [ -f "/config/plugin.yml" ]; then echo "Installing plugins from config..." cat /config/plugin.yml - pluginctl install --config /config/plugin.yml --folderName /headlamp/plugins --watch + # Use a writable cache directory + export NPM_CONFIG_CACHE=/tmp/npm-cache + # Use a writable config directory + export NPM_CONFIG_USERCONFIG=/tmp/npm-userconfig + mkdir -p /tmp/npm-cache /tmp/npm-userconfig + npx --yes @headlamp-k8s/pluginctl@1.0.0 install --config /config/plugin.yml --folderName /headlamp/plugins --watch fi volumeMounts: - name: plugins-dir From 02b9559e8e4bdbf83d48789233ff1d975561b46c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Dudfield?= Date: Thu, 30 Oct 2025 09:10:34 -0300 Subject: [PATCH 59/72] frontend: charts: TopBar: backend/auth: Add me-user-info-url config Which is an oauth2proxy endpoint for getting user info. By default it is turned off. To the charts a config.oidc.meUserInfoURL is added. Make it /oauth2/userinfo if using oauth2proxy default config. Co-authored-by: evangelos <52797224+skoeva@users.noreply.github.com> --- backend/cmd/headlamp.go | 3 + backend/pkg/auth/auth.go | 16 ++- backend/pkg/config/config.go | 16 ++- backend/pkg/config/config_test.go | 2 + charts/headlamp/README.md | 1 + charts/headlamp/templates/deployment.yaml | 21 +++ .../me-user-info-url-directly.yaml | 130 +++++++++++++++++ .../expected_templates/me-user-info-url.yaml | 133 ++++++++++++++++++ .../test_cases/me-user-info-url-directly.yaml | 3 + .../tests/test_cases/me-user-info-url.yaml | 4 + charts/headlamp/values.yaml | 5 + frontend/src/components/App/TopBar.tsx | 32 ++++- 12 files changed, 356 insertions(+), 10 deletions(-) create mode 100644 charts/headlamp/tests/expected_templates/me-user-info-url-directly.yaml create mode 100644 charts/headlamp/tests/expected_templates/me-user-info-url.yaml create mode 100644 charts/headlamp/tests/test_cases/me-user-info-url-directly.yaml create mode 100644 charts/headlamp/tests/test_cases/me-user-info-url.yaml diff --git a/backend/cmd/headlamp.go b/backend/cmd/headlamp.go index 47390061567..69b2f79fa6e 100644 --- a/backend/cmd/headlamp.go +++ b/backend/cmd/headlamp.go @@ -93,6 +93,8 @@ type HeadlampConfig struct { meEmailPaths string // meGroupsPaths lists the JMESPath expressions tried for the groups in /clusters/{cluster}/me. meGroupsPaths string + // MeUserInfoURL is the URL to fetch additional user info for the /me endpoint. /oauth2/userinfo + MeUserInfoURL string } const DrainNodeCacheTTL = 20 // seconds @@ -494,6 +496,7 @@ func createHeadlampHandler(config *HeadlampConfig) http.Handler { UsernamePaths: config.meUsernamePaths, EmailPaths: config.meEmailPaths, GroupsPaths: config.meGroupsPaths, + UserInfoURL: config.MeUserInfoURL, }), ).Methods("GET") diff --git a/backend/pkg/auth/auth.go b/backend/pkg/auth/auth.go index 5a1246e13f6..2691cf9aa29 100644 --- a/backend/pkg/auth/auth.go +++ b/backend/pkg/auth/auth.go @@ -282,14 +282,17 @@ type MeHandlerOptions struct { EmailPaths string // GroupsPaths is a list of JMESPath expressions to resolve group memberships. GroupsPaths string + // UserInfoURL is the URL to fetch additional user info for the /me endpoint. + UserInfoURL string } // HandleMe returns a handler that reads the per-cluster auth cookie and responds with user info. func HandleMe(opts MeHandlerOptions) http.HandlerFunc { - usernamePaths, emailPaths, groupsPaths := cfg.ApplyMeDefaults( + usernamePaths, emailPaths, groupsPaths, userInfoURL := cfg.ApplyMeDefaults( opts.UsernamePaths, opts.EmailPaths, opts.GroupsPaths, + opts.UserInfoURL, ) compiledUsernamePaths := compileJMESPaths(usernamePaths) compiledEmailPaths := compileJMESPaths(emailPaths) @@ -333,7 +336,7 @@ func HandleMe(opts MeHandlerOptions) http.HandlerFunc { email := stringValueFromJMESPaths(claims, compiledEmailPaths) groups := stringSliceFromJMESPaths(claims, compiledGroupsPaths) - writeMeResponse(w, username, email, groups) + writeMeResponse(w, username, email, groups, userInfoURL) } } @@ -353,11 +356,12 @@ func parseClaimsFromToken(token string) (map[string]interface{}, int, string) { } // writeMeResponse serializes the identity payload with the standard cache-busting headers. -func writeMeResponse(w http.ResponseWriter, username, email string, groups []string) { +func writeMeResponse(w http.ResponseWriter, username, email string, groups []string, userInfoURL string) { writeMeJSON(w, http.StatusOK, map[string]interface{}{ - "username": username, - "email": email, - "groups": groups, + "username": username, + "email": email, + "groups": groups, + "userInfoURL": userInfoURL, }) } diff --git a/backend/pkg/config/config.go b/backend/pkg/config/config.go index 467ef014d6b..a7092101349 100644 --- a/backend/pkg/config/config.go +++ b/backend/pkg/config/config.go @@ -24,6 +24,7 @@ const ( DefaultMeUsernamePath = "preferred_username,upn,username,name" DefaultMeEmailPath = "email" DefaultMeGroupsPath = "groups,realm_access.roles" + DefaultMeUserInfoURL = "" ) type Config struct { @@ -56,6 +57,7 @@ type Config struct { MeUsernamePath string `koanf:"me-username-path"` MeEmailPath string `koanf:"me-email-path"` MeGroupsPath string `koanf:"me-groups-path"` + MeUserInfoURL string `koanf:"me-user-info-url"` OidcUsePKCE bool `koanf:"oidc-use-pkce"` // telemetry configs ServiceName string `koanf:"service-name"` @@ -230,7 +232,7 @@ func setKubeConfigPath(config *Config) { } // ApplyMeDefaults trims and applies defaults to the JMESPath expressions used for the /me endpoint. -func ApplyMeDefaults(usernamePath, emailPath, groupsPath string) (string, string, string) { +func ApplyMeDefaults(usernamePath, emailPath, groupsPath, userInfoURL string) (string, string, string, string) { username := strings.TrimSpace(usernamePath) if username == "" { username = DefaultMeUsernamePath @@ -246,15 +248,21 @@ func ApplyMeDefaults(usernamePath, emailPath, groupsPath string) (string, string groups = DefaultMeGroupsPath } - return username, email, groups + userInfo := strings.TrimSpace(userInfoURL) + if userInfo == "" { + userInfo = DefaultMeUserInfoURL + } + + return username, email, groups, userInfo } // setMeDefaults ensures the /clusters/{clusterName}/me claim paths fall back to defaults when unset. func setMeDefaults(config *Config) { - config.MeUsernamePath, config.MeEmailPath, config.MeGroupsPath = ApplyMeDefaults( + config.MeUsernamePath, config.MeEmailPath, config.MeGroupsPath, config.MeUserInfoURL = ApplyMeDefaults( config.MeUsernamePath, config.MeEmailPath, config.MeGroupsPath, + config.MeUserInfoURL, ) } @@ -407,6 +415,8 @@ func addOIDCFlags(f *flag.FlagSet) { "Comma separated JMESPath expressions used to read email from the JWT payload") f.String("me-groups-path", DefaultMeGroupsPath, "Comma separated JMESPath expressions used to read groups from the JWT payload") + f.String("me-user-info-url", DefaultMeUserInfoURL, + "URL to fetch additional user info for the /me endpoint. For oauth2proxy /oauth2/userinfo can be used.") } func addTelemetryFlags(f *flag.FlagSet) { diff --git a/backend/pkg/config/config_test.go b/backend/pkg/config/config_test.go index 44c6ac307a1..8ff162bba58 100644 --- a/backend/pkg/config/config_test.go +++ b/backend/pkg/config/config_test.go @@ -101,11 +101,13 @@ var ParseWithEnvTests = []struct { "HEADLAMP_CONFIG_ME_USERNAME_PATH": "user.name", "HEADLAMP_CONFIG_ME_EMAIL_PATH": "user.email", "HEADLAMP_CONFIG_ME_GROUPS_PATH": "user.groups", + "HEADLAMP_CONFIG_ME_USER_INFO_URL": "/oauth2/userinfo", }, verify: func(t *testing.T, conf *config.Config) { assert.Equal(t, "user.name", conf.MeUsernamePath) assert.Equal(t, "user.email", conf.MeEmailPath) assert.Equal(t, "user.groups", conf.MeGroupsPath) + assert.Equal(t, "/oauth2/userinfo", conf.MeUserInfoURL) }, }, { diff --git a/charts/headlamp/README.md b/charts/headlamp/README.md index babd8ebd26b..f837b817b9f 100644 --- a/charts/headlamp/README.md +++ b/charts/headlamp/README.md @@ -90,6 +90,7 @@ $ helm install my-headlamp headlamp/headlamp \ | config.oidc.secret.name | string | `"oidc"` | Name of the OIDC secret | | config.oidc.externalSecret.enabled | bool | `false` | Enable using external secret for OIDC | | config.oidc.externalSecret.name | string | `""` | Name of external OIDC secret | +| config.oidc.meUserInfoURL | string | `""` | URL to fetch additional user info for the /me endpoint. For oauth2proxy /oauth2/userinfo can be used. | There are three ways to configure OIDC: diff --git a/charts/headlamp/templates/deployment.yaml b/charts/headlamp/templates/deployment.yaml index 4ac500edf6e..81d91fa8f2c 100644 --- a/charts/headlamp/templates/deployment.yaml +++ b/charts/headlamp/templates/deployment.yaml @@ -10,6 +10,7 @@ {{- $validatorIssuerURL := "" }} {{- $usePKCE := "" }} {{- $useAccessToken := "" }} +{{- $meUserInfoURL := "" }} # This block of code is used to extract the values from the env. # This is done to check if the values are non-empty and if they are, they are used in the deployment.yaml. @@ -41,6 +42,9 @@ {{- if eq .name "OIDC_USE_PKCE" }} {{- $usePKCE = .value | toString }} {{- end }} + {{- if eq .name "ME_USER_INFO_URL" }} + {{- $meUserInfoURL = .value | toString }} + {{- end }} {{- end }} apiVersion: apps/v1 @@ -170,6 +174,13 @@ spec: name: {{ $oidc.secret.name }} key: usePKCE {{- end }} + {{- if $oidc.meUserInfoURL }} + - name: ME_USER_INFO_URL + valueFrom: + secretKeyRef: + name: {{ $oidc.secret.name }} + key: meUserInfoURL + {{- end }} {{- else }} {{- if $oidc.clientID }} - name: OIDC_CLIENT_ID @@ -207,6 +218,10 @@ spec: - name: OIDC_USE_PKCE value: {{ $oidc.usePKCE }} {{- end }} + {{- if $oidc.meUserInfoURL }} + - name: ME_USER_INFO_URL + value: {{ $oidc.meUserInfoURL }} + {{- end }} {{- end }} {{- if .Values.env }} {{- toYaml .Values.env | nindent 12 }} @@ -263,6 +278,9 @@ spec: {{- if or (eq ($oidc.usePKCE | toString) "true") (eq $usePKCE "true") }} - "-oidc-use-pkce=$(OIDC_USE_PKCE)" {{- end }} + {{- if or (ne $oidc.meUserInfoURL "") (ne $meUserInfoURL "") }} + - "-me-user-info-url=$(ME_USER_INFO_URL)" + {{- end }} {{- else }} - "-oidc-client-id=$(OIDC_CLIENT_ID)" - "-oidc-client-secret=$(OIDC_CLIENT_SECRET)" @@ -280,6 +298,9 @@ spec: # Check if validatorIssuerURL is non empty either from env or oidc.config - "-oidc-validator-idp-issuer-url=$(OIDC_VALIDATOR_ISSUER_URL)" {{- end }} + {{- if or (ne $oidc.meUserInfoURL "") (ne $meUserInfoURL "") }} + - "-me-user-info-url=$(ME_USER_INFO_URL)" + {{- end }} {{- end }} {{- with .Values.config.baseURL }} - "-base-url={{ . }}" diff --git a/charts/headlamp/tests/expected_templates/me-user-info-url-directly.yaml b/charts/headlamp/tests/expected_templates/me-user-info-url-directly.yaml new file mode 100644 index 00000000000..fb651744626 --- /dev/null +++ b/charts/headlamp/tests/expected_templates/me-user-info-url-directly.yaml @@ -0,0 +1,130 @@ +--- +# Source: headlamp/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.36.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/managed-by: Helm +--- +# Source: headlamp/templates/secret.yaml +apiVersion: v1 +kind: Secret +metadata: + name: oidc + namespace: default +type: Opaque +data: +--- +# Source: headlamp/templates/clusterrolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: headlamp-admin + labels: + helm.sh/chart: headlamp-0.36.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/managed-by: Helm +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: +- kind: ServiceAccount + name: headlamp + namespace: default +--- +# Source: headlamp/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.36.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/managed-by: Helm +spec: + type: ClusterIP + + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp +--- +# Source: headlamp/templates/deployment.yaml +# This block of code is used to extract the values from the env. +# This is done to check if the values are non-empty and if they are, they are used in the deployment.yaml. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.36.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/managed-by: Helm +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + template: + metadata: + labels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + spec: + serviceAccountName: headlamp + automountServiceAccountToken: true + securityContext: + {} + containers: + - name: headlamp + securityContext: + privileged: false + runAsGroup: 101 + runAsNonRoot: true + runAsUser: 100 + image: "ghcr.io/headlamp-k8s/headlamp:v0.36.0" + imagePullPolicy: IfNotPresent + + env: + - name: ME_USER_INFO_URL + value: /oauth2/userinfocustom1 + args: + - "-in-cluster" + - "-plugins-dir=/headlamp/plugins" + # Check if externalSecret is disabled + - "-me-user-info-url=$(ME_USER_INFO_URL)" + ports: + - name: http + containerPort: 4466 + protocol: TCP + livenessProbe: + httpGet: + path: "/" + port: http + readinessProbe: + httpGet: + path: "/" + port: http + resources: + {} diff --git a/charts/headlamp/tests/expected_templates/me-user-info-url.yaml b/charts/headlamp/tests/expected_templates/me-user-info-url.yaml new file mode 100644 index 00000000000..24320d35d07 --- /dev/null +++ b/charts/headlamp/tests/expected_templates/me-user-info-url.yaml @@ -0,0 +1,133 @@ +--- +# Source: headlamp/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.36.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/managed-by: Helm +--- +# Source: headlamp/templates/secret.yaml +apiVersion: v1 +kind: Secret +metadata: + name: oidc + namespace: default +type: Opaque +data: +--- +# Source: headlamp/templates/clusterrolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: headlamp-admin + labels: + helm.sh/chart: headlamp-0.36.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/managed-by: Helm +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: +- kind: ServiceAccount + name: headlamp + namespace: default +--- +# Source: headlamp/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.36.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/managed-by: Helm +spec: + type: ClusterIP + + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp +--- +# Source: headlamp/templates/deployment.yaml +# This block of code is used to extract the values from the env. +# This is done to check if the values are non-empty and if they are, they are used in the deployment.yaml. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.36.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/managed-by: Helm +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + template: + metadata: + labels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + spec: + serviceAccountName: headlamp + automountServiceAccountToken: true + securityContext: + {} + containers: + - name: headlamp + securityContext: + privileged: false + runAsGroup: 101 + runAsNonRoot: true + runAsUser: 100 + image: "ghcr.io/headlamp-k8s/headlamp:v0.36.0" + imagePullPolicy: IfNotPresent + + env: + - name: ME_USER_INFO_URL + valueFrom: + secretKeyRef: + name: oidc + key: meUserInfoURL + args: + - "-in-cluster" + - "-plugins-dir=/headlamp/plugins" + # Check if externalSecret is disabled + - "-me-user-info-url=$(ME_USER_INFO_URL)" + ports: + - name: http + containerPort: 4466 + protocol: TCP + livenessProbe: + httpGet: + path: "/" + port: http + readinessProbe: + httpGet: + path: "/" + port: http + resources: + {} diff --git a/charts/headlamp/tests/test_cases/me-user-info-url-directly.yaml b/charts/headlamp/tests/test_cases/me-user-info-url-directly.yaml new file mode 100644 index 00000000000..149928b4816 --- /dev/null +++ b/charts/headlamp/tests/test_cases/me-user-info-url-directly.yaml @@ -0,0 +1,3 @@ +env: + - name: ME_USER_INFO_URL + value: /oauth2/userinfocustom1 diff --git a/charts/headlamp/tests/test_cases/me-user-info-url.yaml b/charts/headlamp/tests/test_cases/me-user-info-url.yaml new file mode 100644 index 00000000000..76c4f640500 --- /dev/null +++ b/charts/headlamp/tests/test_cases/me-user-info-url.yaml @@ -0,0 +1,4 @@ +# -- Headlamp OIDC me user info URL test case +config: + oidc: + meUserInfoURL: /oauth2/userinfocustom2 diff --git a/charts/headlamp/values.yaml b/charts/headlamp/values.yaml index b44b47a63cb..413bed7c475 100644 --- a/charts/headlamp/values.yaml +++ b/charts/headlamp/values.yaml @@ -96,6 +96,11 @@ config: externalSecret: enabled: false name: "" + + # -- URL to fetch additional user info for the /me endpoint. + # For oauth2proxy /oauth2/userinfo can be used. Empty and it will not be used. + meUserInfoURL: "" + # -- directory to look for plugins pluginsDir: "/headlamp/plugins" enableHelm: false diff --git a/frontend/src/components/App/TopBar.tsx b/frontend/src/components/App/TopBar.tsx index 77aeccb4cb2..d9183dd1e68 100644 --- a/frontend/src/components/App/TopBar.tsx +++ b/frontend/src/components/App/TopBar.tsx @@ -103,7 +103,37 @@ export default function TopBar({}: TopBarProps) { autoLogoutOnAuthError: false, }); - return res ? { username: res.username, email: res.email } : null; + if (!res) { + return null; + } + + if (!(typeof res.userInfoURL === 'string' && res.userInfoURL.length > 0)) { + return { username: res.username, email: res.email }; + } + + const ui: { + preferredUsername?: string; + email?: string; + } = await fetch(res.userInfoURL, { + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + }, + }).then(r => { + if (!r.ok) { + throw new Error(`Could not fetch user info from ${res.userInfoURL}`); + } + return r.json(); + }); + + if (!ui || (!ui.preferredUsername && !ui.email)) { + return null; + } + + return { + username: ui.preferredUsername, + email: ui.email, + }; } catch { return null; } From 303715cb9e40c6ca322c6f2acd235cfe2c1d0878 Mon Sep 17 00:00:00 2001 From: Evangelos Skopelitis Date: Thu, 30 Oct 2025 14:15:17 -0400 Subject: [PATCH 60/72] frontend: TopBar: Hide empty user info fields This change hides the user info menu item when the fields are null. --- .../src/components/App/TopBar.stories.tsx | 12 ++ frontend/src/components/App/TopBar.tsx | 2 +- .../TopBar.EmptyUserInfo.stories.storyshot | 137 ++++++++++++++++++ 3 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 frontend/src/components/App/__snapshots__/TopBar.EmptyUserInfo.stories.storyshot diff --git a/frontend/src/components/App/TopBar.stories.tsx b/frontend/src/components/App/TopBar.stories.tsx index 8c0e644ebf7..a0c1ec573bd 100644 --- a/frontend/src/components/App/TopBar.stories.tsx +++ b/frontend/src/components/App/TopBar.stories.tsx @@ -156,3 +156,15 @@ UndefinedData.args = { clusters: { 'ak8s-desktop': '' }, userInfo: undefined, }; + +export const EmptyUserInfo = PureTemplate.bind({}); +EmptyUserInfo.args = { + appBarActions: [], + logout: () => {}, + cluster: 'ak8s-desktop', + clusters: { 'ak8s-desktop': '' }, + userInfo: { + email: '', + username: '', + }, +}; diff --git a/frontend/src/components/App/TopBar.tsx b/frontend/src/components/App/TopBar.tsx index d9183dd1e68..fe3d4aa92b5 100644 --- a/frontend/src/components/App/TopBar.tsx +++ b/frontend/src/components/App/TopBar.tsx @@ -327,7 +327,7 @@ export const PureTopBar = memo( }, }} > - {userInfo && ( + {!!userDisplayName && ( +
+ +
+ \ No newline at end of file From edfa603b11034ff9e3f6f3cf7befa57cb1005de2 Mon Sep 17 00:00:00 2001 From: Joaquim Rocha Date: Thu, 30 Oct 2025 14:49:36 +0000 Subject: [PATCH 61/72] app: Bump version to 0.37.0 Signed-off-by: Joaquim Rocha --- app/package-lock.json | 4 ++-- app/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/package-lock.json b/app/package-lock.json index 29151351e16..ff1e2eee32e 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -1,12 +1,12 @@ { "name": "headlamp", - "version": "0.36.0", + "version": "0.37.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "headlamp", - "version": "0.36.0", + "version": "0.37.0", "dependencies": { "@langchain/mcp-adapters": "^0.6.0", "copyfiles": "^2.4.1", diff --git a/app/package.json b/app/package.json index c510f5eafac..9c378222e73 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,6 @@ { "name": "headlamp", - "version": "0.36.0", + "version": "0.37.0", "description": "Easy-to-use and extensible Kubernetes web UI", "main": "electron/main.js", "homepage": "https://github.com/kubernetes-sigs/headlamp/#readme", From 74d78ee7aded27266b301862af474639c7764529 Mon Sep 17 00:00:00 2001 From: joaquimrocha Date: Thu, 30 Oct 2025 22:31:15 +0000 Subject: [PATCH 62/72] chocolatey: Bump Headlamp version to 0.37.0 Signed-off-by: joaquimrocha --- app/windows/chocolatey/headlamp.nuspec | 2 +- app/windows/chocolatey/tools/chocolateyinstall.ps1 | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/windows/chocolatey/headlamp.nuspec b/app/windows/chocolatey/headlamp.nuspec index e4ce6d35d89..a695efbb121 100644 --- a/app/windows/chocolatey/headlamp.nuspec +++ b/app/windows/chocolatey/headlamp.nuspec @@ -3,7 +3,7 @@ headlamp - 0.36.0 + 0.37.0 https://github.com/kubernetes-sigs/headlamp/tree/main/app/windows/chocolatey Headlamp Kinvolk diff --git a/app/windows/chocolatey/tools/chocolateyinstall.ps1 b/app/windows/chocolatey/tools/chocolateyinstall.ps1 index c18103bbf7e..e7e4b2ac6d8 100644 --- a/app/windows/chocolatey/tools/chocolateyinstall.ps1 +++ b/app/windows/chocolatey/tools/chocolateyinstall.ps1 @@ -1,8 +1,8 @@ $ErrorActionPreference = 'Stop'; # stop on all errors $toolsDir = "$(Split-Path -parent $MyInvocation.MyCommand.Definition)" -$headlampVersion = '0.36.0' +$headlampVersion = '0.37.0' $url = "https://github.com/kubernetes-sigs/headlamp/releases/download/v${headlampVersion}/Headlamp-${headlampVersion}-win-x64.exe" -$checksum = '612678fabbc41bac8bae4b14e4cbbb4e888f77d24d97b5f125f44d4154648553' +$checksum = 'c48779c5f7da1eeb0eeef636db61a029519f35219206c727d809d39e79453612' $packageArgs = @{ packageName = $env:ChocolateyPackageName From 5f658aba38956e9e7bdd38d3f1f9d8930a263636 Mon Sep 17 00:00:00 2001 From: joaquimrocha Date: Thu, 30 Oct 2025 22:34:06 +0000 Subject: [PATCH 63/72] charts: Bump Headlamp version to v0.37.0 Signed-off-by: joaquimrocha --- charts/headlamp/Chart.yaml | 4 ++-- .../azure-oidc-with-validators.yaml | 18 +++++++-------- .../tests/expected_templates/default.yaml | 18 +++++++-------- .../tests/expected_templates/extra-args.yaml | 18 +++++++-------- .../expected_templates/extra-manifests.yaml | 18 +++++++-------- .../me-user-info-url-directly.yaml | 18 +++++++-------- .../expected_templates/me-user-info-url.yaml | 18 +++++++-------- ...namespace-override-oidc-create-secret.yaml | 18 +++++++-------- .../namespace-override.yaml | 18 +++++++-------- .../expected_templates/non-azure-oidc.yaml | 18 +++++++-------- .../oidc-create-secret.yaml | 18 +++++++-------- .../expected_templates/oidc-directly-env.yaml | 18 +++++++-------- .../expected_templates/oidc-directly.yaml | 18 +++++++-------- .../oidc-external-secret.yaml | 18 +++++++-------- .../tests/expected_templates/oidc-pkce.yaml | 18 +++++++-------- .../oidc-validator-overrides.yaml | 18 +++++++-------- .../expected_templates/pod-disruption.yaml | 22 +++++++++---------- .../expected_templates/security-context.yaml | 22 +++++++++---------- .../tests/expected_templates/tls-added.yaml | 18 +++++++-------- .../expected_templates/volumes-added.yaml | 18 +++++++-------- 20 files changed, 177 insertions(+), 177 deletions(-) diff --git a/charts/headlamp/Chart.yaml b/charts/headlamp/Chart.yaml index 4f67c132b07..fbc369e49c6 100644 --- a/charts/headlamp/Chart.yaml +++ b/charts/headlamp/Chart.yaml @@ -20,8 +20,8 @@ sources: maintainers: - name: kinvolk url: https://kinvolk.io/ -version: 0.36.0 -appVersion: 0.36.0 +version: 0.37.0 +appVersion: 0.37.0 annotations: artifacthub.io/signKey: | fingerprint: 2956B7F7167769370C93730C7264DA7B85D08A37 diff --git a/charts/headlamp/tests/expected_templates/azure-oidc-with-validators.yaml b/charts/headlamp/tests/expected_templates/azure-oidc-with-validators.yaml index 0a5436a766e..ca2e9474ef1 100644 --- a/charts/headlamp/tests/expected_templates/azure-oidc-with-validators.yaml +++ b/charts/headlamp/tests/expected_templates/azure-oidc-with-validators.yaml @@ -6,10 +6,10 @@ metadata: name: headlamp namespace: default labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm --- # Source: headlamp/templates/clusterrolebinding.yaml @@ -18,10 +18,10 @@ kind: ClusterRoleBinding metadata: name: headlamp-admin labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm roleRef: apiGroup: rbac.authorization.k8s.io @@ -39,10 +39,10 @@ metadata: name: headlamp namespace: default labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm spec: type: ClusterIP @@ -66,10 +66,10 @@ metadata: name: headlamp namespace: default labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm spec: replicas: 1 @@ -94,7 +94,7 @@ spec: runAsGroup: 101 runAsNonRoot: true runAsUser: 100 - image: "ghcr.io/headlamp-k8s/headlamp:v0.36.0" + image: "ghcr.io/headlamp-k8s/headlamp:v0.37.0" imagePullPolicy: IfNotPresent env: diff --git a/charts/headlamp/tests/expected_templates/default.yaml b/charts/headlamp/tests/expected_templates/default.yaml index 181ab0a31e2..c55a00079d9 100644 --- a/charts/headlamp/tests/expected_templates/default.yaml +++ b/charts/headlamp/tests/expected_templates/default.yaml @@ -6,10 +6,10 @@ metadata: name: headlamp namespace: default labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm --- # Source: headlamp/templates/secret.yaml @@ -27,10 +27,10 @@ kind: ClusterRoleBinding metadata: name: headlamp-admin labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm roleRef: apiGroup: rbac.authorization.k8s.io @@ -48,10 +48,10 @@ metadata: name: headlamp namespace: default labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm spec: type: ClusterIP @@ -75,10 +75,10 @@ metadata: name: headlamp namespace: default labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm spec: replicas: 1 @@ -103,7 +103,7 @@ spec: runAsGroup: 101 runAsNonRoot: true runAsUser: 100 - image: "ghcr.io/headlamp-k8s/headlamp:v0.36.0" + image: "ghcr.io/headlamp-k8s/headlamp:v0.37.0" imagePullPolicy: IfNotPresent env: diff --git a/charts/headlamp/tests/expected_templates/extra-args.yaml b/charts/headlamp/tests/expected_templates/extra-args.yaml index 000cfe891d1..232c40a31c2 100644 --- a/charts/headlamp/tests/expected_templates/extra-args.yaml +++ b/charts/headlamp/tests/expected_templates/extra-args.yaml @@ -6,10 +6,10 @@ metadata: name: headlamp namespace: default labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm --- # Source: headlamp/templates/secret.yaml @@ -27,10 +27,10 @@ kind: ClusterRoleBinding metadata: name: headlamp-admin labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm roleRef: apiGroup: rbac.authorization.k8s.io @@ -48,10 +48,10 @@ metadata: name: headlamp namespace: default labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm spec: type: ClusterIP @@ -75,10 +75,10 @@ metadata: name: headlamp namespace: default labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm spec: replicas: 1 @@ -103,7 +103,7 @@ spec: runAsGroup: 101 runAsNonRoot: true runAsUser: 100 - image: "ghcr.io/headlamp-k8s/headlamp:v0.36.0" + image: "ghcr.io/headlamp-k8s/headlamp:v0.37.0" imagePullPolicy: IfNotPresent env: diff --git a/charts/headlamp/tests/expected_templates/extra-manifests.yaml b/charts/headlamp/tests/expected_templates/extra-manifests.yaml index 348aae4e04b..54caf8fdb06 100644 --- a/charts/headlamp/tests/expected_templates/extra-manifests.yaml +++ b/charts/headlamp/tests/expected_templates/extra-manifests.yaml @@ -6,10 +6,10 @@ metadata: name: headlamp namespace: default labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm --- # Source: headlamp/templates/secret.yaml @@ -44,10 +44,10 @@ kind: ClusterRoleBinding metadata: name: headlamp-admin labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm roleRef: apiGroup: rbac.authorization.k8s.io @@ -65,10 +65,10 @@ metadata: name: headlamp namespace: default labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm spec: type: ClusterIP @@ -92,10 +92,10 @@ metadata: name: headlamp namespace: default labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm spec: replicas: 1 @@ -120,7 +120,7 @@ spec: runAsGroup: 101 runAsNonRoot: true runAsUser: 100 - image: "ghcr.io/headlamp-k8s/headlamp:v0.36.0" + image: "ghcr.io/headlamp-k8s/headlamp:v0.37.0" imagePullPolicy: IfNotPresent env: diff --git a/charts/headlamp/tests/expected_templates/me-user-info-url-directly.yaml b/charts/headlamp/tests/expected_templates/me-user-info-url-directly.yaml index fb651744626..6fbc748a641 100644 --- a/charts/headlamp/tests/expected_templates/me-user-info-url-directly.yaml +++ b/charts/headlamp/tests/expected_templates/me-user-info-url-directly.yaml @@ -6,10 +6,10 @@ metadata: name: headlamp namespace: default labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm --- # Source: headlamp/templates/secret.yaml @@ -27,10 +27,10 @@ kind: ClusterRoleBinding metadata: name: headlamp-admin labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm roleRef: apiGroup: rbac.authorization.k8s.io @@ -48,10 +48,10 @@ metadata: name: headlamp namespace: default labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm spec: type: ClusterIP @@ -75,10 +75,10 @@ metadata: name: headlamp namespace: default labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm spec: replicas: 1 @@ -103,7 +103,7 @@ spec: runAsGroup: 101 runAsNonRoot: true runAsUser: 100 - image: "ghcr.io/headlamp-k8s/headlamp:v0.36.0" + image: "ghcr.io/headlamp-k8s/headlamp:v0.37.0" imagePullPolicy: IfNotPresent env: diff --git a/charts/headlamp/tests/expected_templates/me-user-info-url.yaml b/charts/headlamp/tests/expected_templates/me-user-info-url.yaml index 24320d35d07..0eb1535488a 100644 --- a/charts/headlamp/tests/expected_templates/me-user-info-url.yaml +++ b/charts/headlamp/tests/expected_templates/me-user-info-url.yaml @@ -6,10 +6,10 @@ metadata: name: headlamp namespace: default labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm --- # Source: headlamp/templates/secret.yaml @@ -27,10 +27,10 @@ kind: ClusterRoleBinding metadata: name: headlamp-admin labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm roleRef: apiGroup: rbac.authorization.k8s.io @@ -48,10 +48,10 @@ metadata: name: headlamp namespace: default labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm spec: type: ClusterIP @@ -75,10 +75,10 @@ metadata: name: headlamp namespace: default labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm spec: replicas: 1 @@ -103,7 +103,7 @@ spec: runAsGroup: 101 runAsNonRoot: true runAsUser: 100 - image: "ghcr.io/headlamp-k8s/headlamp:v0.36.0" + image: "ghcr.io/headlamp-k8s/headlamp:v0.37.0" imagePullPolicy: IfNotPresent env: diff --git a/charts/headlamp/tests/expected_templates/namespace-override-oidc-create-secret.yaml b/charts/headlamp/tests/expected_templates/namespace-override-oidc-create-secret.yaml index 6f088f5140b..5707d18dd6a 100644 --- a/charts/headlamp/tests/expected_templates/namespace-override-oidc-create-secret.yaml +++ b/charts/headlamp/tests/expected_templates/namespace-override-oidc-create-secret.yaml @@ -6,10 +6,10 @@ metadata: name: headlamp namespace: mynamespace2 labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm --- # Source: headlamp/templates/secret.yaml @@ -31,10 +31,10 @@ kind: ClusterRoleBinding metadata: name: headlamp-admin labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm roleRef: apiGroup: rbac.authorization.k8s.io @@ -52,10 +52,10 @@ metadata: name: headlamp namespace: mynamespace2 labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm spec: type: ClusterIP @@ -79,10 +79,10 @@ metadata: name: headlamp namespace: mynamespace2 labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm spec: replicas: 1 @@ -107,7 +107,7 @@ spec: runAsGroup: 101 runAsNonRoot: true runAsUser: 100 - image: "ghcr.io/headlamp-k8s/headlamp:v0.36.0" + image: "ghcr.io/headlamp-k8s/headlamp:v0.37.0" imagePullPolicy: IfNotPresent env: diff --git a/charts/headlamp/tests/expected_templates/namespace-override.yaml b/charts/headlamp/tests/expected_templates/namespace-override.yaml index 6033e353291..ea03730cc63 100644 --- a/charts/headlamp/tests/expected_templates/namespace-override.yaml +++ b/charts/headlamp/tests/expected_templates/namespace-override.yaml @@ -6,10 +6,10 @@ metadata: name: headlamp namespace: mynamespace labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm --- # Source: headlamp/templates/secret.yaml @@ -27,10 +27,10 @@ kind: ClusterRoleBinding metadata: name: headlamp-admin labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm roleRef: apiGroup: rbac.authorization.k8s.io @@ -48,10 +48,10 @@ metadata: name: headlamp namespace: mynamespace labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm spec: type: ClusterIP @@ -75,10 +75,10 @@ metadata: name: headlamp namespace: mynamespace labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm spec: replicas: 1 @@ -103,7 +103,7 @@ spec: runAsGroup: 101 runAsNonRoot: true runAsUser: 100 - image: "ghcr.io/headlamp-k8s/headlamp:v0.36.0" + image: "ghcr.io/headlamp-k8s/headlamp:v0.37.0" imagePullPolicy: IfNotPresent env: diff --git a/charts/headlamp/tests/expected_templates/non-azure-oidc.yaml b/charts/headlamp/tests/expected_templates/non-azure-oidc.yaml index 3032a1e9e61..25110e14dc1 100644 --- a/charts/headlamp/tests/expected_templates/non-azure-oidc.yaml +++ b/charts/headlamp/tests/expected_templates/non-azure-oidc.yaml @@ -6,10 +6,10 @@ metadata: name: headlamp namespace: default labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm --- # Source: headlamp/templates/clusterrolebinding.yaml @@ -18,10 +18,10 @@ kind: ClusterRoleBinding metadata: name: headlamp-admin labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm roleRef: apiGroup: rbac.authorization.k8s.io @@ -39,10 +39,10 @@ metadata: name: headlamp namespace: default labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm spec: type: ClusterIP @@ -66,10 +66,10 @@ metadata: name: headlamp namespace: default labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm spec: replicas: 1 @@ -94,7 +94,7 @@ spec: runAsGroup: 101 runAsNonRoot: true runAsUser: 100 - image: "ghcr.io/headlamp-k8s/headlamp:v0.36.0" + image: "ghcr.io/headlamp-k8s/headlamp:v0.37.0" imagePullPolicy: IfNotPresent env: diff --git a/charts/headlamp/tests/expected_templates/oidc-create-secret.yaml b/charts/headlamp/tests/expected_templates/oidc-create-secret.yaml index bff34aeb6ef..9160482da27 100644 --- a/charts/headlamp/tests/expected_templates/oidc-create-secret.yaml +++ b/charts/headlamp/tests/expected_templates/oidc-create-secret.yaml @@ -6,10 +6,10 @@ metadata: name: headlamp namespace: default labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm --- # Source: headlamp/templates/secret.yaml @@ -31,10 +31,10 @@ kind: ClusterRoleBinding metadata: name: headlamp-admin labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm roleRef: apiGroup: rbac.authorization.k8s.io @@ -52,10 +52,10 @@ metadata: name: headlamp namespace: default labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm spec: type: ClusterIP @@ -79,10 +79,10 @@ metadata: name: headlamp namespace: default labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm spec: replicas: 1 @@ -107,7 +107,7 @@ spec: runAsGroup: 101 runAsNonRoot: true runAsUser: 100 - image: "ghcr.io/headlamp-k8s/headlamp:v0.36.0" + image: "ghcr.io/headlamp-k8s/headlamp:v0.37.0" imagePullPolicy: IfNotPresent env: diff --git a/charts/headlamp/tests/expected_templates/oidc-directly-env.yaml b/charts/headlamp/tests/expected_templates/oidc-directly-env.yaml index 3585650a06a..b6a74a28e3f 100644 --- a/charts/headlamp/tests/expected_templates/oidc-directly-env.yaml +++ b/charts/headlamp/tests/expected_templates/oidc-directly-env.yaml @@ -6,10 +6,10 @@ metadata: name: headlamp namespace: default labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm --- # Source: headlamp/templates/secret.yaml @@ -27,10 +27,10 @@ kind: ClusterRoleBinding metadata: name: headlamp-admin labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm roleRef: apiGroup: rbac.authorization.k8s.io @@ -48,10 +48,10 @@ metadata: name: headlamp namespace: default labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm spec: type: ClusterIP @@ -75,10 +75,10 @@ metadata: name: headlamp namespace: default labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm spec: replicas: 1 @@ -103,7 +103,7 @@ spec: runAsGroup: 101 runAsNonRoot: true runAsUser: 100 - image: "ghcr.io/headlamp-k8s/headlamp:v0.36.0" + image: "ghcr.io/headlamp-k8s/headlamp:v0.37.0" imagePullPolicy: IfNotPresent env: diff --git a/charts/headlamp/tests/expected_templates/oidc-directly.yaml b/charts/headlamp/tests/expected_templates/oidc-directly.yaml index 8e55710efb5..4b09dca2458 100644 --- a/charts/headlamp/tests/expected_templates/oidc-directly.yaml +++ b/charts/headlamp/tests/expected_templates/oidc-directly.yaml @@ -6,10 +6,10 @@ metadata: name: headlamp namespace: default labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm --- # Source: headlamp/templates/clusterrolebinding.yaml @@ -18,10 +18,10 @@ kind: ClusterRoleBinding metadata: name: headlamp-admin labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm roleRef: apiGroup: rbac.authorization.k8s.io @@ -39,10 +39,10 @@ metadata: name: headlamp namespace: default labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm spec: type: ClusterIP @@ -66,10 +66,10 @@ metadata: name: headlamp namespace: default labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm spec: replicas: 1 @@ -94,7 +94,7 @@ spec: runAsGroup: 101 runAsNonRoot: true runAsUser: 100 - image: "ghcr.io/headlamp-k8s/headlamp:v0.36.0" + image: "ghcr.io/headlamp-k8s/headlamp:v0.37.0" imagePullPolicy: IfNotPresent env: diff --git a/charts/headlamp/tests/expected_templates/oidc-external-secret.yaml b/charts/headlamp/tests/expected_templates/oidc-external-secret.yaml index 8046ae39524..f8b72a5bdff 100644 --- a/charts/headlamp/tests/expected_templates/oidc-external-secret.yaml +++ b/charts/headlamp/tests/expected_templates/oidc-external-secret.yaml @@ -6,10 +6,10 @@ metadata: name: headlamp namespace: default labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm --- # Source: headlamp/templates/clusterrolebinding.yaml @@ -18,10 +18,10 @@ kind: ClusterRoleBinding metadata: name: headlamp-admin labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm roleRef: apiGroup: rbac.authorization.k8s.io @@ -39,10 +39,10 @@ metadata: name: headlamp namespace: default labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm spec: type: ClusterIP @@ -66,10 +66,10 @@ metadata: name: headlamp namespace: default labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm spec: replicas: 1 @@ -94,7 +94,7 @@ spec: runAsGroup: 101 runAsNonRoot: true runAsUser: 100 - image: "ghcr.io/headlamp-k8s/headlamp:v0.36.0" + image: "ghcr.io/headlamp-k8s/headlamp:v0.37.0" imagePullPolicy: IfNotPresent # Check if externalSecret is enabled diff --git a/charts/headlamp/tests/expected_templates/oidc-pkce.yaml b/charts/headlamp/tests/expected_templates/oidc-pkce.yaml index 5eef06a23b0..0f5f0d1cf46 100644 --- a/charts/headlamp/tests/expected_templates/oidc-pkce.yaml +++ b/charts/headlamp/tests/expected_templates/oidc-pkce.yaml @@ -6,10 +6,10 @@ metadata: name: headlamp namespace: default labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm --- # Source: headlamp/templates/clusterrolebinding.yaml @@ -18,10 +18,10 @@ kind: ClusterRoleBinding metadata: name: headlamp-admin labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm roleRef: apiGroup: rbac.authorization.k8s.io @@ -39,10 +39,10 @@ metadata: name: headlamp namespace: default labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm spec: type: ClusterIP @@ -66,10 +66,10 @@ metadata: name: headlamp namespace: default labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm spec: replicas: 1 @@ -94,7 +94,7 @@ spec: runAsGroup: 101 runAsNonRoot: true runAsUser: 100 - image: "ghcr.io/headlamp-k8s/headlamp:v0.36.0" + image: "ghcr.io/headlamp-k8s/headlamp:v0.37.0" imagePullPolicy: IfNotPresent env: diff --git a/charts/headlamp/tests/expected_templates/oidc-validator-overrides.yaml b/charts/headlamp/tests/expected_templates/oidc-validator-overrides.yaml index 93d36fb53bd..be55526671f 100644 --- a/charts/headlamp/tests/expected_templates/oidc-validator-overrides.yaml +++ b/charts/headlamp/tests/expected_templates/oidc-validator-overrides.yaml @@ -6,10 +6,10 @@ metadata: name: headlamp namespace: default labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm --- # Source: headlamp/templates/clusterrolebinding.yaml @@ -18,10 +18,10 @@ kind: ClusterRoleBinding metadata: name: headlamp-admin labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm roleRef: apiGroup: rbac.authorization.k8s.io @@ -39,10 +39,10 @@ metadata: name: headlamp namespace: default labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm spec: type: ClusterIP @@ -66,10 +66,10 @@ metadata: name: headlamp namespace: default labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm spec: replicas: 1 @@ -94,7 +94,7 @@ spec: runAsGroup: 101 runAsNonRoot: true runAsUser: 100 - image: "ghcr.io/headlamp-k8s/headlamp:v0.36.0" + image: "ghcr.io/headlamp-k8s/headlamp:v0.37.0" imagePullPolicy: IfNotPresent env: diff --git a/charts/headlamp/tests/expected_templates/pod-disruption.yaml b/charts/headlamp/tests/expected_templates/pod-disruption.yaml index 6176a1be43e..570fce727e2 100644 --- a/charts/headlamp/tests/expected_templates/pod-disruption.yaml +++ b/charts/headlamp/tests/expected_templates/pod-disruption.yaml @@ -6,10 +6,10 @@ metadata: name: headlamp namespace: default labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm spec: maxUnavailable: 1 @@ -26,10 +26,10 @@ metadata: name: headlamp namespace: default labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm --- # Source: headlamp/templates/secret.yaml @@ -47,10 +47,10 @@ kind: ClusterRoleBinding metadata: name: headlamp-admin labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm roleRef: apiGroup: rbac.authorization.k8s.io @@ -68,10 +68,10 @@ metadata: name: headlamp namespace: default labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm spec: type: ClusterIP @@ -95,10 +95,10 @@ metadata: name: headlamp namespace: default labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm spec: replicas: 1 @@ -123,7 +123,7 @@ spec: runAsGroup: 101 runAsNonRoot: true runAsUser: 100 - image: "ghcr.io/headlamp-k8s/headlamp:v0.36.0" + image: "ghcr.io/headlamp-k8s/headlamp:v0.37.0" imagePullPolicy: IfNotPresent env: diff --git a/charts/headlamp/tests/expected_templates/security-context.yaml b/charts/headlamp/tests/expected_templates/security-context.yaml index b3dae60534d..0cefaa4cae3 100644 --- a/charts/headlamp/tests/expected_templates/security-context.yaml +++ b/charts/headlamp/tests/expected_templates/security-context.yaml @@ -6,10 +6,10 @@ metadata: name: headlamp namespace: default labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm --- # Source: headlamp/templates/secret.yaml @@ -28,10 +28,10 @@ metadata: name: headlamp-plugin-config namespace: default labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm data: plugin.yml: | @@ -42,10 +42,10 @@ kind: ClusterRoleBinding metadata: name: headlamp-admin labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm roleRef: apiGroup: rbac.authorization.k8s.io @@ -63,10 +63,10 @@ metadata: name: headlamp namespace: default labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm spec: type: ClusterIP @@ -90,10 +90,10 @@ metadata: name: headlamp namespace: default labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm spec: replicas: 1 @@ -129,7 +129,7 @@ spec: runAsUser: 100 seccompProfile: type: RuntimeDefault - image: "ghcr.io/headlamp-k8s/headlamp:v0.36.0" + image: "ghcr.io/headlamp-k8s/headlamp:v0.37.0" imagePullPolicy: IfNotPresent env: diff --git a/charts/headlamp/tests/expected_templates/tls-added.yaml b/charts/headlamp/tests/expected_templates/tls-added.yaml index 75897e055ec..6bbdbbb0f18 100644 --- a/charts/headlamp/tests/expected_templates/tls-added.yaml +++ b/charts/headlamp/tests/expected_templates/tls-added.yaml @@ -6,10 +6,10 @@ metadata: name: headlamp namespace: default labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm --- # Source: headlamp/templates/secret.yaml @@ -27,10 +27,10 @@ kind: ClusterRoleBinding metadata: name: headlamp-admin labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm roleRef: apiGroup: rbac.authorization.k8s.io @@ -48,10 +48,10 @@ metadata: name: headlamp namespace: default labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm spec: type: ClusterIP @@ -75,10 +75,10 @@ metadata: name: headlamp namespace: default labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm spec: replicas: 1 @@ -103,7 +103,7 @@ spec: runAsGroup: 101 runAsNonRoot: true runAsUser: 100 - image: "ghcr.io/headlamp-k8s/headlamp:v0.36.0" + image: "ghcr.io/headlamp-k8s/headlamp:v0.37.0" imagePullPolicy: IfNotPresent env: diff --git a/charts/headlamp/tests/expected_templates/volumes-added.yaml b/charts/headlamp/tests/expected_templates/volumes-added.yaml index 662717fc1ac..8e47e86cb3e 100644 --- a/charts/headlamp/tests/expected_templates/volumes-added.yaml +++ b/charts/headlamp/tests/expected_templates/volumes-added.yaml @@ -6,10 +6,10 @@ metadata: name: headlamp namespace: default labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm --- # Source: headlamp/templates/secret.yaml @@ -27,10 +27,10 @@ kind: ClusterRoleBinding metadata: name: headlamp-admin labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm roleRef: apiGroup: rbac.authorization.k8s.io @@ -48,10 +48,10 @@ metadata: name: headlamp namespace: default labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm spec: type: ClusterIP @@ -75,10 +75,10 @@ metadata: name: headlamp namespace: default labels: - helm.sh/chart: headlamp-0.36.0 + helm.sh/chart: headlamp-0.37.0 app.kubernetes.io/name: headlamp app.kubernetes.io/instance: headlamp - app.kubernetes.io/version: "0.36.0" + app.kubernetes.io/version: "0.37.0" app.kubernetes.io/managed-by: Helm spec: replicas: 1 @@ -103,7 +103,7 @@ spec: runAsGroup: 101 runAsNonRoot: true runAsUser: 100 - image: "ghcr.io/headlamp-k8s/headlamp:v0.36.0" + image: "ghcr.io/headlamp-k8s/headlamp:v0.37.0" imagePullPolicy: IfNotPresent env: From bb953ef1b76fcedb46da1f2672cadfc3e27ae548 Mon Sep 17 00:00:00 2001 From: Evangelos Skopelitis Date: Fri, 31 Oct 2025 14:45:14 -0400 Subject: [PATCH 64/72] docs: Separate OIDC scopes by comma These changes separate the scopes in the OIDC docs by commas rather than spaces. --- docs/installation/in-cluster/azure-entra-id/index.md | 6 ++++-- docs/installation/in-cluster/oidc.md | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/installation/in-cluster/azure-entra-id/index.md b/docs/installation/in-cluster/azure-entra-id/index.md index 6d4e8856974..bc5fd05343e 100644 --- a/docs/installation/in-cluster/azure-entra-id/index.md +++ b/docs/installation/in-cluster/azure-entra-id/index.md @@ -141,7 +141,7 @@ config: clientID: "" clientSecret: "" issuerURL: "https://login.microsoftonline.com//v2.0" - scopes: "6dae42f8-4368-4678-94ff-3960e28e3630/user.read openid email profile" + scopes: "6dae42f8-4368-4678-94ff-3960e28e3630/user.read,openid,email,profile" validatorClientID: "6dae42f8-4368-4678-94ff-3960e28e3630" validatorIssuerURL: "https://sts.windows.net//" useAccessToken: true @@ -149,6 +149,8 @@ config: Replace ``,``, and ``, with your specific Azure Entra ID app registration details obtained in the steps above. +> Note: The `scopes` string must be comma separated so each scope is passed individually to the OIDC provider. + 4. Save the `values.yaml` file and Install Headlamp using helm with the following commands: ```shell @@ -170,4 +172,4 @@ kubectl port-forward svc/headlamp-oidc 8000:80 -n headlamp 8. Open your web browser and go to . Click on "sign-in." After completing the login flow successfully, you'll gain access to your Kubernetes cluster using Headlamp. -![Headlamp Login](./headlamp_auth_screen.png) \ No newline at end of file +![Headlamp Login](./headlamp_auth_screen.png) diff --git a/docs/installation/in-cluster/oidc.md b/docs/installation/in-cluster/oidc.md index ec1afde4dc4..d0eaba255da 100644 --- a/docs/installation/in-cluster/oidc.md +++ b/docs/installation/in-cluster/oidc.md @@ -96,7 +96,7 @@ For quick reference if you are already familiar with setting up Entra ID, - Set `-oidc-client-id` to your Azure App Registration's clientID - Set `-oidc-client-secret` to your Azure App Registration's clientSecret - Set `-oidc-idp-issuer-url` to `https://login.microsoftonline.com//v2.0` -- Set `-oidc-scopes` to `6dae42f8-4368-4678-94ff-3960e28e3630/user.read openid email profile` +- Set `-oidc-scopes` to `6dae42f8-4368-4678-94ff-3960e28e3630/user.read,openid,email,profile` - Set `--oidc-validator-idp-issuer-url` to `https://sts.windows.net//` - Set `-oidc-validator-client-id` to `6dae42f8-4368-4678-94ff-3960e28e3630` - Set `-oidc-use-access-token=true` From 05861ac0b362afe44b0d80d451bdac9e11a2ec64 Mon Sep 17 00:00:00 2001 From: Joaquim Rocha Date: Fri, 31 Oct 2025 10:52:50 +0000 Subject: [PATCH 65/72] github: Remove the winget action The manifest is bumped automatically from Winget's side. --- .github/workflows/pr-to-update-winget.yml | 112 ---------------------- 1 file changed, 112 deletions(-) delete mode 100644 .github/workflows/pr-to-update-winget.yml diff --git a/.github/workflows/pr-to-update-winget.yml b/.github/workflows/pr-to-update-winget.yml deleted file mode 100644 index 3eac5891e49..00000000000 --- a/.github/workflows/pr-to-update-winget.yml +++ /dev/null @@ -1,112 +0,0 @@ -name: PR for updating winget - -# This action will run after a tag starting with "v" is published -on: - push: - tags: - - "v*" - workflow_dispatch: - -permissions: - contents: read - -jobs: - winget-update: - permissions: - contents: write # for Git to git push - pull-requests: write # for creating PRs - runs-on: ubuntu-latest - steps: - - name: Checkout Headlamp - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - with: - token: ${{ secrets.KINVOLK_REPOS_TOKEN }} - # we need the full history for the git tag command, so fetch all the branches - fetch-depth: 0 - - - name: Configure Git - run: | - user=${{github.actor}} - if [ -z $user ]; then - user=vyncent-t - fi - git config --global user.name "$user" - git config --global user.email "$user@users.noreply.github.com" - - # Set up Node.js environment, pay attention to the version - # Some features might not be available in older versions - - name: Create node.js environment - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 - with: - node-version: "21" - - # Install the dependencies for the winget script - - name: Install winget dependencies - run: | - cd $GITHUB_WORKSPACE/app - npm ci - - # We set the latest tag as an environment variable before we use it in the next steps - # note that we have to echo the variable to the environment file to make it available in the next steps - - name: Set latest tag - run: | - echo "Setting latest tag" - latestTag=$(git tag --list --sort=version:refname 'v*' | tail -1) - # Remove the 'v' from the tag - latestTag=${latestTag#v} - echo "LATEST_HEADLAMP_TAG=$latestTag" >> $GITHUB_ENV - echo $latestTag - - # checkout the winget-pkgs repository - - name: Checkout winget-pkgs - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - with: - repository: headlamp-k8s/winget-pkgs - path: winget-pkgs - token: ${{ secrets.KINVOLK_REPOS_TOKEN }} - # we need the full history for the git tag command, so fetch all the branches - fetch-depth: 0 - - # Run the winget script - - name: Create winget package - run: | - echo "Running winget script" - echo "Repository: ${{ github.repository }}" - echo "Workspace: ${GITHUB_WORKSPACE}" - echo $GITHUB_WORKSPACE - pwd - echo "creating winget pkgs for ${LATEST_HEADLAMP_TAG}" - cd $GITHUB_WORKSPACE/app/windows/winget - node winget-create.js $LATEST_HEADLAMP_TAG $GITHUB_WORKSPACE/winget-pkgs/manifests/h/Headlamp/Headlamp - echo "Script finished" - - - name: Create PR branch - run: | - user=${{github.actor}} - if [ -z $user ]; then - user=vyncent-t - fi - echo "Creating PR branch" - echo "Repository: ${{ github.repository }}" - echo "Workspace: ${GITHUB_WORKSPACE}" - pwd - echo "moving to winget-pkgs directory" - cd $GITHUB_WORKSPACE/winget-pkgs - pwd - ls - echo "moving to Headlamp directory" - cd $GITHUB_WORKSPACE/winget-pkgs/manifests/h/Headlamp/Headlamp - pwd - ls - git checkout -b "hl-ci-winget-update-$LATEST_HEADLAMP_TAG" - git add . - git commit -s -m "Update winget package $LATEST_HEADLAMP_TAG" - git push origin "hl-ci-winget-update-$LATEST_HEADLAMP_TAG" - env: - GITHUB_TOKEN: ${{ secrets.KINVOLK_REPOS_TOKEN }} - - - name: Create Pull Request - run: | - echo "Create pull request" - echo "continue with the following link" - echo "https://github.com/headlamp-k8s/winget-pkgs/pull/new/hl-ci-winget-update-$LATEST_HEADLAMP_TAG" From 3bf0fa30c121327861143b44c235d65a9c71b971 Mon Sep 17 00:00:00 2001 From: Joaquim Rocha Date: Fri, 31 Oct 2025 11:16:32 +0000 Subject: [PATCH 66/72] github: Remove homebrew action Homebrew has the ability to bump Casks from their side. --- .github/workflows/pr-to-update-homebrew.yml | 103 -------------------- 1 file changed, 103 deletions(-) delete mode 100644 .github/workflows/pr-to-update-homebrew.yml diff --git a/.github/workflows/pr-to-update-homebrew.yml b/.github/workflows/pr-to-update-homebrew.yml deleted file mode 100644 index 7c0c5816f6d..00000000000 --- a/.github/workflows/pr-to-update-homebrew.yml +++ /dev/null @@ -1,103 +0,0 @@ -name: PR to update homebrew - -# This action will run after a tag starting with "v" is published -on: - push: - tags: - - 'v*' - workflow_dispatch: - -env: - LATEST_HEADLAMP_TAG: latest -permissions: - contents: read - -jobs: - create_pr_to_upgrade_homebrew: - name: Create PR to upgrade homebrew - runs-on: ubuntu-latest - permissions: - contents: write # needed to push a branch - pull-requests: write # needed to open a pull request - - steps: - - name: Checkout headlamp repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - with: - path: headlamp - fetch-depth: 0 - - name: Configure Git - run: | - user=${{github.actor}} - if [ -z $user ]; then - user=yolossn - fi - git config --global user.name "$user" - git config --global user.email "$user@users.noreply.github.com" - - name: Get headlamp latest tag - run: | - cd headlamp - latestTag=$(git tag --list --sort=version:refname 'v*' | tail -1) - echo "LATEST_HEADLAMP_TAG=$latestTag" >> $GITHUB_ENV - echo $latestTag - - name: Sync homebrew-cask fork from upstream - run: | - gh repo sync headlamp-k8s/homebrew-cask - env: - GITHUB_TOKEN: ${{ secrets.KINVOLK_REPOS_TOKEN }} - - name: Check out homebrew-cask repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - with: - repository: headlamp-k8s/homebrew-cask - path: homebrew-cask - token: ${{ secrets.KINVOLK_REPOS_TOKEN }} - fetch-depth: 0 - - name: Update headlamp version in homebrew-cask - run: | - user=${{github.actor}} - HEADLAMP_VERSION=${LATEST_HEADLAMP_TAG:1} - BRANCH_NAME="hl-ci-update_headlamp_$HEADLAMP_VERSION" - if [ -z $user ]; then - user=yolossn - fi - cd homebrew-cask - if git branch -l | grep -q "$BRANCH_NAME"; then - echo "deleting old branch from local to avoid conflict" - git branch -D "$BRANCH_NAME" - fi - if git branch -a | grep -q "origin/$BRANCH_NAME"; then - echo "deleting old branch from remote to avoid conflict" - git push origin --delete "$BRANCH_NAME" - fi - wget "https://github.com/kubernetes-sigs/headlamp/releases/download/$LATEST_HEADLAMP_TAG/checksums.txt" - ARM_SHA=$(cat checksums.txt | grep .arm64.dmg | awk -F" " '{print $1}') - INTEL_SHA=$(cat checksums.txt | grep .x64.dmg | awk -F" " '{print $1}') - git checkout -b "$BRANCH_NAME" - sed -i "s/version\ .*/version \"$HEADLAMP_VERSION\"/g" ./Casks/h/headlamp.rb - if [ $ARM_SHA ]; then - echo "replacing ARM SHA" - sed -i "s/sha256 arm:\ \".*/sha256 arm:\ \"$ARM_SHA\",/g" ./Casks/h/headlamp.rb - fi - if [ $INTEL_SHA ]; then - echo "replacing Intel SHA" - sed -i "s/ intel:\ \".*/ intel:\ \"$INTEL_SHA\"/g" ./Casks/h/headlamp.rb - fi - git diff - rm ./checksums.txt - git add ./Casks/h/headlamp.rb - git status - git commit --signoff -m "Update Headlamp version to $HEADLAMP_VERSION" - git status - git log -1 - git push origin "$BRANCH_NAME" -f - gh pr create \ - --title "Upgrade Headlamp version to $HEADLAMP_VERSION" \ - --repo "Homebrew/homebrew-cask" \ - --head "headlamp-k8s:$BRANCH_NAME" \ - --base "master" \ - --assignee "$user" \ - --body "Upgrade Headlamp version to $HEADLAMP_VERSION - cc: @$user" \ - env: - LATEST_HEADLAMP_TAG: ${{ env.LATEST_HEADLAMP_TAG }} - GITHUB_TOKEN: ${{ secrets.KINVOLK_REPOS_TOKEN }} From 765570ef0958ebf388869fd1a16938e8e34503b8 Mon Sep 17 00:00:00 2001 From: Joaquim Rocha Date: Fri, 31 Oct 2025 10:44:36 +0000 Subject: [PATCH 67/72] github: Add action to push chocolatey automatically Can also be triggered manually. --- .github/workflows/push-chocolatey-pkg.yml | 50 +++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 .github/workflows/push-chocolatey-pkg.yml diff --git a/.github/workflows/push-chocolatey-pkg.yml b/.github/workflows/push-chocolatey-pkg.yml new file mode 100644 index 00000000000..306c06b96ce --- /dev/null +++ b/.github/workflows/push-chocolatey-pkg.yml @@ -0,0 +1,50 @@ +name: Push Chocolatey Package + +# This action will run when a PR that updates the Chocolatey package is merged to main +# It can also be triggered manually +on: + push: + branches: + - main + paths: + - 'app/windows/chocolatey/headlamp.nuspec' + - 'app/windows/chocolatey/tools/chocolateyinstall.ps1' + workflow_dispatch: + +permissions: + contents: read + +jobs: + push-chocolatey: + runs-on: windows-latest + steps: + - name: Checkout Headlamp + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + with: + ref: main + + - name: Install Chocolatey + run: | + Set-ExecutionPolicy Bypass -Scope Process -Force + [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072 + iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) + shell: powershell + + - name: Pack Chocolatey Package + run: | + cd app/windows/chocolatey + choco pack + shell: powershell + + - name: Push to Chocolatey + run: | + cd app/windows/chocolatey + $nupkg = Get-ChildItem -Filter *.nupkg | Select-Object -First 1 + if ($nupkg) { + Write-Host "Pushing package: $($nupkg.Name)" + choco push $nupkg.FullName --source https://push.chocolatey.org/ --key ${{ secrets.CHOCOLATEY_API_KEY }} + } else { + Write-Error "No .nupkg file found" + exit 1 + } + shell: powershell From 2a7e13c02d59634f846f46d0aad1f9f285cb4d72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Dudfield?= Date: Mon, 3 Nov 2025 10:58:05 -0300 Subject: [PATCH 68/72] backend: server: headlamp: Fix meUserInfoURL and log config items Before there was a mistake with passing through the meUserInfoURL. --- backend/cmd/headlamp.go | 6 +++--- backend/cmd/server.go | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/backend/cmd/headlamp.go b/backend/cmd/headlamp.go index 69b2f79fa6e..3da635de3cf 100644 --- a/backend/cmd/headlamp.go +++ b/backend/cmd/headlamp.go @@ -93,8 +93,8 @@ type HeadlampConfig struct { meEmailPaths string // meGroupsPaths lists the JMESPath expressions tried for the groups in /clusters/{cluster}/me. meGroupsPaths string - // MeUserInfoURL is the URL to fetch additional user info for the /me endpoint. /oauth2/userinfo - MeUserInfoURL string + // meUserInfoURL is the URL to fetch additional user info for the /me endpoint. /oauth2/userinfo + meUserInfoURL string } const DrainNodeCacheTTL = 20 // seconds @@ -496,7 +496,7 @@ func createHeadlampHandler(config *HeadlampConfig) http.Handler { UsernamePaths: config.meUsernamePaths, EmailPaths: config.meEmailPaths, GroupsPaths: config.meGroupsPaths, - UserInfoURL: config.MeUserInfoURL, + UserInfoURL: config.meUserInfoURL, }), ).Methods("GET") diff --git a/backend/cmd/server.go b/backend/cmd/server.go index b1865175a68..73b434746e6 100644 --- a/backend/cmd/server.go +++ b/backend/cmd/server.go @@ -120,6 +120,7 @@ func createHeadlampConfig(conf *config.Config) *HeadlampConfig { meUsernamePaths: conf.MeUsernamePath, meEmailPaths: conf.MeEmailPath, meGroupsPaths: conf.MeGroupsPath, + meUserInfoURL: conf.MeUserInfoURL, cache: cache, multiplexer: multiplexer, telemetryConfig: buildTelemetryConfig(conf), From cb37dcd224cff45459a1de57bc8058d74d484de7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Dudfield?= Date: Mon, 3 Nov 2025 10:58:49 -0300 Subject: [PATCH 69/72] backend: headlamp: Add logging of missing config items To make it easier for people to see what things are being configured. --- backend/cmd/headlamp.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/backend/cmd/headlamp.go b/backend/cmd/headlamp.go index 3da635de3cf..4bda7d1ce67 100644 --- a/backend/cmd/headlamp.go +++ b/backend/cmd/headlamp.go @@ -392,6 +392,13 @@ func createHeadlampHandler(config *HeadlampConfig) http.Handler { logger.Log(logger.LevelInfo, nil, nil, "Proxy URLs: "+fmt.Sprint(config.ProxyURLs)) logger.Log(logger.LevelInfo, nil, nil, "TLS certificate path: "+config.TLSCertPath) logger.Log(logger.LevelInfo, nil, nil, "TLS key path: "+config.TLSKeyPath) + logger.Log(logger.LevelInfo, nil, nil, "me Username Paths: "+config.meUsernamePaths) + logger.Log(logger.LevelInfo, nil, nil, "me Email Paths: "+config.meEmailPaths) + logger.Log(logger.LevelInfo, nil, nil, "me Groups Paths: "+config.meGroupsPaths) + logger.Log(logger.LevelInfo, nil, nil, "me User Info URL: "+config.meUserInfoURL) + logger.Log(logger.LevelInfo, nil, nil, "Base URL: "+config.BaseURL) + logger.Log(logger.LevelInfo, nil, nil, "Use In Cluster: "+fmt.Sprint(config.UseInCluster)) + logger.Log(logger.LevelInfo, nil, nil, "Watch Plugins Changes: "+fmt.Sprint(config.WatchPluginsChanges)) plugins.PopulatePluginsCache(config.StaticPluginDir, config.PluginDir, config.cache) From 6f22306f277f75aeca10bb71f2cb591f719eeb55 Mon Sep 17 00:00:00 2001 From: Oleksandr Dubenko Date: Tue, 4 Nov 2025 11:12:13 -0500 Subject: [PATCH 70/72] frontend: Sidebar: Remove extra border --- frontend/src/components/Sidebar/Sidebar.tsx | 1 + .../__snapshots__/Sidebar.HomeSidebarClosed.stories.storyshot | 2 +- .../__snapshots__/Sidebar.HomeSidebarOpen.stories.storyshot | 2 +- .../Sidebar.InClusterSidebarClosed.stories.storyshot | 2 +- .../Sidebar.InClusterSidebarOpen.stories.storyshot | 2 +- .../Sidebar.SelectedItemWithSidebarOmitted.stories.storyshot | 2 +- 6 files changed, 6 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/Sidebar/Sidebar.tsx b/frontend/src/components/Sidebar/Sidebar.tsx index e8c5d0981e4..4c870ba7b0d 100644 --- a/frontend/src/components/Sidebar/Sidebar.tsx +++ b/frontend/src/components/Sidebar/Sidebar.tsx @@ -385,6 +385,7 @@ export const PureSidebar = memo( variant={isTemporaryDrawer ? 'temporary' : 'permanent'} PaperProps={{ sx: { + borderTop: 'none', position: 'initial', }, }} diff --git a/frontend/src/components/Sidebar/__snapshots__/Sidebar.HomeSidebarClosed.stories.storyshot b/frontend/src/components/Sidebar/__snapshots__/Sidebar.HomeSidebarClosed.stories.storyshot index 45ec8b0137b..f47c69b93db 100644 --- a/frontend/src/components/Sidebar/__snapshots__/Sidebar.HomeSidebarClosed.stories.storyshot +++ b/frontend/src/components/Sidebar/__snapshots__/Sidebar.HomeSidebarClosed.stories.storyshot @@ -8,7 +8,7 @@ class="MuiDrawer-root MuiDrawer-docked css-ohfc2x-MuiDrawer-docked" >