diff --git a/RELEASE_GUIDE.md b/RELEASE_GUIDE.md index f89bfa3295..d46911b0ce 100644 --- a/RELEASE_GUIDE.md +++ b/RELEASE_GUIDE.md @@ -15,8 +15,8 @@ All releases will be made by creating a PR which bumps the version field in the 1. If you are making a patch release (or a prerelease for one) make sure you are on the `release/v.` branch. 1. Run `npm run create-release-pr`. 1. Pick the PRs that you want to include in this release using the keys listed. - - If you are making a patch release this might include fixing up some cherry-picking of commits. These actions should be done in a separate terminal. - - If a package version is having a major version bump then `npm` will complain about `peerDependency` conflicts. These will have to be fixed up separately. + - If you are making a patch release this might include fixing up some cherry-picking of commits. These actions should be done in a separate terminal. + - If a package version is having a major version bump then `npm` will complain about `peerDependency` conflicts. These will have to be fixed up separately. 1. Once the PR is created, approved, and then merged the `Release Open Lens` workflow will create a tag and release for you. 1. If you are making a major or minor release, create a `release/v.` branch and push it to `origin` so that future patch releases can be made from it. 1. If you released a major or minor version, create a new patch milestone and move all bug issues to that milestone and all enhancement issues to the next minor milestone. diff --git a/package-lock.json b/package-lock.json index fc13b55540..946b5f8168 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2099,92 +2099,25 @@ } }, "node_modules/@electron/get": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@electron/get/-/get-1.14.1.tgz", - "integrity": "sha512-BrZYyL/6m0ZXz/lDxy/nlVhQz+WF+iPS6qXolEU8atw7h6v1aYkjwJZ63m+bJMBTxDE66X+r2tPS4a/8C82sZw==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.2.tgz", + "integrity": "sha512-eFZVFoRXb3GFGd7Ak7W4+6jBl9wBtiZ4AaYOse97ej6mKj5tkyO0dUnUChs1IhJZtx1BENo4/p4WUTXpi6vT+g==", "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", - "got": "^9.6.0", + "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "engines": { - "node": ">=8.6" + "node": ">=12" }, "optionalDependencies": { - "global-agent": "^3.0.0", - "global-tunnel-ng": "^2.7.1" + "global-agent": "^3.0.0" } }, - "node_modules/@electron/get/node_modules/@sindresorhus/is": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", - "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/@electron/get/node_modules/@szmarczak/http-timer": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", - "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", - "dependencies": { - "defer-to-connect": "^1.0.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@electron/get/node_modules/cacheable-request": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", - "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", - "dependencies": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^3.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^4.1.0", - "responselike": "^1.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@electron/get/node_modules/cacheable-request/node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@electron/get/node_modules/decompress-response": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", - "integrity": "sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==", - "dependencies": { - "mimic-response": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@electron/get/node_modules/defer-to-connect": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", - "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==" - }, "node_modules/@electron/get/node_modules/fs-extra": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", @@ -2198,51 +2131,6 @@ "node": ">=6 <7 || >=8" } }, - "node_modules/@electron/get/node_modules/get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@electron/get/node_modules/got": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", - "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", - "dependencies": { - "@sindresorhus/is": "^0.14.0", - "@szmarczak/http-timer": "^1.1.2", - "cacheable-request": "^6.0.0", - "decompress-response": "^3.3.0", - "duplexer3": "^0.1.4", - "get-stream": "^4.1.0", - "lowercase-keys": "^1.0.1", - "mimic-response": "^1.0.1", - "p-cancelable": "^1.0.0", - "to-readable-stream": "^1.0.0", - "url-parse-lax": "^3.0.0" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/@electron/get/node_modules/got/node_modules/lowercase-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", - "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@electron/get/node_modules/json-buffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", - "integrity": "sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==" - }, "node_modules/@electron/get/node_modules/jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", @@ -2251,55 +2139,6 @@ "graceful-fs": "^4.1.6" } }, - "node_modules/@electron/get/node_modules/keyv": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", - "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", - "dependencies": { - "json-buffer": "3.0.0" - } - }, - "node_modules/@electron/get/node_modules/normalize-url": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz", - "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==", - "engines": { - "node": ">=8" - } - }, - "node_modules/@electron/get/node_modules/p-cancelable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", - "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==", - "engines": { - "node": ">=6" - } - }, - "node_modules/@electron/get/node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/@electron/get/node_modules/responselike": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", - "integrity": "sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==", - "dependencies": { - "lowercase-keys": "^1.0.0" - } - }, - "node_modules/@electron/get/node_modules/responselike/node_modules/lowercase-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", - "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/@electron/get/node_modules/semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", @@ -4685,6 +4524,10 @@ "resolved": "packages/bump-version-for-cron", "link": true }, + "node_modules/@k8slens/cluster-settings": { + "resolved": "packages/cluster-settings", + "link": true + }, "node_modules/@k8slens/computed-channel": { "resolved": "packages/technical-features/messaging/computed-channel", "link": true @@ -7977,6 +7820,16 @@ "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==" }, + "node_modules/@types/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==", + "optional": true, + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "5.55.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.55.0.tgz", @@ -11054,6 +10907,7 @@ "version": "1.6.2", "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, "engines": [ "node >= 0.8" ], @@ -11067,12 +10921,14 @@ "node_modules/concat-stream/node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true }, "node_modules/concat-stream/node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -11087,6 +10943,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, "dependencies": { "safe-buffer": "~5.1.0" } @@ -11178,7 +11035,7 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.12.tgz", "integrity": "sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==", - "devOptional": true, + "dev": true, "dependencies": { "ini": "^1.3.4", "proto-list": "~1.2.1" @@ -12657,11 +12514,6 @@ "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", "dev": true }, - "node_modules/duplexer3": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.5.tgz", - "integrity": "sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==" - }, "node_modules/duplexify": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", @@ -12736,20 +12588,20 @@ } }, "node_modules/electron": { - "version": "19.1.9", - "resolved": "https://registry.npmjs.org/electron/-/electron-19.1.9.tgz", - "integrity": "sha512-XT5LkTzIHB+ZtD3dTmNnKjVBWrDWReCKt9G1uAFLz6uJMEVcIUiYO+fph5pLXETiBw/QZBx8egduMEfIccLx+g==", + "version": "22.3.3", + "resolved": "https://registry.npmjs.org/electron/-/electron-22.3.3.tgz", + "integrity": "sha512-+ZJDVfyhw7J2A46/kGKscktIhzOisTeJKrUBJLXa7PTB+U+cwyoxCBIaIOnDsdicBCX4nAc1mo6YMQjQQdAmgw==", "hasInstallScript": true, "dependencies": { - "@electron/get": "^1.14.1", + "@electron/get": "^2.0.0", "@types/node": "^16.11.26", - "extract-zip": "^1.0.3" + "extract-zip": "^2.0.1" }, "bin": { "electron": "cli.js" }, "engines": { - "node": ">= 8.6" + "node": ">= 12.20.55" } }, "node_modules/electron-builder": { @@ -13054,7 +12906,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "devOptional": true, + "dev": true, "engines": { "node": ">= 0.8" } @@ -15013,42 +14865,46 @@ } }, "node_modules/extract-zip": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz", - "integrity": "sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", "dependencies": { - "concat-stream": "^1.6.2", - "debug": "^2.6.9", - "mkdirp": "^0.5.4", + "debug": "^4.1.1", + "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "bin": { "extract-zip": "cli.js" - } - }, - "node_modules/extract-zip/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/extract-zip/node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dependencies": { - "minimist": "^1.2.6" }, - "bin": { - "mkdirp": "bin/cmd.js" + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" } }, - "node_modules/extract-zip/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "node_modules/extract-zip/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/extract-zip/node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } }, "node_modules/extsprintf": { "version": "1.3.0", @@ -16106,22 +15962,6 @@ "node": ">=10.0" } }, - "node_modules/global-tunnel-ng": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/global-tunnel-ng/-/global-tunnel-ng-2.7.1.tgz", - "integrity": "sha512-4s+DyciWBV0eK148wqXxcmVAbFVPqtc3sEtUE/GTQfuU80rySLcMhUmHKSHI7/LDj8q0gDYI1lIhRRB7ieRAqg==", - "optional": true, - "peer": true, - "dependencies": { - "encodeurl": "^1.0.2", - "lodash": "^4.17.10", - "npm-conf": "^1.1.3", - "tunnel": "^0.0.6" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -24084,30 +23924,6 @@ "npm-normalize-package-bin": "^1.0.1" } }, - "node_modules/npm-conf": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/npm-conf/-/npm-conf-1.1.3.tgz", - "integrity": "sha512-Yic4bZHJOt9RCFbRP3GgpqhScOY4HH3V2P8yBj6CeYq118Qr+BLXqT2JvpJ00mryLESpgOxf5XlFv4ZjXxLScw==", - "optional": true, - "peer": true, - "dependencies": { - "config-chain": "^1.1.11", - "pify": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/npm-conf/node_modules/pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", - "optional": true, - "peer": true, - "engines": { - "node": ">=4" - } - }, "node_modules/npm-install-checks": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-5.0.0.tgz", @@ -28525,14 +28341,6 @@ "node": ">= 0.8.0" } }, - "node_modules/prepend-http": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", - "integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==", - "engines": { - "node": ">=4" - } - }, "node_modules/prettier": { "version": "2.8.4", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.4.tgz", @@ -28727,7 +28535,7 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", - "devOptional": true + "dev": true }, "node_modules/protocols": { "version": "2.0.1", @@ -30131,9 +29939,9 @@ } }, "node_modules/rimraf/node_modules/glob": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-9.2.1.tgz", - "integrity": "sha512-Pxxgq3W0HyA3XUvSXcFhRSs+43Jsx0ddxcFrbjxNGkL2Ak5BAUBxLqI5G6ADDeCHLfzzXFhe0b1yYcctGmytMA==", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-9.3.0.tgz", + "integrity": "sha512-EAZejC7JvnQINayvB/7BJbpZpNOJ8Lrw2OZNEvQxe0vaLn1SuwMcfV7/MNaX8L/T0wmptBFI4YMtDvSBxYDc7w==", "dev": true, "dependencies": { "fs.realpath": "^1.0.0", @@ -32431,14 +32239,6 @@ "node": ">=0.10.0" } }, - "node_modules/to-readable-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", - "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==", - "engines": { - "node": ">=6" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -32751,16 +32551,6 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, - "node_modules/tunnel": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", - "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", - "optional": true, - "peer": true, - "engines": { - "node": ">=0.6.11 <=0.7.0 || >=0.7.3" - } - }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -32846,7 +32636,8 @@ "node_modules/typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "dev": true }, "node_modules/typedoc": { "version": "0.23.25", @@ -33170,17 +32961,6 @@ "requires-port": "^1.0.0" } }, - "node_modules/url-parse-lax": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", - "integrity": "sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ==", - "dependencies": { - "prepend-http": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/use-isomorphic-layout-effect": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", @@ -34366,6 +34146,25 @@ "integrity": "sha512-ZOzvDRWp8dCVBmgnkIqYCArgdFOO9YzocZp8Ra25N/RStKiWvMOXHMz+GjSeVNe5TstaTmTWPucGJkDw0XXJWA==", "dev": true }, + "packages/cluster-settings": { + "name": "@k8slens/cluster-settings", + "version": "6.5.0-alpha.1", + "license": "MIT", + "devDependencies": { + "@ogre-tools/injectable": "^15.1.2", + "@swc/cli": "^0.1.61", + "@swc/core": "^1.3.37", + "@types/node": "^16.18.11", + "@types/semver": "^7.3.13", + "rimraf": "^4.1.2" + } + }, + "packages/cluster-settings/node_modules/@types/node": { + "version": "16.18.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.18.tgz", + "integrity": "sha512-fwGw1uvQAzabxL1pyoknPlJIF2t7+K90uTqynleKRx24n3lYcxWa3+KByLhgkF8GEAK2c7hC8Ki0RkNM5H15jQ==", + "dev": true + }, "packages/core": { "name": "@k8slens/core", "version": "6.5.0-alpha.3", @@ -34374,6 +34173,7 @@ "@astronautlabs/jsonpath": "^1.1.0", "@hapi/call": "^9.0.1", "@hapi/subtext": "^7.1.0", + "@k8slens/cluster-settings": "^6.5.0-alpha.1", "@k8slens/node-fetch": "^6.5.0-alpha.1", "@kubernetes/client-node": "^0.18.1", "@material-ui/styles": "^4.11.5", @@ -34511,7 +34311,7 @@ "css-loader": "^6.7.3", "deepdash": "^5.3.9", "dompurify": "^2.4.4", - "electron": "^19.1.9", + "electron": "^22.3.3", "electron-builder": "^23.6.0", "esbuild": "^0.17.8", "esbuild-loader": "^2.21.0", @@ -36698,7 +36498,7 @@ "copy-webpack-plugin": "^11.0.0", "cross-env": "^7.0.3", "css-loader": "^6.7.2", - "electron": "^19.1.9", + "electron": "^22.3.3", "electron-builder": "^23.6.0", "electron-notarize": "^0.3.0", "esbuild-loader": "^2.20.0", @@ -37192,7 +36992,7 @@ "@k8slens/feature-core": "^6.5.0-alpha.0", "@ogre-tools/injectable": "^15.1.2", "@ogre-tools/injectable-extension-for-auto-registration": "^15.1.2", - "electron": "^19.1.9" + "electron": "^22.3.3" } }, "packages/technical-features/application/legacy-extensions": { @@ -37269,7 +37069,7 @@ "@k8slens/messaging": "^1.0.0-alpha.1", "@ogre-tools/injectable": "^15.1.2", "@ogre-tools/injectable-extension-for-auto-registration": "^15.1.2", - "electron": "^19.1.8", + "electron": "^22.3.3", "lodash": "^4.17.21" } }, @@ -37287,7 +37087,7 @@ "@k8slens/startable-stoppable": "^1.0.0-alpha.1", "@ogre-tools/injectable": "^15.1.2", "@ogre-tools/injectable-extension-for-auto-registration": "^15.1.2", - "electron": "^19.1.8", + "electron": "^22.3.3", "lodash": "^4.17.21" } }, diff --git a/packages/cluster-settings/.swcrc b/packages/cluster-settings/.swcrc new file mode 100644 index 0000000000..8e7a530f16 --- /dev/null +++ b/packages/cluster-settings/.swcrc @@ -0,0 +1,9 @@ +{ + "$schema": "https://json.schemastore.org/swcrc", + "jsc": { + "parser": { + "syntax": "typescript" + }, + "target": "es2022" + } +} diff --git a/packages/cluster-settings/README.md b/packages/cluster-settings/README.md new file mode 100644 index 0000000000..c3d3b890f4 --- /dev/null +++ b/packages/cluster-settings/README.md @@ -0,0 +1,3 @@ +# Description + +The package exports tokens needed for external configuration of Cluster Settings page. diff --git a/packages/cluster-settings/package.json b/packages/cluster-settings/package.json new file mode 100644 index 0000000000..199347dcf4 --- /dev/null +++ b/packages/cluster-settings/package.json @@ -0,0 +1,31 @@ +{ + "name": "@k8slens/cluster-settings", + "version": "6.5.0-alpha.1", + "description": "Injection token exporter for cluster settings configuration", + "license": "MIT", + "private": false, + "mode": "production", + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "clean": "rimraf dist/", + "generate-types": "tsc --d --declarationDir ./dist --declarationMap --emitDeclarationOnly", + "build": "npm run generate-types && swc ./src/index.ts -d ./dist", + "prepare:test": "npm run build" + }, + "devDependencies": { + "@ogre-tools/injectable": "^15.1.2", + "@swc/cli": "^0.1.61", + "@swc/core": "^1.3.37", + "@types/node": "^16.18.11", + "@types/semver": "^7.3.13", + "rimraf": "^4.1.2" + } +} diff --git a/packages/cluster-settings/src/index.ts b/packages/cluster-settings/src/index.ts new file mode 100644 index 0000000000..181c69d46a --- /dev/null +++ b/packages/cluster-settings/src/index.ts @@ -0,0 +1,30 @@ +import { getInjectionToken } from "@ogre-tools/injectable"; + +type ClusterPreferences = { + clusterName?: string; + icon?: string | null; +} + +export interface ClusterIconMenuItem { + id: string; + title: string; + disabled?: (preferences: ClusterPreferences) => boolean; + onClick: (preferences: ClusterPreferences) => void; +} + +export interface ClusterIconSettingComponentProps { + preferences: ClusterPreferences; +} + +export interface ClusterIconSettingsComponent { + id: string; + Component: React.ComponentType; +} + +export const clusterIconSettingsMenuInjectionToken = getInjectionToken({ + id: "cluster-icon-settings-menu-injection-token", +}); + +export const clusterIconSettingsComponentInjectionToken = getInjectionToken({ + id: "cluster-icon-settings-component-injection-token", +}); \ No newline at end of file diff --git a/packages/cluster-settings/tsconfig.json b/packages/cluster-settings/tsconfig.json new file mode 100644 index 0000000000..534a5fd447 --- /dev/null +++ b/packages/cluster-settings/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist/", + "paths": { + "*": [ + "node_modules/*", + "types/*" + ] + }, + }, + "include": [ + "src/**/*", + ], + "exclude": [ + "node_modules", + ] +} diff --git a/packages/core/package.json b/packages/core/package.json index 15c79d774d..ad6e7e45ae 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -120,6 +120,7 @@ "@astronautlabs/jsonpath": "^1.1.0", "@hapi/call": "^9.0.1", "@hapi/subtext": "^7.1.0", + "@k8slens/cluster-settings": "^6.5.0-alpha.1", "@k8slens/node-fetch": "^6.5.0-alpha.1", "@kubernetes/client-node": "^0.18.1", "@material-ui/styles": "^4.11.5", @@ -257,7 +258,7 @@ "css-loader": "^6.7.3", "deepdash": "^5.3.9", "dompurify": "^2.4.4", - "electron": "^19.1.9", + "electron": "^22.3.3", "electron-builder": "^23.6.0", "esbuild": "^0.17.8", "esbuild-loader": "^2.21.0", diff --git a/packages/core/src/common/app-paths/app-path-names.ts b/packages/core/src/common/app-paths/app-path-names.ts index 8e3d2c440e..b7b829a5e3 100644 --- a/packages/core/src/common/app-paths/app-path-names.ts +++ b/packages/core/src/common/app-paths/app-path-names.ts @@ -11,7 +11,7 @@ export const pathNames: PathName[] = [ "home", "appData", "userData", - "cache", + "sessionData", "temp", "exe", "module", diff --git a/packages/core/src/common/app-paths/app-paths.test.ts b/packages/core/src/common/app-paths/app-paths.test.ts index f847346dad..851ece4390 100644 --- a/packages/core/src/common/app-paths/app-paths.test.ts +++ b/packages/core/src/common/app-paths/app-paths.test.ts @@ -21,7 +21,6 @@ describe("app-paths", () => { const defaultAppPathsStub: AppPaths = { currentApp: "/some-current-app", appData: "/some-app-data", - cache: "/some-cache", crashDumps: "/some-crash-dumps", desktop: "/some-desktop", documents: "/some-documents", @@ -36,6 +35,7 @@ describe("app-paths", () => { temp: "/some-temp", videos: "/some-videos", userData: "/some-irrelevant-user-data", + sessionData: "/some-irrelevant-user-data", // By default this points to userData }; builder.beforeApplicationStart(({ mainDi }) => { @@ -73,7 +73,6 @@ describe("app-paths", () => { expect(actual).toEqual({ currentApp: "/some-current-app", appData: "/some-app-data", - cache: "/some-cache", crashDumps: "/some-crash-dumps", desktop: "/some-desktop", documents: "/some-documents", @@ -88,6 +87,7 @@ describe("app-paths", () => { temp: "/some-temp", videos: "/some-videos", userData: "/some-app-data/some-product-name", + sessionData: "/some-app-data/some-product-name", }); }); @@ -97,7 +97,6 @@ describe("app-paths", () => { expect(actual).toEqual({ currentApp: "/some-current-app", appData: "/some-app-data", - cache: "/some-cache", crashDumps: "/some-crash-dumps", desktop: "/some-desktop", documents: "/some-documents", @@ -112,6 +111,7 @@ describe("app-paths", () => { temp: "/some-temp", videos: "/some-videos", userData: "/some-app-data/some-product-name", + sessionData: "/some-app-data/some-product-name", }); }); }); diff --git a/packages/core/src/common/cluster/authorization-review.injectable.ts b/packages/core/src/common/cluster/authorization-review.injectable.ts deleted file mode 100644 index 3352423377..0000000000 --- a/packages/core/src/common/cluster/authorization-review.injectable.ts +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import type { KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node"; -import { AuthorizationV1Api } from "@kubernetes/client-node"; -import { getInjectable } from "@ogre-tools/injectable"; -import loggerInjectable from "../logger.injectable"; - -/** - * Requests the permissions for actions on the kube cluster - * @param resourceAttributes The descriptor of the action that is desired to be known if it is allowed - * @returns `true` if the actions described are allowed - */ -export type CanI = (resourceAttributes: V1ResourceAttributes) => Promise; - -/** - * @param proxyConfig This config's `currentContext` field must be set, and will be used as the target cluster - */ -export type CreateAuthorizationReview = (proxyConfig: KubeConfig) => CanI; - -const createAuthorizationReviewInjectable = getInjectable({ - id: "authorization-review", - instantiate: (di): CreateAuthorizationReview => { - const logger = di.inject(loggerInjectable); - - return (proxyConfig) => { - const api = proxyConfig.makeApiClient(AuthorizationV1Api); - - return async (resourceAttributes: V1ResourceAttributes): Promise => { - try { - const { body } = await api.createSelfSubjectAccessReview({ - apiVersion: "authorization.k8s.io/v1", - kind: "SelfSubjectAccessReview", - spec: { resourceAttributes }, - }); - - return body.status?.allowed ?? false; - } catch (error) { - logger.error(`[AUTHORIZATION-REVIEW]: failed to create access review: ${error}`, { resourceAttributes }); - - return false; - } - }; - }; - }, -}); - -export default createAuthorizationReviewInjectable; diff --git a/packages/core/src/common/cluster/create-authorization-api.injectable.ts b/packages/core/src/common/cluster/create-authorization-api.injectable.ts new file mode 100644 index 0000000000..c0658a4a34 --- /dev/null +++ b/packages/core/src/common/cluster/create-authorization-api.injectable.ts @@ -0,0 +1,16 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { AuthorizationV1Api } from "@kubernetes/client-node"; +import type { KubeConfig } from "@kubernetes/client-node"; +import { getInjectable } from "@ogre-tools/injectable"; + +export type CreateAuthorizationApi = (config: KubeConfig) => AuthorizationV1Api; + +const createAuthorizationApiInjectable = getInjectable({ + id: "create-authorization-api", + instantiate: (): CreateAuthorizationApi => (config) => config.makeApiClient(AuthorizationV1Api), +}); + +export default createAuthorizationApiInjectable; diff --git a/packages/core/src/common/cluster/create-can-i.injectable.ts b/packages/core/src/common/cluster/create-can-i.injectable.ts new file mode 100644 index 0000000000..59d30dbe77 --- /dev/null +++ b/packages/core/src/common/cluster/create-can-i.injectable.ts @@ -0,0 +1,42 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type { AuthorizationV1Api, V1ResourceAttributes } from "@kubernetes/client-node"; +import { getInjectable } from "@ogre-tools/injectable"; +import loggerInjectable from "../logger.injectable"; + +/** + * Requests the permissions for actions on the kube cluster + * @param resourceAttributes The descriptor of the action that is desired to be known if it is allowed + * @returns `true` if the actions described are allowed + */ +export type CanI = (resourceAttributes: V1ResourceAttributes) => Promise; + +export type CreateCanI = (api: AuthorizationV1Api) => CanI; + +const createCanIInjectable = getInjectable({ + id: "create-can-i", + instantiate: (di): CreateCanI => { + const logger = di.inject(loggerInjectable); + + return (api) => async (resourceAttributes: V1ResourceAttributes): Promise => { + try { + const { body } = await api.createSelfSubjectAccessReview({ + apiVersion: "authorization.k8s.io/v1", + kind: "SelfSubjectAccessReview", + spec: { resourceAttributes }, + }); + + return body.status?.allowed ?? false; + } catch (error) { + logger.error(`[AUTHORIZATION-REVIEW]: failed to create access review: ${error}`, { resourceAttributes }); + + return false; + } + }; + }, +}); + +export default createCanIInjectable; diff --git a/packages/core/src/common/cluster/create-core-api.injectable.ts b/packages/core/src/common/cluster/create-core-api.injectable.ts new file mode 100644 index 0000000000..2389b12478 --- /dev/null +++ b/packages/core/src/common/cluster/create-core-api.injectable.ts @@ -0,0 +1,16 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { KubeConfig } from "@kubernetes/client-node"; +import { CoreV1Api } from "@kubernetes/client-node"; +import { getInjectable } from "@ogre-tools/injectable"; + +export type CreateCoreApi = (config: KubeConfig) => CoreV1Api; + +const createCoreApiInjectable = getInjectable({ + id: "create-core-api", + instantiate: (): CreateCoreApi => config => config.makeApiClient(CoreV1Api), +}); + +export default createCoreApiInjectable; diff --git a/packages/core/src/common/cluster/create-request-namespace-list-permissions.injectable.ts b/packages/core/src/common/cluster/create-request-namespace-list-permissions.injectable.ts new file mode 100644 index 0000000000..b3e8875dc1 --- /dev/null +++ b/packages/core/src/common/cluster/create-request-namespace-list-permissions.injectable.ts @@ -0,0 +1,57 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type { AuthorizationV1Api } from "@kubernetes/client-node"; +import { getInjectable } from "@ogre-tools/injectable"; +import loggerInjectable from "../logger.injectable"; +import type { KubeApiResource } from "../rbac"; + +export type CanListResource = (resource: KubeApiResource) => boolean; + +/** + * Requests the permissions for actions on the kube cluster + * @param namespace The namespace of the resources + */ +export type RequestNamespaceListPermissions = (namespace: string) => Promise; + +export type CreateRequestNamespaceListPermissions = (api: AuthorizationV1Api) => RequestNamespaceListPermissions; + +const createRequestNamespaceListPermissionsInjectable = getInjectable({ + id: "create-request-namespace-list-permissions", + instantiate: (di): CreateRequestNamespaceListPermissions => { + const logger = di.inject(loggerInjectable); + + return (api) => async (namespace) => { + try { + const { body: { status }} = await api.createSelfSubjectRulesReview({ + apiVersion: "authorization.k8s.io/v1", + kind: "SelfSubjectRulesReview", + spec: { namespace }, + }); + + if (!status || status.incomplete) { + logger.warn(`[AUTHORIZATION-NAMESPACE-REVIEW]: allowing all resources in namespace="${namespace}" due to incomplete SelfSubjectRulesReview: ${status?.evaluationError}`); + + return () => true; + } + + const { resourceRules } = status; + + return (resource) => ( + resourceRules + .filter(({ apiGroups = ["*"] }) => apiGroups.includes("*") || apiGroups.includes(resource.group)) + .filter(({ resources = ["*"] }) => resources.includes("*") || resources.includes(resource.apiName)) + .some(({ verbs }) => verbs.includes("*") || verbs.includes("list")) + ); + } catch (error) { + logger.error(`[AUTHORIZATION-NAMESPACE-REVIEW]: failed to create subject rules review`, { namespace, error }); + + return () => true; + } + }; + }, +}); + +export default createRequestNamespaceListPermissionsInjectable; diff --git a/packages/core/src/common/cluster/list-namespaces.injectable.ts b/packages/core/src/common/cluster/list-namespaces.injectable.ts index 363a10abb1..04acb0671b 100644 --- a/packages/core/src/common/cluster/list-namespaces.injectable.ts +++ b/packages/core/src/common/cluster/list-namespaces.injectable.ts @@ -2,27 +2,21 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type { KubeConfig } from "@kubernetes/client-node"; -import { CoreV1Api } from "@kubernetes/client-node"; +import type { CoreV1Api } from "@kubernetes/client-node"; import { getInjectable } from "@ogre-tools/injectable"; import { isDefined } from "@k8slens/utilities"; export type ListNamespaces = () => Promise; - -export type CreateListNamespaces = (config: KubeConfig) => ListNamespaces; +export type CreateListNamespaces = (api: CoreV1Api) => ListNamespaces; const createListNamespacesInjectable = getInjectable({ id: "create-list-namespaces", - instantiate: (): CreateListNamespaces => (config) => { - const coreApi = config.makeApiClient(CoreV1Api); + instantiate: (): CreateListNamespaces => (api) => async () => { + const { body: { items }} = await api.listNamespace(); - return async () => { - const { body: { items }} = await coreApi.listNamespace(); - - return items - .map(ns => ns.metadata?.name) - .filter(isDefined); - }; + return items + .map(ns => ns.metadata?.name) + .filter(isDefined); }, }); diff --git a/packages/core/src/common/cluster/request-namespace-list-permissions.injectable.ts b/packages/core/src/common/cluster/request-namespace-list-permissions.injectable.ts deleted file mode 100644 index 4b1aadeee6..0000000000 --- a/packages/core/src/common/cluster/request-namespace-list-permissions.injectable.ts +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import type { KubeConfig } from "@kubernetes/client-node"; -import { AuthorizationV1Api } from "@kubernetes/client-node"; -import { getInjectable } from "@ogre-tools/injectable"; -import loggerInjectable from "../logger.injectable"; -import type { KubeApiResource } from "../rbac"; - -export type CanListResource = (resource: KubeApiResource) => boolean; - -/** - * Requests the permissions for actions on the kube cluster - * @param namespace The namespace of the resources - */ -export type RequestNamespaceListPermissions = (namespace: string) => Promise; - -/** - * @param proxyConfig This config's `currentContext` field must be set, and will be used as the target cluster - */ -export type RequestNamespaceListPermissionsFor = (proxyConfig: KubeConfig) => RequestNamespaceListPermissions; - -const requestNamespaceListPermissionsForInjectable = getInjectable({ - id: "request-namespace-list-permissions-for", - instantiate: (di): RequestNamespaceListPermissionsFor => { - const logger = di.inject(loggerInjectable); - - return (proxyConfig) => { - const api = proxyConfig.makeApiClient(AuthorizationV1Api); - - return async (namespace) => { - try { - const { body: { status }} = await api.createSelfSubjectRulesReview({ - apiVersion: "authorization.k8s.io/v1", - kind: "SelfSubjectRulesReview", - spec: { namespace }, - }); - - if (!status || status.incomplete) { - logger.warn(`[AUTHORIZATION-NAMESPACE-REVIEW]: allowing all resources in namespace="${namespace}" due to incomplete SelfSubjectRulesReview: ${status?.evaluationError}`); - - return () => true; - } - - const { resourceRules } = status; - - return (resource) => { - const rules = resourceRules.filter(({ - apiGroups = ["*"], - resources = ["*"], - }) => { - const isAboutRelevantApiGroup = apiGroups.includes("*") || apiGroups.includes(resource.group); - const isAboutResource = resources.includes("*") || resources.includes(resource.apiName); - - return isAboutRelevantApiGroup && isAboutResource; - }); - - return rules.some(({ verbs }) => verbs.includes("*") || verbs.includes("list")); - }; - } catch (error) { - logger.error(`[AUTHORIZATION-NAMESPACE-REVIEW]: failed to create subject rules review`, { namespace, error }); - - return () => true; - } - }; - }; - }, -}); - -export default requestNamespaceListPermissionsForInjectable; diff --git a/packages/core/src/common/cluster/request-namespace-list-permissions.test.ts b/packages/core/src/common/cluster/request-namespace-list-permissions.test.ts index c62f69ca8e..194b5ce7e5 100644 --- a/packages/core/src/common/cluster/request-namespace-list-permissions.test.ts +++ b/packages/core/src/common/cluster/request-namespace-list-permissions.test.ts @@ -3,334 +3,225 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type { V1SubjectRulesReviewStatus } from "@kubernetes/client-node"; +import type { AsyncFnMock } from "@async-fn/jest"; +import asyncFn from "@async-fn/jest"; +import type { AuthorizationV1Api, V1SubjectRulesReviewStatus } from "@kubernetes/client-node"; import type { DiContainer } from "@ogre-tools/injectable"; +import type { IncomingMessage } from "http"; +import { anyObject } from "jest-mock-extended"; import { getDiForUnitTesting } from "../../main/getDiForUnitTesting"; -import type { RequestNamespaceListPermissionsFor } from "./request-namespace-list-permissions.injectable"; -import requestNamespaceListPermissionsForInjectable from "./request-namespace-list-permissions.injectable"; +import { cast } from "../../test-utils/cast"; +import type { KubeApiResource } from "../rbac"; +import type { RequestNamespaceListPermissions } from "./create-request-namespace-list-permissions.injectable"; +import createRequestNamespaceListPermissionsInjectable from "./create-request-namespace-list-permissions.injectable"; -const createStubProxyConfig = (statusResponse: Promise<{ body: { status: V1SubjectRulesReviewStatus }}>) => ({ - makeApiClient: () => ({ - createSelfSubjectRulesReview: (): Promise<{ body: { status: V1SubjectRulesReviewStatus }}> => statusResponse, - }), -}); +interface TestCase { + description: string; + status: V1SubjectRulesReviewStatus; + expected: boolean; +} describe("requestNamespaceListPermissions", () => { let di: DiContainer; - let requestNamespaceListPermissions: RequestNamespaceListPermissionsFor; + let createSelfSubjectRulesReviewMock: AsyncFnMock; + let requestNamespaceListPermissions: RequestNamespaceListPermissions; beforeEach(() => { di = getDiForUnitTesting(); - requestNamespaceListPermissions = di.inject(requestNamespaceListPermissionsForInjectable); + + const createRequestNamespaceListPermissions = di.inject(createRequestNamespaceListPermissionsInjectable); + + createSelfSubjectRulesReviewMock = asyncFn(); + + requestNamespaceListPermissions = createRequestNamespaceListPermissions(cast({ + createSelfSubjectRulesReview: createSelfSubjectRulesReviewMock, + })); }); - describe("when api returns incomplete data", () => { - it("returns truthy function", async () => { - const requestPermissions = requestNamespaceListPermissions(createStubProxyConfig( - new Promise((resolve) => resolve({ - body: { - status: { - incomplete: true, - resourceRules: [], - nonResourceRules: [], - }, - }, - })), - ) as any); + describe("when a request for list permissions in a namespace has been started", () => { + let request: ReturnType; - const permissionCheck = await requestPermissions("irrelevant-namespace"); - - expect(permissionCheck({ - apiName: "pods", - group: "", - kind: "Pod", - namespaced: true, - })).toBeTruthy(); + beforeEach(() => { + request = requestNamespaceListPermissions("irrelevant-namespace"); }); - }); - describe("when api rejects", () => { - it("returns truthy function", async () => { - const requestPermissions = requestNamespaceListPermissions(createStubProxyConfig( - new Promise((resolve, reject) => reject("unknown error")), - ) as any); - - const permissionCheck = await requestPermissions("irrelevant-namespace"); - - expect(permissionCheck({ - apiName: "pods", - group: "", - kind: "Pod", - namespaced: true, - })).toBeTruthy(); + it("should request the creation of a SelfSubjectRulesReview", () => { + expect(createSelfSubjectRulesReviewMock).toBeCalledWith(anyObject({ + spec: { + namespace: "irrelevant-namespace", + }, + })); }); - }); - describe("when first resourceRule has all permissions for everything", () => { - it("return truthy function", async () => { - const requestPermissions = requestNamespaceListPermissions(createStubProxyConfig( - new Promise((resolve) => resolve({ - body: { - status: { - incomplete: false, - resourceRules: [ - { - apiGroups: ["*"], - verbs: ["*"], - }, - { - apiGroups: ["*"], - verbs: ["get"], - }, - ], - nonResourceRules: [], + ([ + { + description: "incomplete data", + status: { + incomplete: true, + resourceRules: [], + nonResourceRules: [], + }, + expected: true, + }, + { + description: "first resourceRule has all permissions for everything", + status: { + incomplete: false, + resourceRules: [ + { + apiGroups: ["*"], + verbs: ["*"], }, - }, - })), - ) as any); + { + apiGroups: ["*"], + verbs: ["get"], + }, + ], + nonResourceRules: [], + }, + expected: true, + }, + { + description: "first resourceRule has list permissions for everything", + status: { + incomplete: false, + resourceRules: [ + { + apiGroups: ["*"], + verbs: ["list"], + }, + { + apiGroups: ["*"], + verbs: ["get"], + }, + ], + nonResourceRules: [], + }, + expected: true, + }, + { + description: "first resourceRule has list permissions for asked resource", + status: { + incomplete: false, + resourceRules: [ + { + apiGroups: ["some-api-group"], + resources: ["some-kind"], + verbs: ["list"], + }, + { + apiGroups: ["*"], + verbs: ["get"], + }, + ], + nonResourceRules: [], + }, + expected: true, + }, + { + description: "last resourceRule has all permissions for everything", + status: { + incomplete: false, + resourceRules: [ + { + apiGroups: ["*"], + verbs: ["get"], + }, + { + apiGroups: ["*"], + verbs: ["*"], + }, + ], + nonResourceRules: [], + }, + expected: true, + }, + { + description: "last resourceRule has list permissions for asked resource", + status: { + incomplete: false, + resourceRules: [ + { + apiGroups: ["*"], + verbs: ["get"], + }, + { + apiGroups: ["some-api-group"], + resources: ["some-kind"], + verbs: ["list"], + }, + ], + nonResourceRules: [], + }, + expected: true, + }, + { + description: "resourceRules has matching resource without list verb", + status: { + incomplete: false, + resourceRules: [ + { + apiGroups: ["some-api-group"], + resources: ["some-kind"], + verbs: ["get"], + }, + ], + nonResourceRules: [], + }, + expected: false, + }, + { + description: "resourceRules has no matching resource with list verb", + status: { + incomplete: false, + resourceRules: [ + { + apiGroups: [""], + resources: ["services"], + verbs: ["list"], + }, + ], + nonResourceRules: [], + }, + expected: false, + }, + ] as TestCase[]).forEach(({ description, status, expected }) => { + describe(`when api returns ${description}`, () => { + beforeEach(async () => { + await createSelfSubjectRulesReviewMock.resolve({ + body: { + status, + spec: {}, + }, + response: null as unknown as IncomingMessage, + }); + }); - const permissionCheck = await requestPermissions("irrelevant-namespace"); + it(`allows the request to complete, and 'canListResource' will return ${expected}`, async () => { + const canListResource = await request; - expect(permissionCheck({ - apiName: "pods", - group: "", - kind: "Pod", - namespaced: true, - })).toBeTruthy(); + expect(canListResource(someKubeResource)).toBe(expected); + }); + }); }); - }); - describe("when first resourceRule has list permissions for everything", () => { - it("return truthy function", async () => { - const requestPermissions = requestNamespaceListPermissions(createStubProxyConfig( - new Promise((resolve) => resolve({ - body: { - status: { - incomplete: false, - resourceRules: [ - { - apiGroups: ["*"], - verbs: ["list"], - }, - { - apiGroups: ["*"], - verbs: ["get"], - }, - ], - nonResourceRules: [], - }, - }, - })), - ) as any); + describe("when api rejects", () => { + beforeEach(async () => { + await createSelfSubjectRulesReviewMock.reject(new Error("unknown error")); + }); - const permissionCheck = await requestPermissions("irrelevant-namespace"); + it("allows the request to complete, and 'canListResource' will return true", async () => { + const canListResource = await request; - expect(permissionCheck({ - apiName: "pods", - group: "", - kind: "Pod", - namespaced: true, - })).toBeTruthy(); - }); - }); - - describe("when first resourceRule has list permissions for asked resource", () => { - it("return truthy function", async () => { - const requestPermissions = requestNamespaceListPermissions(createStubProxyConfig( - new Promise((resolve) => resolve({ - body: { - status: { - incomplete: false, - resourceRules: [ - { - apiGroups: [""], - resources: ["pods"], - verbs: ["list"], - }, - { - apiGroups: ["*"], - verbs: ["get"], - }, - ], - nonResourceRules: [], - }, - }, - })), - ) as any); - - const permissionCheck = await requestPermissions("irrelevant-namespace"); - - expect(permissionCheck({ - apiName: "pods", - group: "", - kind: "Pod", - namespaced: true, - })).toBeTruthy(); - }); - }); - - describe("when last resourceRule has all permissions for everything", () => { - it("return truthy function", async () => { - const requestPermissions = requestNamespaceListPermissions(createStubProxyConfig( - new Promise((resolve) => resolve({ - body: { - status: { - incomplete: false, - resourceRules: [ - { - apiGroups: ["*"], - verbs: ["get"], - }, - { - apiGroups: ["*"], - verbs: ["*"], - }, - ], - nonResourceRules: [], - }, - }, - })), - ) as any); - - const permissionCheck = await requestPermissions("irrelevant-namespace"); - - expect(permissionCheck({ - apiName: "pods", - group: "", - kind: "Pod", - namespaced: true, - })).toBeTruthy(); - }); - }); - - describe("when last resourceRule has list permissions for everything", () => { - it("return truthy function", async () => { - const requestPermissions = requestNamespaceListPermissions(createStubProxyConfig( - new Promise((resolve) => resolve({ - body: { - status: { - incomplete: false, - resourceRules: [ - { - apiGroups: ["*"], - verbs: ["get"], - }, - { - apiGroups: ["*"], - verbs: ["list"], - }, - ], - nonResourceRules: [], - }, - }, - })), - ) as any); - - const permissionCheck = await requestPermissions("irrelevant-namespace"); - - expect(permissionCheck({ - apiName: "pods", - group: "", - kind: "Pod", - namespaced: true, - })).toBeTruthy(); - }); - }); - - describe("when last resourceRule has list permissions for asked resource", () => { - it("return truthy function", async () => { - const requestPermissions = requestNamespaceListPermissions(createStubProxyConfig( - new Promise((resolve) => resolve({ - body: { - status: { - incomplete: false, - resourceRules: [ - { - apiGroups: ["*"], - verbs: ["get"], - }, - { - apiGroups: [""], - resources: ["pods"], - verbs: ["list"], - }, - ], - nonResourceRules: [], - }, - }, - })), - ) as any); - - const permissionCheck = await requestPermissions("irrelevant-namespace"); - - expect(permissionCheck({ - apiName: "pods", - group: "", - kind: "Pod", - namespaced: true, - })).toBeTruthy(); - }); - }); - - describe("when resourceRules has matching resource without list verb", () => { - it("return falsy function", async () => { - const requestPermissions = requestNamespaceListPermissions(createStubProxyConfig( - new Promise((resolve) => resolve({ - body: { - status: { - incomplete: false, - resourceRules: [ - { - apiGroups: [""], - resources: ["pods"], - verbs: ["get"], - }, - ], - nonResourceRules: [], - }, - }, - })), - ) as any); - - const permissionCheck = await requestPermissions("irrelevant-namespace"); - - expect(permissionCheck({ - apiName: "pods", - group: "", - kind: "Pod", - namespaced: true, - })).toBeFalsy(); - }); - }); - - describe("when resourceRules has no matching resource with list verb", () => { - it("return falsy function", async () => { - const requestPermissions = requestNamespaceListPermissions(createStubProxyConfig( - new Promise((resolve) => resolve({ - body: { - status: { - incomplete: false, - resourceRules: [ - { - apiGroups: [""], - resources: ["services"], - verbs: ["list"], - }, - ], - nonResourceRules: [], - }, - }, - })), - ) as any); - - const permissionCheck = await requestPermissions("irrelevant-namespace"); - - expect(permissionCheck({ - apiName: "pods", - group: "", - kind: "Pod", - namespaced: true, - })).toBeFalsy(); + expect(canListResource(someKubeResource)).toBe(true); + }); }); }); }); + +const someKubeResource: KubeApiResource = { + apiName: "some-kind", + group: "some-api-group", + kind: "SomeKind", + namespaced: true, +}; diff --git a/packages/core/src/common/error-reporting/initialize-sentry-reporting.injectable.ts b/packages/core/src/common/error-reporting/initialize-sentry-reporting.injectable.ts index 778f959739..677c18a586 100644 --- a/packages/core/src/common/error-reporting/initialize-sentry-reporting.injectable.ts +++ b/packages/core/src/common/error-reporting/initialize-sentry-reporting.injectable.ts @@ -13,7 +13,7 @@ import userStoreInjectable from "../user-store/user-store.injectable"; export type InitializeSentryReportingWith = (initSentry: (opts: BrowserOptions | ElectronMainOptions) => void) => void; -const mapProcessName = (type: "browser" | "renderer" | "worker") => type === "browser" ? "main" : type; +const mapProcessName = (type: "browser" | "renderer" | "worker" | "utility") => type === "browser" ? "main" : type; const initializeSentryReportingWithInjectable = getInjectable({ id: "initialize-sentry-reporting-with", diff --git a/packages/core/src/common/fs/copy.global-override-for-injectable.ts b/packages/core/src/common/fs/copy.global-override-for-injectable.ts deleted file mode 100644 index 3799d3b760..0000000000 --- a/packages/core/src/common/fs/copy.global-override-for-injectable.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import { getGlobalOverride } from "@k8slens/test-utils"; -import copyInjectable from "./copy.injectable"; - -export default getGlobalOverride(copyInjectable, () => async () => { - throw new Error("tried to copy filepaths without override"); -}); diff --git a/packages/core/src/common/fs/lstat.global-override-for-injectable.ts b/packages/core/src/common/fs/lstat.global-override-for-injectable.ts deleted file mode 100644 index 155fac7451..0000000000 --- a/packages/core/src/common/fs/lstat.global-override-for-injectable.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import { getGlobalOverride } from "@k8slens/test-utils"; -import lstatInjectable from "./lstat.injectable"; - -export default getGlobalOverride(lstatInjectable, () => async () => { - throw new Error("tried to lstat a filepath without override"); -}); diff --git a/packages/core/src/common/fs/read-directory.global-override-for-injectable.ts b/packages/core/src/common/fs/read-directory.global-override-for-injectable.ts deleted file mode 100644 index 72d9b523f4..0000000000 --- a/packages/core/src/common/fs/read-directory.global-override-for-injectable.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import { getGlobalOverride } from "@k8slens/test-utils"; -import readDirectoryInjectable from "./read-directory.injectable"; - -export default getGlobalOverride(readDirectoryInjectable, () => async () => { - throw new Error("tried to read a directory's content without override"); -}); diff --git a/packages/core/src/common/fs/remove.global-override-for-injectable.ts b/packages/core/src/common/fs/remove.global-override-for-injectable.ts deleted file mode 100644 index 58fb0f9dce..0000000000 --- a/packages/core/src/common/fs/remove.global-override-for-injectable.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import { getGlobalOverride } from "@k8slens/test-utils"; -import removePathInjectable from "./remove.injectable"; - -export default getGlobalOverride(removePathInjectable, () => async () => { - throw new Error("tried to remove path without override"); -}); diff --git a/packages/core/src/common/fs/write-file.global-override-for-injectable.ts b/packages/core/src/common/fs/write-file.global-override-for-injectable.ts deleted file mode 100644 index e87f648305..0000000000 --- a/packages/core/src/common/fs/write-file.global-override-for-injectable.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import { getGlobalOverride } from "@k8slens/test-utils"; -import writeFileInjectable from "./write-file.injectable"; - -export default getGlobalOverride(writeFileInjectable, () => async () => { - throw new Error("tried to write file without override"); -}); diff --git a/packages/core/src/common/k8s-api/__tests__/api-manager.test.ts b/packages/core/src/common/k8s-api/__tests__/api-manager.test.ts index 6ea6327038..48fdb3e544 100644 --- a/packages/core/src/common/k8s-api/__tests__/api-manager.test.ts +++ b/packages/core/src/common/k8s-api/__tests__/api-manager.test.ts @@ -4,6 +4,7 @@ */ import type { DiContainer } from "@ogre-tools/injectable"; +import { getInjectable } from "@ogre-tools/injectable"; import clusterFrameContextForNamespacedResourcesInjectable from "../../../renderer/cluster-frame-context/for-namespaced-resources.injectable"; import hostedClusterInjectable from "../../../renderer/cluster-frame-context/hosted-cluster.injectable"; import { getDiForUnitTesting } from "../../../renderer/getDiForUnitTesting"; @@ -21,6 +22,9 @@ import maybeKubeApiInjectable from "../maybe-kube-api.injectable"; // eslint-disable-next-line no-restricted-imports import { KubeApi as ExternalKubeApi } from "../../../extensions/common-api/k8s-api"; import { Cluster } from "../../cluster/cluster"; +import { runInAction } from "mobx"; +import { customResourceDefinitionApiInjectionToken } from "../api-manager/crd-api-token"; +import assert from "assert"; class TestApi extends KubeApi { protected async checkPreferredVersion() { @@ -117,4 +121,90 @@ describe("ApiManager", () => { }); }); }); + + describe("given than a CRD has a default KubeApi registered for it", () => { + const apiBase = "/apis/aquasecurity.github.io/v1alpha1/vulnerabilityreports"; + + beforeEach(() => { + runInAction(() => { + di.register(getInjectable({ + id: `default-kube-api-for-custom-resource-definition-${apiBase}`, + instantiate: (di) => { + const objectConstructor = class extends KubeObject { + static readonly kind = "VulnerabilityReport"; + static readonly namespaced = true; + static readonly apiBase = apiBase; + }; + + return Object.assign( + new KubeApi({ + logger: di.inject(loggerInjectable), + maybeKubeApi: di.inject(maybeKubeApiInjectable), + }, { objectConstructor }), + { + myField: 1, + }, + ); + }, + injectionToken: customResourceDefinitionApiInjectionToken, + })); + }); + }); + + it("can be retrieved from apiManager", () => { + expect(apiManager.getApi(apiBase)).toMatchObject({ + myField: 1, + }); + }); + + it("can have a default KubeObjectStore instance retrieved for it", () => { + expect(apiManager.getStore(apiBase)).toBeInstanceOf(KubeObjectStore); + }); + + describe("given that an extension registers an api with the same apibase", () => { + beforeEach(() => { + void Object.assign(new ExternalKubeApi({ + objectConstructor: KubeObject, + apiBase, + kind: "VulnerabilityReport", + }), { + myField: 2, + }); + }); + + it("the extension's instance is retrievable instead from apiManager", () => { + expect(apiManager.getApi(apiBase)).toMatchObject({ + myField: 2, + }); + }); + + it("can have a default KubeObjectStore instance retrieved for it", () => { + expect(apiManager.getStore(apiBase)).toBeInstanceOf(KubeObjectStore); + }); + + describe("given that an extension registers a store for the same apibase", () => { + beforeEach(() => { + const api = apiManager.getApi(apiBase); + + assert(api); + + apiManager.registerStore(Object.assign( + new KubeObjectStore({ + context: di.inject(clusterFrameContextForNamespacedResourcesInjectable), + logger: di.inject(loggerInjectable), + }, api), + { + someField: 2, + }, + )); + }); + + it("can gets the custom KubeObjectStore instance instead", () => { + expect(apiManager.getStore(apiBase)).toMatchObject({ + someField: 2, + }); + }); + }); + }); + }); }); diff --git a/packages/core/src/common/k8s-api/api-manager/api-manager.ts b/packages/core/src/common/k8s-api/api-manager/api-manager.ts index f6c2758921..6b60ba8e7a 100644 --- a/packages/core/src/common/k8s-api/api-manager/api-manager.ts +++ b/packages/core/src/common/k8s-api/api-manager/api-manager.ts @@ -10,7 +10,8 @@ import { autorun, action, observable } from "mobx"; import type { KubeApi } from "../kube-api"; import type { KubeObject, ObjectReference } from "../kube-object"; import { parseKubeApi, createKubeApiURL } from "../kube-api-parse"; -import { iter } from "@k8slens/utilities"; +import { getOrInsertWith, iter } from "@k8slens/utilities"; +import type { CreateCustomResourceStore } from "./create-custom-resource-store.injectable"; export type RegisterableStore = Store extends KubeObjectStore ? Store @@ -26,13 +27,15 @@ export type FindApiCallback = (api: KubeApi) => boolean; interface Dependencies { readonly apis: IComputedValue; + readonly crdApis: IComputedValue; readonly stores: IComputedValue; + createCustomResourceStore: CreateCustomResourceStore; } export class ApiManager { private readonly externalApis = observable.array(); private readonly externalStores = observable.array(); - + private readonly defaultCrdStores = observable.map(); private readonly apis = observable.map(); constructor(private readonly dependencies: Dependencies) { @@ -56,6 +59,12 @@ export class ApiManager { } } + for (const crdApi of this.dependencies.crdApis.get()) { + if (!newState.has(crdApi.apiBase)) { + newState.set(crdApi.apiBase, crdApi); + } + } + this.apis.replace(newState); }); } @@ -110,6 +119,16 @@ export class ApiManager { this.externalStores.push(store); } + private apiIsDefaultCrdApi(api: KubeApi): boolean { + for (const crdApi of this.dependencies.crdApis.get()) { + if (crdApi.apiBase === api.apiBase) { + return true; + } + } + + return false; + } + getStore(api: string | undefined): KubeObjectStore | undefined; getStore(api: RegisterableApi): KubeObjectStoreFrom | undefined; /** @@ -130,9 +149,19 @@ export class ApiManager { return undefined; } - return iter.chain(this.dependencies.stores.get().values()) + const defaultResult = iter.chain(this.dependencies.stores.get().values()) .concat(this.externalStores.values()) .find(store => store.api.apiBase === api.apiBase); + + if (defaultResult) { + return defaultResult; + } + + if (this.apiIsDefaultCrdApi(api)) { + return getOrInsertWith(this.defaultCrdStores, api.apiBase, () => this.dependencies.createCustomResourceStore(api)); + } + + return undefined; } lookupApiLink(ref: ObjectReference, parentObject?: KubeObject): string { diff --git a/packages/core/src/common/k8s-api/api-manager/auto-registration-emitter.injectable.ts b/packages/core/src/common/k8s-api/api-manager/auto-registration-emitter.injectable.ts index d9a68a988c..714e9d8952 100644 --- a/packages/core/src/common/k8s-api/api-manager/auto-registration-emitter.injectable.ts +++ b/packages/core/src/common/k8s-api/api-manager/auto-registration-emitter.injectable.ts @@ -5,11 +5,9 @@ import { getInjectable } from "@ogre-tools/injectable"; import EventEmitter from "events"; import type TypedEventEmitter from "typed-emitter"; -import type { CustomResourceDefinition } from "../endpoints"; import type { KubeApi } from "../kube-api"; export interface LegacyAutoRegistration { - customResourceDefinition: (crd: CustomResourceDefinition) => void; kubeApi: (api: KubeApi) => void; } diff --git a/packages/core/src/common/k8s-api/api-manager/crd-api-token.ts b/packages/core/src/common/k8s-api/api-manager/crd-api-token.ts new file mode 100644 index 0000000000..d8f0c918c2 --- /dev/null +++ b/packages/core/src/common/k8s-api/api-manager/crd-api-token.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getInjectionToken } from "@ogre-tools/injectable"; +import type { KubeApi } from "../kube-api"; + +export const customResourceDefinitionApiInjectionToken = getInjectionToken({ + id: "custom-resource-definition-api-token", +}); diff --git a/packages/core/src/common/k8s-api/api-manager/create-custom-resource-store.injectable.ts b/packages/core/src/common/k8s-api/api-manager/create-custom-resource-store.injectable.ts new file mode 100644 index 0000000000..fbf46e2b42 --- /dev/null +++ b/packages/core/src/common/k8s-api/api-manager/create-custom-resource-store.injectable.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import clusterFrameContextForNamespacedResourcesInjectable from "../../../renderer/cluster-frame-context/for-namespaced-resources.injectable"; +import loggerInjectable from "../../logger.injectable"; +import type { KubeApi } from "../kube-api"; +import type { KubeObject } from "../kube-object"; +import type { KubeObjectStoreDependencies } from "../kube-object.store"; +import { CustomResourceStore } from "./resource.store"; + +export type CreateCustomResourceStore = (api: KubeApi) => CustomResourceStore; + +const createCustomResourceStoreInjectable = getInjectable({ + id: "create-custom-resource-store", + instantiate: (di): CreateCustomResourceStore => { + const deps: KubeObjectStoreDependencies = { + context: di.inject(clusterFrameContextForNamespacedResourcesInjectable), + logger: di.inject(loggerInjectable), + }; + + return (api) => new CustomResourceStore(deps, api); + }, +}); + +export default createCustomResourceStoreInjectable; diff --git a/packages/core/src/common/k8s-api/api-manager/manager.injectable.ts b/packages/core/src/common/k8s-api/api-manager/manager.injectable.ts index f0b61c28b6..2ffd41a296 100644 --- a/packages/core/src/common/k8s-api/api-manager/manager.injectable.ts +++ b/packages/core/src/common/k8s-api/api-manager/manager.injectable.ts @@ -9,6 +9,8 @@ import { computedInjectManyInjectable } from "@ogre-tools/injectable-extension-f import { kubeObjectStoreInjectionToken } from "./kube-object-store-token"; import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token"; import { computed } from "mobx"; +import { customResourceDefinitionApiInjectionToken } from "./crd-api-token"; +import createCustomResourceStoreInjectable from "./create-custom-resource-store.injectable"; const apiManagerInjectable = getInjectable({ id: "api-manager", @@ -23,6 +25,10 @@ const apiManagerInjectable = getInjectable({ stores: storesAndApisCanBeCreated ? computedInjectMany(kubeObjectStoreInjectionToken) : computed(() => []), + crdApis: storesAndApisCanBeCreated + ? computedInjectMany(customResourceDefinitionApiInjectionToken) + : computed(() => []), + createCustomResourceStore: di.inject(createCustomResourceStoreInjectable), }); }, }); diff --git a/packages/core/src/common/k8s-api/kube-object.store.ts b/packages/core/src/common/k8s-api/kube-object.store.ts index 09b26d4142..fa17fd1f2c 100644 --- a/packages/core/src/common/k8s-api/kube-object.store.ts +++ b/packages/core/src/common/k8s-api/kube-object.store.ts @@ -88,7 +88,7 @@ export interface KubeObjectStoreDependencies { readonly logger: Logger; } -export abstract class KubeObjectStore< +export class KubeObjectStore< K extends KubeObject = KubeObject, A extends KubeApi = KubeApi>, D extends KubeJsonApiDataFor = KubeApiDataFrom, diff --git a/packages/core/src/common/utils/registrator-helper.ts b/packages/core/src/common/utils/registrator-helper.ts new file mode 100644 index 0000000000..4a9cc5c2d2 --- /dev/null +++ b/packages/core/src/common/utils/registrator-helper.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { iter } from "@k8slens/utilities"; +import type { DiContainerForInjection, Injectable } from "@ogre-tools/injectable"; + +// Register new injectables and deregister removed injectables by id + +export const injectableDifferencingRegistratorWith = (di: DiContainerForInjection) => ( + (rawCurrent: Injectable[], rawPrevious: Injectable[] = []) => { + const current = new Map(rawCurrent.map(inj => [inj.id, inj])); + const previous = new Map(rawPrevious.map(inj => [inj.id, inj])); + const toAdd = iter.chain(current.entries()) + .filter(([id]) => !previous.has(id)) + .collect(entries => new Map(entries)); + const toRemove = iter.chain(previous.entries()) + .filter(([id]) => !current.has(id)) + .collect(entries => new Map(entries)); + + di.deregister(...toRemove.values()); + di.register(...toAdd.values()); + } +); diff --git a/packages/core/src/extensions/extension-loader/extension/extension.injectable.ts b/packages/core/src/extensions/extension-loader/extension/extension.injectable.ts index 07f054d3fd..d54d997d09 100644 --- a/packages/core/src/extensions/extension-loader/extension/extension.injectable.ts +++ b/packages/core/src/extensions/extension-loader/extension/extension.injectable.ts @@ -2,29 +2,18 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type { Injectable } from "@ogre-tools/injectable"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; -import { difference, find, map } from "lodash"; import { reaction, runInAction } from "mobx"; import { disposer } from "@k8slens/utilities"; import type { LensExtension } from "../../lens-extension"; import { extensionRegistratorInjectionToken } from "../extension-registrator-injection-token"; +import { injectableDifferencingRegistratorWith } from "../../../common/utils/registrator-helper"; export interface Extension { register: () => void; deregister: () => void; } -const idsToInjectables = (ids: string[], injectables: Injectable[]) => ids.map(id => { - const injectable = find(injectables, { id }); - - if (!injectable) { - throw new Error(`Injectable ${id} not found`); - } - - return injectable; -}); - const extensionInjectable = getInjectable({ id: "extension", @@ -35,36 +24,27 @@ const extensionInjectable = getInjectable({ instantiate: (childDi) => { const extensionRegistrators = childDi.injectMany(extensionRegistratorInjectionToken); const reactionDisposer = disposer(); + const injectableDifferencingRegistrator = injectableDifferencingRegistratorWith(childDi); return { register: () => { - extensionRegistrators.forEach((getInjectablesOfExtension) => { - const injectables = getInjectablesOfExtension(instance); + for (const extensionRegistrator of extensionRegistrators) { + const injectables = extensionRegistrator(instance); - reactionDisposer.push( - // injectables is either an array or a computed array, in which case - // we need to update the registered injectables with a reaction every time they change - reaction( - () => Array.isArray(injectables) ? injectables : injectables.get(), - (currentInjectables, previousInjectables = []) => { - // Register new injectables and deregister removed injectables by id - const currentIds = map(currentInjectables, "id"); - const previousIds = map(previousInjectables, "id"); - const idsToAdd = difference(currentIds, previousIds); - const idsToRemove = previousIds.filter(previousId => !currentIds.includes(previousId)); - - if (idsToRemove.length > 0) { - childDi.deregister(...idsToInjectables(idsToRemove, previousInjectables)); - } - - if (idsToAdd.length > 0) { - childDi.register(...idsToInjectables(idsToAdd, currentInjectables)); - } - }, { + if (Array.isArray(injectables)) { + runInAction(() => { + injectableDifferencingRegistrator(injectables); + }); + } else { + reactionDisposer.push(reaction( + () => injectables.get(), + injectableDifferencingRegistrator, + { fireImmediately: true, }, )); - }); + } + } }, deregister: () => { diff --git a/packages/core/src/features/application-update/child-features/application-update-using-application-menu/main/check-for-updates-menu-item.injectable.ts b/packages/core/src/features/application-update/child-features/application-update-using-application-menu/main/check-for-updates-menu-item.injectable.ts index 9fc21375f2..0bbf22abce 100644 --- a/packages/core/src/features/application-update/child-features/application-update-using-application-menu/main/check-for-updates-menu-item.injectable.ts +++ b/packages/core/src/features/application-update/child-features/application-update-using-application-menu/main/check-for-updates-menu-item.injectable.ts @@ -26,7 +26,7 @@ const checkForUpdatesMenuItemInjectable = getInjectable({ id: "check-for-updates", parentId: isMac ? "mac" : "help", orderNumber: isMac ? 20 : 50, - label: "Check for updates", + label: "Check for Updates...", isShown: updatingIsEnabled, onClick: async () => { diff --git a/packages/core/src/features/application-update/child-features/application-update-using-tray/installing-update-using-tray.test.ts b/packages/core/src/features/application-update/child-features/application-update-using-tray/installing-update-using-tray.test.ts index d5caa63149..f18f288469 100644 --- a/packages/core/src/features/application-update/child-features/application-update-using-tray/installing-update-using-tray.test.ts +++ b/packages/core/src/features/application-update/child-features/application-update-using-tray/installing-update-using-tray.test.ts @@ -146,7 +146,7 @@ describe("installing update using tray", () => { it("name of tray item for checking updates indicates that checking is happening", () => { expect( builder.tray.get("check-for-updates")?.label, - ).toBe("Checking for updates..."); + ).toBe("Checking for Updates..."); }); it("user cannot install update yet", () => { @@ -177,7 +177,7 @@ describe("installing update using tray", () => { it("name of tray item for checking updates no longer indicates that checking is happening", () => { expect( builder.tray.get("check-for-updates")?.label, - ).toBe("Check for updates"); + ).toBe("Check for Updates..."); }); it("renders", () => { @@ -241,7 +241,7 @@ describe("installing update using tray", () => { it("name of tray item for checking updates no longer indicates that downloading is happening", () => { expect( builder.tray.get("check-for-updates")?.label, - ).toBe("Check for updates"); + ).toBe("Check for Updates..."); }); it("renders", () => { @@ -269,7 +269,7 @@ describe("installing update using tray", () => { it("name of tray item for checking updates no longer indicates that downloading is happening", () => { expect( builder.tray.get("check-for-updates")?.label, - ).toBe("Check for updates"); + ).toBe("Check for Updates..."); }); it("renders", () => { diff --git a/packages/core/src/features/application-update/child-features/application-update-using-tray/main/tray-items/check-for-updates-tray-item.injectable.ts b/packages/core/src/features/application-update/child-features/application-update-using-tray/main/tray-items/check-for-updates-tray-item.injectable.ts index ccbb71c323..dd655708a8 100644 --- a/packages/core/src/features/application-update/child-features/application-update-using-tray/main/tray-items/check-for-updates-tray-item.injectable.ts +++ b/packages/core/src/features/application-update/child-features/application-update-using-tray/main/tray-items/check-for-updates-tray-item.injectable.ts @@ -49,10 +49,10 @@ const checkForUpdatesTrayItemInjectable = getInjectable({ } if (checkingForUpdatesState.value.get()) { - return "Checking for updates..."; + return "Checking for Updates..."; } - return "Check for updates"; + return "Check for Updates..."; }), enabled: computed(() => !checkingForUpdatesState.value.get() && !downloadingUpdateState.value.get()), diff --git a/packages/core/src/features/cluster/refresh-accessibility-technical.test.ts b/packages/core/src/features/cluster/refresh-accessibility-technical.test.ts new file mode 100644 index 0000000000..73ff6151b9 --- /dev/null +++ b/packages/core/src/features/cluster/refresh-accessibility-technical.test.ts @@ -0,0 +1,637 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type { AsyncFnMock } from "@async-fn/jest"; +import asyncFn from "@async-fn/jest"; +import type { AuthorizationV1Api, CoreV1Api, V1APIGroupList, V1APIVersions, V1NamespaceList, V1SelfSubjectAccessReview, V1SelfSubjectRulesReview } from "@kubernetes/client-node"; +import clusterStoreInjectable from "../../common/cluster-store/cluster-store.injectable"; +import type { Cluster } from "../../common/cluster/cluster"; +import createAuthorizationApiInjectable from "../../common/cluster/create-authorization-api.injectable"; +import writeJsonFileInjectable from "../../common/fs/write-json-file.injectable"; +import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; +import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; +import broadcastMessageInjectable from "../../common/ipc/broadcast-message.injectable"; +import type { PartialDeep } from "type-fest"; +import { anyObject } from "jest-mock-extended"; +import createCoreApiInjectable from "../../common/cluster/create-core-api.injectable"; +import type { K8sRequest } from "../../main/k8s-request.injectable"; +import k8sRequestInjectable from "../../main/k8s-request.injectable"; +import type { DetectClusterMetadata } from "../../main/cluster-detectors/detect-cluster-metadata.injectable"; +import detectClusterMetadataInjectable from "../../main/cluster-detectors/detect-cluster-metadata.injectable"; +import type { ClusterConnection } from "../../main/cluster/cluster-connection.injectable"; +import clusterConnectionInjectable from "../../main/cluster/cluster-connection.injectable"; +import type { KubeAuthProxy } from "../../main/kube-auth-proxy/create-kube-auth-proxy.injectable"; +import createKubeAuthProxyInjectable from "../../main/kube-auth-proxy/create-kube-auth-proxy.injectable"; +import type { Mocked } from "../../test-utils/mock-interface"; +import { flushPromises } from "@k8slens/test-utils"; + +describe("Refresh Cluster Accessibility Technical Tests", () => { + let builder: ApplicationBuilder; + let createSelfSubjectRulesReviewMock: AsyncFnMock; + let createSelfSubjectAccessReviewMock: AsyncFnMock; + let listNamespaceMock: AsyncFnMock; + let k8sRequestMock: AsyncFnMock; + let detectClusterMetadataMock: AsyncFnMock; + let kubeAuthProxyMock: Mocked; + + beforeEach(async () => { + builder = getApplicationBuilder(); + + const mainDi = builder.mainDi; + + mainDi.override(broadcastMessageInjectable, () => async () => {}); + + kubeAuthProxyMock = { + apiPrefix: "/some-api-prefix", + port: 0, + exit: jest.fn(), + run: asyncFn(), + }; + mainDi.override(createKubeAuthProxyInjectable, () => () => kubeAuthProxyMock); + + detectClusterMetadataMock = asyncFn(); + mainDi.override(detectClusterMetadataInjectable, () => detectClusterMetadataMock); + + k8sRequestMock = asyncFn(); + mainDi.override(k8sRequestInjectable, () => k8sRequestMock); + + createSelfSubjectRulesReviewMock = asyncFn(); + createSelfSubjectAccessReviewMock = asyncFn(); + mainDi.override(createAuthorizationApiInjectable, () => () => ({ + createSelfSubjectRulesReview: createSelfSubjectRulesReviewMock, + createSelfSubjectAccessReview: createSelfSubjectAccessReviewMock, + } as any)); + + listNamespaceMock = asyncFn(); + mainDi.override(createCoreApiInjectable, () => () => ({ + listNamespace: listNamespaceMock, + } as any)); + + await builder.render(); + }); + + describe("given a cluster with no configured preferences", () => { + let cluster: Cluster; + let clusterConnection: ClusterConnection; + let refreshPromise: Promise; + + beforeEach(async () => { + const mainDi = builder.mainDi; + const clusterStore = mainDi.inject(clusterStoreInjectable); + const writeJsonFile = mainDi.inject(writeJsonFileInjectable); + + await writeJsonFile("/some-kube-config-path", { + apiVersion: "v1", + kind: "Config", + clusters: [{ + name: "some-cluster-name", + cluster: { + server: "https://localhost:8989", + }, + }], + users: [{ + name: "some-user-name", + }], + contexts: [{ + name: "some-cluster-context", + context: { + user: "some-user-name", + cluster: "some-cluster-name", + }, + }], + }); + + clusterStore.addCluster({ + contextName: "some-cluster-context", + id: "some-cluster-id", + kubeConfigPath: "/some-kube-config-path", + }); + + cluster = clusterStore.getById("some-cluster-id") ?? (() => { throw new Error("missing cluster"); })(); + clusterConnection = mainDi.inject(clusterConnectionInjectable, cluster); + refreshPromise = clusterConnection.refreshAccessibilityAndMetadata(); + }); + + it("starts kubeAuthProxy", () => { + expect(kubeAuthProxyMock.run).toBeCalled(); + }); + + describe("when kubeAuthProxy has started running and its port is found", () => { + beforeEach(async () => { + kubeAuthProxyMock.port = 1235; + await kubeAuthProxyMock.run.resolve(); + await flushPromises(); + }); + + it("requests if cluster has admin permissions", async () => { + expect(createSelfSubjectAccessReviewMock).toBeCalledWith(anyObject({ + spec: { + namespace: "kube-system", + resource: "*", + verb: "create", + }, + })); + }); + + describe.each([ true, false ])("when cluster admin request resolves to %p", (isAdmin) => { + beforeEach(async () => { + await createSelfSubjectAccessReviewMock.resolve({ + body: { + status: { + allowed: isAdmin, + }, + } as PartialDeep, + } as any); + }); + + it("requests if cluster has global watch permissions", () => { + expect(createSelfSubjectAccessReviewMock).toBeCalledWith(anyObject({ + spec: { + verb: "watch", + resource: "*", + }, + })); + }); + + describe.each([ true, false ])("when cluster global watch request resolves with %p", (globalWatch) => { + beforeEach(async () => { + await createSelfSubjectAccessReviewMock.resolve({ + body: { + status: { + allowed: globalWatch, + }, + } as PartialDeep, + } as any); + }); + + it("requests namespaces", () => { + expect(listNamespaceMock).toBeCalled(); + }); + + describe("when list namespaces resolves", () => { + beforeEach(async () => { + await listNamespaceMock.resolve(listNamespaceResponse); + }); + + it("requests core api versions", () => { + expect(k8sRequestMock).toBeCalledWith( + anyObject({ id: "some-cluster-id" }), + "/api", + ); + }); + + describe("when core api versions request resolves", () => { + beforeEach(async () => { + await k8sRequestMock.resolve({ + serverAddressByClientCIDRs: [], + versions: [ + "v1", + ], + } as V1APIVersions); + }); + + it("requests non-core api resource kinds", () => { + expect(k8sRequestMock).toBeCalledWith( + anyObject({ id: "some-cluster-id" }), + "/apis", + ); + }); + + describe("when non-core api resource kinds request resolves", () => { + beforeEach(async () => { + await k8sRequestMock.resolve(nonCoreApiResponse); + }); + + it("requests specific resource kinds in core", () => { + expect(k8sRequestMock).toBeCalledWith( + anyObject({ id: "some-cluster-id" }), + "/api/v1", + ); + }); + + describe("when core specific resource kinds request resolves", () => { + beforeEach(async () => { + await k8sRequestMock.resolve(coreApiKindsResponse); + }); + + it("requests specific resources kinds from the first non-core response", () => { + expect(k8sRequestMock).toBeCalledWith( + anyObject({ id: "some-cluster-id" }), + "/apis/node.k8s.io/v1", + ); + }); + + describe("when first specific resource kinds request resolves", () => { + beforeEach(async () => { + await k8sRequestMock.resolve(nodeK8sIoKindsResponse); + }); + + it("requests specific resources kinds from the second non-core response", () => { + expect(k8sRequestMock).toBeCalledWith( + anyObject({ id: "some-cluster-id" }), + "/apis/discovery.k8s.io/v1", + ); + }); + + describe("when second specific resource kinds request resolves", () => { + beforeEach(async () => { + await k8sRequestMock.resolve(discoveryK8sIoKindsResponse); + }); + + it("requests namespace list permissions for 'default' namespace", () => { + expect(createSelfSubjectRulesReviewMock).toBeCalledWith(anyObject({ + spec: { + namespace: "default", + }, + })); + }); + + describe("when the permissions are incomplete", () => { + beforeEach(async () => { + await createSelfSubjectRulesReviewMock.resolve(defaultIncompletePermissions); + }); + + it("requests namespace list permissions for 'my-namespace' namespace", () => { + expect(createSelfSubjectRulesReviewMock).toBeCalledWith(anyObject({ + spec: { + namespace: "my-namespace", + }, + })); + }); + + describe("when the permissions request for 'my-namespace' resolves as empty", () => { + beforeEach(async () => { + await createSelfSubjectRulesReviewMock.resolve(emptyPermissions); + }); + + it("requests cluster metadata", () => { + expect(detectClusterMetadataMock).toBeCalledWith(anyObject({ id: "some-cluster-id" })); + }); + + describe("when cluster metadata request resolves", () => { + beforeEach(async () => { + await detectClusterMetadataMock.resolve({}); + }); + + it("allows the call to refreshAccessibilityAndMetadata to resolve", async () => { + await refreshPromise; + }); + + it("should have the cluster displaying 'pods'", () => { + expect(cluster.resourcesToShow.has("pods")).toBe(true); + }); + + it("should have the cluster displaying 'namespaces'", () => { + expect(cluster.resourcesToShow.has("namespaces")).toBe(true); + }); + }); + }); + + describe.skip("when the permissions are incomplete", () => {}); + describe.skip("when the permissions resolve to a single entry with 'list' verb", () => {}); + describe.skip("when the permissions resolve to multiple entries with the 'list' verb not on the first entry", () => {}); + }); + + describe("when the permissions resolve to an empty list", () => { + beforeEach(async () => { + await createSelfSubjectRulesReviewMock.resolve(emptyPermissions); + }); + + it("requests namespace list permissions for 'my-namespace' namespace", () => { + expect(createSelfSubjectRulesReviewMock).toBeCalledWith(anyObject({ + spec: { + namespace: "my-namespace", + }, + })); + }); + + describe("when the permissions request for 'my-namespace' resolves as empty", () => { + beforeEach(async () => { + await createSelfSubjectRulesReviewMock.resolve(emptyPermissions); + }); + + it("requests cluster metadata", () => { + expect(detectClusterMetadataMock).toBeCalledWith(anyObject({ id: "some-cluster-id" })); + }); + + describe("when cluster metadata request resolves", () => { + beforeEach(async () => { + await detectClusterMetadataMock.resolve({}); + }); + + it("allows the call to refreshAccessibilityAndMetadata to resolve", async () => { + await refreshPromise; + }); + + it("should have the cluster displaying 'pods'", () => { + expect(cluster.resourcesToShow.has("pods")).toBe(false); + }); + + it("should have the cluster not displaying 'namespaces'", () => { + expect(cluster.resourcesToShow.has("namespaces")).toBe(false); + }); + }); + }); + + describe.skip("when the permissions are incomplete", () => {}); + describe.skip("when the permissions resolve to a single entry with 'list' verb", () => {}); + describe.skip("when the permissions resolve to multiple entries with the 'list' verb not on the first entry", () => {}); + }); + + describe("when the permissions resolve to a single entry with 'list' verb", () => { + beforeEach(async () => { + await createSelfSubjectRulesReviewMock.resolve(defaultSingleListPermissions); + }); + + it("requests namespace list permissions for 'my-namespace' namespace", () => { + expect(createSelfSubjectRulesReviewMock).toBeCalledWith(anyObject({ + spec: { + namespace: "my-namespace", + }, + })); + }); + + describe("when the permissions request for 'my-namespace' resolves as empty", () => { + beforeEach(async () => { + await createSelfSubjectRulesReviewMock.resolve(emptyPermissions); + }); + + it("requests cluster metadata", () => { + expect(detectClusterMetadataMock).toBeCalledWith(anyObject({ id: "some-cluster-id" })); + }); + + describe("when cluster metadata request resolves", () => { + beforeEach(async () => { + await detectClusterMetadataMock.resolve({}); + }); + + it("allows the call to refreshAccessibilityAndMetadata to resolve", async () => { + await refreshPromise; + }); + + it("should have the cluster displaying 'pods'", () => { + expect(cluster.resourcesToShow.has("pods")).toBe(true); + }); + + it("should have the cluster not displaying 'namespaces'", () => { + expect(cluster.resourcesToShow.has("namespaces")).toBe(false); + }); + }); + }); + + describe.skip("when the permissions are incomplete", () => {}); + describe.skip("when the permissions resolve to a single entry with 'list' verb", () => {}); + describe.skip("when the permissions resolve to multiple entries with the 'list' verb not on the first entry", () => {}); + }); + + describe("when the permissions resolve to multiple entries with the 'list' verb not on the first entry", () => { + beforeEach(async () => { + await createSelfSubjectRulesReviewMock.resolve(defaultMultipleListPermissions); + }); + + it("requests namespace list permissions for 'my-namespace' namespace", () => { + expect(createSelfSubjectRulesReviewMock).toBeCalledWith(anyObject({ + spec: { + namespace: "my-namespace", + }, + })); + }); + + describe("when the permissions request for 'my-namespace' resolves as empty", () => { + beforeEach(async () => { + await createSelfSubjectRulesReviewMock.resolve(emptyPermissions); + }); + + it("requests cluster metadata", () => { + expect(detectClusterMetadataMock).toBeCalledWith(anyObject({ id: "some-cluster-id" })); + }); + + describe("when cluster metadata request resolves", () => { + beforeEach(async () => { + await detectClusterMetadataMock.resolve({}); + }); + + it("allows the call to refreshAccessibilityAndMetadata to resolve", async () => { + await refreshPromise; + }); + + it("should have the cluster displaying 'pods'", () => { + expect(cluster.resourcesToShow.has("pods")).toBe(true); + }); + + it("should have the cluster not displaying 'namespaces'", () => { + expect(cluster.resourcesToShow.has("namespaces")).toBe(false); + }); + }); + }); + + describe.skip("when the permissions are incomplete", () => {}); + describe.skip("when the permissions resolve to a single entry with 'list' verb", () => {}); + describe.skip("when the permissions resolve to multiple entries with the 'list' verb not on the first entry", () => {}); + }); + }); + + describe.skip("when second specific resource kinds rejects", () => {}); + }); + }); + + describe.skip("when first specific resource kinds rejects", () => {}); + }); + }); + }); + }); + }); + }); + }); +}); + +const nonCoreApiResponse = { + groups: [ + { + name: "node.k8s.io", + versions: [ + { + groupVersion: "node.k8s.io/v1", + version: "v1", + }, + ], + preferredVersion: { + groupVersion: "node.k8s.io/v1", + version: "v1", + }, + }, + { + name: "discovery.k8s.io", + versions: [ + { + groupVersion: "discovery.k8s.io/v1", + version: "v1", + }, + ], + preferredVersion: { + groupVersion: "discovery.k8s.io/v1", + version: "v1", + }, + }, + ], +} as V1APIGroupList; + +const listNamespaceResponse = { + body: { + items: [ + { + metadata: { + name: "default", + }, + }, + { + metadata: { + name: "my-namespace", + }, + }, + ], + } as PartialDeep, +} as Awaited>; + +const coreApiKindsResponse = { + kind: "APIResourceList", + groupVersion: "v1", + resources: [ + { + name: "namespaces", + singularName: "", + namespaced: false, + kind: "Namespace", + verbs: ["create", "delete", "get", "list", "patch", "update", "watch"], + shortNames: ["ns"], + storageVersionHash: "Q3oi5N2YM8M=", + }, + { + name: "pods", + singularName: "", + namespaced: true, + kind: "Pod", + verbs: [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch", + ], + shortNames: ["po"], + categories: ["all"], + storageVersionHash: "xPOwRZ+Yhw8=", + }, + { + name: "pods/attach", + singularName: "", + namespaced: true, + kind: "PodAttachOptions", + verbs: ["create", "get"], + }, + ], +}; + +const nodeK8sIoKindsResponse = { + kind: "APIResourceList", + apiVersion: "v1", + groupVersion: "node.k8s.io/v1", + resources: [ + { + name: "runtimeclasses", + singularName: "", + namespaced: false, + kind: "RuntimeClass", + verbs: [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch", + ], + storageVersionHash: "WQTu1GL3T2Q=", + }, + ], +}; + +const discoveryK8sIoKindsResponse = { + kind: "APIResourceList", + apiVersion: "v1", + groupVersion: "discovery.k8s.io/v1", + resources: [ + { + name: "endpointslices", + singularName: "", + namespaced: true, + kind: "EndpointSlice", + verbs: [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch", + ], + storageVersionHash: "Nx3SIv6I0mE=", + }, + ], +}; + +type CreateSelfSubjectRulesReviewRes = Awaited>; + +const defaultIncompletePermissions = { + body: { + status: { + incomplete: true, + }, + } as PartialDeep, +} as CreateSelfSubjectRulesReviewRes; + +const emptyPermissions = { + body: { + status: { + resourceRules: [], + }, + } as PartialDeep, +} as CreateSelfSubjectRulesReviewRes; + +const defaultSingleListPermissions = { + body: { + status: { + resourceRules: [{ + apiGroups: [""], + resources: ["pods"], + verbs: ["list"], + }], + }, + } as PartialDeep, +} as CreateSelfSubjectRulesReviewRes; + +const defaultMultipleListPermissions = { + body: { + status: { + resourceRules: [ + { + apiGroups: [""], + resources: ["pods"], + verbs: ["get"], + }, + { + apiGroups: [""], + resources: ["pods"], + verbs: ["list"], + }, + ], + }, + } as PartialDeep, +} as CreateSelfSubjectRulesReviewRes; diff --git a/packages/core/src/features/extension-special-characters-in-page-registrations.test.tsx b/packages/core/src/features/extension-special-characters-in-page-registrations.test.tsx index ac038a5743..b9760eba79 100644 --- a/packages/core/src/features/extension-special-characters-in-page-registrations.test.tsx +++ b/packages/core/src/features/extension-special-characters-in-page-registrations.test.tsx @@ -31,8 +31,7 @@ describe("extension special characters in page registrations", () => { describe("when navigating to route with ID having special characters", () => { beforeEach(() => { - const testExtension = - builder.extensions.get("some-extension-id").applicationWindows.only; + const testExtension = builder.extensions.get("some-extension-id").applicationWindows.only; testExtension.navigate("/some-page-id/"); }); diff --git a/packages/core/src/main/__test__/cluster.test.ts b/packages/core/src/main/__test__/cluster.test.ts index bb495af847..205a23d5ab 100644 --- a/packages/core/src/main/__test__/cluster.test.ts +++ b/packages/core/src/main/__test__/cluster.test.ts @@ -5,10 +5,6 @@ import { Cluster } from "../../common/cluster/cluster"; import { Kubectl } from "../kubectl/kubectl"; import { getDiForUnitTesting } from "../getDiForUnitTesting"; -import createAuthorizationReviewInjectable from "../../common/cluster/authorization-review.injectable"; -import requestNamespaceListPermissionsForInjectable from "../../common/cluster/request-namespace-list-permissions.injectable"; -import createListNamespacesInjectable from "../../common/cluster/list-namespaces.injectable"; -import prometheusHandlerInjectable from "../cluster/prometheus-handler/prometheus-handler.injectable"; import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; import directoryForTempInjectable from "../../common/app-paths/directory-for-temp/directory-for-temp.injectable"; import normalizedPlatformInjectable from "../../common/vars/normalized-platform.injectable"; @@ -19,6 +15,10 @@ import clusterConnectionInjectable from "../cluster/cluster-connection.injectabl import kubeconfigManagerInjectable from "../kubeconfig-manager/kubeconfig-manager.injectable"; import type { KubeconfigManager } from "../kubeconfig-manager/kubeconfig-manager"; import broadcastConnectionUpdateInjectable from "../cluster/broadcast-connection-update.injectable"; +import createCanIInjectable from "../../common/cluster/create-can-i.injectable"; +import createRequestNamespaceListPermissionsInjectable from "../../common/cluster/create-request-namespace-list-permissions.injectable"; +import createListNamespacesInjectable from "../../common/cluster/list-namespaces.injectable"; +import prometheusHandlerInjectable from "../cluster/prometheus-handler/prometheus-handler.injectable"; describe("create clusters", () => { let cluster: Cluster; @@ -34,8 +34,8 @@ describe("create clusters", () => { di.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64"); di.override(normalizedPlatformInjectable, () => "darwin"); di.override(broadcastConnectionUpdateInjectable, () => async () => {}); - di.override(createAuthorizationReviewInjectable, () => () => () => Promise.resolve(true)); - di.override(requestNamespaceListPermissionsForInjectable, () => () => async () => () => true); + di.override(createCanIInjectable, () => () => () => Promise.resolve(true)); + di.override(createRequestNamespaceListPermissionsInjectable, () => () => async () => () => true); di.override(createListNamespacesInjectable, () => () => () => Promise.resolve([ "default" ])); di.override(prometheusHandlerInjectable, () => ({ getPrometheusDetails: jest.fn(), diff --git a/packages/core/src/main/__test__/context-handler.test.ts b/packages/core/src/main/__test__/context-handler.test.ts deleted file mode 100644 index 61bd12398d..0000000000 --- a/packages/core/src/main/__test__/context-handler.test.ts +++ /dev/null @@ -1,203 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import { getDiForUnitTesting } from "../getDiForUnitTesting"; -import { Cluster } from "../../common/cluster/cluster"; -import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable"; -import type { DiContainer } from "@ogre-tools/injectable"; -import { getInjectable } from "@ogre-tools/injectable"; -import type { PrometheusProvider } from "../prometheus/provider"; -import { prometheusProviderInjectionToken } from "../prometheus/provider"; -import { runInAction } from "mobx"; -import prometheusHandlerInjectable from "../cluster/prometheus-handler/prometheus-handler.injectable"; -import directoryForTempInjectable from "../../common/app-paths/directory-for-temp/directory-for-temp.injectable"; -import lensProxyPortInjectable from "../lens-proxy/lens-proxy-port.injectable"; -import type { KubeAuthProxy } from "../kube-auth-proxy/kube-auth-proxy"; -import loadProxyKubeconfigInjectable from "../cluster/load-proxy-kubeconfig.injectable"; -import type { KubeConfig } from "@kubernetes/client-node"; - -enum ServiceResult { - Success, - Failure, -} - -const createTestPrometheusProvider = (kind: string, alwaysFail: ServiceResult): PrometheusProvider => ({ - kind, - name: "TestProvider1", - isConfigurable: false, - getQuery: () => { - throw new Error("getQuery is not implemented."); - }, - getPrometheusService: async () => { - switch (alwaysFail) { - case ServiceResult.Success: - return { - kind, - namespace: "default", - port: 7000, - service: "", - }; - case ServiceResult.Failure: - throw new Error("does fail"); - } - }, -}); - -describe("ContextHandler", () => { - let di: DiContainer; - let cluster: Cluster; - - beforeEach(() => { - di = getDiForUnitTesting(); - - di.override(loadProxyKubeconfigInjectable, () => async () => ({ - makeApiClient: () => ({} as any), - } as Partial)); - - di.override(createKubeAuthProxyInjectable, () => () => ({ - run: async () => {}, - } as KubeAuthProxy)); - di.override(directoryForTempInjectable, () => "/some-directory-for-tmp"); - di.inject(lensProxyPortInjectable).set(9968); - - cluster = new Cluster({ - contextName: "some-context-name", - id: "some-cluster-id", - kubeConfigPath: "/some-kubeconfig-path", - }, { - clusterServerUrl: "https://some-website.com", - }); - }); - - describe("getPrometheusService", () => { - it.each([ - [0], - [1], - [2], - [3], - ])("should throw after %d failure(s)", async (failures) => { - runInAction(() => { - for (let i = 0; i < failures; i += 1) { - di.register(getInjectable({ - id: `test-prometheus-provider-failure-${i}`, - injectionToken: prometheusProviderInjectionToken, - instantiate: () => createTestPrometheusProvider(`id_failure_${i}`, ServiceResult.Failure), - })); - } - }); - - expect(() => di.inject(prometheusHandlerInjectable, cluster).getPrometheusDetails()).rejects.toThrowError(); - }); - - it.each([ - [1, 0], - [1, 1], - [1, 2], - [1, 3], - [2, 0], - [2, 1], - [2, 2], - [2, 3], - ])("should pick the first provider of %d success(es) after %d failure(s)", async (successes, failures) => { - runInAction(() => { - for (let i = 0; i < failures; i += 1) { - di.register(getInjectable({ - id: `test-prometheus-provider-failure-${i}`, - injectionToken: prometheusProviderInjectionToken, - instantiate: () => createTestPrometheusProvider(`id_failure_${i}`, ServiceResult.Failure), - })); - } - - for (let i = 0; i < successes; i += 1) { - di.register(getInjectable({ - id: `test-prometheus-provider-success-${i}`, - injectionToken: prometheusProviderInjectionToken, - instantiate: () => createTestPrometheusProvider(`id_success_${i}`, ServiceResult.Success), - })); - } - }); - - const details = await di.inject(prometheusHandlerInjectable, cluster).getPrometheusDetails(); - - expect(details.provider.kind === `id_failure_${failures}`); - }); - - it.each([ - [1, 0], - [1, 1], - [1, 2], - [1, 3], - [2, 0], - [2, 1], - [2, 2], - [2, 3], - ])("should pick the first provider of %d success(es) before %d failure(s)", async (successes, failures) => { - runInAction(() => { - for (let i = 0; i < failures; i += 1) { - di.register(getInjectable({ - id: `test-prometheus-provider-failure-${i}`, - injectionToken: prometheusProviderInjectionToken, - instantiate: () => createTestPrometheusProvider(`id_failure_${i}`, ServiceResult.Failure), - })); - } - - for (let i = 0; i < successes; i += 1) { - di.register(getInjectable({ - id: `test-prometheus-provider-success-${i}`, - injectionToken: prometheusProviderInjectionToken, - instantiate: () => createTestPrometheusProvider(`id_success_${i}`, ServiceResult.Success), - })); - } - }); - - const details = await di.inject(prometheusHandlerInjectable, cluster).getPrometheusDetails(); - - expect(details.provider.kind === "id_failure_0"); - }); - - it.each([ - [1, 0], - [1, 1], - [1, 2], - [1, 3], - [2, 0], - [2, 1], - [2, 2], - [2, 3], - ])("should pick the first provider of %d success(es) between %d failure(s)", async (successes, failures) => { - const beforeSuccesses = Math.floor(successes / 2); - - runInAction(() => { - for (let i = 0; i < beforeSuccesses; i += 1) { - di.register(getInjectable({ - id: `test-prometheus-provider-success-${i}`, - injectionToken: prometheusProviderInjectionToken, - instantiate: () => createTestPrometheusProvider(`id_success_${i}`, ServiceResult.Success), - })); - } - - for (let i = 0; i < failures; i += 1) { - di.register(getInjectable({ - id: `test-prometheus-provider-failure-${i}`, - injectionToken: prometheusProviderInjectionToken, - instantiate: () => createTestPrometheusProvider(`id_failure_${i}`, ServiceResult.Failure), - })); - } - - for (let i = beforeSuccesses; i < successes; i += 1) { - di.register(getInjectable({ - id: `test-prometheus-provider-success-${i}`, - injectionToken: prometheusProviderInjectionToken, - instantiate: () => createTestPrometheusProvider(`id_success_${i}`, ServiceResult.Success), - })); - } - }); - - const details = await di.inject(prometheusHandlerInjectable, cluster).getPrometheusDetails(); - - expect(details.provider.kind === "id_success_0"); - }); - }); -}); diff --git a/packages/core/src/main/__test__/kube-auth-proxy.test.ts b/packages/core/src/main/__test__/kube-auth-proxy.test.ts index 8fa8a175a5..dc4878f806 100644 --- a/packages/core/src/main/__test__/kube-auth-proxy.test.ts +++ b/packages/core/src/main/__test__/kube-auth-proxy.test.ts @@ -5,7 +5,6 @@ import waitUntilPortIsUsedInjectable from "../kube-auth-proxy/wait-until-port-is-used/wait-until-port-is-used.injectable"; import { Cluster } from "../../common/cluster/cluster"; -import type { KubeAuthProxy } from "../kube-auth-proxy/kube-auth-proxy"; import type { ChildProcess } from "child_process"; import { Kubectl } from "../kubectl/kubectl"; import type { DeepMockProxy } from "jest-mock-extended"; @@ -13,6 +12,7 @@ import { mockDeep, mock } from "jest-mock-extended"; import type { Readable } from "stream"; import { EventEmitter } from "stream"; import { getDiForUnitTesting } from "../getDiForUnitTesting"; +import type { CreateKubeAuthProxy, KubeAuthProxy } from "../kube-auth-proxy/create-kube-auth-proxy.injectable"; import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable"; import spawnInjectable from "../child-process/spawn.injectable"; import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; @@ -29,7 +29,7 @@ import getBasenameOfPathInjectable from "../../common/path/get-basename.injectab const clusterServerUrl = "https://192.168.64.3:8443"; describe("kube auth proxy tests", () => { - let createKubeAuthProxy: (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => KubeAuthProxy; + let createKubeAuthProxy: CreateKubeAuthProxy; let spawnMock: jest.Mock; let waitUntilPortIsUsedMock: jest.Mock; let broadcastMessageMock: jest.Mock; diff --git a/packages/core/src/main/__test__/prometheus-handler.test.ts b/packages/core/src/main/__test__/prometheus-handler.test.ts index 59671553bb..74cab1ea74 100644 --- a/packages/core/src/main/__test__/prometheus-handler.test.ts +++ b/packages/core/src/main/__test__/prometheus-handler.test.ts @@ -43,7 +43,7 @@ const createTestPrometheusProvider = (kind: string, alwaysFail: ServiceResult): }, }); -describe("ContextHandler", () => { +describe("PrometheusHandler", () => { let di: DiContainer; let cluster: Cluster; diff --git a/packages/core/src/main/app-paths/setup-app-paths.injectable.ts b/packages/core/src/main/app-paths/setup-app-paths.injectable.ts index 34178547c3..255beffae0 100644 --- a/packages/core/src/main/app-paths/setup-app-paths.injectable.ts +++ b/packages/core/src/main/app-paths/setup-app-paths.injectable.ts @@ -34,6 +34,7 @@ const setupAppPathsInjectable = getInjectable({ const appDataPath = getElectronAppPath("appData"); setElectronAppPath("userData", joinPaths(appDataPath, appName)); + setElectronAppPath("sessionData", getElectronAppPath("userData")); const appPaths = pipeline( pathNames, diff --git a/packages/core/src/main/cluster/request-api-versions.ts b/packages/core/src/main/cluster/api-versions-requester.ts similarity index 64% rename from packages/core/src/main/cluster/request-api-versions.ts rename to packages/core/src/main/cluster/api-versions-requester.ts index a9b5074549..0bb2c65f6c 100644 --- a/packages/core/src/main/cluster/request-api-versions.ts +++ b/packages/core/src/main/cluster/api-versions-requester.ts @@ -15,8 +15,11 @@ export interface ClusterData { readonly id: string; } -export type RequestApiVersions = (cluster: ClusterData) => AsyncResult; +export interface ApiVersionsRequester { + request(cluster: ClusterData): AsyncResult; + readonly orderNumber: number; +} -export const requestApiVersionsInjectionToken = getInjectionToken({ +export const apiVersionsRequesterInjectionToken = getInjectionToken({ id: "request-api-versions-token", }); diff --git a/packages/core/src/main/cluster/cluster-connection.injectable.ts b/packages/core/src/main/cluster/cluster-connection.injectable.ts index c5d9a5f165..3f40898fc7 100644 --- a/packages/core/src/main/cluster/cluster-connection.injectable.ts +++ b/packages/core/src/main/cluster/cluster-connection.injectable.ts @@ -6,10 +6,7 @@ import { type KubeConfig, HttpError } from "@kubernetes/client-node"; import { reaction, comparer, runInAction } from "mobx"; import { ClusterStatus } from "../../common/cluster-types"; -import type { CreateAuthorizationReview } from "../../common/cluster/authorization-review.injectable"; -import type { Cluster } from "../../common/cluster/cluster"; import type { CreateListNamespaces } from "../../common/cluster/list-namespaces.injectable"; -import type { RequestNamespaceListPermissionsFor, RequestNamespaceListPermissions } from "../../common/cluster/request-namespace-list-permissions.injectable"; import type { BroadcastMessage } from "../../common/ipc/broadcast-message.injectable"; import { clusterListNamespaceForbiddenChannel } from "../../common/ipc/cluster"; import type { Logger } from "../../common/logger"; @@ -25,7 +22,6 @@ import type { RequestApiResources } from "./request-api-resources.injectable"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import broadcastConnectionUpdateInjectable from "./broadcast-connection-update.injectable"; import broadcastMessageInjectable from "../../common/ipc/broadcast-message.injectable"; -import createAuthorizationReviewInjectable from "../../common/cluster/authorization-review.injectable"; import createListNamespacesInjectable from "../../common/cluster/list-namespaces.injectable"; import kubeAuthProxyServerInjectable from "./kube-auth-proxy-server.injectable"; import loadProxyKubeconfigInjectable from "./load-proxy-kubeconfig.injectable"; @@ -33,21 +29,31 @@ import loggerInjectable from "../../common/logger.injectable"; import prometheusHandlerInjectable from "./prometheus-handler/prometheus-handler.injectable"; import removeProxyKubeconfigInjectable from "./remove-proxy-kubeconfig.injectable"; import requestApiResourcesInjectable from "./request-api-resources.injectable"; -import requestNamespaceListPermissionsForInjectable from "../../common/cluster/request-namespace-list-permissions.injectable"; import type { DetectClusterMetadata } from "../cluster-detectors/detect-cluster-metadata.injectable"; import type { FallibleOnlyClusterMetadataDetector } from "../cluster-detectors/token"; import clusterVersionDetectorInjectable from "../cluster-detectors/cluster-version-detector.injectable"; import detectClusterMetadataInjectable from "../cluster-detectors/detect-cluster-metadata.injectable"; import { replaceObservableObject } from "../../common/utils/replace-observable-object"; +import type { CreateAuthorizationApi } from "../../common/cluster/create-authorization-api.injectable"; +import type { CreateCanI } from "../../common/cluster/create-can-i.injectable"; +import type { CreateRequestNamespaceListPermissions, RequestNamespaceListPermissions } from "../../common/cluster/create-request-namespace-list-permissions.injectable"; +import type { Cluster } from "../../common/cluster/cluster"; +import createAuthorizationApiInjectable from "../../common/cluster/create-authorization-api.injectable"; +import createCanIInjectable from "../../common/cluster/create-can-i.injectable"; +import createRequestNamespaceListPermissionsInjectable from "../../common/cluster/create-request-namespace-list-permissions.injectable"; +import type { CreateCoreApi } from "../../common/cluster/create-core-api.injectable"; +import createCoreApiInjectable from "../../common/cluster/create-core-api.injectable"; interface Dependencies { readonly logger: Logger; readonly prometheusHandler: ClusterPrometheusHandler; readonly kubeAuthProxyServer: KubeAuthProxyServer; readonly clusterVersionDetector: FallibleOnlyClusterMetadataDetector; - createAuthorizationReview: CreateAuthorizationReview; + createCanI: CreateCanI; requestApiResources: RequestApiResources; - requestNamespaceListPermissionsFor: RequestNamespaceListPermissionsFor; + createRequestNamespaceListPermissions: CreateRequestNamespaceListPermissions; + createAuthorizationApi: CreateAuthorizationApi; + createCoreApi: CreateCoreApi; createListNamespaces: CreateListNamespaces; detectClusterMetadata: DetectClusterMetadata; broadcastMessage: BroadcastMessage; @@ -224,8 +230,9 @@ class ClusterConnection { private async refreshAccessibility(): Promise { this.dependencies.logger.info(`[CLUSTER]: refreshAccessibility`, this.cluster.getMeta()); const proxyConfig = await this.dependencies.loadProxyKubeconfig(); - const canI = this.dependencies.createAuthorizationReview(proxyConfig); - const requestNamespaceListPermissions = this.dependencies.requestNamespaceListPermissionsFor(proxyConfig); + const api = this.dependencies.createAuthorizationApi(proxyConfig); + const canI = this.dependencies.createCanI(api); + const requestNamespaceListPermissions = this.dependencies.createRequestNamespaceListPermissions(api); const isAdmin = await canI({ namespace: "kube-system", @@ -360,7 +367,8 @@ class ClusterConnection { } try { - const listNamespaces = this.dependencies.createListNamespaces(proxyConfig); + const api = this.dependencies.createCoreApi(proxyConfig); + const listNamespaces = this.dependencies.createListNamespaces(api); return await listNamespaces(); } catch (error) { @@ -403,13 +411,15 @@ const clusterConnectionInjectable = getInjectable({ prometheusHandler: di.inject(prometheusHandlerInjectable, cluster), broadcastConnectionUpdate: di.inject(broadcastConnectionUpdateInjectable, cluster), broadcastMessage: di.inject(broadcastMessageInjectable), - createAuthorizationReview: di.inject(createAuthorizationReviewInjectable), createListNamespaces: di.inject(createListNamespacesInjectable), detectClusterMetadata: di.inject(detectClusterMetadataInjectable), loadProxyKubeconfig: di.inject(loadProxyKubeconfigInjectable, cluster), removeProxyKubeconfig: di.inject(removeProxyKubeconfigInjectable, cluster), requestApiResources: di.inject(requestApiResourcesInjectable), - requestNamespaceListPermissionsFor: di.inject(requestNamespaceListPermissionsForInjectable), + createAuthorizationApi: di.inject(createAuthorizationApiInjectable), + createCoreApi: di.inject(createCoreApiInjectable), + createCanI: di.inject(createCanIInjectable), + createRequestNamespaceListPermissions: di.inject(createRequestNamespaceListPermissionsInjectable), }, cluster, ), diff --git a/packages/core/src/main/cluster/kube-auth-proxy-server.injectable.ts b/packages/core/src/main/cluster/kube-auth-proxy-server.injectable.ts index 9942c40168..ff70af3a7b 100644 --- a/packages/core/src/main/cluster/kube-auth-proxy-server.injectable.ts +++ b/packages/core/src/main/cluster/kube-auth-proxy-server.injectable.ts @@ -8,7 +8,7 @@ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import type { Cluster } from "../../common/cluster/cluster"; import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable"; import kubeAuthProxyCertificateInjectable from "../kube-auth-proxy/kube-auth-proxy-certificate.injectable"; -import type { KubeAuthProxy } from "../kube-auth-proxy/kube-auth-proxy"; +import type { KubeAuthProxy } from "../kube-auth-proxy/create-kube-auth-proxy.injectable"; export interface KubeAuthProxyServer { getApiTarget(isLongRunningRequest?: boolean): Promise; diff --git a/packages/core/src/main/cluster/request-api-resources.injectable.ts b/packages/core/src/main/cluster/request-api-resources.injectable.ts index d6a21e3ce4..14776bb93a 100644 --- a/packages/core/src/main/cluster/request-api-resources.injectable.ts +++ b/packages/core/src/main/cluster/request-api-resources.injectable.ts @@ -7,11 +7,12 @@ import { getInjectable } from "@ogre-tools/injectable"; import loggerInjectable from "../../common/logger.injectable"; import type { KubeApiResource } from "../../common/rbac"; import type { Cluster } from "../../common/cluster/cluster"; -import { requestApiVersionsInjectionToken } from "./request-api-versions"; +import { apiVersionsRequesterInjectionToken } from "./api-versions-requester"; import { backoffCaller, withConcurrencyLimit } from "@k8slens/utilities"; import requestKubeApiResourcesForInjectable from "./request-kube-api-resources-for.injectable"; import type { AsyncResult } from "@k8slens/utilities"; import broadcastConnectionUpdateInjectable from "./broadcast-connection-update.injectable"; +import { byOrderNumber } from "../../common/utils/composable-responsibilities/orderable/orderable"; export type RequestApiResources = (cluster: Cluster) => AsyncResult; @@ -24,7 +25,8 @@ const requestApiResourcesInjectable = getInjectable({ id: "request-api-resources", instantiate: (di): RequestApiResources => { const logger = di.inject(loggerInjectable); - const apiVersionRequesters = di.injectMany(requestApiVersionsInjectionToken); + const apiVersionRequesters = di.injectMany(apiVersionsRequesterInjectionToken) + .sort(byOrderNumber); const requestKubeApiResourcesFor = di.inject(requestKubeApiResourcesForInjectable); return async (...args) => { @@ -35,7 +37,7 @@ const requestApiResourcesInjectable = getInjectable({ const groupLists: KubeResourceListGroup[] = []; for (const apiVersionRequester of apiVersionRequesters) { - const result = await backoffCaller(() => apiVersionRequester(cluster), { + const result = await backoffCaller(() => apiVersionRequester.request(cluster), { onIntermediateError: (error, attempt) => { broadcastConnectionUpdate({ message: `Failed to list kube API resource kinds, attempt ${attempt}: ${error}`, diff --git a/packages/core/src/main/cluster/request-core-api-versions.injectable.ts b/packages/core/src/main/cluster/request-core-api-versions.injectable.ts index b709eb9356..5b059d6f7f 100644 --- a/packages/core/src/main/cluster/request-core-api-versions.injectable.ts +++ b/packages/core/src/main/cluster/request-core-api-versions.injectable.ts @@ -5,33 +5,36 @@ import type { V1APIVersions } from "@kubernetes/client-node"; import { getInjectable } from "@ogre-tools/injectable"; import k8sRequestInjectable from "../k8s-request.injectable"; -import { requestApiVersionsInjectionToken } from "./request-api-versions"; +import { apiVersionsRequesterInjectionToken } from "./api-versions-requester"; const requestCoreApiVersionsInjectable = getInjectable({ id: "request-core-api-versions", instantiate: (di) => { const k8sRequest = di.inject(k8sRequestInjectable); - return async (cluster) => { - try { - const { versions } = await k8sRequest(cluster, "/api") as V1APIVersions; + return { + request: async (cluster) => { + try { + const { versions } = await k8sRequest(cluster, "/api") as V1APIVersions; - return { - callWasSuccessful: true, - response: versions.map(version => ({ - group: "", - path: `/api/${version}`, - })), - }; - } catch (error) { - return { - callWasSuccessful: false, - error: error as Error, - }; - } + return { + callWasSuccessful: true, + response: versions.map(version => ({ + group: "", + path: `/api/${version}`, + })), + }; + } catch (error) { + return { + callWasSuccessful: false, + error: error as Error, + }; + } + }, + orderNumber: 10, }; }, - injectionToken: requestApiVersionsInjectionToken, + injectionToken: apiVersionsRequesterInjectionToken, }); export default requestCoreApiVersionsInjectable; diff --git a/packages/core/src/main/cluster/request-kube-api-resources-for.injectable.ts b/packages/core/src/main/cluster/request-kube-api-resources-for.injectable.ts index 65e326eed7..80720a9ef1 100644 --- a/packages/core/src/main/cluster/request-kube-api-resources-for.injectable.ts +++ b/packages/core/src/main/cluster/request-kube-api-resources-for.injectable.ts @@ -8,7 +8,7 @@ import type { Cluster } from "../../common/cluster/cluster"; import type { KubeApiResource } from "../../common/rbac"; import type { AsyncResult } from "@k8slens/utilities"; import k8sRequestInjectable from "../k8s-request.injectable"; -import type { KubeResourceListGroup } from "./request-api-versions"; +import type { KubeResourceListGroup } from "./api-versions-requester"; export type RequestKubeApiResources = (grouping: KubeResourceListGroup) => AsyncResult; diff --git a/packages/core/src/main/cluster/request-non-core-api-versions.injectable.ts b/packages/core/src/main/cluster/request-non-core-api-versions.injectable.ts index 84e62f6b80..7e5abbc44d 100644 --- a/packages/core/src/main/cluster/request-non-core-api-versions.injectable.ts +++ b/packages/core/src/main/cluster/request-non-core-api-versions.injectable.ts @@ -6,35 +6,40 @@ import type { V1APIGroupList } from "@kubernetes/client-node"; import { getInjectable } from "@ogre-tools/injectable"; import { iter } from "@k8slens/utilities"; import k8sRequestInjectable from "../k8s-request.injectable"; -import { requestApiVersionsInjectionToken } from "./request-api-versions"; +import { apiVersionsRequesterInjectionToken } from "./api-versions-requester"; const requestNonCoreApiVersionsInjectable = getInjectable({ id: "request-non-core-api-versions", instantiate: (di) => { const k8sRequest = di.inject(k8sRequestInjectable); - return async (cluster) => { - try { - const { groups } = await k8sRequest(cluster, "/apis") as V1APIGroupList; + return { + request: async (cluster) => { + try { + const { groups } = (await k8sRequest(cluster, "/apis")) as V1APIGroupList; - return { - callWasSuccessful: true, - response: iter.chain(groups.values()) - .flatMap(group => group.versions.map(version => ({ - group: group.name, - path: `/apis/${version.groupVersion}`, - }))) - .collect(v => [...v]), - }; - } catch (error) { - return { - callWasSuccessful: false, - error: error as Error, - }; - } + return { + callWasSuccessful: true, + response: iter.chain(groups.values()) + .flatMap((group) => + group.versions.map((version) => ({ + group: group.name, + path: `/apis/${version.groupVersion}`, + })), + ) + .collect((v) => [...v]), + }; + } catch (error) { + return { + callWasSuccessful: false, + error: error as Error, + }; + } + }, + orderNumber: 20, }; }, - injectionToken: requestApiVersionsInjectionToken, + injectionToken: apiVersionsRequesterInjectionToken, }); export default requestNonCoreApiVersionsInjectable; diff --git a/packages/core/src/main/cluster/request-non-core-api-versions.test.ts b/packages/core/src/main/cluster/request-non-core-api-versions.test.ts index c38359b6a5..ca4dcd01b7 100644 --- a/packages/core/src/main/cluster/request-non-core-api-versions.test.ts +++ b/packages/core/src/main/cluster/request-non-core-api-versions.test.ts @@ -10,13 +10,13 @@ import type { DiContainer } from "@ogre-tools/injectable"; import { getDiForUnitTesting } from "../getDiForUnitTesting"; import type { K8sRequest } from "../k8s-request.injectable"; import k8sRequestInjectable from "../k8s-request.injectable"; -import type { RequestApiVersions } from "./request-api-versions"; +import type { ApiVersionsRequester } from "./api-versions-requester"; import requestNonCoreApiVersionsInjectable from "./request-non-core-api-versions.injectable"; describe("requestNonCoreApiVersions", () => { let di: DiContainer; let k8sRequestMock: AsyncFnMock; - let requestNonCoreApiVersions: RequestApiVersions; + let requestNonCoreApiVersions: ApiVersionsRequester; beforeEach(() => { di = getDiForUnitTesting(); @@ -28,10 +28,10 @@ describe("requestNonCoreApiVersions", () => { }); describe("when called", () => { - let versionsRequest: ReturnType; + let versionsRequest: ReturnType; beforeEach(() => { - versionsRequest = requestNonCoreApiVersions({ id: "some-cluster-id" }); + versionsRequest = requestNonCoreApiVersions.request({ id: "some-cluster-id" }); }); it("should request all api groups", () => { diff --git a/packages/core/src/main/kube-auth-proxy/create-kube-auth-proxy.injectable.ts b/packages/core/src/main/kube-auth-proxy/create-kube-auth-proxy.injectable.ts index 56260347bf..fd4acf0d7a 100644 --- a/packages/core/src/main/kube-auth-proxy/create-kube-auth-proxy.injectable.ts +++ b/packages/core/src/main/kube-auth-proxy/create-kube-auth-proxy.injectable.ts @@ -4,7 +4,7 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import type { KubeAuthProxyDependencies } from "./kube-auth-proxy"; -import { KubeAuthProxy } from "./kube-auth-proxy"; +import { KubeAuthProxyImpl } from "./kube-auth-proxy"; import type { Cluster } from "../../common/cluster/cluster"; import spawnInjectable from "../child-process/spawn.injectable"; import kubeAuthProxyCertificateInjectable from "./kube-auth-proxy-certificate.injectable"; @@ -15,6 +15,13 @@ import getPortFromStreamInjectable from "../utils/get-port-from-stream.injectabl import getDirnameOfPathInjectable from "../../common/path/get-dirname.injectable"; import broadcastConnectionUpdateInjectable from "../cluster/broadcast-connection-update.injectable"; +export interface KubeAuthProxy { + readonly apiPrefix: string; + readonly port: number; + run: () => Promise; + exit: () => void; +} + export type CreateKubeAuthProxy = (cluster: Cluster, env: NodeJS.ProcessEnv) => KubeAuthProxy; const createKubeAuthProxyInjectable = getInjectable({ @@ -33,7 +40,7 @@ const createKubeAuthProxyInjectable = getInjectable({ return (cluster, env) => { const clusterUrl = new URL(cluster.apiUrl.get()); - return new KubeAuthProxy({ + return new KubeAuthProxyImpl({ ...dependencies, proxyCert: di.inject(kubeAuthProxyCertificateInjectable, clusterUrl.hostname), broadcastConnectionUpdate: di.inject(broadcastConnectionUpdateInjectable, cluster), diff --git a/packages/core/src/main/kube-auth-proxy/kube-auth-proxy.ts b/packages/core/src/main/kube-auth-proxy/kube-auth-proxy.ts index 9a9b5a249f..160566c3a5 100644 --- a/packages/core/src/main/kube-auth-proxy/kube-auth-proxy.ts +++ b/packages/core/src/main/kube-auth-proxy/kube-auth-proxy.ts @@ -16,6 +16,7 @@ import type { Logger } from "../../common/logger"; import type { WaitUntilPortIsUsed } from "./wait-until-port-is-used/wait-until-port-is-used.injectable"; import type { GetDirnameOfPath } from "../../common/path/get-dirname.injectable"; import type { BroadcastConnectionUpdate } from "../cluster/broadcast-connection-update.injectable"; +import type { KubeAuthProxy } from "./create-kube-auth-proxy.injectable"; const startingServeMatcher = "starting to serve on (?
.+)"; const startingServeRegex = Object.assign(TypedRegEx(startingServeMatcher, "i"), { @@ -33,7 +34,7 @@ export interface KubeAuthProxyDependencies { broadcastConnectionUpdate: BroadcastConnectionUpdate; } -export class KubeAuthProxy { +export class KubeAuthProxyImpl implements KubeAuthProxy { public readonly apiPrefix = `/${randomBytes(8).toString("hex")}`; public get port(): number { diff --git a/packages/core/src/main/kubeconfig-manager/kubeconfig-manager.ts b/packages/core/src/main/kubeconfig-manager/kubeconfig-manager.ts index 9d28306bbf..276692c500 100644 --- a/packages/core/src/main/kubeconfig-manager/kubeconfig-manager.ts +++ b/packages/core/src/main/kubeconfig-manager/kubeconfig-manager.ts @@ -85,7 +85,7 @@ export class KubeconfigManager { return this.tempFilePath = await this.createProxyKubeconfig(); } catch (error) { - throw new Error(`Failed to creat temp config for auth-proxy: ${error}`); + throw new Error(`Failed to create temp config for auth-proxy: ${error}`); } } diff --git a/packages/core/src/main/utils/resolve-system-proxy/resolve-system-proxy-window.injectable.ts b/packages/core/src/main/utils/resolve-system-proxy/resolve-system-proxy-window.injectable.ts index 88e4319fa0..baa0da6c39 100644 --- a/packages/core/src/main/utils/resolve-system-proxy/resolve-system-proxy-window.injectable.ts +++ b/packages/core/src/main/utils/resolve-system-proxy/resolve-system-proxy-window.injectable.ts @@ -8,7 +8,11 @@ import { BrowserWindow } from "electron"; const resolveSystemProxyWindowInjectable = getInjectable({ id: "resolve-system-proxy-window", instantiate: () => { - return new BrowserWindow({ show: false, paintWhenInitiallyHidden: false }); + const window = new BrowserWindow({ show: false }); + + window.hide(); + + return window; }, causesSideEffects: true, }); diff --git a/packages/core/src/renderer/before-frame-starts/runnables/setup-auto-crd-api-creations.injectable.ts b/packages/core/src/renderer/before-frame-starts/runnables/setup-auto-crd-api-creations.injectable.ts new file mode 100644 index 0000000000..c23a34d767 --- /dev/null +++ b/packages/core/src/renderer/before-frame-starts/runnables/setup-auto-crd-api-creations.injectable.ts @@ -0,0 +1,53 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { reaction } from "mobx"; +import { customResourceDefinitionApiInjectionToken } from "../../../common/k8s-api/api-manager/crd-api-token"; +import type { CustomResourceDefinition } from "../../../common/k8s-api/endpoints"; +import { KubeApi } from "../../../common/k8s-api/kube-api"; +import { KubeObject } from "../../../common/k8s-api/kube-object"; +import maybeKubeApiInjectable from "../../../common/k8s-api/maybe-kube-api.injectable"; +import loggerInjectable from "../../../common/logger.injectable"; +import { injectableDifferencingRegistratorWith } from "../../../common/utils/registrator-helper"; +import customResourceDefinitionStoreInjectable from "../../components/+custom-resources/definition.store.injectable"; +import { beforeClusterFrameStartsSecondInjectionToken } from "../tokens"; + +const setupAutoCrdApiCreationsInjectable = getInjectable({ + id: "setup-auto-crd-api-creations", + instantiate: (di) => ({ + run: () => { + const customResourceDefinitionStore = di.inject(customResourceDefinitionStoreInjectable); + const injectableDifferencingRegistrator = injectableDifferencingRegistratorWith(di); + + reaction( + () => customResourceDefinitionStore.getItems().map(toCrdApiInjectable), + injectableDifferencingRegistrator, + { + fireImmediately: true, + }, + ); + }, + }), + injectionToken: beforeClusterFrameStartsSecondInjectionToken, +}); + +export default setupAutoCrdApiCreationsInjectable; + +const toCrdApiInjectable = (crd: CustomResourceDefinition) => getInjectable({ + id: `default-kube-api-for-custom-resource-definition-${crd.getResourceApiBase()}`, + instantiate: (di) => { + const objectConstructor = class extends KubeObject { + static readonly kind = crd.getResourceKind(); + static readonly namespaced = crd.isNamespaced(); + static readonly apiBase = crd.getResourceApiBase(); + }; + + return new KubeApi({ + logger: di.inject(loggerInjectable), + maybeKubeApi: di.inject(maybeKubeApiInjectable), + }, { objectConstructor }); + }, + injectionToken: customResourceDefinitionApiInjectionToken, +}); diff --git a/packages/core/src/renderer/before-frame-starts/runnables/setup-auto-registration.injectable.ts b/packages/core/src/renderer/before-frame-starts/runnables/setup-auto-registration.injectable.ts index dbc77a92b8..78b1f90015 100644 --- a/packages/core/src/renderer/before-frame-starts/runnables/setup-auto-registration.injectable.ts +++ b/packages/core/src/renderer/before-frame-starts/runnables/setup-auto-registration.injectable.ts @@ -5,71 +5,22 @@ import { getInjectable } from "@ogre-tools/injectable"; import autoRegistrationEmitterInjectable from "../../../common/k8s-api/api-manager/auto-registration-emitter.injectable"; import apiManagerInjectable from "../../../common/k8s-api/api-manager/manager.injectable"; -import { CustomResourceStore } from "../../../common/k8s-api/api-manager/resource.store"; -import type { CustomResourceDefinition } from "../../../common/k8s-api/endpoints"; -import type { KubeApiDependencies } from "../../../common/k8s-api/kube-api"; -import { KubeApi } from "../../../common/k8s-api/kube-api"; -import { KubeObject } from "../../../common/k8s-api/kube-object"; +import type { KubeApi } from "../../../common/k8s-api/kube-api"; import { beforeClusterFrameStartsSecondInjectionToken } from "../tokens"; -import type { KubeObjectStoreDependencies } from "../../../common/k8s-api/kube-object.store"; -import clusterFrameContextForNamespacedResourcesInjectable from "../../cluster-frame-context/for-namespaced-resources.injectable"; -import loggerInjectable from "../../../common/logger.injectable"; -import maybeKubeApiInjectable from "../../../common/k8s-api/maybe-kube-api.injectable"; const setupAutoRegistrationInjectable = getInjectable({ id: "setup-auto-registration", instantiate: (di) => ({ run: () => { const autoRegistrationEmitter = di.inject(autoRegistrationEmitterInjectable); - const beforeApiManagerInitializationCrds: CustomResourceDefinition[] = []; const beforeApiManagerInitializationApis: KubeApi[] = []; - const kubeApiDependencies: KubeApiDependencies = { - logger: di.inject(loggerInjectable), - maybeKubeApi: di.inject(maybeKubeApiInjectable), - }; - const kubeObjectStoreDependencies: KubeObjectStoreDependencies = { - context: di.inject(clusterFrameContextForNamespacedResourcesInjectable), - logger: di.inject(loggerInjectable), - }; let initialized = false; - const autoInitCustomResourceStore = (crd: CustomResourceDefinition) => { - const objectConstructor = class extends KubeObject { - static readonly kind = crd.getResourceKind(); - static readonly namespaced = crd.isNamespaced(); - static readonly apiBase = crd.getResourceApiBase(); - }; - - const api = (() => { - const rawApi = apiManager.getApi(objectConstructor.apiBase); - - if (rawApi) { - return rawApi; - } - - const api = new KubeApi(kubeApiDependencies, { objectConstructor }); - - apiManager.registerApi(api); - - return api; - })(); - - if (!apiManager.getStore(api)) { - apiManager.registerStore(new CustomResourceStore(kubeObjectStoreDependencies, api)); - } - }; const autoInitKubeApi = (api: KubeApi) => { apiManager.registerApi(api); }; autoRegistrationEmitter - .on("customResourceDefinition", (crd) => { - if (initialized) { - autoInitCustomResourceStore(crd); - } else { - beforeApiManagerInitializationCrds.push(crd); - } - }) .on("kubeApi", (api) => { if (initialized) { autoInitKubeApi(api); @@ -81,7 +32,6 @@ const setupAutoRegistrationInjectable = getInjectable({ // NOTE: this MUST happen after the event emitter listeners are registered const apiManager = di.inject(apiManagerInjectable); - beforeApiManagerInitializationCrds.forEach(autoInitCustomResourceStore); beforeApiManagerInitializationApis.forEach(autoInitKubeApi); initialized = true; }, diff --git a/packages/core/src/renderer/components/+config-horizontal-pod-autoscalers/hpa-details.test.tsx b/packages/core/src/renderer/components/+config-horizontal-pod-autoscalers/hpa-details.test.tsx index 48c2183055..aa2d2a3b32 100644 --- a/packages/core/src/renderer/components/+config-horizontal-pod-autoscalers/hpa-details.test.tsx +++ b/packages/core/src/renderer/components/+config-horizontal-pod-autoscalers/hpa-details.test.tsx @@ -4,8 +4,13 @@ */ import type { RenderResult } from "@testing-library/react"; import React from "react"; +import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable"; +import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; +import { Cluster } from "../../../common/cluster/cluster"; import { HorizontalPodAutoscaler, HpaMetricType } from "../../../common/k8s-api/endpoints"; +import hostedClusterInjectable from "../../cluster-frame-context/hosted-cluster.injectable"; import { getDiForUnitTesting } from "../../getDiForUnitTesting"; +import storesAndApisCanBeCreatedInjectable from "../../stores-apis-can-be-created.injectable"; import type { DiRender } from "../test-utils/renderFor"; import { renderFor } from "../test-utils/renderFor"; import { HpaDetails } from "./hpa-details"; @@ -41,6 +46,17 @@ describe("", () => { beforeEach(() => { const di = getDiForUnitTesting(); + di.override(directoryForUserDataInjectable, () => "/some-user-store-path"); + di.override(directoryForKubeConfigsInjectable, () => "/some-kube-configs"); + di.override(storesAndApisCanBeCreatedInjectable, () => true); + di.override(hostedClusterInjectable, () => new Cluster({ + contextName: "some-context-name", + id: "some-cluster-id", + kubeConfigPath: "/some-path-to-a-kubeconfig", + }, { + clusterServerUrl: "https://localhost:8080", + })); + render = renderFor(di); }); diff --git a/packages/core/src/renderer/components/+custom-resources/definition.store.injectable.ts b/packages/core/src/renderer/components/+custom-resources/definition.store.injectable.ts index f1d8f635b8..dbbd2460aa 100644 --- a/packages/core/src/renderer/components/+custom-resources/definition.store.injectable.ts +++ b/packages/core/src/renderer/components/+custom-resources/definition.store.injectable.ts @@ -4,7 +4,6 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import assert from "assert"; -import autoRegistrationEmitterInjectable from "../../../common/k8s-api/api-manager/auto-registration-emitter.injectable"; import { kubeObjectStoreInjectionToken } from "../../../common/k8s-api/api-manager/kube-object-store-token"; import customResourceDefinitionApiInjectable from "../../../common/k8s-api/endpoints/custom-resource-definition.api.injectable"; import loggerInjectable from "../../../common/logger.injectable"; @@ -20,7 +19,6 @@ const customResourceDefinitionStoreInjectable = getInjectable({ const api = di.inject(customResourceDefinitionApiInjectable); return new CustomResourceDefinitionStore({ - autoRegistration: di.inject(autoRegistrationEmitterInjectable), context: di.inject(clusterFrameContextForClusterScopedResourcesInjectable), logger: di.inject(loggerInjectable), }, api); diff --git a/packages/core/src/renderer/components/+custom-resources/definition.store.ts b/packages/core/src/renderer/components/+custom-resources/definition.store.ts index 310e51709a..78ef3982a9 100644 --- a/packages/core/src/renderer/components/+custom-resources/definition.store.ts +++ b/packages/core/src/renderer/components/+custom-resources/definition.store.ts @@ -3,37 +3,22 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { computed, reaction, makeObservable } from "mobx"; +import { computed, makeObservable } from "mobx"; import type { KubeObjectStoreDependencies, KubeObjectStoreOptions } from "../../../common/k8s-api/kube-object.store"; import { KubeObjectStore } from "../../../common/k8s-api/kube-object.store"; import type { CustomResourceDefinition, CustomResourceDefinitionApi } from "../../../common/k8s-api/endpoints/custom-resource-definition.api"; import type { KubeObject } from "../../../common/k8s-api/kube-object"; -import type TypedEventEmitter from "typed-emitter"; -import type { LegacyAutoRegistration } from "../../../common/k8s-api/api-manager/auto-registration-emitter.injectable"; import autoBind from "auto-bind"; -export interface CustomResourceDefinitionStoreDependencies extends KubeObjectStoreDependencies { - readonly autoRegistration: TypedEventEmitter; -} - export class CustomResourceDefinitionStore extends KubeObjectStore { constructor( - protected readonly dependencies: CustomResourceDefinitionStoreDependencies, + dependencies: KubeObjectStoreDependencies, api: CustomResourceDefinitionApi, opts?: KubeObjectStoreOptions, ) { super(dependencies, api, opts); makeObservable(this); autoBind(this); - - reaction( - () => this.getItems(), - crds => { - for (const crd of crds) { - this.dependencies.autoRegistration.emit("customResourceDefinition", crd); - } - }, - ); } protected sortItems(items: CustomResourceDefinition[]) { diff --git a/packages/core/src/renderer/components/cluster-settings/__tests__/__snapshots__/icon-settings.test.tsx.snap b/packages/core/src/renderer/components/cluster-settings/__tests__/__snapshots__/icon-settings.test.tsx.snap index 97eac8fca2..6c22436089 100644 --- a/packages/core/src/renderer/components/cluster-settings/__tests__/__snapshots__/icon-settings.test.tsx.snap +++ b/packages/core/src/renderer/components/cluster-settings/__tests__/__snapshots__/icon-settings.test.tsx.snap @@ -51,3 +51,55 @@ exports[`Icon settings given no external registrations for cluster settings menu `; + +exports[`Icon settings given no registrations for cluster settings component injection token renders 1`] = ` + +
+
+
+
+
+ + +
+
+ + + more_horiz + + +
+
+
+ +`; diff --git a/packages/core/src/renderer/components/cluster-settings/__tests__/icon-settings.test.tsx b/packages/core/src/renderer/components/cluster-settings/__tests__/icon-settings.test.tsx index 74fd2a6654..31bf671854 100644 --- a/packages/core/src/renderer/components/cluster-settings/__tests__/icon-settings.test.tsx +++ b/packages/core/src/renderer/components/cluster-settings/__tests__/icon-settings.test.tsx @@ -2,8 +2,7 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type { DiContainer } from "@ogre-tools/injectable"; -import { getInjectable } from "@ogre-tools/injectable"; + import type { RenderResult } from "@testing-library/react"; import React from "react"; import { KubernetesCluster } from "../../../../common/catalog-entities"; @@ -13,13 +12,18 @@ import { renderFor } from "../../test-utils/renderFor"; import { ClusterIconSetting } from "../icon-settings"; import { screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import { clusterIconSettingsMenuInjectionToken } from "../cluster-settings-menu-injection-token"; +import type { ClusterIconSettingComponentProps } from "@k8slens/cluster-settings"; +import { clusterIconSettingsComponentInjectionToken, clusterIconSettingsMenuInjectionToken } from "@k8slens/cluster-settings"; import { runInAction } from "mobx"; +import { getInjectable, type DiContainer } from "@ogre-tools/injectable"; const cluster = new Cluster({ contextName: "some-context", id: "some-id", kubeConfigPath: "/some/path/to/kubeconfig", + preferences: { + clusterName: "some-cluster-name", + }, }, { clusterServerUrl: "https://localhost:9999", }); @@ -53,6 +57,29 @@ const newMenuItem = getInjectable({ injectionToken: clusterIconSettingsMenuInjectionToken, }); +function CustomSettingsComponent(props: ClusterIconSettingComponentProps) { + return ( +
+ Test React Component + + Cluster + {props.preferences.clusterName} + +
+ ); +} + +const newSettingsReactComponent = getInjectable({ + id: "cluster-icon-settings-react-component", + + instantiate: () => ({ + id: "test-react-component", + Component: CustomSettingsComponent, + }), + + injectionToken: clusterIconSettingsComponentInjectionToken, +}); + describe("Icon settings", () => { let rendered: RenderResult; let di: DiContainer; @@ -98,4 +125,30 @@ describe("Icon settings", () => { expect(rendered.getByText("Hello World")).toBeInTheDocument(); }); }); + + describe("given no registrations for cluster settings component injection token", () => { + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + it("does not have any external components", async () => { + expect(rendered.queryByTestId("test-react-component")).not.toBeInTheDocument(); + }); + }); + + describe("given registration for cluster settings component injection token", () => { + beforeEach(() => { + runInAction(() => { + di.register(newSettingsReactComponent); + }); + }); + + it("renders external component", async () => { + expect(rendered.queryByTestId("my-react-component")).toBeInTheDocument(); + }); + + it("external component has cluster preferences in props", async () => { + expect(rendered.getByText(/some-cluster-name/)).toBeInTheDocument(); + }); + }); }); diff --git a/packages/core/src/renderer/components/cluster-settings/cluster-settings-menu-clear-item.injectable.ts b/packages/core/src/renderer/components/cluster-settings/cluster-settings-menu-clear-item.injectable.ts index 697c68bb40..87f8c87940 100644 --- a/packages/core/src/renderer/components/cluster-settings/cluster-settings-menu-clear-item.injectable.ts +++ b/packages/core/src/renderer/components/cluster-settings/cluster-settings-menu-clear-item.injectable.ts @@ -3,7 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import { clusterIconSettingsMenuInjectionToken } from "./cluster-settings-menu-injection-token"; +import { clusterIconSettingsMenuInjectionToken } from "@k8slens/cluster-settings"; const clusterIconSettingsMenuClearItem = getInjectable({ id: "cluster-icon-settings-menu-clear-item", diff --git a/packages/core/src/renderer/components/cluster-settings/cluster-settings-menu-injection-token.ts b/packages/core/src/renderer/components/cluster-settings/cluster-settings-menu-injection-token.ts deleted file mode 100644 index 15dbba2754..0000000000 --- a/packages/core/src/renderer/components/cluster-settings/cluster-settings-menu-injection-token.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectionToken } from "@ogre-tools/injectable"; -import type { ClusterPreferences } from "../../../common/cluster-types"; - -export interface ClusterIconMenuItem { - id: string; - title: string; - disabled?: (preferences: ClusterPreferences) => boolean; - onClick: (preferences: ClusterPreferences) => void; -} - -export const clusterIconSettingsMenuInjectionToken = getInjectionToken({ - id: "cluster-icon-settings-menu-injection-token", -}); diff --git a/packages/core/src/renderer/components/cluster-settings/icon-settings.tsx b/packages/core/src/renderer/components/cluster-settings/icon-settings.tsx index 94706f17a5..a802807c17 100644 --- a/packages/core/src/renderer/components/cluster-settings/icon-settings.tsx +++ b/packages/core/src/renderer/components/cluster-settings/icon-settings.tsx @@ -15,8 +15,8 @@ import { FilePicker, OverSizeLimitStyle } from "../file-picker"; import { MenuActions, MenuItem } from "../menu"; import type { ShowNotification } from "../notifications"; import showErrorNotificationInjectable from "../notifications/show-error-notification.injectable"; -import type { ClusterIconMenuItem } from "./cluster-settings-menu-injection-token"; -import { clusterIconSettingsMenuInjectionToken } from "./cluster-settings-menu-injection-token"; +import { clusterIconSettingsComponentInjectionToken, clusterIconSettingsMenuInjectionToken } from "@k8slens/cluster-settings"; +import type { ClusterIconMenuItem, ClusterIconSettingsComponent } from "@k8slens/cluster-settings"; export interface ClusterIconSettingProps { cluster: Cluster; @@ -25,6 +25,7 @@ export interface ClusterIconSettingProps { interface Dependencies { menuItems: IComputedValue; + settingComponents: IComputedValue; showErrorNotification: ShowNotification; } @@ -95,6 +96,14 @@ const NonInjectedClusterIconSetting = observer((props: ClusterIconSettingProps & )} + {props.settingComponents.get().map(item => { + return ( + + ); + })} ); }); @@ -106,6 +115,7 @@ export const ClusterIconSetting = withInjectables { }, enable: (...extensions) => { - builder.afterWindowStart(({ windowDi }) => { - const rendererExtensionInstances = extensions.map((options) => - getExtensionFakeForRenderer( - windowDi, - options.id, - options.name, - options.rendererOptions || {}, - ), - ); + builder.afterWindowStart(action(({ windowDi }) => { + extensions + .map(getRendererExtensionFakeWith(windowDi)) + .forEach(enableExtensionFor(windowDi, rendererExtensionsStateInjectable)); + })); - rendererExtensionInstances.forEach( - enableExtensionFor(windowDi, rendererExtensionsStateInjectable), - ); - }); - - builder.afterApplicationStart(({ mainDi }) => { - const mainExtensionInstances = extensions.map((extension) => - getExtensionFakeForMain(mainDi, extension.id, extension.name, extension.mainOptions || {}), - ); - - runInAction(() => { - mainExtensionInstances.forEach( - enableExtensionFor(mainDi, mainExtensionsStateInjectable), - ); - }); - }); + builder.afterApplicationStart(action(({ mainDi }) => { + extensions + .map(getMainExtensionFakeWith(mainDi)) + .forEach(enableExtensionFor(mainDi, mainExtensionsStateInjectable)); + })); }, disable: (...extensions) => { builder.afterWindowStart(({ windowDi }) => { extensions - .map((ext) => ext.id) - .forEach( - disableExtensionFor(windowDi, rendererExtensionsStateInjectable), - ); + .forEach(disableExtensionFor(windowDi, rendererExtensionsStateInjectable)); }); builder.afterApplicationStart(({ mainDi }) => { extensions - .map((ext) => ext.id) - .forEach( - disableExtensionFor(mainDi, mainExtensionsStateInjectable), - ); + .forEach(disableExtensionFor(mainDi, mainExtensionsStateInjectable)); }); }, }, @@ -835,49 +814,29 @@ const selectOptionFor = (builder: ApplicationBuilder, menuId: string) => (labelT userEvent.click(option); }; -const enableExtensionFor = ( - di: DiContainer, - stateInjectable: Injectable, any, any>, -) => { +function enableExtensionFor(di: DiContainer, stateInjectable: Injectable, any, any>) { const extensionState = di.inject(stateInjectable); - const getExtension = (extension: LensExtension) => - di.inject(extensionInjectable, extension); + return (instance: LensExtension) => { + const extension = di.inject(extensionInjectable, instance); - return (extensionInstance: LensExtension) => { - const extension = getExtension(extensionInstance); - - runInAction(() => { - extension.register(); - extensionState.set(extensionInstance.id, extensionInstance); - }); + extension.register(); + extensionState.set(instance.id, instance); }; -}; +} -const disableExtensionFor = - ( - di: DiContainer, - stateInjectable: Injectable, unknown, void>, - ) => - (id: string) => { - const getExtension = (extension: LensExtension) => - di.inject(extensionInjectable, extension); +function disableExtensionFor(di: DiContainer, stateInjectable: Injectable, unknown, void>) { + return (extension: FakeExtensionOptions) => { + const extensionsState = di.inject(stateInjectable); + const instance = extensionsState.get(extension.id); - const extensionsState = di.inject(stateInjectable); + if (!instance) { + throw new Error(`Tried to disable extension with ID "${extension.id}", but it wasn't enabled`); + } - const instance = extensionsState.get(id); + const injectable = di.inject(extensionInjectable, instance); - if (!instance) { - throw new Error( - `Tried to disable extension with ID "${id}", but it wasn't enabled`, - ); - } - - const injectable = getExtension(instance); - - runInAction(() => { - injectable.deregister(); - - extensionsState.delete(id); - }); - }; + injectable.deregister(); + extensionsState.delete(extension.id); + }; +} diff --git a/packages/core/src/renderer/components/test-utils/get-extension-fake.ts b/packages/core/src/renderer/components/test-utils/get-extension-fake.ts index 8079207daa..675b7e93e2 100644 --- a/packages/core/src/renderer/components/test-utils/get-extension-fake.ts +++ b/packages/core/src/renderer/components/test-utils/get-extension-fake.ts @@ -27,7 +27,7 @@ export interface FakeExtensionOptions { mainOptions?: Partial; } -export const getExtensionFakeForMain = (di: DiContainer, id: string, name: string, options: Partial) => { +export const getMainExtensionFakeWith = (di: DiContainer) => ({ id, name, mainOptions = {}}: FakeExtensionOptions) => { const instance = new TestExtensionMain({ id, absolutePath: "irrelevant", @@ -44,7 +44,7 @@ export const getExtensionFakeForMain = (di: DiContainer, id: string, name: strin manifestPath: "irrelevant", }); - Object.assign(instance, options); + Object.assign(instance, mainOptions); (instance as Writable)[lensExtensionDependencies] = { fileSystemProvisionerStore: di.inject(fileSystemProvisionerStoreInjectable), @@ -56,7 +56,7 @@ export const getExtensionFakeForMain = (di: DiContainer, id: string, name: strin return instance; }; -export const getExtensionFakeForRenderer = (di: DiContainer, id: string, name: string, options: Partial) => { +export const getRendererExtensionFakeWith = (di: DiContainer) => ({ id, name, rendererOptions = {}}: FakeExtensionOptions) => { const instance = new TestExtensionRenderer({ id, absolutePath: "irrelevant", @@ -73,7 +73,7 @@ export const getExtensionFakeForRenderer = (di: DiContainer, id: string, name: s manifestPath: "irrelevant", }); - Object.assign(instance, options); + Object.assign(instance, rendererOptions); (instance as Writable)[lensExtensionDependencies] = { categoryRegistry: di.inject(catalogCategoryRegistryInjectable), diff --git a/packages/core/src/test-utils/cast.ts b/packages/core/src/test-utils/cast.ts new file mode 100644 index 0000000000..a3e7329cdd --- /dev/null +++ b/packages/core/src/test-utils/cast.ts @@ -0,0 +1,6 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +export const cast = (data: Partial): T => data as T; diff --git a/packages/core/src/test-utils/mock-interface.ts b/packages/core/src/test-utils/mock-interface.ts new file mode 100644 index 0000000000..e9870282b3 --- /dev/null +++ b/packages/core/src/test-utils/mock-interface.ts @@ -0,0 +1,17 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type { AsyncFnMock } from "@async-fn/jest"; + +type GetMockedType = + T extends (...args: any[]) => Promise + ? AsyncFnMock + : T extends (...args: any[]) => any + ? jest.MockedFunction + : T; + +export type Mocked = { + -readonly [P in keyof T]: GetMockedType; +}; diff --git a/packages/open-lens/package.json b/packages/open-lens/package.json index f59563de3f..577c1a2a6b 100644 --- a/packages/open-lens/package.json +++ b/packages/open-lens/package.json @@ -96,7 +96,7 @@ }, "build": { "npmRebuild": false, - "electronVersion": "19.1.9", + "electronVersion": "22.3.3", "generateUpdatesFilesForAllChannels": true, "files": [ "static/**/*", @@ -251,7 +251,7 @@ "copy-webpack-plugin": "^11.0.0", "cross-env": "^7.0.3", "css-loader": "^6.7.2", - "electron": "^19.1.9", + "electron": "^22.3.3", "electron-builder": "^23.6.0", "electron-notarize": "^0.3.0", "esbuild-loader": "^2.20.0", diff --git a/packages/technical-features/application/electron-main/package.json b/packages/technical-features/application/electron-main/package.json index f32de6f301..96d649a362 100644 --- a/packages/technical-features/application/electron-main/package.json +++ b/packages/technical-features/application/electron-main/package.json @@ -35,7 +35,7 @@ "@k8slens/feature-core": "^6.5.0-alpha.0", "@ogre-tools/injectable": "^15.1.2", "@ogre-tools/injectable-extension-for-auto-registration": "^15.1.2", - "electron": "^19.1.9" + "electron": "^22.3.3" }, "devDependencies": { "@async-fn/jest": "^1.6.4", diff --git a/packages/technical-features/messaging/agnostic/src/features/actual/message/message-channel-listener-injection-token.ts b/packages/technical-features/messaging/agnostic/src/features/actual/message/message-channel-listener-injection-token.ts index 386c9f9e5e..0558bf6598 100644 --- a/packages/technical-features/messaging/agnostic/src/features/actual/message/message-channel-listener-injection-token.ts +++ b/packages/technical-features/messaging/agnostic/src/features/actual/message/message-channel-listener-injection-token.ts @@ -9,7 +9,7 @@ export interface MessageChannel { export type ExtraData = { processId: number; frameId: number }; export type MessageChannelHandler = Channel extends MessageChannel - ? (message: Message, data: ExtraData) => void + ? (message: Message, data?: ExtraData) => void : never; export interface MessageChannelListener { diff --git a/packages/technical-features/messaging/electron/main/package.json b/packages/technical-features/messaging/electron/main/package.json index 63d66ea6d0..ff1a76a7d3 100644 --- a/packages/technical-features/messaging/electron/main/package.json +++ b/packages/technical-features/messaging/electron/main/package.json @@ -38,7 +38,7 @@ "@k8slens/messaging": "^1.0.0-alpha.1", "@ogre-tools/injectable": "^15.1.2", "@ogre-tools/injectable-extension-for-auto-registration": "^15.1.2", - "electron": "^19.1.8", + "electron": "^22.3.3", "lodash": "^4.17.21" }, "devDependencies": { diff --git a/packages/technical-features/messaging/electron/renderer/package.json b/packages/technical-features/messaging/electron/renderer/package.json index dcb9a7a185..75fcb8e38f 100644 --- a/packages/technical-features/messaging/electron/renderer/package.json +++ b/packages/technical-features/messaging/electron/renderer/package.json @@ -39,7 +39,7 @@ "@k8slens/startable-stoppable": "^1.0.0-alpha.1", "@ogre-tools/injectable": "^15.1.2", "@ogre-tools/injectable-extension-for-auto-registration": "^15.1.2", - "electron": "^19.1.8", + "electron": "^22.3.3", "lodash": "^4.17.21" }, "devDependencies": { diff --git a/packages/technical-features/messaging/electron/renderer/src/listening-of-messages/enlist-message-channel-listener.injectable.ts b/packages/technical-features/messaging/electron/renderer/src/listening-of-messages/enlist-message-channel-listener.injectable.ts index 64e9b1f873..6948e51073 100644 --- a/packages/technical-features/messaging/electron/renderer/src/listening-of-messages/enlist-message-channel-listener.injectable.ts +++ b/packages/technical-features/messaging/electron/renderer/src/listening-of-messages/enlist-message-channel-listener.injectable.ts @@ -10,7 +10,7 @@ const enlistMessageChannelListenerInjectable = getInjectable({ const ipcRenderer = di.inject(ipcRendererInjectable); return ({ channel, handler }) => { - const nativeCallback = (_: IpcRendererEvent, message: unknown) => { + const nativeCallback = (event: IpcRendererEvent, message: unknown) => { handler(message); }; diff --git a/packages/utility-features/test-utils/src/flush-promises.ts b/packages/utility-features/test-utils/src/flush-promises.ts index c2fdeff99e..fa3271654a 100644 --- a/packages/utility-features/test-utils/src/flush-promises.ts +++ b/packages/utility-features/test-utils/src/flush-promises.ts @@ -2,6 +2,9 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { setImmediate } from "timers"; +import { setImmediate, setTimeout } from "timers/promises"; -export const flushPromises = () => new Promise(setImmediate); +export const flushPromises = async () => { + await setImmediate(); + await setTimeout(5); +};