From a920f2c057a53e38e6627faf273b2a67d80e749e Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Tue, 21 Mar 2023 17:07:27 +0200 Subject: [PATCH 1/5] Electron 22.3.3 (#7389) * electron 22.3.3. Signed-off-by: Jari Kolehmainen * fix typo Signed-off-by: Jari Kolehmainen * fix crash on quit Signed-off-by: Jari Kolehmainen * fix sessionData app path Signed-off-by: Jari Kolehmainen * Fix errors after merging new feature Signed-off-by: Sebastian Malton --------- Signed-off-by: Jari Kolehmainen Signed-off-by: Sebastian Malton Co-authored-by: Sebastian Malton --- package-lock.json | 368 ++++-------------- packages/core/package.json | 2 +- .../src/common/app-paths/app-path-names.ts | 2 +- .../src/common/app-paths/app-paths.test.ts | 6 +- .../initialize-sentry-reporting.injectable.ts | 2 +- .../app-paths/setup-app-paths.injectable.ts | 1 + .../resolve-system-proxy-window.injectable.ts | 6 +- packages/open-lens/package.json | 4 +- .../application/electron-main/package.json | 2 +- .../messaging/electron/main/package.json | 2 +- .../messaging/electron/renderer/package.json | 2 +- 11 files changed, 89 insertions(+), 308 deletions(-) diff --git a/package-lock.json b/package-lock.json index fc13b55540..d00efc567c 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", @@ -7977,6 +7816,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 +10903,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 +10917,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 +10939,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 +11031,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 +12510,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 +12584,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 +12902,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 +14861,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 +15958,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 +23920,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 +28337,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 +28531,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 +29935,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 +32235,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 +32547,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 +32632,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 +32957,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", @@ -34511,7 +34287,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 +36474,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 +36968,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 +37045,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 +37063,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/core/package.json b/packages/core/package.json index 15c79d774d..42c39d6181 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -257,7 +257,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/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/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/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/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/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": { From 517e2fe17d07a5e91109ea448db6defb3f65737c Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Tue, 21 Mar 2023 11:12:29 -0400 Subject: [PATCH 2/5] Fix type error in new @k8slens/messaging (#7392) * Fix type error in new @k8slens/messaging Signed-off-by: Sebastian Malton * Better fix to conform to tests Signed-off-by: Sebastian Malton --------- Signed-off-by: Sebastian Malton --- .../actual/message/message-channel-listener-injection-token.ts | 2 +- .../enlist-message-channel-listener.injectable.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/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); }; From 48db54ec9e42b712d97a67b8a8b85f8096e1422d Mon Sep 17 00:00:00 2001 From: Sami Tiilikainen <97873007+samitiilikainen@users.noreply.github.com> Date: Tue, 21 Mar 2023 21:04:22 +0200 Subject: [PATCH 3/5] Renderer file logging through IPC (#7300) * Renderer file logging through IPC Signed-off-by: Sami Tiilikainen <97873007+samitiilikainen@users.noreply.github.com> * Remove pagehide event listener as it may cause UI to freeze Pagehide was needed in cluster frame to better handle main frame close/reload situation. But even empty pagehide listener in cluster frame seems to freeze the UI at least on some situations (multiple clusters open). Beforeunload is not always executed in cluster frame when main frame is reloaded/closed, leaving log files open. To fix that, `stopIpcLoggingInjectable` is introduced to close all log files. Signed-off-by: Sami Tiilikainen <97873007+samitiilikainen@users.noreply.github.com> * Remove unnecessary formatting changes Signed-off-by: Sami Tiilikainen <97873007+samitiilikainen@users.noreply.github.com> * Lint fix Signed-off-by: Sami Tiilikainen <97873007+samitiilikainen@users.noreply.github.com> * Winston logger override Signed-off-by: Sami Tiilikainen <97873007+samitiilikainen@users.noreply.github.com> * Remove usage of doGeneralOverrides as it has been removed Signed-off-by: Sami Tiilikainen <97873007+samitiilikainen@users.noreply.github.com> * Update imports to match the new base Signed-off-by: Sami Tiilikainen <97873007+samitiilikainen@users.noreply.github.com> * Remove unnecessary id Signed-off-by: Sami Tiilikainen <97873007+samitiilikainen@users.noreply.github.com> * Review improvements Signed-off-by: Sami Tiilikainen <97873007+samitiilikainen@users.noreply.github.com> * Extract beforeunload listener to injectable Signed-off-by: Sami Tiilikainen <97873007+samitiilikainen@users.noreply.github.com> * Typo fix Signed-off-by: Sami Tiilikainen <97873007+samitiilikainen@users.noreply.github.com> --------- Signed-off-by: Sami Tiilikainen <97873007+samitiilikainen@users.noreply.github.com> --- packages/core/src/common/logger.injectable.ts | 11 +- .../common/logger/ipc-file-logger-channel.ts | 24 +++ ...n-logger.global-override-for-injectable.ts | 23 +++ .../src/common/winston-logger.injectable.ts | 18 ++ .../close-ipc-logging-listener.injectable.ts | 21 +++ ...ransport.global-override-for-injectable.ts | 18 ++ .../create-ipc-file-transport.injectable.ts | 28 +++ .../main/logger/file-transport.injectable.ts | 6 +- .../main/logger/ipc-file-logger.injectable.ts | 55 ++++++ .../src/main/logger/ipc-file-logger.test.ts | 160 ++++++++++++++++++ .../logger/ipc-logging-listener.injectable.ts | 39 +++++ .../main/logger/ipc-logging-listener.test.ts | 31 ++++ .../logger/stop-ipc-logging.injectable.ts | 27 +++ .../runnables/listen-unload.injectable.ts | 46 +++++ packages/core/src/renderer/bootstrap.tsx | 4 +- .../init-cluster-frame/init-cluster-frame.ts | 95 +++++------ .../root-frame/init-root-frame.injectable.ts | 10 +- .../logger/close-renderer-log-file-id.test.ts | 56 ++++++ .../close-renderer-log-file.injectable.ts | 28 +++ .../logger/ipc-transport.injectable.ts | 60 +++++++ .../src/renderer/logger/ipc-transport.test.ts | 48 ++++++ .../core/src/renderer/logger/ipc-transport.ts | 41 +++++ .../logger/renderer-log-file-id.injectable.ts | 29 ++++ .../logger/renderer-log-file-id.test.ts | 34 ++++ 24 files changed, 836 insertions(+), 76 deletions(-) create mode 100644 packages/core/src/common/logger/ipc-file-logger-channel.ts create mode 100644 packages/core/src/common/winston-logger.global-override-for-injectable.ts create mode 100644 packages/core/src/common/winston-logger.injectable.ts create mode 100644 packages/core/src/main/logger/close-ipc-logging-listener.injectable.ts create mode 100644 packages/core/src/main/logger/create-ipc-file-transport.global-override-for-injectable.ts create mode 100644 packages/core/src/main/logger/create-ipc-file-transport.injectable.ts create mode 100644 packages/core/src/main/logger/ipc-file-logger.injectable.ts create mode 100644 packages/core/src/main/logger/ipc-file-logger.test.ts create mode 100644 packages/core/src/main/logger/ipc-logging-listener.injectable.ts create mode 100644 packages/core/src/main/logger/ipc-logging-listener.test.ts create mode 100644 packages/core/src/main/logger/stop-ipc-logging.injectable.ts create mode 100644 packages/core/src/renderer/before-frame-starts/runnables/listen-unload.injectable.ts create mode 100644 packages/core/src/renderer/logger/close-renderer-log-file-id.test.ts create mode 100644 packages/core/src/renderer/logger/close-renderer-log-file.injectable.ts create mode 100644 packages/core/src/renderer/logger/ipc-transport.injectable.ts create mode 100644 packages/core/src/renderer/logger/ipc-transport.test.ts create mode 100644 packages/core/src/renderer/logger/ipc-transport.ts create mode 100644 packages/core/src/renderer/logger/renderer-log-file-id.injectable.ts create mode 100644 packages/core/src/renderer/logger/renderer-log-file-id.test.ts diff --git a/packages/core/src/common/logger.injectable.ts b/packages/core/src/common/logger.injectable.ts index bc1c5de71b..e64978e44b 100644 --- a/packages/core/src/common/logger.injectable.ts +++ b/packages/core/src/common/logger.injectable.ts @@ -3,20 +3,13 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import { createLogger, format } from "winston"; import type { Logger } from "./logger"; -import { loggerTransportInjectionToken } from "./logger/transports"; +import winstonLoggerInjectable from "./winston-logger.injectable"; const loggerInjectable = getInjectable({ id: "logger", instantiate: (di): Logger => { - const baseLogger = createLogger({ - format: format.combine( - format.splat(), - format.simple(), - ), - transports: di.injectMany(loggerTransportInjectionToken), - }); + const baseLogger = di.inject(winstonLoggerInjectable); return { debug: (message, ...data) => baseLogger.debug(message, ...data), diff --git a/packages/core/src/common/logger/ipc-file-logger-channel.ts b/packages/core/src/common/logger/ipc-file-logger-channel.ts new file mode 100644 index 0000000000..7550f4f314 --- /dev/null +++ b/packages/core/src/common/logger/ipc-file-logger-channel.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { MessageChannel } from "../utils/channel/message-channel-listener-injection-token"; + +export interface IpcFileLogObject { + fileId: string; + entry: { + level: string; + message: string; + internalMessage: string; + }; +} + +export type IpcFileLoggerChannel = MessageChannel; + +export const ipcFileLoggerChannel: IpcFileLoggerChannel = { + id: "ipc-file-logger-channel", +}; + +export const closeIpcFileLoggerChannel: MessageChannel = { + id: "close-ipc-file-logger-channel", +}; diff --git a/packages/core/src/common/winston-logger.global-override-for-injectable.ts b/packages/core/src/common/winston-logger.global-override-for-injectable.ts new file mode 100644 index 0000000000..3d55f914dd --- /dev/null +++ b/packages/core/src/common/winston-logger.global-override-for-injectable.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type winston from "winston"; +import { getGlobalOverride } from "@k8slens/test-utils"; +import { noop } from "@k8slens/utilities"; +import winstonLoggerInjectable from "./winston-logger.injectable"; + +export default getGlobalOverride(winstonLoggerInjectable, () => ({ + log: noop, + add: noop, + remove: noop, + clear: noop, + close: noop, + + warn: noop, + debug: noop, + error: noop, + info: noop, + silly: noop, +}) as winston.Logger); diff --git a/packages/core/src/common/winston-logger.injectable.ts b/packages/core/src/common/winston-logger.injectable.ts new file mode 100644 index 0000000000..481d520fac --- /dev/null +++ b/packages/core/src/common/winston-logger.injectable.ts @@ -0,0 +1,18 @@ +/** + * 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 { createLogger, format } from "winston"; +import { loggerTransportInjectionToken } from "./logger/transports"; + +const winstonLoggerInjectable = getInjectable({ + id: "winston-logger", + instantiate: (di) => + createLogger({ + format: format.combine(format.splat(), format.simple()), + transports: di.injectMany(loggerTransportInjectionToken), + }), +}); + +export default winstonLoggerInjectable; diff --git a/packages/core/src/main/logger/close-ipc-logging-listener.injectable.ts b/packages/core/src/main/logger/close-ipc-logging-listener.injectable.ts new file mode 100644 index 0000000000..6870a29c61 --- /dev/null +++ b/packages/core/src/main/logger/close-ipc-logging-listener.injectable.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import ipcFileLoggerInjectable from "./ipc-file-logger.injectable"; +import { getMessageChannelListenerInjectable } from "../../common/utils/channel/message-channel-listener-injection-token"; +import { + closeIpcFileLoggerChannel, +} from "../../common/logger/ipc-file-logger-channel"; + +const closeIpcFileLoggingListenerInjectable = getMessageChannelListenerInjectable({ + id: "close-ipc-file-logging", + channel: closeIpcFileLoggerChannel, + handler: (di) => { + const ipcFileLogger = di.inject(ipcFileLoggerInjectable); + + return (fileId) => ipcFileLogger.close(fileId); + }, +}); + +export default closeIpcFileLoggingListenerInjectable; diff --git a/packages/core/src/main/logger/create-ipc-file-transport.global-override-for-injectable.ts b/packages/core/src/main/logger/create-ipc-file-transport.global-override-for-injectable.ts new file mode 100644 index 0000000000..98fc62da49 --- /dev/null +++ b/packages/core/src/main/logger/create-ipc-file-transport.global-override-for-injectable.ts @@ -0,0 +1,18 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type { transports } from "winston"; +import { getGlobalOverride } from "@k8slens/test-utils"; +import { noop } from "@k8slens/utilities"; +import createIpcFileLoggerTransportInjectable from "./create-ipc-file-transport.injectable"; + +export default getGlobalOverride( + createIpcFileLoggerTransportInjectable, + () => () => + ({ + log: noop, + close: noop, + } as typeof transports.File), +); diff --git a/packages/core/src/main/logger/create-ipc-file-transport.injectable.ts b/packages/core/src/main/logger/create-ipc-file-transport.injectable.ts new file mode 100644 index 0000000000..f29e02fc90 --- /dev/null +++ b/packages/core/src/main/logger/create-ipc-file-transport.injectable.ts @@ -0,0 +1,28 @@ +/** + * 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 { transports } from "winston"; +import directoryForLogsInjectable from "../../common/app-paths/directory-for-logs.injectable"; + +const createIpcFileLoggerTransportInjectable = getInjectable({ + id: "create-ipc-file-logger-transport", + instantiate: (di) => { + const options = { + dirname: di.inject(directoryForLogsInjectable), + maxsize: 1024 * 1024, + maxFiles: 2, + tailable: true, + }; + + return (fileId: string) => + new transports.File({ + ...options, + filename: `lens-${fileId}.log`, + }); + }, + causesSideEffects: true, +}); + +export default createIpcFileLoggerTransportInjectable; diff --git a/packages/core/src/main/logger/file-transport.injectable.ts b/packages/core/src/main/logger/file-transport.injectable.ts index fcf855eec4..c71b44a2a0 100644 --- a/packages/core/src/main/logger/file-transport.injectable.ts +++ b/packages/core/src/main/logger/file-transport.injectable.ts @@ -7,8 +7,8 @@ import { transports } from "winston"; import directoryForLogsInjectable from "../../common/app-paths/directory-for-logs.injectable"; import { loggerTransportInjectionToken } from "../../common/logger/transports"; -const fileLoggerTranportInjectable = getInjectable({ - id: "file-logger-tranport", +const fileLoggerTransportInjectable = getInjectable({ + id: "file-logger-transport", instantiate: (di) => new transports.File({ handleExceptions: false, level: "debug", @@ -26,4 +26,4 @@ const fileLoggerTranportInjectable = getInjectable({ decorable: false, }); -export default fileLoggerTranportInjectable; +export default fileLoggerTransportInjectable; diff --git a/packages/core/src/main/logger/ipc-file-logger.injectable.ts b/packages/core/src/main/logger/ipc-file-logger.injectable.ts new file mode 100644 index 0000000000..df82ef7c6b --- /dev/null +++ b/packages/core/src/main/logger/ipc-file-logger.injectable.ts @@ -0,0 +1,55 @@ +/** + * 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 { getOrInsertWith } from "@k8slens/utilities"; +import type { LogEntry, transports } from "winston"; +import createIpcFileLoggerTransportInjectable from "./create-ipc-file-transport.injectable"; + +export interface IpcFileLogger { + log: (fileLog: { fileId: string; entry: LogEntry }) => void; + close: (fileId: string) => void; + closeAll: () => void; +} + +const ipcFileLoggerInjectable = getInjectable({ + id: "ipc-file-logger", + instantiate: (di): IpcFileLogger => { + const createIpcFileTransport = di.inject(createIpcFileLoggerTransportInjectable); + const fileTransports = new Map(); + + function log({ fileId, entry }: { fileId: string; entry: LogEntry }) { + const transport = getOrInsertWith( + fileTransports, + fileId, + () => createIpcFileTransport(fileId), + ); + + transport?.log?.(entry, () => {}); + } + + function close(fileId: string) { + const transport = fileTransports.get(fileId); + + if (transport) { + transport.close?.(); + fileTransports.delete(fileId); + } + } + + function closeAll() { + for (const fileId of fileTransports.keys()) { + close(fileId); + } + } + + return { + log, + close, + closeAll, + }; + }, +}); + +export default ipcFileLoggerInjectable; diff --git a/packages/core/src/main/logger/ipc-file-logger.test.ts b/packages/core/src/main/logger/ipc-file-logger.test.ts new file mode 100644 index 0000000000..1ff727e200 --- /dev/null +++ b/packages/core/src/main/logger/ipc-file-logger.test.ts @@ -0,0 +1,160 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getDiForUnitTesting } from "../getDiForUnitTesting"; +import createIpcFileLoggerTransportInjectable from "./create-ipc-file-transport.injectable"; +import type { IpcFileLogger } from "./ipc-file-logger.injectable"; +import ipcFileLoggerInjectable from "./ipc-file-logger.injectable"; + +describe("ipc file logger in main", () => { + let logMock: jest.Mock; + let closeMock: jest.Mock; + let createFileTransportMock: jest.Mock; + let logger: IpcFileLogger; + + beforeEach(() => { + logMock = jest.fn(); + closeMock = jest.fn(); + createFileTransportMock = jest.fn(() => ({ + log: logMock, + close: closeMock, + })); + + const di = getDiForUnitTesting(); + + di.override(createIpcFileLoggerTransportInjectable, () => createFileTransportMock); + logger = di.inject(ipcFileLoggerInjectable); + }); + + it("creates a transport for new log file", () => { + logger.log({ + fileId: "some-log-file", + entry: { level: "irrelevant", message: "irrelevant" }, + }); + + expect(createFileTransportMock).toHaveBeenCalledWith("some-log-file"); + }); + + it("uses existing transport for log file", () => { + logger.log({ + fileId: "some-log-file", + entry: { level: "irrelevant", message: "irrelevant" }, + }); + + logger.log({ + fileId: "some-log-file", + entry: { level: "irrelevant", message: "irrelevant" }, + }); + + logger.log({ + fileId: "some-log-file", + entry: { level: "irrelevant", message: "irrelevant" }, + }); + + expect(createFileTransportMock).toHaveBeenCalledTimes(1); + + expect(createFileTransportMock).toHaveBeenCalledWith("some-log-file"); + }); + + it("creates separate transport for each log file", () => { + logger.log({ + fileId: "some-log-file", + entry: { level: "irrelevant", message: "irrelevant" }, + }); + + logger.log({ + fileId: "some-other-log-file", + entry: { level: "irrelevant", message: "irrelevant" }, + }); + + logger.log({ + fileId: "some-yet-another-log-file", + entry: { level: "irrelevant", message: "irrelevant" }, + }); + + expect(createFileTransportMock).toHaveBeenCalledTimes(3); + + expect(createFileTransportMock).toHaveBeenCalledWith("some-log-file"); + + expect(createFileTransportMock).toHaveBeenCalledWith("some-other-log-file"); + + expect(createFileTransportMock).toHaveBeenCalledWith("some-yet-another-log-file"); + }); + + it("logs using file transport", () => { + logger.log({ + fileId: "some-log-file", + entry: { level: "irrelevant", message: "some-log-message" }, + }); + expect(logMock.mock.calls[0][0]).toEqual({ + level: "irrelevant", + message: "some-log-message", + }); + }); + + it("logs to correct files", () => { + const someLogMock = jest.fn(); + const someOthertLogMock = jest.fn(); + + createFileTransportMock.mockImplementation((fileId: string) => { + if (fileId === "some-log-file") { + return { log: someLogMock }; + } + + if (fileId === "some-other-log-file") { + return { log: someOthertLogMock }; + } + + return null; + }); + + logger.log({ + fileId: "some-log-file", + entry: { level: "irrelevant", message: "some-log-message" }, + }); + logger.log({ + fileId: "some-other-log-file", + entry: { level: "irrelevant", message: "some-other-log-message" }, + }); + + expect(someLogMock).toHaveBeenCalledTimes(1); + expect(someLogMock.mock.calls[0][0]).toEqual({ + level: "irrelevant", + message: "some-log-message", + }); + expect(someOthertLogMock).toHaveBeenCalledTimes(1); + expect(someOthertLogMock.mock.calls[0][0]).toEqual({ + level: "irrelevant", + message: "some-other-log-message", + }); + }); + + it("closes transport (to ensure no file handles are left open)", () => { + logger.log({ + fileId: "some-log-file", + entry: { level: "irrelevant", message: "irrelevant" }, + }); + + logger.close("some-log-file"); + + expect(closeMock).toHaveBeenCalled(); + }); + + it("creates a new transport once needed after closing previous", () => { + logger.log({ + fileId: "some-log-file", + entry: { level: "irrelevant", message: "irrelevant" }, + }); + + logger.close("some-log-file"); + + logger.log({ + fileId: "some-log-file", + entry: { level: "irrelevant", message: "irrelevant" }, + }); + + expect(createFileTransportMock).toHaveBeenCalledTimes(2); + expect(logMock).toHaveBeenCalledTimes(2); + }); +}); diff --git a/packages/core/src/main/logger/ipc-logging-listener.injectable.ts b/packages/core/src/main/logger/ipc-logging-listener.injectable.ts new file mode 100644 index 0000000000..3a3748846b --- /dev/null +++ b/packages/core/src/main/logger/ipc-logging-listener.injectable.ts @@ -0,0 +1,39 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import ipcFileLoggerInjectable from "./ipc-file-logger.injectable"; +import { getMessageChannelListenerInjectable } from "../../common/utils/channel/message-channel-listener-injection-token"; +import type { IpcFileLogObject } from "../../common/logger/ipc-file-logger-channel"; +import { ipcFileLoggerChannel } from "../../common/logger/ipc-file-logger-channel"; +import { MESSAGE } from "triple-beam"; + +/** + * Winston uses symbol property for the actual message. + * + * For that to get through IPC, use the internalMessage property instead + */ +export function deserializeLogFromIpc(ipcFileLogObject: IpcFileLogObject) { + const { internalMessage, ...standardEntry } = ipcFileLogObject.entry; + + return { + ...ipcFileLogObject, + entry: { + ...standardEntry, + [MESSAGE]: internalMessage, + }, + }; +} + +const ipcFileLoggingListenerInjectable = getMessageChannelListenerInjectable({ + id: "ipc-file-logging", + channel: ipcFileLoggerChannel, + handler: (di) => { + const logger = di.inject(ipcFileLoggerInjectable); + + return (ipcFileLogObject) => + logger.log(deserializeLogFromIpc(ipcFileLogObject)); + }, +}); + +export default ipcFileLoggingListenerInjectable; diff --git a/packages/core/src/main/logger/ipc-logging-listener.test.ts b/packages/core/src/main/logger/ipc-logging-listener.test.ts new file mode 100644 index 0000000000..55bb64f4c4 --- /dev/null +++ b/packages/core/src/main/logger/ipc-logging-listener.test.ts @@ -0,0 +1,31 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { MESSAGE } from "triple-beam"; +import { deserializeLogFromIpc } from "./ipc-logging-listener.injectable"; + +describe("Ipc log deserialization", () => { + it("fills in the unique symbol message property Winston transports use internally", () => { + const logObject = { + fileId: "irrelevant", + entry: { + level: "irrelevant", + message: "some public message", + internalMessage: "some internal message", + someProperty: "irrelevant", + }, + }; + + expect(deserializeLogFromIpc(logObject)).toEqual({ + entry: { + level: "irrelevant", + message: "some public message", + [MESSAGE]: "some internal message", + someProperty: "irrelevant", + }, + fileId: "irrelevant", + }); + }); +}); diff --git a/packages/core/src/main/logger/stop-ipc-logging.injectable.ts b/packages/core/src/main/logger/stop-ipc-logging.injectable.ts new file mode 100644 index 0000000000..bdb94a412e --- /dev/null +++ b/packages/core/src/main/logger/stop-ipc-logging.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 { beforeQuitOfFrontEndInjectionToken } from "../start-main-application/runnable-tokens/phases"; +import ipcFileLoggerInjectable from "./ipc-file-logger.injectable"; + +const stopIpcLoggingInjectable = getInjectable({ + id: "stop-ipc-logging", + + instantiate: (di) => { + const ipcFileLogger = di.inject(ipcFileLoggerInjectable); + + return { + run: () => { + ipcFileLogger.closeAll(); + + return undefined; + }, + }; + }, + + injectionToken: beforeQuitOfFrontEndInjectionToken, +}); + +export default stopIpcLoggingInjectable; diff --git a/packages/core/src/renderer/before-frame-starts/runnables/listen-unload.injectable.ts b/packages/core/src/renderer/before-frame-starts/runnables/listen-unload.injectable.ts new file mode 100644 index 0000000000..6b6e0e751c --- /dev/null +++ b/packages/core/src/renderer/before-frame-starts/runnables/listen-unload.injectable.ts @@ -0,0 +1,46 @@ +/** + * 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 currentlyInClusterFrameInjectable from "../../routes/currently-in-cluster-frame.injectable"; +import { beforeFrameStartsSecondInjectionToken } from "../tokens"; +import loggerInjectable from "../../../common/logger.injectable"; +import hostedClusterInjectable from "../../cluster-frame-context/hosted-cluster.injectable"; +import frameRoutingIdInjectable from "../../frames/cluster-frame/init-cluster-frame/frame-routing-id/frame-routing-id.injectable"; +import closeRendererLogFileInjectable from "../../logger/close-renderer-log-file.injectable"; +import { unmountComponentAtNode } from "react-dom"; + +const listenUnloadInjectable = getInjectable({ + id: "listen-unload", + instantiate: (di) => ({ + run: () => { + const closeRendererLogFile = di.inject(closeRendererLogFileInjectable); + const isClusterFrame = di.inject(currentlyInClusterFrameInjectable); + const logger = di.inject(loggerInjectable); + + window.addEventListener("beforeunload", () => { + if (isClusterFrame) { + const hostedCluster = di.inject(hostedClusterInjectable); + const frameRoutingId = di.inject(frameRoutingIdInjectable); + + logger.info( + `[CLUSTER-FRAME] Unload dashboard, clusterId=${hostedCluster?.id}, frameId=${frameRoutingId}`, + ); + } else { + logger.info("[ROOT-FRAME]: Unload app"); + } + + closeRendererLogFile(); + const rootElem = document.getElementById("app"); + + if (rootElem) { + unmountComponentAtNode(rootElem); + } + }); + }, + }), + injectionToken: beforeFrameStartsSecondInjectionToken, +}); + +export default listenUnloadInjectable; diff --git a/packages/core/src/renderer/bootstrap.tsx b/packages/core/src/renderer/bootstrap.tsx index a811f07d5c..75439fce13 100644 --- a/packages/core/src/renderer/bootstrap.tsx +++ b/packages/core/src/renderer/bootstrap.tsx @@ -6,7 +6,7 @@ import "./components/app.scss"; import React from "react"; -import { render, unmountComponentAtNode } from "react-dom"; +import { render } from "react-dom"; import { DefaultProps } from "./mui-base-theme"; import { DiContextProvider } from "@ogre-tools/injectable-react"; import type { @@ -43,7 +43,7 @@ export async function bootstrap(di: DiContainerForInjection) { } try { - await initializeApp(() => unmountComponentAtNode(rootElem)); + await initializeApp(); } catch (error) { console.error(`[BOOTSTRAP]: view initialization error: ${error}`, { origin: location.href, diff --git a/packages/core/src/renderer/frames/cluster-frame/init-cluster-frame/init-cluster-frame.ts b/packages/core/src/renderer/frames/cluster-frame/init-cluster-frame/init-cluster-frame.ts index 9bd0a26a3c..9e901a8060 100644 --- a/packages/core/src/renderer/frames/cluster-frame/init-cluster-frame/init-cluster-frame.ts +++ b/packages/core/src/renderer/frames/cluster-frame/init-cluster-frame/init-cluster-frame.ts @@ -22,62 +22,51 @@ interface Dependencies { const logPrefix = "[CLUSTER-FRAME]:"; -export const initClusterFrame = ({ - hostedCluster, - loadExtensions, - catalogEntityRegistry, - frameRoutingId, - emitAppEvent, - logger, - showErrorNotification, -}: Dependencies) => - async (unmountRoot: () => void) => { +export const initClusterFrame = + ({ + hostedCluster, + loadExtensions, + catalogEntityRegistry, + frameRoutingId, + emitAppEvent, + logger, + showErrorNotification, + }: Dependencies) => + async () => { // TODO: Make catalogEntityRegistry already initialized when passed as dependency - catalogEntityRegistry.init(); + catalogEntityRegistry.init(); - logger.info( - `${logPrefix} Init dashboard, clusterId=${hostedCluster.id}, frameId=${frameRoutingId}`, - ); - - await requestSetClusterFrameId(hostedCluster.id); - await when(() => hostedCluster.ready.get()); // cluster.activate() is done at this point - - catalogEntityRegistry.activeEntity = hostedCluster.id; - - // Only load the extensions once the catalog has been populated. - // Note that the Catalog might still have unprocessed entities until the extensions are fully loaded. - when( - () => catalogEntityRegistry.items.get().length > 0, - () => - loadExtensions(), - { - timeout: 15_000, - onError: (error) => { - logger.warn( - "[CLUSTER-FRAME]: error from activeEntity when()", - error, - ); - - showErrorNotification("Failed to get KubernetesCluster for this view. Extensions will not be loaded."); - }, - }, - ); - - setTimeout(() => { - emitAppEvent({ - name: "cluster", - action: "open", - params: { - clusterId: hostedCluster.id, - }, - }); - }); - - window.onbeforeunload = () => { logger.info( - `${logPrefix} Unload dashboard, clusterId=${(hostedCluster.id)}, frameId=${frameRoutingId}`, + `${logPrefix} Init dashboard, clusterId=${hostedCluster.id}, frameId=${frameRoutingId}`, ); - unmountRoot(); + await requestSetClusterFrameId(hostedCluster.id); + await when(() => hostedCluster.ready.get()); // cluster.activate() is done at this point + + catalogEntityRegistry.activeEntity = hostedCluster.id; + + // Only load the extensions once the catalog has been populated. + // Note that the Catalog might still have unprocessed entities until the extensions are fully loaded. + when( + () => catalogEntityRegistry.items.get().length > 0, + () => loadExtensions(), + { + timeout: 15_000, + onError: (error) => { + logger.warn("[CLUSTER-FRAME]: error from activeEntity when()", error); + + showErrorNotification("Failed to get KubernetesCluster for this view. Extensions will not be loaded."); + }, + }, + ); + + setTimeout(() => { + emitAppEvent({ + name: "cluster", + action: "open", + params: { + clusterId: hostedCluster.id, + }, + }); + }); }; - }; diff --git a/packages/core/src/renderer/frames/root-frame/init-root-frame.injectable.ts b/packages/core/src/renderer/frames/root-frame/init-root-frame.injectable.ts index f1e3024d80..e1bedb0c88 100644 --- a/packages/core/src/renderer/frames/root-frame/init-root-frame.injectable.ts +++ b/packages/core/src/renderer/frames/root-frame/init-root-frame.injectable.ts @@ -9,7 +9,6 @@ import lensProtocolRouterRendererInjectable from "../../protocol-handler/lens-pr import catalogEntityRegistryInjectable from "../../api/catalog/entity/registry.injectable"; import registerIpcListenersInjectable from "../../ipc/register-ipc-listeners.injectable"; import loadExtensionsInjectable from "../load-extensions.injectable"; -import loggerInjectable from "../../../common/logger.injectable"; import { delay } from "@k8slens/utilities"; import { broadcastMessage } from "../../../common/ipc"; import { bundledExtensionsLoaded } from "../../../common/ipc/extension-handling"; @@ -23,9 +22,8 @@ const initRootFrameInjectable = getInjectable({ const bindProtocolAddRouteHandlers = di.inject(bindProtocolAddRouteHandlersInjectable); const lensProtocolRouterRenderer = di.inject(lensProtocolRouterRendererInjectable); const catalogEntityRegistry = di.inject(catalogEntityRegistryInjectable); - const logger = di.inject(loggerInjectable); - return async (unmountRoot: () => void) => { + return async () => { catalogEntityRegistry.init(); try { @@ -56,12 +54,6 @@ const initRootFrameInjectable = getInjectable({ window.addEventListener("online", () => broadcastMessage("network:online")); registerIpcListeners(); - - window.addEventListener("beforeunload", () => { - logger.info("[ROOT-FRAME]: Unload app"); - - unmountRoot(); - }); }; }, }); diff --git a/packages/core/src/renderer/logger/close-renderer-log-file-id.test.ts b/packages/core/src/renderer/logger/close-renderer-log-file-id.test.ts new file mode 100644 index 0000000000..1520844c30 --- /dev/null +++ b/packages/core/src/renderer/logger/close-renderer-log-file-id.test.ts @@ -0,0 +1,56 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import winstonLoggerInjectable from "../../common/winston-logger.injectable"; +import { getDiForUnitTesting } from "../getDiForUnitTesting"; +import closeRendererLogFileInjectable from "./close-renderer-log-file.injectable"; +import type { DiContainer } from "@ogre-tools/injectable"; +import type winston from "winston"; +import type { SendMessageToChannel } from "../../common/utils/channel/message-to-channel-injection-token"; +import { sendMessageToChannelInjectionToken } from "../../common/utils/channel/message-to-channel-injection-token"; +import rendererLogFileIdInjectable from "./renderer-log-file-id.injectable"; +import ipcLogTransportInjectable from "./ipc-transport.injectable"; +import type IpcLogTransport from "./ipc-transport"; + +describe("close renderer file logging", () => { + let di: DiContainer; + let sendIpcMock: SendMessageToChannel; + let winstonMock: winston.Logger; + let ipcTransportMock: IpcLogTransport; + + beforeEach(() => { + di = getDiForUnitTesting(); + sendIpcMock = jest.fn(); + winstonMock = { + remove: jest.fn(), + } as any as winston.Logger; + ipcTransportMock = { name: "ipc-renderer-transport" } as IpcLogTransport; + + di.override(winstonLoggerInjectable, () => winstonMock); + di.override(sendMessageToChannelInjectionToken, () => sendIpcMock); + di.override(rendererLogFileIdInjectable, () => "some-log-id"); + di.override(ipcLogTransportInjectable, () => ipcTransportMock); + }); + + it("sends the ipc close message with correct log id", () => { + const closeLog = di.inject(closeRendererLogFileInjectable); + + closeLog(); + + expect(sendIpcMock).toHaveBeenCalledWith( + { id: "close-ipc-file-logger-channel" }, + "some-log-id", + ); + }); + + it("removes the transport to prevent further logging to closed file", () => { + const closeLog = di.inject(closeRendererLogFileInjectable); + + closeLog(); + + expect(winstonMock.remove).toHaveBeenCalledWith({ + name: "ipc-renderer-transport", + }); + }); +}); diff --git a/packages/core/src/renderer/logger/close-renderer-log-file.injectable.ts b/packages/core/src/renderer/logger/close-renderer-log-file.injectable.ts new file mode 100644 index 0000000000..8015708d84 --- /dev/null +++ b/packages/core/src/renderer/logger/close-renderer-log-file.injectable.ts @@ -0,0 +1,28 @@ +/** + * 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 winstonLoggerInjectable from "../../common/winston-logger.injectable"; +import { closeIpcFileLoggerChannel } from "../../common/logger/ipc-file-logger-channel"; +import { sendMessageToChannelInjectionToken } from "../../common/utils/channel/message-to-channel-injection-token"; +import rendererLogFileIdInjectable from "./renderer-log-file-id.injectable"; +import ipcLogTransportInjectable from "./ipc-transport.injectable"; + +const closeRendererLogFileInjectable = getInjectable({ + id: "close-renderer-log-file", + instantiate: (di) => { + const winstonLogger = di.inject(winstonLoggerInjectable); + const ipcLogTransport = di.inject(ipcLogTransportInjectable); + const messageToChannel = di.inject(sendMessageToChannelInjectionToken); + const fileId = di.inject(rendererLogFileIdInjectable); + + + return () => { + messageToChannel(closeIpcFileLoggerChannel, fileId); + winstonLogger.remove(ipcLogTransport); + }; + }, +}); + +export default closeRendererLogFileInjectable; diff --git a/packages/core/src/renderer/logger/ipc-transport.injectable.ts b/packages/core/src/renderer/logger/ipc-transport.injectable.ts new file mode 100644 index 0000000000..45139cd917 --- /dev/null +++ b/packages/core/src/renderer/logger/ipc-transport.injectable.ts @@ -0,0 +1,60 @@ +/** + * 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 { loggerTransportInjectionToken } from "../../common/logger/transports"; +import type winston from "winston"; +import { MESSAGE } from "triple-beam"; + +import IpcLogTransport from "./ipc-transport"; +import { sendMessageToChannelInjectionToken } from "../../common/utils/channel/message-to-channel-injection-token"; +import type { + IpcFileLogObject } from "../../common/logger/ipc-file-logger-channel"; +import { + closeIpcFileLoggerChannel, + ipcFileLoggerChannel, +} from "../../common/logger/ipc-file-logger-channel"; +import rendererLogFileIdInjectable from "./renderer-log-file-id.injectable"; + +/** + * Winston uses symbol property for the actual message. + * + * For that to get through IPC, use the internalMessage property instead + */ +function serializeLogForIpc( + fileId: string, + entry: winston.LogEntry, +): IpcFileLogObject { + return { + fileId, + entry: { + level: entry.level, + message: entry.message, + internalMessage: Object.getOwnPropertyDescriptor(entry, MESSAGE)?.value, + }, + }; +} + +const ipcLogTransportInjectable = getInjectable({ + id: "renderer-file-logger-transport", + instantiate: (di) => { + const messageToChannel = di.inject(sendMessageToChannelInjectionToken); + const fileId = di.inject(rendererLogFileIdInjectable); + + return new IpcLogTransport({ + sendIpcLogMessage: (entry) => + messageToChannel( + ipcFileLoggerChannel, + serializeLogForIpc(fileId, entry), + ), + closeIpcLogging: () => + messageToChannel(closeIpcFileLoggerChannel, fileId), + handleExceptions: false, + level: "info", + }); + }, + injectionToken: loggerTransportInjectionToken, +}); + +export default ipcLogTransportInjectable; diff --git a/packages/core/src/renderer/logger/ipc-transport.test.ts b/packages/core/src/renderer/logger/ipc-transport.test.ts new file mode 100644 index 0000000000..931df377c7 --- /dev/null +++ b/packages/core/src/renderer/logger/ipc-transport.test.ts @@ -0,0 +1,48 @@ +/** + * 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 type { SendMessageToChannel } from "../../common/utils/channel/message-to-channel-injection-token"; +import { sendMessageToChannelInjectionToken } from "../../common/utils/channel/message-to-channel-injection-token"; +import { getDiForUnitTesting } from "../getDiForUnitTesting"; +import rendererLogFileIdInjectable from "./renderer-log-file-id.injectable"; +import ipcLogTransportInjectable from "./ipc-transport.injectable"; +import { MESSAGE } from "triple-beam"; + +describe("renderer log transport through ipc", () => { + let di: DiContainer; + let sendIpcMock: SendMessageToChannel; + + beforeEach(() => { + sendIpcMock = jest.fn(); + di = getDiForUnitTesting(); + di.override(sendMessageToChannelInjectionToken, () => sendIpcMock); + di.override(rendererLogFileIdInjectable, () => "some-log-id"); + }); + + it("send serialized ipc messages on log", () => { + const logTransport = di.inject(ipcLogTransportInjectable); + + logTransport.log( + { + level: "info", + message: "some log text", + [MESSAGE]: "actual winston log text", + }, + () => {}, + ); + + expect(sendIpcMock).toHaveBeenCalledWith( + { id: "ipc-file-logger-channel" }, + { + entry: { + level: "info", + message: "some log text", + internalMessage: "actual winston log text", + }, + fileId: "some-log-id", + }, + ); + }); +}); diff --git a/packages/core/src/renderer/logger/ipc-transport.ts b/packages/core/src/renderer/logger/ipc-transport.ts new file mode 100644 index 0000000000..a1f6fa819e --- /dev/null +++ b/packages/core/src/renderer/logger/ipc-transport.ts @@ -0,0 +1,41 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { LogEntry } from "winston"; +import type { TransportStreamOptions } from "winston-transport"; +import TransportStream from "winston-transport"; + +interface IpcLogTransportOptions extends TransportStreamOptions { + sendIpcLogMessage: (entry: LogEntry) => void; + closeIpcLogging: () => void; +} + +class IpcLogTransport extends TransportStream { + sendIpcLogMessage: (entry: LogEntry) => void; + closeIpcLogging: () => void; + name = "ipc-renderer-transport"; + + constructor(options: IpcLogTransportOptions) { + const { sendIpcLogMessage, closeIpcLogging, ...winstonOptions } = options; + + super(winstonOptions); + + this.sendIpcLogMessage = sendIpcLogMessage; + this.closeIpcLogging = closeIpcLogging; + } + + log(logEntry: LogEntry, next: () => void) { + setImmediate(() => { + this.emit("logged", logEntry); + }); + this.sendIpcLogMessage(logEntry); + next(); + } + + close() { + this.closeIpcLogging(); + } +} + +export default IpcLogTransport; diff --git a/packages/core/src/renderer/logger/renderer-log-file-id.injectable.ts b/packages/core/src/renderer/logger/renderer-log-file-id.injectable.ts new file mode 100644 index 0000000000..81f4196b08 --- /dev/null +++ b/packages/core/src/renderer/logger/renderer-log-file-id.injectable.ts @@ -0,0 +1,29 @@ +/** + * 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 windowLocationInjectable from "../../common/k8s-api/window-location.injectable"; +import currentlyInClusterFrameInjectable from "../routes/currently-in-cluster-frame.injectable"; +import { getClusterIdFromHost } from "../../common/utils"; + +const rendererLogFileIdInjectable = getInjectable({ + id: "renderer-log-file-id", + instantiate: (di) => { + let frameId: string; + const currentlyInClusterFrame = di.inject(currentlyInClusterFrameInjectable); + + if (currentlyInClusterFrame) { + const { host } = di.inject(windowLocationInjectable); + const clusterId = getClusterIdFromHost(host); + + frameId = `cluster-${clusterId}`; + } else { + frameId = "main"; + } + + return `renderer-${frameId}`; + }, +}); + +export default rendererLogFileIdInjectable; diff --git a/packages/core/src/renderer/logger/renderer-log-file-id.test.ts b/packages/core/src/renderer/logger/renderer-log-file-id.test.ts new file mode 100644 index 0000000000..bd2abf7a63 --- /dev/null +++ b/packages/core/src/renderer/logger/renderer-log-file-id.test.ts @@ -0,0 +1,34 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import windowLocationInjectable from "../../common/k8s-api/window-location.injectable"; +import { getDiForUnitTesting } from "../getDiForUnitTesting"; +import currentlyInClusterFrameInjectable from "../routes/currently-in-cluster-frame.injectable"; +import rendererLogFileIdInjectable from "./renderer-log-file-id.injectable"; + +describe("renderer log file id", () => { + + it("clearly names log for renderer main frame", () => { + const di = getDiForUnitTesting(); + + di.override(currentlyInClusterFrameInjectable, () => false); + + const mainFileId = di.inject(rendererLogFileIdInjectable); + + expect(mainFileId).toBe("renderer-main"); + }); + + it("includes cluster id in renderer log file names", () => { + const di = getDiForUnitTesting(); + + di.override(currentlyInClusterFrameInjectable, () => true); + di.override(windowLocationInjectable, () => ({ + host: "some-cluster.lens.app", + port: "irrelevant", + })); + const clusterFileId = di.inject(rendererLogFileIdInjectable); + + expect(clusterFileId).toBe("renderer-cluster-some-cluster"); + }); +}); From 54093242367717292312df01905d052b66017953 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Tue, 21 Mar 2023 16:13:27 -0400 Subject: [PATCH 4/5] Revert "Renderer file logging through IPC" (#7393) This reverts commit 48db54ec9e42b712d97a67b8a8b85f8096e1422d. --- packages/core/src/common/logger.injectable.ts | 11 +- .../common/logger/ipc-file-logger-channel.ts | 24 --- ...n-logger.global-override-for-injectable.ts | 23 --- .../src/common/winston-logger.injectable.ts | 18 -- .../close-ipc-logging-listener.injectable.ts | 21 --- ...ransport.global-override-for-injectable.ts | 18 -- .../create-ipc-file-transport.injectable.ts | 28 --- .../main/logger/file-transport.injectable.ts | 6 +- .../main/logger/ipc-file-logger.injectable.ts | 55 ------ .../src/main/logger/ipc-file-logger.test.ts | 160 ------------------ .../logger/ipc-logging-listener.injectable.ts | 39 ----- .../main/logger/ipc-logging-listener.test.ts | 31 ---- .../logger/stop-ipc-logging.injectable.ts | 27 --- .../runnables/listen-unload.injectable.ts | 46 ----- packages/core/src/renderer/bootstrap.tsx | 4 +- .../init-cluster-frame/init-cluster-frame.ts | 87 +++++----- .../root-frame/init-root-frame.injectable.ts | 10 +- .../logger/close-renderer-log-file-id.test.ts | 56 ------ .../close-renderer-log-file.injectable.ts | 28 --- .../logger/ipc-transport.injectable.ts | 60 ------- .../src/renderer/logger/ipc-transport.test.ts | 48 ------ .../core/src/renderer/logger/ipc-transport.ts | 41 ----- .../logger/renderer-log-file-id.injectable.ts | 29 ---- .../logger/renderer-log-file-id.test.ts | 34 ---- 24 files changed, 72 insertions(+), 832 deletions(-) delete mode 100644 packages/core/src/common/logger/ipc-file-logger-channel.ts delete mode 100644 packages/core/src/common/winston-logger.global-override-for-injectable.ts delete mode 100644 packages/core/src/common/winston-logger.injectable.ts delete mode 100644 packages/core/src/main/logger/close-ipc-logging-listener.injectable.ts delete mode 100644 packages/core/src/main/logger/create-ipc-file-transport.global-override-for-injectable.ts delete mode 100644 packages/core/src/main/logger/create-ipc-file-transport.injectable.ts delete mode 100644 packages/core/src/main/logger/ipc-file-logger.injectable.ts delete mode 100644 packages/core/src/main/logger/ipc-file-logger.test.ts delete mode 100644 packages/core/src/main/logger/ipc-logging-listener.injectable.ts delete mode 100644 packages/core/src/main/logger/ipc-logging-listener.test.ts delete mode 100644 packages/core/src/main/logger/stop-ipc-logging.injectable.ts delete mode 100644 packages/core/src/renderer/before-frame-starts/runnables/listen-unload.injectable.ts delete mode 100644 packages/core/src/renderer/logger/close-renderer-log-file-id.test.ts delete mode 100644 packages/core/src/renderer/logger/close-renderer-log-file.injectable.ts delete mode 100644 packages/core/src/renderer/logger/ipc-transport.injectable.ts delete mode 100644 packages/core/src/renderer/logger/ipc-transport.test.ts delete mode 100644 packages/core/src/renderer/logger/ipc-transport.ts delete mode 100644 packages/core/src/renderer/logger/renderer-log-file-id.injectable.ts delete mode 100644 packages/core/src/renderer/logger/renderer-log-file-id.test.ts diff --git a/packages/core/src/common/logger.injectable.ts b/packages/core/src/common/logger.injectable.ts index e64978e44b..bc1c5de71b 100644 --- a/packages/core/src/common/logger.injectable.ts +++ b/packages/core/src/common/logger.injectable.ts @@ -3,13 +3,20 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; +import { createLogger, format } from "winston"; import type { Logger } from "./logger"; -import winstonLoggerInjectable from "./winston-logger.injectable"; +import { loggerTransportInjectionToken } from "./logger/transports"; const loggerInjectable = getInjectable({ id: "logger", instantiate: (di): Logger => { - const baseLogger = di.inject(winstonLoggerInjectable); + const baseLogger = createLogger({ + format: format.combine( + format.splat(), + format.simple(), + ), + transports: di.injectMany(loggerTransportInjectionToken), + }); return { debug: (message, ...data) => baseLogger.debug(message, ...data), diff --git a/packages/core/src/common/logger/ipc-file-logger-channel.ts b/packages/core/src/common/logger/ipc-file-logger-channel.ts deleted file mode 100644 index 7550f4f314..0000000000 --- a/packages/core/src/common/logger/ipc-file-logger-channel.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import type { MessageChannel } from "../utils/channel/message-channel-listener-injection-token"; - -export interface IpcFileLogObject { - fileId: string; - entry: { - level: string; - message: string; - internalMessage: string; - }; -} - -export type IpcFileLoggerChannel = MessageChannel; - -export const ipcFileLoggerChannel: IpcFileLoggerChannel = { - id: "ipc-file-logger-channel", -}; - -export const closeIpcFileLoggerChannel: MessageChannel = { - id: "close-ipc-file-logger-channel", -}; diff --git a/packages/core/src/common/winston-logger.global-override-for-injectable.ts b/packages/core/src/common/winston-logger.global-override-for-injectable.ts deleted file mode 100644 index 3d55f914dd..0000000000 --- a/packages/core/src/common/winston-logger.global-override-for-injectable.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import type winston from "winston"; -import { getGlobalOverride } from "@k8slens/test-utils"; -import { noop } from "@k8slens/utilities"; -import winstonLoggerInjectable from "./winston-logger.injectable"; - -export default getGlobalOverride(winstonLoggerInjectable, () => ({ - log: noop, - add: noop, - remove: noop, - clear: noop, - close: noop, - - warn: noop, - debug: noop, - error: noop, - info: noop, - silly: noop, -}) as winston.Logger); diff --git a/packages/core/src/common/winston-logger.injectable.ts b/packages/core/src/common/winston-logger.injectable.ts deleted file mode 100644 index 481d520fac..0000000000 --- a/packages/core/src/common/winston-logger.injectable.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * 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 { createLogger, format } from "winston"; -import { loggerTransportInjectionToken } from "./logger/transports"; - -const winstonLoggerInjectable = getInjectable({ - id: "winston-logger", - instantiate: (di) => - createLogger({ - format: format.combine(format.splat(), format.simple()), - transports: di.injectMany(loggerTransportInjectionToken), - }), -}); - -export default winstonLoggerInjectable; diff --git a/packages/core/src/main/logger/close-ipc-logging-listener.injectable.ts b/packages/core/src/main/logger/close-ipc-logging-listener.injectable.ts deleted file mode 100644 index 6870a29c61..0000000000 --- a/packages/core/src/main/logger/close-ipc-logging-listener.injectable.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import ipcFileLoggerInjectable from "./ipc-file-logger.injectable"; -import { getMessageChannelListenerInjectable } from "../../common/utils/channel/message-channel-listener-injection-token"; -import { - closeIpcFileLoggerChannel, -} from "../../common/logger/ipc-file-logger-channel"; - -const closeIpcFileLoggingListenerInjectable = getMessageChannelListenerInjectable({ - id: "close-ipc-file-logging", - channel: closeIpcFileLoggerChannel, - handler: (di) => { - const ipcFileLogger = di.inject(ipcFileLoggerInjectable); - - return (fileId) => ipcFileLogger.close(fileId); - }, -}); - -export default closeIpcFileLoggingListenerInjectable; diff --git a/packages/core/src/main/logger/create-ipc-file-transport.global-override-for-injectable.ts b/packages/core/src/main/logger/create-ipc-file-transport.global-override-for-injectable.ts deleted file mode 100644 index 98fc62da49..0000000000 --- a/packages/core/src/main/logger/create-ipc-file-transport.global-override-for-injectable.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import type { transports } from "winston"; -import { getGlobalOverride } from "@k8slens/test-utils"; -import { noop } from "@k8slens/utilities"; -import createIpcFileLoggerTransportInjectable from "./create-ipc-file-transport.injectable"; - -export default getGlobalOverride( - createIpcFileLoggerTransportInjectable, - () => () => - ({ - log: noop, - close: noop, - } as typeof transports.File), -); diff --git a/packages/core/src/main/logger/create-ipc-file-transport.injectable.ts b/packages/core/src/main/logger/create-ipc-file-transport.injectable.ts deleted file mode 100644 index f29e02fc90..0000000000 --- a/packages/core/src/main/logger/create-ipc-file-transport.injectable.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * 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 { transports } from "winston"; -import directoryForLogsInjectable from "../../common/app-paths/directory-for-logs.injectable"; - -const createIpcFileLoggerTransportInjectable = getInjectable({ - id: "create-ipc-file-logger-transport", - instantiate: (di) => { - const options = { - dirname: di.inject(directoryForLogsInjectable), - maxsize: 1024 * 1024, - maxFiles: 2, - tailable: true, - }; - - return (fileId: string) => - new transports.File({ - ...options, - filename: `lens-${fileId}.log`, - }); - }, - causesSideEffects: true, -}); - -export default createIpcFileLoggerTransportInjectable; diff --git a/packages/core/src/main/logger/file-transport.injectable.ts b/packages/core/src/main/logger/file-transport.injectable.ts index c71b44a2a0..fcf855eec4 100644 --- a/packages/core/src/main/logger/file-transport.injectable.ts +++ b/packages/core/src/main/logger/file-transport.injectable.ts @@ -7,8 +7,8 @@ import { transports } from "winston"; import directoryForLogsInjectable from "../../common/app-paths/directory-for-logs.injectable"; import { loggerTransportInjectionToken } from "../../common/logger/transports"; -const fileLoggerTransportInjectable = getInjectable({ - id: "file-logger-transport", +const fileLoggerTranportInjectable = getInjectable({ + id: "file-logger-tranport", instantiate: (di) => new transports.File({ handleExceptions: false, level: "debug", @@ -26,4 +26,4 @@ const fileLoggerTransportInjectable = getInjectable({ decorable: false, }); -export default fileLoggerTransportInjectable; +export default fileLoggerTranportInjectable; diff --git a/packages/core/src/main/logger/ipc-file-logger.injectable.ts b/packages/core/src/main/logger/ipc-file-logger.injectable.ts deleted file mode 100644 index df82ef7c6b..0000000000 --- a/packages/core/src/main/logger/ipc-file-logger.injectable.ts +++ /dev/null @@ -1,55 +0,0 @@ -/** - * 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 { getOrInsertWith } from "@k8slens/utilities"; -import type { LogEntry, transports } from "winston"; -import createIpcFileLoggerTransportInjectable from "./create-ipc-file-transport.injectable"; - -export interface IpcFileLogger { - log: (fileLog: { fileId: string; entry: LogEntry }) => void; - close: (fileId: string) => void; - closeAll: () => void; -} - -const ipcFileLoggerInjectable = getInjectable({ - id: "ipc-file-logger", - instantiate: (di): IpcFileLogger => { - const createIpcFileTransport = di.inject(createIpcFileLoggerTransportInjectable); - const fileTransports = new Map(); - - function log({ fileId, entry }: { fileId: string; entry: LogEntry }) { - const transport = getOrInsertWith( - fileTransports, - fileId, - () => createIpcFileTransport(fileId), - ); - - transport?.log?.(entry, () => {}); - } - - function close(fileId: string) { - const transport = fileTransports.get(fileId); - - if (transport) { - transport.close?.(); - fileTransports.delete(fileId); - } - } - - function closeAll() { - for (const fileId of fileTransports.keys()) { - close(fileId); - } - } - - return { - log, - close, - closeAll, - }; - }, -}); - -export default ipcFileLoggerInjectable; diff --git a/packages/core/src/main/logger/ipc-file-logger.test.ts b/packages/core/src/main/logger/ipc-file-logger.test.ts deleted file mode 100644 index 1ff727e200..0000000000 --- a/packages/core/src/main/logger/ipc-file-logger.test.ts +++ /dev/null @@ -1,160 +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 createIpcFileLoggerTransportInjectable from "./create-ipc-file-transport.injectable"; -import type { IpcFileLogger } from "./ipc-file-logger.injectable"; -import ipcFileLoggerInjectable from "./ipc-file-logger.injectable"; - -describe("ipc file logger in main", () => { - let logMock: jest.Mock; - let closeMock: jest.Mock; - let createFileTransportMock: jest.Mock; - let logger: IpcFileLogger; - - beforeEach(() => { - logMock = jest.fn(); - closeMock = jest.fn(); - createFileTransportMock = jest.fn(() => ({ - log: logMock, - close: closeMock, - })); - - const di = getDiForUnitTesting(); - - di.override(createIpcFileLoggerTransportInjectable, () => createFileTransportMock); - logger = di.inject(ipcFileLoggerInjectable); - }); - - it("creates a transport for new log file", () => { - logger.log({ - fileId: "some-log-file", - entry: { level: "irrelevant", message: "irrelevant" }, - }); - - expect(createFileTransportMock).toHaveBeenCalledWith("some-log-file"); - }); - - it("uses existing transport for log file", () => { - logger.log({ - fileId: "some-log-file", - entry: { level: "irrelevant", message: "irrelevant" }, - }); - - logger.log({ - fileId: "some-log-file", - entry: { level: "irrelevant", message: "irrelevant" }, - }); - - logger.log({ - fileId: "some-log-file", - entry: { level: "irrelevant", message: "irrelevant" }, - }); - - expect(createFileTransportMock).toHaveBeenCalledTimes(1); - - expect(createFileTransportMock).toHaveBeenCalledWith("some-log-file"); - }); - - it("creates separate transport for each log file", () => { - logger.log({ - fileId: "some-log-file", - entry: { level: "irrelevant", message: "irrelevant" }, - }); - - logger.log({ - fileId: "some-other-log-file", - entry: { level: "irrelevant", message: "irrelevant" }, - }); - - logger.log({ - fileId: "some-yet-another-log-file", - entry: { level: "irrelevant", message: "irrelevant" }, - }); - - expect(createFileTransportMock).toHaveBeenCalledTimes(3); - - expect(createFileTransportMock).toHaveBeenCalledWith("some-log-file"); - - expect(createFileTransportMock).toHaveBeenCalledWith("some-other-log-file"); - - expect(createFileTransportMock).toHaveBeenCalledWith("some-yet-another-log-file"); - }); - - it("logs using file transport", () => { - logger.log({ - fileId: "some-log-file", - entry: { level: "irrelevant", message: "some-log-message" }, - }); - expect(logMock.mock.calls[0][0]).toEqual({ - level: "irrelevant", - message: "some-log-message", - }); - }); - - it("logs to correct files", () => { - const someLogMock = jest.fn(); - const someOthertLogMock = jest.fn(); - - createFileTransportMock.mockImplementation((fileId: string) => { - if (fileId === "some-log-file") { - return { log: someLogMock }; - } - - if (fileId === "some-other-log-file") { - return { log: someOthertLogMock }; - } - - return null; - }); - - logger.log({ - fileId: "some-log-file", - entry: { level: "irrelevant", message: "some-log-message" }, - }); - logger.log({ - fileId: "some-other-log-file", - entry: { level: "irrelevant", message: "some-other-log-message" }, - }); - - expect(someLogMock).toHaveBeenCalledTimes(1); - expect(someLogMock.mock.calls[0][0]).toEqual({ - level: "irrelevant", - message: "some-log-message", - }); - expect(someOthertLogMock).toHaveBeenCalledTimes(1); - expect(someOthertLogMock.mock.calls[0][0]).toEqual({ - level: "irrelevant", - message: "some-other-log-message", - }); - }); - - it("closes transport (to ensure no file handles are left open)", () => { - logger.log({ - fileId: "some-log-file", - entry: { level: "irrelevant", message: "irrelevant" }, - }); - - logger.close("some-log-file"); - - expect(closeMock).toHaveBeenCalled(); - }); - - it("creates a new transport once needed after closing previous", () => { - logger.log({ - fileId: "some-log-file", - entry: { level: "irrelevant", message: "irrelevant" }, - }); - - logger.close("some-log-file"); - - logger.log({ - fileId: "some-log-file", - entry: { level: "irrelevant", message: "irrelevant" }, - }); - - expect(createFileTransportMock).toHaveBeenCalledTimes(2); - expect(logMock).toHaveBeenCalledTimes(2); - }); -}); diff --git a/packages/core/src/main/logger/ipc-logging-listener.injectable.ts b/packages/core/src/main/logger/ipc-logging-listener.injectable.ts deleted file mode 100644 index 3a3748846b..0000000000 --- a/packages/core/src/main/logger/ipc-logging-listener.injectable.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import ipcFileLoggerInjectable from "./ipc-file-logger.injectable"; -import { getMessageChannelListenerInjectable } from "../../common/utils/channel/message-channel-listener-injection-token"; -import type { IpcFileLogObject } from "../../common/logger/ipc-file-logger-channel"; -import { ipcFileLoggerChannel } from "../../common/logger/ipc-file-logger-channel"; -import { MESSAGE } from "triple-beam"; - -/** - * Winston uses symbol property for the actual message. - * - * For that to get through IPC, use the internalMessage property instead - */ -export function deserializeLogFromIpc(ipcFileLogObject: IpcFileLogObject) { - const { internalMessage, ...standardEntry } = ipcFileLogObject.entry; - - return { - ...ipcFileLogObject, - entry: { - ...standardEntry, - [MESSAGE]: internalMessage, - }, - }; -} - -const ipcFileLoggingListenerInjectable = getMessageChannelListenerInjectable({ - id: "ipc-file-logging", - channel: ipcFileLoggerChannel, - handler: (di) => { - const logger = di.inject(ipcFileLoggerInjectable); - - return (ipcFileLogObject) => - logger.log(deserializeLogFromIpc(ipcFileLogObject)); - }, -}); - -export default ipcFileLoggingListenerInjectable; diff --git a/packages/core/src/main/logger/ipc-logging-listener.test.ts b/packages/core/src/main/logger/ipc-logging-listener.test.ts deleted file mode 100644 index 55bb64f4c4..0000000000 --- a/packages/core/src/main/logger/ipc-logging-listener.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import { MESSAGE } from "triple-beam"; -import { deserializeLogFromIpc } from "./ipc-logging-listener.injectable"; - -describe("Ipc log deserialization", () => { - it("fills in the unique symbol message property Winston transports use internally", () => { - const logObject = { - fileId: "irrelevant", - entry: { - level: "irrelevant", - message: "some public message", - internalMessage: "some internal message", - someProperty: "irrelevant", - }, - }; - - expect(deserializeLogFromIpc(logObject)).toEqual({ - entry: { - level: "irrelevant", - message: "some public message", - [MESSAGE]: "some internal message", - someProperty: "irrelevant", - }, - fileId: "irrelevant", - }); - }); -}); diff --git a/packages/core/src/main/logger/stop-ipc-logging.injectable.ts b/packages/core/src/main/logger/stop-ipc-logging.injectable.ts deleted file mode 100644 index bdb94a412e..0000000000 --- a/packages/core/src/main/logger/stop-ipc-logging.injectable.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * 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 { beforeQuitOfFrontEndInjectionToken } from "../start-main-application/runnable-tokens/phases"; -import ipcFileLoggerInjectable from "./ipc-file-logger.injectable"; - -const stopIpcLoggingInjectable = getInjectable({ - id: "stop-ipc-logging", - - instantiate: (di) => { - const ipcFileLogger = di.inject(ipcFileLoggerInjectable); - - return { - run: () => { - ipcFileLogger.closeAll(); - - return undefined; - }, - }; - }, - - injectionToken: beforeQuitOfFrontEndInjectionToken, -}); - -export default stopIpcLoggingInjectable; diff --git a/packages/core/src/renderer/before-frame-starts/runnables/listen-unload.injectable.ts b/packages/core/src/renderer/before-frame-starts/runnables/listen-unload.injectable.ts deleted file mode 100644 index 6b6e0e751c..0000000000 --- a/packages/core/src/renderer/before-frame-starts/runnables/listen-unload.injectable.ts +++ /dev/null @@ -1,46 +0,0 @@ -/** - * 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 currentlyInClusterFrameInjectable from "../../routes/currently-in-cluster-frame.injectable"; -import { beforeFrameStartsSecondInjectionToken } from "../tokens"; -import loggerInjectable from "../../../common/logger.injectable"; -import hostedClusterInjectable from "../../cluster-frame-context/hosted-cluster.injectable"; -import frameRoutingIdInjectable from "../../frames/cluster-frame/init-cluster-frame/frame-routing-id/frame-routing-id.injectable"; -import closeRendererLogFileInjectable from "../../logger/close-renderer-log-file.injectable"; -import { unmountComponentAtNode } from "react-dom"; - -const listenUnloadInjectable = getInjectable({ - id: "listen-unload", - instantiate: (di) => ({ - run: () => { - const closeRendererLogFile = di.inject(closeRendererLogFileInjectable); - const isClusterFrame = di.inject(currentlyInClusterFrameInjectable); - const logger = di.inject(loggerInjectable); - - window.addEventListener("beforeunload", () => { - if (isClusterFrame) { - const hostedCluster = di.inject(hostedClusterInjectable); - const frameRoutingId = di.inject(frameRoutingIdInjectable); - - logger.info( - `[CLUSTER-FRAME] Unload dashboard, clusterId=${hostedCluster?.id}, frameId=${frameRoutingId}`, - ); - } else { - logger.info("[ROOT-FRAME]: Unload app"); - } - - closeRendererLogFile(); - const rootElem = document.getElementById("app"); - - if (rootElem) { - unmountComponentAtNode(rootElem); - } - }); - }, - }), - injectionToken: beforeFrameStartsSecondInjectionToken, -}); - -export default listenUnloadInjectable; diff --git a/packages/core/src/renderer/bootstrap.tsx b/packages/core/src/renderer/bootstrap.tsx index 75439fce13..a811f07d5c 100644 --- a/packages/core/src/renderer/bootstrap.tsx +++ b/packages/core/src/renderer/bootstrap.tsx @@ -6,7 +6,7 @@ import "./components/app.scss"; import React from "react"; -import { render } from "react-dom"; +import { render, unmountComponentAtNode } from "react-dom"; import { DefaultProps } from "./mui-base-theme"; import { DiContextProvider } from "@ogre-tools/injectable-react"; import type { @@ -43,7 +43,7 @@ export async function bootstrap(di: DiContainerForInjection) { } try { - await initializeApp(); + await initializeApp(() => unmountComponentAtNode(rootElem)); } catch (error) { console.error(`[BOOTSTRAP]: view initialization error: ${error}`, { origin: location.href, diff --git a/packages/core/src/renderer/frames/cluster-frame/init-cluster-frame/init-cluster-frame.ts b/packages/core/src/renderer/frames/cluster-frame/init-cluster-frame/init-cluster-frame.ts index 9e901a8060..9bd0a26a3c 100644 --- a/packages/core/src/renderer/frames/cluster-frame/init-cluster-frame/init-cluster-frame.ts +++ b/packages/core/src/renderer/frames/cluster-frame/init-cluster-frame/init-cluster-frame.ts @@ -22,51 +22,62 @@ interface Dependencies { const logPrefix = "[CLUSTER-FRAME]:"; -export const initClusterFrame = - ({ - hostedCluster, - loadExtensions, - catalogEntityRegistry, - frameRoutingId, - emitAppEvent, - logger, - showErrorNotification, - }: Dependencies) => - async () => { +export const initClusterFrame = ({ + hostedCluster, + loadExtensions, + catalogEntityRegistry, + frameRoutingId, + emitAppEvent, + logger, + showErrorNotification, +}: Dependencies) => + async (unmountRoot: () => void) => { // TODO: Make catalogEntityRegistry already initialized when passed as dependency - catalogEntityRegistry.init(); + catalogEntityRegistry.init(); - logger.info( - `${logPrefix} Init dashboard, clusterId=${hostedCluster.id}, frameId=${frameRoutingId}`, - ); + logger.info( + `${logPrefix} Init dashboard, clusterId=${hostedCluster.id}, frameId=${frameRoutingId}`, + ); - await requestSetClusterFrameId(hostedCluster.id); - await when(() => hostedCluster.ready.get()); // cluster.activate() is done at this point + await requestSetClusterFrameId(hostedCluster.id); + await when(() => hostedCluster.ready.get()); // cluster.activate() is done at this point - catalogEntityRegistry.activeEntity = hostedCluster.id; + catalogEntityRegistry.activeEntity = hostedCluster.id; - // Only load the extensions once the catalog has been populated. - // Note that the Catalog might still have unprocessed entities until the extensions are fully loaded. - when( - () => catalogEntityRegistry.items.get().length > 0, - () => loadExtensions(), - { - timeout: 15_000, - onError: (error) => { - logger.warn("[CLUSTER-FRAME]: error from activeEntity when()", error); + // Only load the extensions once the catalog has been populated. + // Note that the Catalog might still have unprocessed entities until the extensions are fully loaded. + when( + () => catalogEntityRegistry.items.get().length > 0, + () => + loadExtensions(), + { + timeout: 15_000, + onError: (error) => { + logger.warn( + "[CLUSTER-FRAME]: error from activeEntity when()", + error, + ); - showErrorNotification("Failed to get KubernetesCluster for this view. Extensions will not be loaded."); - }, + showErrorNotification("Failed to get KubernetesCluster for this view. Extensions will not be loaded."); }, + }, + ); + + setTimeout(() => { + emitAppEvent({ + name: "cluster", + action: "open", + params: { + clusterId: hostedCluster.id, + }, + }); + }); + + window.onbeforeunload = () => { + logger.info( + `${logPrefix} Unload dashboard, clusterId=${(hostedCluster.id)}, frameId=${frameRoutingId}`, ); - setTimeout(() => { - emitAppEvent({ - name: "cluster", - action: "open", - params: { - clusterId: hostedCluster.id, - }, - }); - }); + unmountRoot(); }; + }; diff --git a/packages/core/src/renderer/frames/root-frame/init-root-frame.injectable.ts b/packages/core/src/renderer/frames/root-frame/init-root-frame.injectable.ts index e1bedb0c88..f1e3024d80 100644 --- a/packages/core/src/renderer/frames/root-frame/init-root-frame.injectable.ts +++ b/packages/core/src/renderer/frames/root-frame/init-root-frame.injectable.ts @@ -9,6 +9,7 @@ import lensProtocolRouterRendererInjectable from "../../protocol-handler/lens-pr import catalogEntityRegistryInjectable from "../../api/catalog/entity/registry.injectable"; import registerIpcListenersInjectable from "../../ipc/register-ipc-listeners.injectable"; import loadExtensionsInjectable from "../load-extensions.injectable"; +import loggerInjectable from "../../../common/logger.injectable"; import { delay } from "@k8slens/utilities"; import { broadcastMessage } from "../../../common/ipc"; import { bundledExtensionsLoaded } from "../../../common/ipc/extension-handling"; @@ -22,8 +23,9 @@ const initRootFrameInjectable = getInjectable({ const bindProtocolAddRouteHandlers = di.inject(bindProtocolAddRouteHandlersInjectable); const lensProtocolRouterRenderer = di.inject(lensProtocolRouterRendererInjectable); const catalogEntityRegistry = di.inject(catalogEntityRegistryInjectable); + const logger = di.inject(loggerInjectable); - return async () => { + return async (unmountRoot: () => void) => { catalogEntityRegistry.init(); try { @@ -54,6 +56,12 @@ const initRootFrameInjectable = getInjectable({ window.addEventListener("online", () => broadcastMessage("network:online")); registerIpcListeners(); + + window.addEventListener("beforeunload", () => { + logger.info("[ROOT-FRAME]: Unload app"); + + unmountRoot(); + }); }; }, }); diff --git a/packages/core/src/renderer/logger/close-renderer-log-file-id.test.ts b/packages/core/src/renderer/logger/close-renderer-log-file-id.test.ts deleted file mode 100644 index 1520844c30..0000000000 --- a/packages/core/src/renderer/logger/close-renderer-log-file-id.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import winstonLoggerInjectable from "../../common/winston-logger.injectable"; -import { getDiForUnitTesting } from "../getDiForUnitTesting"; -import closeRendererLogFileInjectable from "./close-renderer-log-file.injectable"; -import type { DiContainer } from "@ogre-tools/injectable"; -import type winston from "winston"; -import type { SendMessageToChannel } from "../../common/utils/channel/message-to-channel-injection-token"; -import { sendMessageToChannelInjectionToken } from "../../common/utils/channel/message-to-channel-injection-token"; -import rendererLogFileIdInjectable from "./renderer-log-file-id.injectable"; -import ipcLogTransportInjectable from "./ipc-transport.injectable"; -import type IpcLogTransport from "./ipc-transport"; - -describe("close renderer file logging", () => { - let di: DiContainer; - let sendIpcMock: SendMessageToChannel; - let winstonMock: winston.Logger; - let ipcTransportMock: IpcLogTransport; - - beforeEach(() => { - di = getDiForUnitTesting(); - sendIpcMock = jest.fn(); - winstonMock = { - remove: jest.fn(), - } as any as winston.Logger; - ipcTransportMock = { name: "ipc-renderer-transport" } as IpcLogTransport; - - di.override(winstonLoggerInjectable, () => winstonMock); - di.override(sendMessageToChannelInjectionToken, () => sendIpcMock); - di.override(rendererLogFileIdInjectable, () => "some-log-id"); - di.override(ipcLogTransportInjectable, () => ipcTransportMock); - }); - - it("sends the ipc close message with correct log id", () => { - const closeLog = di.inject(closeRendererLogFileInjectable); - - closeLog(); - - expect(sendIpcMock).toHaveBeenCalledWith( - { id: "close-ipc-file-logger-channel" }, - "some-log-id", - ); - }); - - it("removes the transport to prevent further logging to closed file", () => { - const closeLog = di.inject(closeRendererLogFileInjectable); - - closeLog(); - - expect(winstonMock.remove).toHaveBeenCalledWith({ - name: "ipc-renderer-transport", - }); - }); -}); diff --git a/packages/core/src/renderer/logger/close-renderer-log-file.injectable.ts b/packages/core/src/renderer/logger/close-renderer-log-file.injectable.ts deleted file mode 100644 index 8015708d84..0000000000 --- a/packages/core/src/renderer/logger/close-renderer-log-file.injectable.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * 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 winstonLoggerInjectable from "../../common/winston-logger.injectable"; -import { closeIpcFileLoggerChannel } from "../../common/logger/ipc-file-logger-channel"; -import { sendMessageToChannelInjectionToken } from "../../common/utils/channel/message-to-channel-injection-token"; -import rendererLogFileIdInjectable from "./renderer-log-file-id.injectable"; -import ipcLogTransportInjectable from "./ipc-transport.injectable"; - -const closeRendererLogFileInjectable = getInjectable({ - id: "close-renderer-log-file", - instantiate: (di) => { - const winstonLogger = di.inject(winstonLoggerInjectable); - const ipcLogTransport = di.inject(ipcLogTransportInjectable); - const messageToChannel = di.inject(sendMessageToChannelInjectionToken); - const fileId = di.inject(rendererLogFileIdInjectable); - - - return () => { - messageToChannel(closeIpcFileLoggerChannel, fileId); - winstonLogger.remove(ipcLogTransport); - }; - }, -}); - -export default closeRendererLogFileInjectable; diff --git a/packages/core/src/renderer/logger/ipc-transport.injectable.ts b/packages/core/src/renderer/logger/ipc-transport.injectable.ts deleted file mode 100644 index 45139cd917..0000000000 --- a/packages/core/src/renderer/logger/ipc-transport.injectable.ts +++ /dev/null @@ -1,60 +0,0 @@ -/** - * 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 { loggerTransportInjectionToken } from "../../common/logger/transports"; -import type winston from "winston"; -import { MESSAGE } from "triple-beam"; - -import IpcLogTransport from "./ipc-transport"; -import { sendMessageToChannelInjectionToken } from "../../common/utils/channel/message-to-channel-injection-token"; -import type { - IpcFileLogObject } from "../../common/logger/ipc-file-logger-channel"; -import { - closeIpcFileLoggerChannel, - ipcFileLoggerChannel, -} from "../../common/logger/ipc-file-logger-channel"; -import rendererLogFileIdInjectable from "./renderer-log-file-id.injectable"; - -/** - * Winston uses symbol property for the actual message. - * - * For that to get through IPC, use the internalMessage property instead - */ -function serializeLogForIpc( - fileId: string, - entry: winston.LogEntry, -): IpcFileLogObject { - return { - fileId, - entry: { - level: entry.level, - message: entry.message, - internalMessage: Object.getOwnPropertyDescriptor(entry, MESSAGE)?.value, - }, - }; -} - -const ipcLogTransportInjectable = getInjectable({ - id: "renderer-file-logger-transport", - instantiate: (di) => { - const messageToChannel = di.inject(sendMessageToChannelInjectionToken); - const fileId = di.inject(rendererLogFileIdInjectable); - - return new IpcLogTransport({ - sendIpcLogMessage: (entry) => - messageToChannel( - ipcFileLoggerChannel, - serializeLogForIpc(fileId, entry), - ), - closeIpcLogging: () => - messageToChannel(closeIpcFileLoggerChannel, fileId), - handleExceptions: false, - level: "info", - }); - }, - injectionToken: loggerTransportInjectionToken, -}); - -export default ipcLogTransportInjectable; diff --git a/packages/core/src/renderer/logger/ipc-transport.test.ts b/packages/core/src/renderer/logger/ipc-transport.test.ts deleted file mode 100644 index 931df377c7..0000000000 --- a/packages/core/src/renderer/logger/ipc-transport.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -/** - * 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 type { SendMessageToChannel } from "../../common/utils/channel/message-to-channel-injection-token"; -import { sendMessageToChannelInjectionToken } from "../../common/utils/channel/message-to-channel-injection-token"; -import { getDiForUnitTesting } from "../getDiForUnitTesting"; -import rendererLogFileIdInjectable from "./renderer-log-file-id.injectable"; -import ipcLogTransportInjectable from "./ipc-transport.injectable"; -import { MESSAGE } from "triple-beam"; - -describe("renderer log transport through ipc", () => { - let di: DiContainer; - let sendIpcMock: SendMessageToChannel; - - beforeEach(() => { - sendIpcMock = jest.fn(); - di = getDiForUnitTesting(); - di.override(sendMessageToChannelInjectionToken, () => sendIpcMock); - di.override(rendererLogFileIdInjectable, () => "some-log-id"); - }); - - it("send serialized ipc messages on log", () => { - const logTransport = di.inject(ipcLogTransportInjectable); - - logTransport.log( - { - level: "info", - message: "some log text", - [MESSAGE]: "actual winston log text", - }, - () => {}, - ); - - expect(sendIpcMock).toHaveBeenCalledWith( - { id: "ipc-file-logger-channel" }, - { - entry: { - level: "info", - message: "some log text", - internalMessage: "actual winston log text", - }, - fileId: "some-log-id", - }, - ); - }); -}); diff --git a/packages/core/src/renderer/logger/ipc-transport.ts b/packages/core/src/renderer/logger/ipc-transport.ts deleted file mode 100644 index a1f6fa819e..0000000000 --- a/packages/core/src/renderer/logger/ipc-transport.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import type { LogEntry } from "winston"; -import type { TransportStreamOptions } from "winston-transport"; -import TransportStream from "winston-transport"; - -interface IpcLogTransportOptions extends TransportStreamOptions { - sendIpcLogMessage: (entry: LogEntry) => void; - closeIpcLogging: () => void; -} - -class IpcLogTransport extends TransportStream { - sendIpcLogMessage: (entry: LogEntry) => void; - closeIpcLogging: () => void; - name = "ipc-renderer-transport"; - - constructor(options: IpcLogTransportOptions) { - const { sendIpcLogMessage, closeIpcLogging, ...winstonOptions } = options; - - super(winstonOptions); - - this.sendIpcLogMessage = sendIpcLogMessage; - this.closeIpcLogging = closeIpcLogging; - } - - log(logEntry: LogEntry, next: () => void) { - setImmediate(() => { - this.emit("logged", logEntry); - }); - this.sendIpcLogMessage(logEntry); - next(); - } - - close() { - this.closeIpcLogging(); - } -} - -export default IpcLogTransport; diff --git a/packages/core/src/renderer/logger/renderer-log-file-id.injectable.ts b/packages/core/src/renderer/logger/renderer-log-file-id.injectable.ts deleted file mode 100644 index 81f4196b08..0000000000 --- a/packages/core/src/renderer/logger/renderer-log-file-id.injectable.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * 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 windowLocationInjectable from "../../common/k8s-api/window-location.injectable"; -import currentlyInClusterFrameInjectable from "../routes/currently-in-cluster-frame.injectable"; -import { getClusterIdFromHost } from "../../common/utils"; - -const rendererLogFileIdInjectable = getInjectable({ - id: "renderer-log-file-id", - instantiate: (di) => { - let frameId: string; - const currentlyInClusterFrame = di.inject(currentlyInClusterFrameInjectable); - - if (currentlyInClusterFrame) { - const { host } = di.inject(windowLocationInjectable); - const clusterId = getClusterIdFromHost(host); - - frameId = `cluster-${clusterId}`; - } else { - frameId = "main"; - } - - return `renderer-${frameId}`; - }, -}); - -export default rendererLogFileIdInjectable; diff --git a/packages/core/src/renderer/logger/renderer-log-file-id.test.ts b/packages/core/src/renderer/logger/renderer-log-file-id.test.ts deleted file mode 100644 index bd2abf7a63..0000000000 --- a/packages/core/src/renderer/logger/renderer-log-file-id.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import windowLocationInjectable from "../../common/k8s-api/window-location.injectable"; -import { getDiForUnitTesting } from "../getDiForUnitTesting"; -import currentlyInClusterFrameInjectable from "../routes/currently-in-cluster-frame.injectable"; -import rendererLogFileIdInjectable from "./renderer-log-file-id.injectable"; - -describe("renderer log file id", () => { - - it("clearly names log for renderer main frame", () => { - const di = getDiForUnitTesting(); - - di.override(currentlyInClusterFrameInjectable, () => false); - - const mainFileId = di.inject(rendererLogFileIdInjectable); - - expect(mainFileId).toBe("renderer-main"); - }); - - it("includes cluster id in renderer log file names", () => { - const di = getDiForUnitTesting(); - - di.override(currentlyInClusterFrameInjectable, () => true); - di.override(windowLocationInjectable, () => ({ - host: "some-cluster.lens.app", - port: "irrelevant", - })); - const clusterFileId = di.inject(rendererLogFileIdInjectable); - - expect(clusterFileId).toBe("renderer-cluster-some-cluster"); - }); -}); From 8a80607d8516736128dd22bde83d8e3351cb00db Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Tue, 21 Mar 2023 22:00:12 -0400 Subject: [PATCH 5/5] Add behavioural tests for Cluster Menu K8s Resources in Sidebar menu not being shown (#7280) * Add behavioural tests to cover bug fix Signed-off-by: Sebastian Malton * Remove previous fix to fix last test Signed-off-by: Sebastian Malton * More consistent impl of flushPromises Signed-off-by: Sebastian Malton * Fixup tests Signed-off-by: Sebastian Malton * Remove ContextHandler test (dead code) Signed-off-by: Sebastian Malton * Fix PrometheusHandler describe text Signed-off-by: Sebastian Malton * Fix type errors Signed-off-by: Sebastian Malton * Add useful case test-utils helper Signed-off-by: Sebastian Malton * Rename file to match token Signed-off-by: Sebastian Malton * Cleanup tests to fix type errors and use tables Signed-off-by: Sebastian Malton --------- Signed-off-by: Sebastian Malton --- .../authorization-review.injectable.ts | 50 -- .../create-authorization-api.injectable.ts | 16 + .../common/cluster/create-can-i.injectable.ts | 42 ++ .../cluster/create-core-api.injectable.ts | 16 + ...t-namespace-list-permissions.injectable.ts | 57 ++ .../cluster/list-namespaces.injectable.ts | 20 +- ...t-namespace-list-permissions.injectable.ts | 72 -- ...request-namespace-list-permissions.test.ts | 501 ++++++-------- .../fs/copy.global-override-for-injectable.ts | 11 - .../lstat.global-override-for-injectable.ts | 11 - ...irectory.global-override-for-injectable.ts | 11 - .../remove.global-override-for-injectable.ts | 11 - ...ite-file.global-override-for-injectable.ts | 11 - .../refresh-accessibility-technical.test.ts | 637 ++++++++++++++++++ .../core/src/main/__test__/cluster.test.ts | 12 +- .../src/main/__test__/context-handler.test.ts | 203 ------ .../src/main/__test__/kube-auth-proxy.test.ts | 4 +- .../main/__test__/prometheus-handler.test.ts | 2 +- ...-versions.ts => api-versions-requester.ts} | 7 +- .../cluster/cluster-connection.injectable.ts | 34 +- .../kube-auth-proxy-server.injectable.ts | 2 +- .../request-api-resources.injectable.ts | 8 +- .../request-core-api-versions.injectable.ts | 39 +- ...quest-kube-api-resources-for.injectable.ts | 2 +- ...equest-non-core-api-versions.injectable.ts | 45 +- .../request-non-core-api-versions.test.ts | 8 +- .../create-kube-auth-proxy.injectable.ts | 11 +- .../main/kube-auth-proxy/kube-auth-proxy.ts | 3 +- .../kubeconfig-manager/kubeconfig-manager.ts | 2 +- packages/core/src/test-utils/cast.ts | 6 + .../core/src/test-utils/mock-interface.ts | 17 + .../test-utils/src/flush-promises.ts | 7 +- 32 files changed, 1104 insertions(+), 774 deletions(-) delete mode 100644 packages/core/src/common/cluster/authorization-review.injectable.ts create mode 100644 packages/core/src/common/cluster/create-authorization-api.injectable.ts create mode 100644 packages/core/src/common/cluster/create-can-i.injectable.ts create mode 100644 packages/core/src/common/cluster/create-core-api.injectable.ts create mode 100644 packages/core/src/common/cluster/create-request-namespace-list-permissions.injectable.ts delete mode 100644 packages/core/src/common/cluster/request-namespace-list-permissions.injectable.ts delete mode 100644 packages/core/src/common/fs/copy.global-override-for-injectable.ts delete mode 100644 packages/core/src/common/fs/lstat.global-override-for-injectable.ts delete mode 100644 packages/core/src/common/fs/read-directory.global-override-for-injectable.ts delete mode 100644 packages/core/src/common/fs/remove.global-override-for-injectable.ts delete mode 100644 packages/core/src/common/fs/write-file.global-override-for-injectable.ts create mode 100644 packages/core/src/features/cluster/refresh-accessibility-technical.test.ts delete mode 100644 packages/core/src/main/__test__/context-handler.test.ts rename packages/core/src/main/cluster/{request-api-versions.ts => api-versions-requester.ts} (64%) create mode 100644 packages/core/src/test-utils/cast.ts create mode 100644 packages/core/src/test-utils/mock-interface.ts 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/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/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/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/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/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/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); +};