1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Merge branch 'master' into no-updates-available-popup

This commit is contained in:
Gabriel Accettola 2023-03-23 12:40:00 +01:00 committed by GitHub
commit cf47e8b38c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
76 changed files with 1756 additions and 1305 deletions

View File

@ -15,8 +15,8 @@ All releases will be made by creating a PR which bumps the version field in the
1. If you are making a patch release (or a prerelease for one) make sure you are on the `release/v<MAJOR>.<MINOR>` branch. 1. If you are making a patch release (or a prerelease for one) make sure you are on the `release/v<MAJOR>.<MINOR>` branch.
1. Run `npm run create-release-pr`. 1. Run `npm run create-release-pr`.
1. Pick the PRs that you want to include in this release using the keys listed. 1. Pick the PRs that you want to include in this release using the keys listed.
- If you are making a patch release this might include fixing up some cherry-picking of commits. These actions should be done in a separate terminal. - If you are making a patch release this might include fixing up some cherry-picking of commits. These actions should be done in a separate terminal.
- If a package version is having a major version bump then `npm` will complain about `peerDependency` conflicts. These will have to be fixed up separately. - If a package version is having a major version bump then `npm` will complain about `peerDependency` conflicts. These will have to be fixed up separately.
1. Once the PR is created, approved, and then merged the `Release Open Lens` workflow will create a tag and release for you. 1. Once the PR is created, approved, and then merged the `Release Open Lens` workflow will create a tag and release for you.
1. If you are making a major or minor release, create a `release/v<MAJOR>.<MINOR>` branch and push it to `origin` so that future patch releases can be made from it. 1. If you are making a major or minor release, create a `release/v<MAJOR>.<MINOR>` branch and push it to `origin` so that future patch releases can be made from it.
1. If you released a major or minor version, create a new patch milestone and move all bug issues to that milestone and all enhancement issues to the next minor milestone. 1. If you released a major or minor version, create a new patch milestone and move all bug issues to that milestone and all enhancement issues to the next minor milestone.

392
package-lock.json generated
View File

@ -2099,92 +2099,25 @@
} }
}, },
"node_modules/@electron/get": { "node_modules/@electron/get": {
"version": "1.14.1", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/@electron/get/-/get-1.14.1.tgz", "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.2.tgz",
"integrity": "sha512-BrZYyL/6m0ZXz/lDxy/nlVhQz+WF+iPS6qXolEU8atw7h6v1aYkjwJZ63m+bJMBTxDE66X+r2tPS4a/8C82sZw==", "integrity": "sha512-eFZVFoRXb3GFGd7Ak7W4+6jBl9wBtiZ4AaYOse97ej6mKj5tkyO0dUnUChs1IhJZtx1BENo4/p4WUTXpi6vT+g==",
"dependencies": { "dependencies": {
"debug": "^4.1.1", "debug": "^4.1.1",
"env-paths": "^2.2.0", "env-paths": "^2.2.0",
"fs-extra": "^8.1.0", "fs-extra": "^8.1.0",
"got": "^9.6.0", "got": "^11.8.5",
"progress": "^2.0.3", "progress": "^2.0.3",
"semver": "^6.2.0", "semver": "^6.2.0",
"sumchecker": "^3.0.1" "sumchecker": "^3.0.1"
}, },
"engines": { "engines": {
"node": ">=8.6" "node": ">=12"
}, },
"optionalDependencies": { "optionalDependencies": {
"global-agent": "^3.0.0", "global-agent": "^3.0.0"
"global-tunnel-ng": "^2.7.1"
} }
}, },
"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": { "node_modules/@electron/get/node_modules/fs-extra": {
"version": "8.1.0", "version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
@ -2198,51 +2131,6 @@
"node": ">=6 <7 || >=8" "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": { "node_modules/@electron/get/node_modules/jsonfile": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
@ -2251,55 +2139,6 @@
"graceful-fs": "^4.1.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": { "node_modules/@electron/get/node_modules/semver": {
"version": "6.3.0", "version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
@ -4685,6 +4524,10 @@
"resolved": "packages/bump-version-for-cron", "resolved": "packages/bump-version-for-cron",
"link": true "link": true
}, },
"node_modules/@k8slens/cluster-settings": {
"resolved": "packages/cluster-settings",
"link": true
},
"node_modules/@k8slens/computed-channel": { "node_modules/@k8slens/computed-channel": {
"resolved": "packages/technical-features/messaging/computed-channel", "resolved": "packages/technical-features/messaging/computed-channel",
"link": true "link": true
@ -7977,6 +7820,16 @@
"resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz",
"integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==" "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": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "5.55.0", "version": "5.55.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.55.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.55.0.tgz",
@ -11054,6 +10907,7 @@
"version": "1.6.2", "version": "1.6.2",
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
"integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
"dev": true,
"engines": [ "engines": [
"node >= 0.8" "node >= 0.8"
], ],
@ -11067,12 +10921,14 @@
"node_modules/concat-stream/node_modules/isarray": { "node_modules/concat-stream/node_modules/isarray": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "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": { "node_modules/concat-stream/node_modules/readable-stream": {
"version": "2.3.8", "version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"dev": true,
"dependencies": { "dependencies": {
"core-util-is": "~1.0.0", "core-util-is": "~1.0.0",
"inherits": "~2.0.3", "inherits": "~2.0.3",
@ -11087,6 +10943,7 @@
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"dependencies": { "dependencies": {
"safe-buffer": "~5.1.0" "safe-buffer": "~5.1.0"
} }
@ -11178,7 +11035,7 @@
"version": "1.1.12", "version": "1.1.12",
"resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.12.tgz", "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.12.tgz",
"integrity": "sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==", "integrity": "sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==",
"devOptional": true, "dev": true,
"dependencies": { "dependencies": {
"ini": "^1.3.4", "ini": "^1.3.4",
"proto-list": "~1.2.1" "proto-list": "~1.2.1"
@ -12657,11 +12514,6 @@
"integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==",
"dev": true "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": { "node_modules/duplexify": {
"version": "3.7.1", "version": "3.7.1",
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz",
@ -12736,20 +12588,20 @@
} }
}, },
"node_modules/electron": { "node_modules/electron": {
"version": "19.1.9", "version": "22.3.3",
"resolved": "https://registry.npmjs.org/electron/-/electron-19.1.9.tgz", "resolved": "https://registry.npmjs.org/electron/-/electron-22.3.3.tgz",
"integrity": "sha512-XT5LkTzIHB+ZtD3dTmNnKjVBWrDWReCKt9G1uAFLz6uJMEVcIUiYO+fph5pLXETiBw/QZBx8egduMEfIccLx+g==", "integrity": "sha512-+ZJDVfyhw7J2A46/kGKscktIhzOisTeJKrUBJLXa7PTB+U+cwyoxCBIaIOnDsdicBCX4nAc1mo6YMQjQQdAmgw==",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@electron/get": "^1.14.1", "@electron/get": "^2.0.0",
"@types/node": "^16.11.26", "@types/node": "^16.11.26",
"extract-zip": "^1.0.3" "extract-zip": "^2.0.1"
}, },
"bin": { "bin": {
"electron": "cli.js" "electron": "cli.js"
}, },
"engines": { "engines": {
"node": ">= 8.6" "node": ">= 12.20.55"
} }
}, },
"node_modules/electron-builder": { "node_modules/electron-builder": {
@ -13054,7 +12906,7 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
"devOptional": true, "dev": true,
"engines": { "engines": {
"node": ">= 0.8" "node": ">= 0.8"
} }
@ -15013,42 +14865,46 @@
} }
}, },
"node_modules/extract-zip": { "node_modules/extract-zip": {
"version": "1.7.0", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
"integrity": "sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==", "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
"dependencies": { "dependencies": {
"concat-stream": "^1.6.2", "debug": "^4.1.1",
"debug": "^2.6.9", "get-stream": "^5.1.0",
"mkdirp": "^0.5.4",
"yauzl": "^2.10.0" "yauzl": "^2.10.0"
}, },
"bin": { "bin": {
"extract-zip": "cli.js" "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": { "engines": {
"mkdirp": "bin/cmd.js" "node": ">= 10.17.0"
},
"optionalDependencies": {
"@types/yauzl": "^2.9.1"
} }
}, },
"node_modules/extract-zip/node_modules/ms": { "node_modules/extract-zip/node_modules/get-stream": {
"version": "2.0.0", "version": "5.2.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" "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": { "node_modules/extsprintf": {
"version": "1.3.0", "version": "1.3.0",
@ -16106,22 +15962,6 @@
"node": ">=10.0" "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": { "node_modules/globals": {
"version": "11.12.0", "version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
@ -24084,30 +23924,6 @@
"npm-normalize-package-bin": "^1.0.1" "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": { "node_modules/npm-install-checks": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-5.0.0.tgz", "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-5.0.0.tgz",
@ -28525,14 +28341,6 @@
"node": ">= 0.8.0" "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": { "node_modules/prettier": {
"version": "2.8.4", "version": "2.8.4",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.4.tgz", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.4.tgz",
@ -28727,7 +28535,7 @@
"version": "1.2.4", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
"integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==",
"devOptional": true "dev": true
}, },
"node_modules/protocols": { "node_modules/protocols": {
"version": "2.0.1", "version": "2.0.1",
@ -30131,9 +29939,9 @@
} }
}, },
"node_modules/rimraf/node_modules/glob": { "node_modules/rimraf/node_modules/glob": {
"version": "9.2.1", "version": "9.3.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-9.2.1.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-9.3.0.tgz",
"integrity": "sha512-Pxxgq3W0HyA3XUvSXcFhRSs+43Jsx0ddxcFrbjxNGkL2Ak5BAUBxLqI5G6ADDeCHLfzzXFhe0b1yYcctGmytMA==", "integrity": "sha512-EAZejC7JvnQINayvB/7BJbpZpNOJ8Lrw2OZNEvQxe0vaLn1SuwMcfV7/MNaX8L/T0wmptBFI4YMtDvSBxYDc7w==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"fs.realpath": "^1.0.0", "fs.realpath": "^1.0.0",
@ -32431,14 +32239,6 @@
"node": ">=0.10.0" "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": { "node_modules/to-regex-range": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@ -32751,16 +32551,6 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" "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": { "node_modules/tunnel-agent": {
"version": "0.6.0", "version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
@ -32846,7 +32636,8 @@
"node_modules/typedarray": { "node_modules/typedarray": {
"version": "0.0.6", "version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "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": { "node_modules/typedoc": {
"version": "0.23.25", "version": "0.23.25",
@ -33170,17 +32961,6 @@
"requires-port": "^1.0.0" "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": { "node_modules/use-isomorphic-layout-effect": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz",
@ -34366,6 +34146,25 @@
"integrity": "sha512-ZOzvDRWp8dCVBmgnkIqYCArgdFOO9YzocZp8Ra25N/RStKiWvMOXHMz+GjSeVNe5TstaTmTWPucGJkDw0XXJWA==", "integrity": "sha512-ZOzvDRWp8dCVBmgnkIqYCArgdFOO9YzocZp8Ra25N/RStKiWvMOXHMz+GjSeVNe5TstaTmTWPucGJkDw0XXJWA==",
"dev": true "dev": true
}, },
"packages/cluster-settings": {
"name": "@k8slens/cluster-settings",
"version": "6.5.0-alpha.1",
"license": "MIT",
"devDependencies": {
"@ogre-tools/injectable": "^15.1.2",
"@swc/cli": "^0.1.61",
"@swc/core": "^1.3.37",
"@types/node": "^16.18.11",
"@types/semver": "^7.3.13",
"rimraf": "^4.1.2"
}
},
"packages/cluster-settings/node_modules/@types/node": {
"version": "16.18.18",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.18.tgz",
"integrity": "sha512-fwGw1uvQAzabxL1pyoknPlJIF2t7+K90uTqynleKRx24n3lYcxWa3+KByLhgkF8GEAK2c7hC8Ki0RkNM5H15jQ==",
"dev": true
},
"packages/core": { "packages/core": {
"name": "@k8slens/core", "name": "@k8slens/core",
"version": "6.5.0-alpha.3", "version": "6.5.0-alpha.3",
@ -34374,6 +34173,7 @@
"@astronautlabs/jsonpath": "^1.1.0", "@astronautlabs/jsonpath": "^1.1.0",
"@hapi/call": "^9.0.1", "@hapi/call": "^9.0.1",
"@hapi/subtext": "^7.1.0", "@hapi/subtext": "^7.1.0",
"@k8slens/cluster-settings": "^6.5.0-alpha.1",
"@k8slens/node-fetch": "^6.5.0-alpha.1", "@k8slens/node-fetch": "^6.5.0-alpha.1",
"@kubernetes/client-node": "^0.18.1", "@kubernetes/client-node": "^0.18.1",
"@material-ui/styles": "^4.11.5", "@material-ui/styles": "^4.11.5",
@ -34511,7 +34311,7 @@
"css-loader": "^6.7.3", "css-loader": "^6.7.3",
"deepdash": "^5.3.9", "deepdash": "^5.3.9",
"dompurify": "^2.4.4", "dompurify": "^2.4.4",
"electron": "^19.1.9", "electron": "^22.3.3",
"electron-builder": "^23.6.0", "electron-builder": "^23.6.0",
"esbuild": "^0.17.8", "esbuild": "^0.17.8",
"esbuild-loader": "^2.21.0", "esbuild-loader": "^2.21.0",
@ -36698,7 +36498,7 @@
"copy-webpack-plugin": "^11.0.0", "copy-webpack-plugin": "^11.0.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"css-loader": "^6.7.2", "css-loader": "^6.7.2",
"electron": "^19.1.9", "electron": "^22.3.3",
"electron-builder": "^23.6.0", "electron-builder": "^23.6.0",
"electron-notarize": "^0.3.0", "electron-notarize": "^0.3.0",
"esbuild-loader": "^2.20.0", "esbuild-loader": "^2.20.0",
@ -37192,7 +36992,7 @@
"@k8slens/feature-core": "^6.5.0-alpha.0", "@k8slens/feature-core": "^6.5.0-alpha.0",
"@ogre-tools/injectable": "^15.1.2", "@ogre-tools/injectable": "^15.1.2",
"@ogre-tools/injectable-extension-for-auto-registration": "^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": { "packages/technical-features/application/legacy-extensions": {
@ -37269,7 +37069,7 @@
"@k8slens/messaging": "^1.0.0-alpha.1", "@k8slens/messaging": "^1.0.0-alpha.1",
"@ogre-tools/injectable": "^15.1.2", "@ogre-tools/injectable": "^15.1.2",
"@ogre-tools/injectable-extension-for-auto-registration": "^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" "lodash": "^4.17.21"
} }
}, },
@ -37287,7 +37087,7 @@
"@k8slens/startable-stoppable": "^1.0.0-alpha.1", "@k8slens/startable-stoppable": "^1.0.0-alpha.1",
"@ogre-tools/injectable": "^15.1.2", "@ogre-tools/injectable": "^15.1.2",
"@ogre-tools/injectable-extension-for-auto-registration": "^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" "lodash": "^4.17.21"
} }
}, },

View File

@ -0,0 +1,9 @@
{
"$schema": "https://json.schemastore.org/swcrc",
"jsc": {
"parser": {
"syntax": "typescript"
},
"target": "es2022"
}
}

View File

@ -0,0 +1,3 @@
# Description
The package exports tokens needed for external configuration of Cluster Settings page.

View File

@ -0,0 +1,31 @@
{
"name": "@k8slens/cluster-settings",
"version": "6.5.0-alpha.1",
"description": "Injection token exporter for cluster settings configuration",
"license": "MIT",
"private": false,
"mode": "production",
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"clean": "rimraf dist/",
"generate-types": "tsc --d --declarationDir ./dist --declarationMap --emitDeclarationOnly",
"build": "npm run generate-types && swc ./src/index.ts -d ./dist",
"prepare:test": "npm run build"
},
"devDependencies": {
"@ogre-tools/injectable": "^15.1.2",
"@swc/cli": "^0.1.61",
"@swc/core": "^1.3.37",
"@types/node": "^16.18.11",
"@types/semver": "^7.3.13",
"rimraf": "^4.1.2"
}
}

View File

@ -0,0 +1,30 @@
import { getInjectionToken } from "@ogre-tools/injectable";
type ClusterPreferences = {
clusterName?: string;
icon?: string | null;
}
export interface ClusterIconMenuItem {
id: string;
title: string;
disabled?: (preferences: ClusterPreferences) => boolean;
onClick: (preferences: ClusterPreferences) => void;
}
export interface ClusterIconSettingComponentProps {
preferences: ClusterPreferences;
}
export interface ClusterIconSettingsComponent {
id: string;
Component: React.ComponentType<ClusterIconSettingComponentProps>;
}
export const clusterIconSettingsMenuInjectionToken = getInjectionToken<ClusterIconMenuItem>({
id: "cluster-icon-settings-menu-injection-token",
});
export const clusterIconSettingsComponentInjectionToken = getInjectionToken<ClusterIconSettingsComponent>({
id: "cluster-icon-settings-component-injection-token",
});

View File

@ -0,0 +1,18 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist/",
"paths": {
"*": [
"node_modules/*",
"types/*"
]
},
},
"include": [
"src/**/*",
],
"exclude": [
"node_modules",
]
}

View File

@ -120,6 +120,7 @@
"@astronautlabs/jsonpath": "^1.1.0", "@astronautlabs/jsonpath": "^1.1.0",
"@hapi/call": "^9.0.1", "@hapi/call": "^9.0.1",
"@hapi/subtext": "^7.1.0", "@hapi/subtext": "^7.1.0",
"@k8slens/cluster-settings": "^6.5.0-alpha.1",
"@k8slens/node-fetch": "^6.5.0-alpha.1", "@k8slens/node-fetch": "^6.5.0-alpha.1",
"@kubernetes/client-node": "^0.18.1", "@kubernetes/client-node": "^0.18.1",
"@material-ui/styles": "^4.11.5", "@material-ui/styles": "^4.11.5",
@ -257,7 +258,7 @@
"css-loader": "^6.7.3", "css-loader": "^6.7.3",
"deepdash": "^5.3.9", "deepdash": "^5.3.9",
"dompurify": "^2.4.4", "dompurify": "^2.4.4",
"electron": "^19.1.9", "electron": "^22.3.3",
"electron-builder": "^23.6.0", "electron-builder": "^23.6.0",
"esbuild": "^0.17.8", "esbuild": "^0.17.8",
"esbuild-loader": "^2.21.0", "esbuild-loader": "^2.21.0",

View File

@ -11,7 +11,7 @@ export const pathNames: PathName[] = [
"home", "home",
"appData", "appData",
"userData", "userData",
"cache", "sessionData",
"temp", "temp",
"exe", "exe",
"module", "module",

View File

@ -21,7 +21,6 @@ describe("app-paths", () => {
const defaultAppPathsStub: AppPaths = { const defaultAppPathsStub: AppPaths = {
currentApp: "/some-current-app", currentApp: "/some-current-app",
appData: "/some-app-data", appData: "/some-app-data",
cache: "/some-cache",
crashDumps: "/some-crash-dumps", crashDumps: "/some-crash-dumps",
desktop: "/some-desktop", desktop: "/some-desktop",
documents: "/some-documents", documents: "/some-documents",
@ -36,6 +35,7 @@ describe("app-paths", () => {
temp: "/some-temp", temp: "/some-temp",
videos: "/some-videos", videos: "/some-videos",
userData: "/some-irrelevant-user-data", userData: "/some-irrelevant-user-data",
sessionData: "/some-irrelevant-user-data", // By default this points to userData
}; };
builder.beforeApplicationStart(({ mainDi }) => { builder.beforeApplicationStart(({ mainDi }) => {
@ -73,7 +73,6 @@ describe("app-paths", () => {
expect(actual).toEqual({ expect(actual).toEqual({
currentApp: "/some-current-app", currentApp: "/some-current-app",
appData: "/some-app-data", appData: "/some-app-data",
cache: "/some-cache",
crashDumps: "/some-crash-dumps", crashDumps: "/some-crash-dumps",
desktop: "/some-desktop", desktop: "/some-desktop",
documents: "/some-documents", documents: "/some-documents",
@ -88,6 +87,7 @@ describe("app-paths", () => {
temp: "/some-temp", temp: "/some-temp",
videos: "/some-videos", videos: "/some-videos",
userData: "/some-app-data/some-product-name", userData: "/some-app-data/some-product-name",
sessionData: "/some-app-data/some-product-name",
}); });
}); });
@ -97,7 +97,6 @@ describe("app-paths", () => {
expect(actual).toEqual({ expect(actual).toEqual({
currentApp: "/some-current-app", currentApp: "/some-current-app",
appData: "/some-app-data", appData: "/some-app-data",
cache: "/some-cache",
crashDumps: "/some-crash-dumps", crashDumps: "/some-crash-dumps",
desktop: "/some-desktop", desktop: "/some-desktop",
documents: "/some-documents", documents: "/some-documents",
@ -112,6 +111,7 @@ describe("app-paths", () => {
temp: "/some-temp", temp: "/some-temp",
videos: "/some-videos", videos: "/some-videos",
userData: "/some-app-data/some-product-name", userData: "/some-app-data/some-product-name",
sessionData: "/some-app-data/some-product-name",
}); });
}); });
}); });

View File

@ -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<boolean>;
/**
* @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<boolean> => {
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;

View File

@ -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;

View File

@ -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<boolean>;
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<boolean> => {
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;

View File

@ -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;

View File

@ -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<CanListResource>;
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;

View File

@ -2,27 +2,21 @@
* Copyright (c) OpenLens Authors. All rights reserved. * Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import type { KubeConfig } from "@kubernetes/client-node"; import type { CoreV1Api } from "@kubernetes/client-node";
import { CoreV1Api } from "@kubernetes/client-node";
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import { isDefined } from "@k8slens/utilities"; import { isDefined } from "@k8slens/utilities";
export type ListNamespaces = () => Promise<string[]>; export type ListNamespaces = () => Promise<string[]>;
export type CreateListNamespaces = (api: CoreV1Api) => ListNamespaces;
export type CreateListNamespaces = (config: KubeConfig) => ListNamespaces;
const createListNamespacesInjectable = getInjectable({ const createListNamespacesInjectable = getInjectable({
id: "create-list-namespaces", id: "create-list-namespaces",
instantiate: (): CreateListNamespaces => (config) => { instantiate: (): CreateListNamespaces => (api) => async () => {
const coreApi = config.makeApiClient(CoreV1Api); const { body: { items }} = await api.listNamespace();
return async () => { return items
const { body: { items }} = await coreApi.listNamespace(); .map(ns => ns.metadata?.name)
.filter(isDefined);
return items
.map(ns => ns.metadata?.name)
.filter(isDefined);
};
}, },
}); });

View File

@ -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<CanListResource>;
/**
* @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;

View File

@ -3,334 +3,225 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * 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 { DiContainer } from "@ogre-tools/injectable";
import type { IncomingMessage } from "http";
import { anyObject } from "jest-mock-extended";
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting"; import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
import type { RequestNamespaceListPermissionsFor } from "./request-namespace-list-permissions.injectable"; import { cast } from "../../test-utils/cast";
import requestNamespaceListPermissionsForInjectable from "./request-namespace-list-permissions.injectable"; 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 }}>) => ({ interface TestCase {
makeApiClient: () => ({ description: string;
createSelfSubjectRulesReview: (): Promise<{ body: { status: V1SubjectRulesReviewStatus }}> => statusResponse, status: V1SubjectRulesReviewStatus;
}), expected: boolean;
}); }
describe("requestNamespaceListPermissions", () => { describe("requestNamespaceListPermissions", () => {
let di: DiContainer; let di: DiContainer;
let requestNamespaceListPermissions: RequestNamespaceListPermissionsFor; let createSelfSubjectRulesReviewMock: AsyncFnMock<AuthorizationV1Api["createSelfSubjectRulesReview"]>;
let requestNamespaceListPermissions: RequestNamespaceListPermissions;
beforeEach(() => { beforeEach(() => {
di = getDiForUnitTesting(); di = getDiForUnitTesting();
requestNamespaceListPermissions = di.inject(requestNamespaceListPermissionsForInjectable);
const createRequestNamespaceListPermissions = di.inject(createRequestNamespaceListPermissionsInjectable);
createSelfSubjectRulesReviewMock = asyncFn();
requestNamespaceListPermissions = createRequestNamespaceListPermissions(cast<AuthorizationV1Api>({
createSelfSubjectRulesReview: createSelfSubjectRulesReviewMock,
}));
}); });
describe("when api returns incomplete data", () => { describe("when a request for list permissions in a namespace has been started", () => {
it("returns truthy function", async () => { let request: ReturnType<RequestNamespaceListPermissions>;
const requestPermissions = requestNamespaceListPermissions(createStubProxyConfig(
new Promise((resolve) => resolve({
body: {
status: {
incomplete: true,
resourceRules: [],
nonResourceRules: [],
},
},
})),
) as any);
const permissionCheck = await requestPermissions("irrelevant-namespace"); beforeEach(() => {
request = requestNamespaceListPermissions("irrelevant-namespace");
expect(permissionCheck({
apiName: "pods",
group: "",
kind: "Pod",
namespaced: true,
})).toBeTruthy();
}); });
});
describe("when api rejects", () => { it("should request the creation of a SelfSubjectRulesReview", () => {
it("returns truthy function", async () => { expect(createSelfSubjectRulesReviewMock).toBeCalledWith(anyObject({
const requestPermissions = requestNamespaceListPermissions(createStubProxyConfig( spec: {
new Promise((resolve, reject) => reject("unknown error")), namespace: "irrelevant-namespace",
) as any); },
}));
const permissionCheck = await requestPermissions("irrelevant-namespace");
expect(permissionCheck({
apiName: "pods",
group: "",
kind: "Pod",
namespaced: true,
})).toBeTruthy();
}); });
});
describe("when first resourceRule has all permissions for everything", () => { ([
it("return truthy function", async () => { {
const requestPermissions = requestNamespaceListPermissions(createStubProxyConfig( description: "incomplete data",
new Promise((resolve) => resolve({ status: {
body: { incomplete: true,
status: { resourceRules: [],
incomplete: false, nonResourceRules: [],
resourceRules: [ },
{ expected: true,
apiGroups: ["*"], },
verbs: ["*"], {
}, description: "first resourceRule has all permissions for everything",
{ status: {
apiGroups: ["*"], incomplete: false,
verbs: ["get"], resourceRules: [
}, {
], apiGroups: ["*"],
nonResourceRules: [], verbs: ["*"],
}, },
}, {
})), apiGroups: ["*"],
) as any); 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({ expect(canListResource(someKubeResource)).toBe(expected);
apiName: "pods", });
group: "", });
kind: "Pod",
namespaced: true,
})).toBeTruthy();
}); });
});
describe("when first resourceRule has list permissions for everything", () => { describe("when api rejects", () => {
it("return truthy function", async () => { beforeEach(async () => {
const requestPermissions = requestNamespaceListPermissions(createStubProxyConfig( await createSelfSubjectRulesReviewMock.reject(new Error("unknown error"));
new Promise((resolve) => resolve({ });
body: {
status: {
incomplete: false,
resourceRules: [
{
apiGroups: ["*"],
verbs: ["list"],
},
{
apiGroups: ["*"],
verbs: ["get"],
},
],
nonResourceRules: [],
},
},
})),
) as any);
const permissionCheck = await requestPermissions("irrelevant-namespace"); it("allows the request to complete, and 'canListResource' will return true", async () => {
const canListResource = await request;
expect(permissionCheck({ expect(canListResource(someKubeResource)).toBe(true);
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();
}); });
}); });
}); });
const someKubeResource: KubeApiResource = {
apiName: "some-kind",
group: "some-api-group",
kind: "SomeKind",
namespaced: true,
};

View File

@ -13,7 +13,7 @@ import userStoreInjectable from "../user-store/user-store.injectable";
export type InitializeSentryReportingWith = (initSentry: (opts: BrowserOptions | ElectronMainOptions) => void) => void; 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({ const initializeSentryReportingWithInjectable = getInjectable({
id: "initialize-sentry-reporting-with", id: "initialize-sentry-reporting-with",

View File

@ -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");
});

View File

@ -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");
});

View File

@ -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");
});

View File

@ -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");
});

View File

@ -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");
});

View File

@ -4,6 +4,7 @@
*/ */
import type { DiContainer } from "@ogre-tools/injectable"; import type { DiContainer } from "@ogre-tools/injectable";
import { getInjectable } from "@ogre-tools/injectable";
import clusterFrameContextForNamespacedResourcesInjectable from "../../../renderer/cluster-frame-context/for-namespaced-resources.injectable"; import clusterFrameContextForNamespacedResourcesInjectable from "../../../renderer/cluster-frame-context/for-namespaced-resources.injectable";
import hostedClusterInjectable from "../../../renderer/cluster-frame-context/hosted-cluster.injectable"; import hostedClusterInjectable from "../../../renderer/cluster-frame-context/hosted-cluster.injectable";
import { getDiForUnitTesting } from "../../../renderer/getDiForUnitTesting"; import { getDiForUnitTesting } from "../../../renderer/getDiForUnitTesting";
@ -21,6 +22,9 @@ import maybeKubeApiInjectable from "../maybe-kube-api.injectable";
// eslint-disable-next-line no-restricted-imports // eslint-disable-next-line no-restricted-imports
import { KubeApi as ExternalKubeApi } from "../../../extensions/common-api/k8s-api"; import { KubeApi as ExternalKubeApi } from "../../../extensions/common-api/k8s-api";
import { Cluster } from "../../cluster/cluster"; import { Cluster } from "../../cluster/cluster";
import { runInAction } from "mobx";
import { customResourceDefinitionApiInjectionToken } from "../api-manager/crd-api-token";
import assert from "assert";
class TestApi extends KubeApi<KubeObject> { class TestApi extends KubeApi<KubeObject> {
protected async checkPreferredVersion() { protected async checkPreferredVersion() {
@ -117,4 +121,90 @@ describe("ApiManager", () => {
}); });
}); });
}); });
describe("given than a CRD has a default KubeApi registered for it", () => {
const apiBase = "/apis/aquasecurity.github.io/v1alpha1/vulnerabilityreports";
beforeEach(() => {
runInAction(() => {
di.register(getInjectable({
id: `default-kube-api-for-custom-resource-definition-${apiBase}`,
instantiate: (di) => {
const objectConstructor = class extends KubeObject {
static readonly kind = "VulnerabilityReport";
static readonly namespaced = true;
static readonly apiBase = apiBase;
};
return Object.assign(
new KubeApi({
logger: di.inject(loggerInjectable),
maybeKubeApi: di.inject(maybeKubeApiInjectable),
}, { objectConstructor }),
{
myField: 1,
},
);
},
injectionToken: customResourceDefinitionApiInjectionToken,
}));
});
});
it("can be retrieved from apiManager", () => {
expect(apiManager.getApi(apiBase)).toMatchObject({
myField: 1,
});
});
it("can have a default KubeObjectStore instance retrieved for it", () => {
expect(apiManager.getStore(apiBase)).toBeInstanceOf(KubeObjectStore);
});
describe("given that an extension registers an api with the same apibase", () => {
beforeEach(() => {
void Object.assign(new ExternalKubeApi({
objectConstructor: KubeObject,
apiBase,
kind: "VulnerabilityReport",
}), {
myField: 2,
});
});
it("the extension's instance is retrievable instead from apiManager", () => {
expect(apiManager.getApi(apiBase)).toMatchObject({
myField: 2,
});
});
it("can have a default KubeObjectStore instance retrieved for it", () => {
expect(apiManager.getStore(apiBase)).toBeInstanceOf(KubeObjectStore);
});
describe("given that an extension registers a store for the same apibase", () => {
beforeEach(() => {
const api = apiManager.getApi(apiBase);
assert(api);
apiManager.registerStore(Object.assign(
new KubeObjectStore({
context: di.inject(clusterFrameContextForNamespacedResourcesInjectable),
logger: di.inject(loggerInjectable),
}, api),
{
someField: 2,
},
));
});
it("can gets the custom KubeObjectStore instance instead", () => {
expect(apiManager.getStore(apiBase)).toMatchObject({
someField: 2,
});
});
});
});
});
}); });

View File

@ -10,7 +10,8 @@ import { autorun, action, observable } from "mobx";
import type { KubeApi } from "../kube-api"; import type { KubeApi } from "../kube-api";
import type { KubeObject, ObjectReference } from "../kube-object"; import type { KubeObject, ObjectReference } from "../kube-object";
import { parseKubeApi, createKubeApiURL } from "../kube-api-parse"; import { parseKubeApi, createKubeApiURL } from "../kube-api-parse";
import { iter } from "@k8slens/utilities"; import { getOrInsertWith, iter } from "@k8slens/utilities";
import type { CreateCustomResourceStore } from "./create-custom-resource-store.injectable";
export type RegisterableStore<Store> = Store extends KubeObjectStore<any, any, any> export type RegisterableStore<Store> = Store extends KubeObjectStore<any, any, any>
? Store ? Store
@ -26,13 +27,15 @@ export type FindApiCallback = (api: KubeApi<KubeObject>) => boolean;
interface Dependencies { interface Dependencies {
readonly apis: IComputedValue<KubeApi[]>; readonly apis: IComputedValue<KubeApi[]>;
readonly crdApis: IComputedValue<KubeApi[]>;
readonly stores: IComputedValue<KubeObjectStore[]>; readonly stores: IComputedValue<KubeObjectStore[]>;
createCustomResourceStore: CreateCustomResourceStore;
} }
export class ApiManager { export class ApiManager {
private readonly externalApis = observable.array<KubeApi>(); private readonly externalApis = observable.array<KubeApi>();
private readonly externalStores = observable.array<KubeObjectStore>(); private readonly externalStores = observable.array<KubeObjectStore>();
private readonly defaultCrdStores = observable.map<string, KubeObjectStore>();
private readonly apis = observable.map<string, KubeApi>(); private readonly apis = observable.map<string, KubeApi>();
constructor(private readonly dependencies: Dependencies) { constructor(private readonly dependencies: Dependencies) {
@ -56,6 +59,12 @@ export class ApiManager {
} }
} }
for (const crdApi of this.dependencies.crdApis.get()) {
if (!newState.has(crdApi.apiBase)) {
newState.set(crdApi.apiBase, crdApi);
}
}
this.apis.replace(newState); this.apis.replace(newState);
}); });
} }
@ -110,6 +119,16 @@ export class ApiManager {
this.externalStores.push(store); this.externalStores.push(store);
} }
private apiIsDefaultCrdApi(api: KubeApi): boolean {
for (const crdApi of this.dependencies.crdApis.get()) {
if (crdApi.apiBase === api.apiBase) {
return true;
}
}
return false;
}
getStore(api: string | undefined): KubeObjectStore | undefined; getStore(api: string | undefined): KubeObjectStore | undefined;
getStore<Api>(api: RegisterableApi<Api>): KubeObjectStoreFrom<Api> | undefined; getStore<Api>(api: RegisterableApi<Api>): KubeObjectStoreFrom<Api> | undefined;
/** /**
@ -130,9 +149,19 @@ export class ApiManager {
return undefined; return undefined;
} }
return iter.chain(this.dependencies.stores.get().values()) const defaultResult = iter.chain(this.dependencies.stores.get().values())
.concat(this.externalStores.values()) .concat(this.externalStores.values())
.find(store => store.api.apiBase === api.apiBase); .find(store => store.api.apiBase === api.apiBase);
if (defaultResult) {
return defaultResult;
}
if (this.apiIsDefaultCrdApi(api)) {
return getOrInsertWith(this.defaultCrdStores, api.apiBase, () => this.dependencies.createCustomResourceStore(api));
}
return undefined;
} }
lookupApiLink(ref: ObjectReference, parentObject?: KubeObject): string { lookupApiLink(ref: ObjectReference, parentObject?: KubeObject): string {

View File

@ -5,11 +5,9 @@
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import EventEmitter from "events"; import EventEmitter from "events";
import type TypedEventEmitter from "typed-emitter"; import type TypedEventEmitter from "typed-emitter";
import type { CustomResourceDefinition } from "../endpoints";
import type { KubeApi } from "../kube-api"; import type { KubeApi } from "../kube-api";
export interface LegacyAutoRegistration { export interface LegacyAutoRegistration {
customResourceDefinition: (crd: CustomResourceDefinition) => void;
kubeApi: (api: KubeApi<any, any>) => void; kubeApi: (api: KubeApi<any, any>) => void;
} }

View File

@ -0,0 +1,11 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectionToken } from "@ogre-tools/injectable";
import type { KubeApi } from "../kube-api";
export const customResourceDefinitionApiInjectionToken = getInjectionToken<KubeApi>({
id: "custom-resource-definition-api-token",
});

View File

@ -0,0 +1,27 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import clusterFrameContextForNamespacedResourcesInjectable from "../../../renderer/cluster-frame-context/for-namespaced-resources.injectable";
import loggerInjectable from "../../logger.injectable";
import type { KubeApi } from "../kube-api";
import type { KubeObject } from "../kube-object";
import type { KubeObjectStoreDependencies } from "../kube-object.store";
import { CustomResourceStore } from "./resource.store";
export type CreateCustomResourceStore = <K extends KubeObject>(api: KubeApi<K>) => CustomResourceStore<K>;
const createCustomResourceStoreInjectable = getInjectable({
id: "create-custom-resource-store",
instantiate: (di): CreateCustomResourceStore => {
const deps: KubeObjectStoreDependencies = {
context: di.inject(clusterFrameContextForNamespacedResourcesInjectable),
logger: di.inject(loggerInjectable),
};
return (api) => new CustomResourceStore(deps, api);
},
});
export default createCustomResourceStoreInjectable;

View File

@ -9,6 +9,8 @@ import { computedInjectManyInjectable } from "@ogre-tools/injectable-extension-f
import { kubeObjectStoreInjectionToken } from "./kube-object-store-token"; import { kubeObjectStoreInjectionToken } from "./kube-object-store-token";
import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token"; import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
import { computed } from "mobx"; import { computed } from "mobx";
import { customResourceDefinitionApiInjectionToken } from "./crd-api-token";
import createCustomResourceStoreInjectable from "./create-custom-resource-store.injectable";
const apiManagerInjectable = getInjectable({ const apiManagerInjectable = getInjectable({
id: "api-manager", id: "api-manager",
@ -23,6 +25,10 @@ const apiManagerInjectable = getInjectable({
stores: storesAndApisCanBeCreated stores: storesAndApisCanBeCreated
? computedInjectMany(kubeObjectStoreInjectionToken) ? computedInjectMany(kubeObjectStoreInjectionToken)
: computed(() => []), : computed(() => []),
crdApis: storesAndApisCanBeCreated
? computedInjectMany(customResourceDefinitionApiInjectionToken)
: computed(() => []),
createCustomResourceStore: di.inject(createCustomResourceStoreInjectable),
}); });
}, },
}); });

View File

@ -88,7 +88,7 @@ export interface KubeObjectStoreDependencies {
readonly logger: Logger; readonly logger: Logger;
} }
export abstract class KubeObjectStore< export class KubeObjectStore<
K extends KubeObject = KubeObject, K extends KubeObject = KubeObject,
A extends KubeApi<K, D> = KubeApi<K, KubeJsonApiDataFor<K>>, A extends KubeApi<K, D> = KubeApi<K, KubeJsonApiDataFor<K>>,
D extends KubeJsonApiDataFor<K> = KubeApiDataFrom<K, A>, D extends KubeJsonApiDataFor<K> = KubeApiDataFrom<K, A>,

View File

@ -0,0 +1,25 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { iter } from "@k8slens/utilities";
import type { DiContainerForInjection, Injectable } from "@ogre-tools/injectable";
// Register new injectables and deregister removed injectables by id
export const injectableDifferencingRegistratorWith = (di: DiContainerForInjection) => (
(rawCurrent: Injectable<any, any, any>[], rawPrevious: Injectable<any, any, any>[] = []) => {
const current = new Map(rawCurrent.map(inj => [inj.id, inj]));
const previous = new Map(rawPrevious.map(inj => [inj.id, inj]));
const toAdd = iter.chain(current.entries())
.filter(([id]) => !previous.has(id))
.collect(entries => new Map(entries));
const toRemove = iter.chain(previous.entries())
.filter(([id]) => !current.has(id))
.collect(entries => new Map(entries));
di.deregister(...toRemove.values());
di.register(...toAdd.values());
}
);

View File

@ -2,29 +2,18 @@
* Copyright (c) OpenLens Authors. All rights reserved. * Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import type { Injectable } from "@ogre-tools/injectable";
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { difference, find, map } from "lodash";
import { reaction, runInAction } from "mobx"; import { reaction, runInAction } from "mobx";
import { disposer } from "@k8slens/utilities"; import { disposer } from "@k8slens/utilities";
import type { LensExtension } from "../../lens-extension"; import type { LensExtension } from "../../lens-extension";
import { extensionRegistratorInjectionToken } from "../extension-registrator-injection-token"; import { extensionRegistratorInjectionToken } from "../extension-registrator-injection-token";
import { injectableDifferencingRegistratorWith } from "../../../common/utils/registrator-helper";
export interface Extension { export interface Extension {
register: () => void; register: () => void;
deregister: () => void; deregister: () => void;
} }
const idsToInjectables = (ids: string[], injectables: Injectable<any, any, any>[]) => ids.map(id => {
const injectable = find(injectables, { id });
if (!injectable) {
throw new Error(`Injectable ${id} not found`);
}
return injectable;
});
const extensionInjectable = getInjectable({ const extensionInjectable = getInjectable({
id: "extension", id: "extension",
@ -35,36 +24,27 @@ const extensionInjectable = getInjectable({
instantiate: (childDi) => { instantiate: (childDi) => {
const extensionRegistrators = childDi.injectMany(extensionRegistratorInjectionToken); const extensionRegistrators = childDi.injectMany(extensionRegistratorInjectionToken);
const reactionDisposer = disposer(); const reactionDisposer = disposer();
const injectableDifferencingRegistrator = injectableDifferencingRegistratorWith(childDi);
return { return {
register: () => { register: () => {
extensionRegistrators.forEach((getInjectablesOfExtension) => { for (const extensionRegistrator of extensionRegistrators) {
const injectables = getInjectablesOfExtension(instance); const injectables = extensionRegistrator(instance);
reactionDisposer.push( if (Array.isArray(injectables)) {
// injectables is either an array or a computed array, in which case runInAction(() => {
// we need to update the registered injectables with a reaction every time they change injectableDifferencingRegistrator(injectables);
reaction( });
() => Array.isArray(injectables) ? injectables : injectables.get(), } else {
(currentInjectables, previousInjectables = []) => { reactionDisposer.push(reaction(
// Register new injectables and deregister removed injectables by id () => injectables.get(),
const currentIds = map(currentInjectables, "id"); injectableDifferencingRegistrator,
const previousIds = map(previousInjectables, "id"); {
const idsToAdd = difference(currentIds, previousIds);
const idsToRemove = previousIds.filter(previousId => !currentIds.includes(previousId));
if (idsToRemove.length > 0) {
childDi.deregister(...idsToInjectables(idsToRemove, previousInjectables));
}
if (idsToAdd.length > 0) {
childDi.register(...idsToInjectables(idsToAdd, currentInjectables));
}
}, {
fireImmediately: true, fireImmediately: true,
}, },
)); ));
}); }
}
}, },
deregister: () => { deregister: () => {

View File

@ -26,7 +26,7 @@ const checkForUpdatesMenuItemInjectable = getInjectable({
id: "check-for-updates", id: "check-for-updates",
parentId: isMac ? "mac" : "help", parentId: isMac ? "mac" : "help",
orderNumber: isMac ? 20 : 50, orderNumber: isMac ? 20 : 50,
label: "Check for updates", label: "Check for Updates...",
isShown: updatingIsEnabled, isShown: updatingIsEnabled,
onClick: async () => { onClick: async () => {

View File

@ -146,7 +146,7 @@ describe("installing update using tray", () => {
it("name of tray item for checking updates indicates that checking is happening", () => { it("name of tray item for checking updates indicates that checking is happening", () => {
expect( expect(
builder.tray.get("check-for-updates")?.label, builder.tray.get("check-for-updates")?.label,
).toBe("Checking for updates..."); ).toBe("Checking for Updates...");
}); });
it("user cannot install update yet", () => { it("user cannot install update yet", () => {
@ -177,7 +177,7 @@ describe("installing update using tray", () => {
it("name of tray item for checking updates no longer indicates that checking is happening", () => { it("name of tray item for checking updates no longer indicates that checking is happening", () => {
expect( expect(
builder.tray.get("check-for-updates")?.label, builder.tray.get("check-for-updates")?.label,
).toBe("Check for updates"); ).toBe("Check for Updates...");
}); });
it("renders", () => { it("renders", () => {
@ -241,7 +241,7 @@ describe("installing update using tray", () => {
it("name of tray item for checking updates no longer indicates that downloading is happening", () => { it("name of tray item for checking updates no longer indicates that downloading is happening", () => {
expect( expect(
builder.tray.get("check-for-updates")?.label, builder.tray.get("check-for-updates")?.label,
).toBe("Check for updates"); ).toBe("Check for Updates...");
}); });
it("renders", () => { it("renders", () => {
@ -269,7 +269,7 @@ describe("installing update using tray", () => {
it("name of tray item for checking updates no longer indicates that downloading is happening", () => { it("name of tray item for checking updates no longer indicates that downloading is happening", () => {
expect( expect(
builder.tray.get("check-for-updates")?.label, builder.tray.get("check-for-updates")?.label,
).toBe("Check for updates"); ).toBe("Check for Updates...");
}); });
it("renders", () => { it("renders", () => {

View File

@ -49,10 +49,10 @@ const checkForUpdatesTrayItemInjectable = getInjectable({
} }
if (checkingForUpdatesState.value.get()) { if (checkingForUpdatesState.value.get()) {
return "Checking for updates..."; return "Checking for Updates...";
} }
return "Check for updates"; return "Check for Updates...";
}), }),
enabled: computed(() => !checkingForUpdatesState.value.get() && !downloadingUpdateState.value.get()), enabled: computed(() => !checkingForUpdatesState.value.get() && !downloadingUpdateState.value.get()),

View File

@ -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<AuthorizationV1Api["createSelfSubjectRulesReview"]>;
let createSelfSubjectAccessReviewMock: AsyncFnMock<AuthorizationV1Api["createSelfSubjectAccessReview"]>;
let listNamespaceMock: AsyncFnMock<CoreV1Api["listNamespace"]>;
let k8sRequestMock: AsyncFnMock<K8sRequest>;
let detectClusterMetadataMock: AsyncFnMock<DetectClusterMetadata>;
let kubeAuthProxyMock: Mocked<KubeAuthProxy>;
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<void>;
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<V1SelfSubjectAccessReview>,
} 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<V1SelfSubjectAccessReview>,
} 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<V1NamespaceList>,
} as Awaited<ReturnType<CoreV1Api["listNamespace"]>>;
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<ReturnType<AuthorizationV1Api["createSelfSubjectRulesReview"]>>;
const defaultIncompletePermissions = {
body: {
status: {
incomplete: true,
},
} as PartialDeep<V1SelfSubjectRulesReview>,
} as CreateSelfSubjectRulesReviewRes;
const emptyPermissions = {
body: {
status: {
resourceRules: [],
},
} as PartialDeep<V1SelfSubjectRulesReview>,
} as CreateSelfSubjectRulesReviewRes;
const defaultSingleListPermissions = {
body: {
status: {
resourceRules: [{
apiGroups: [""],
resources: ["pods"],
verbs: ["list"],
}],
},
} as PartialDeep<V1SelfSubjectRulesReview>,
} as CreateSelfSubjectRulesReviewRes;
const defaultMultipleListPermissions = {
body: {
status: {
resourceRules: [
{
apiGroups: [""],
resources: ["pods"],
verbs: ["get"],
},
{
apiGroups: [""],
resources: ["pods"],
verbs: ["list"],
},
],
},
} as PartialDeep<V1SelfSubjectRulesReview>,
} as CreateSelfSubjectRulesReviewRes;

View File

@ -31,8 +31,7 @@ describe("extension special characters in page registrations", () => {
describe("when navigating to route with ID having special characters", () => { describe("when navigating to route with ID having special characters", () => {
beforeEach(() => { beforeEach(() => {
const testExtension = const testExtension = builder.extensions.get("some-extension-id").applicationWindows.only;
builder.extensions.get("some-extension-id").applicationWindows.only;
testExtension.navigate("/some-page-id/"); testExtension.navigate("/some-page-id/");
}); });

View File

@ -5,10 +5,6 @@
import { Cluster } from "../../common/cluster/cluster"; import { Cluster } from "../../common/cluster/cluster";
import { Kubectl } from "../kubectl/kubectl"; import { Kubectl } from "../kubectl/kubectl";
import { getDiForUnitTesting } from "../getDiForUnitTesting"; 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 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 directoryForTempInjectable from "../../common/app-paths/directory-for-temp/directory-for-temp.injectable";
import normalizedPlatformInjectable from "../../common/vars/normalized-platform.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 kubeconfigManagerInjectable from "../kubeconfig-manager/kubeconfig-manager.injectable";
import type { KubeconfigManager } from "../kubeconfig-manager/kubeconfig-manager"; import type { KubeconfigManager } from "../kubeconfig-manager/kubeconfig-manager";
import broadcastConnectionUpdateInjectable from "../cluster/broadcast-connection-update.injectable"; 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", () => { describe("create clusters", () => {
let cluster: Cluster; let cluster: Cluster;
@ -34,8 +34,8 @@ describe("create clusters", () => {
di.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64"); di.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64");
di.override(normalizedPlatformInjectable, () => "darwin"); di.override(normalizedPlatformInjectable, () => "darwin");
di.override(broadcastConnectionUpdateInjectable, () => async () => {}); di.override(broadcastConnectionUpdateInjectable, () => async () => {});
di.override(createAuthorizationReviewInjectable, () => () => () => Promise.resolve(true)); di.override(createCanIInjectable, () => () => () => Promise.resolve(true));
di.override(requestNamespaceListPermissionsForInjectable, () => () => async () => () => true); di.override(createRequestNamespaceListPermissionsInjectable, () => () => async () => () => true);
di.override(createListNamespacesInjectable, () => () => () => Promise.resolve([ "default" ])); di.override(createListNamespacesInjectable, () => () => () => Promise.resolve([ "default" ]));
di.override(prometheusHandlerInjectable, () => ({ di.override(prometheusHandlerInjectable, () => ({
getPrometheusDetails: jest.fn(), getPrometheusDetails: jest.fn(),

View File

@ -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<KubeConfig>));
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");
});
});
});

View File

@ -5,7 +5,6 @@
import waitUntilPortIsUsedInjectable from "../kube-auth-proxy/wait-until-port-is-used/wait-until-port-is-used.injectable"; import waitUntilPortIsUsedInjectable from "../kube-auth-proxy/wait-until-port-is-used/wait-until-port-is-used.injectable";
import { Cluster } from "../../common/cluster/cluster"; import { Cluster } from "../../common/cluster/cluster";
import type { KubeAuthProxy } from "../kube-auth-proxy/kube-auth-proxy";
import type { ChildProcess } from "child_process"; import type { ChildProcess } from "child_process";
import { Kubectl } from "../kubectl/kubectl"; import { Kubectl } from "../kubectl/kubectl";
import type { DeepMockProxy } from "jest-mock-extended"; import type { DeepMockProxy } from "jest-mock-extended";
@ -13,6 +12,7 @@ import { mockDeep, mock } from "jest-mock-extended";
import type { Readable } from "stream"; import type { Readable } from "stream";
import { EventEmitter } from "stream"; import { EventEmitter } from "stream";
import { getDiForUnitTesting } from "../getDiForUnitTesting"; 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 createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable";
import spawnInjectable from "../child-process/spawn.injectable"; import spawnInjectable from "../child-process/spawn.injectable";
import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.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"; const clusterServerUrl = "https://192.168.64.3:8443";
describe("kube auth proxy tests", () => { describe("kube auth proxy tests", () => {
let createKubeAuthProxy: (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => KubeAuthProxy; let createKubeAuthProxy: CreateKubeAuthProxy;
let spawnMock: jest.Mock; let spawnMock: jest.Mock;
let waitUntilPortIsUsedMock: jest.Mock; let waitUntilPortIsUsedMock: jest.Mock;
let broadcastMessageMock: jest.Mock; let broadcastMessageMock: jest.Mock;

View File

@ -43,7 +43,7 @@ const createTestPrometheusProvider = (kind: string, alwaysFail: ServiceResult):
}, },
}); });
describe("ContextHandler", () => { describe("PrometheusHandler", () => {
let di: DiContainer; let di: DiContainer;
let cluster: Cluster; let cluster: Cluster;

View File

@ -34,6 +34,7 @@ const setupAppPathsInjectable = getInjectable({
const appDataPath = getElectronAppPath("appData"); const appDataPath = getElectronAppPath("appData");
setElectronAppPath("userData", joinPaths(appDataPath, appName)); setElectronAppPath("userData", joinPaths(appDataPath, appName));
setElectronAppPath("sessionData", getElectronAppPath("userData"));
const appPaths = pipeline( const appPaths = pipeline(
pathNames, pathNames,

View File

@ -15,8 +15,11 @@ export interface ClusterData {
readonly id: string; readonly id: string;
} }
export type RequestApiVersions = (cluster: ClusterData) => AsyncResult<KubeResourceListGroup[], Error>; export interface ApiVersionsRequester {
request(cluster: ClusterData): AsyncResult<KubeResourceListGroup[], Error>;
readonly orderNumber: number;
}
export const requestApiVersionsInjectionToken = getInjectionToken<RequestApiVersions>({ export const apiVersionsRequesterInjectionToken = getInjectionToken<ApiVersionsRequester>({
id: "request-api-versions-token", id: "request-api-versions-token",
}); });

View File

@ -6,10 +6,7 @@
import { type KubeConfig, HttpError } from "@kubernetes/client-node"; import { type KubeConfig, HttpError } from "@kubernetes/client-node";
import { reaction, comparer, runInAction } from "mobx"; import { reaction, comparer, runInAction } from "mobx";
import { ClusterStatus } from "../../common/cluster-types"; 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 { 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 type { BroadcastMessage } from "../../common/ipc/broadcast-message.injectable";
import { clusterListNamespaceForbiddenChannel } from "../../common/ipc/cluster"; import { clusterListNamespaceForbiddenChannel } from "../../common/ipc/cluster";
import type { Logger } from "../../common/logger"; 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 { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import broadcastConnectionUpdateInjectable from "./broadcast-connection-update.injectable"; import broadcastConnectionUpdateInjectable from "./broadcast-connection-update.injectable";
import broadcastMessageInjectable from "../../common/ipc/broadcast-message.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 createListNamespacesInjectable from "../../common/cluster/list-namespaces.injectable";
import kubeAuthProxyServerInjectable from "./kube-auth-proxy-server.injectable"; import kubeAuthProxyServerInjectable from "./kube-auth-proxy-server.injectable";
import loadProxyKubeconfigInjectable from "./load-proxy-kubeconfig.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 prometheusHandlerInjectable from "./prometheus-handler/prometheus-handler.injectable";
import removeProxyKubeconfigInjectable from "./remove-proxy-kubeconfig.injectable"; import removeProxyKubeconfigInjectable from "./remove-proxy-kubeconfig.injectable";
import requestApiResourcesInjectable from "./request-api-resources.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 { DetectClusterMetadata } from "../cluster-detectors/detect-cluster-metadata.injectable";
import type { FallibleOnlyClusterMetadataDetector } from "../cluster-detectors/token"; import type { FallibleOnlyClusterMetadataDetector } from "../cluster-detectors/token";
import clusterVersionDetectorInjectable from "../cluster-detectors/cluster-version-detector.injectable"; import clusterVersionDetectorInjectable from "../cluster-detectors/cluster-version-detector.injectable";
import detectClusterMetadataInjectable from "../cluster-detectors/detect-cluster-metadata.injectable"; import detectClusterMetadataInjectable from "../cluster-detectors/detect-cluster-metadata.injectable";
import { replaceObservableObject } from "../../common/utils/replace-observable-object"; 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 { interface Dependencies {
readonly logger: Logger; readonly logger: Logger;
readonly prometheusHandler: ClusterPrometheusHandler; readonly prometheusHandler: ClusterPrometheusHandler;
readonly kubeAuthProxyServer: KubeAuthProxyServer; readonly kubeAuthProxyServer: KubeAuthProxyServer;
readonly clusterVersionDetector: FallibleOnlyClusterMetadataDetector; readonly clusterVersionDetector: FallibleOnlyClusterMetadataDetector;
createAuthorizationReview: CreateAuthorizationReview; createCanI: CreateCanI;
requestApiResources: RequestApiResources; requestApiResources: RequestApiResources;
requestNamespaceListPermissionsFor: RequestNamespaceListPermissionsFor; createRequestNamespaceListPermissions: CreateRequestNamespaceListPermissions;
createAuthorizationApi: CreateAuthorizationApi;
createCoreApi: CreateCoreApi;
createListNamespaces: CreateListNamespaces; createListNamespaces: CreateListNamespaces;
detectClusterMetadata: DetectClusterMetadata; detectClusterMetadata: DetectClusterMetadata;
broadcastMessage: BroadcastMessage; broadcastMessage: BroadcastMessage;
@ -224,8 +230,9 @@ class ClusterConnection {
private async refreshAccessibility(): Promise<void> { private async refreshAccessibility(): Promise<void> {
this.dependencies.logger.info(`[CLUSTER]: refreshAccessibility`, this.cluster.getMeta()); this.dependencies.logger.info(`[CLUSTER]: refreshAccessibility`, this.cluster.getMeta());
const proxyConfig = await this.dependencies.loadProxyKubeconfig(); const proxyConfig = await this.dependencies.loadProxyKubeconfig();
const canI = this.dependencies.createAuthorizationReview(proxyConfig); const api = this.dependencies.createAuthorizationApi(proxyConfig);
const requestNamespaceListPermissions = this.dependencies.requestNamespaceListPermissionsFor(proxyConfig); const canI = this.dependencies.createCanI(api);
const requestNamespaceListPermissions = this.dependencies.createRequestNamespaceListPermissions(api);
const isAdmin = await canI({ const isAdmin = await canI({
namespace: "kube-system", namespace: "kube-system",
@ -360,7 +367,8 @@ class ClusterConnection {
} }
try { try {
const listNamespaces = this.dependencies.createListNamespaces(proxyConfig); const api = this.dependencies.createCoreApi(proxyConfig);
const listNamespaces = this.dependencies.createListNamespaces(api);
return await listNamespaces(); return await listNamespaces();
} catch (error) { } catch (error) {
@ -403,13 +411,15 @@ const clusterConnectionInjectable = getInjectable({
prometheusHandler: di.inject(prometheusHandlerInjectable, cluster), prometheusHandler: di.inject(prometheusHandlerInjectable, cluster),
broadcastConnectionUpdate: di.inject(broadcastConnectionUpdateInjectable, cluster), broadcastConnectionUpdate: di.inject(broadcastConnectionUpdateInjectable, cluster),
broadcastMessage: di.inject(broadcastMessageInjectable), broadcastMessage: di.inject(broadcastMessageInjectable),
createAuthorizationReview: di.inject(createAuthorizationReviewInjectable),
createListNamespaces: di.inject(createListNamespacesInjectable), createListNamespaces: di.inject(createListNamespacesInjectable),
detectClusterMetadata: di.inject(detectClusterMetadataInjectable), detectClusterMetadata: di.inject(detectClusterMetadataInjectable),
loadProxyKubeconfig: di.inject(loadProxyKubeconfigInjectable, cluster), loadProxyKubeconfig: di.inject(loadProxyKubeconfigInjectable, cluster),
removeProxyKubeconfig: di.inject(removeProxyKubeconfigInjectable, cluster), removeProxyKubeconfig: di.inject(removeProxyKubeconfigInjectable, cluster),
requestApiResources: di.inject(requestApiResourcesInjectable), 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, cluster,
), ),

View File

@ -8,7 +8,7 @@ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import type { Cluster } from "../../common/cluster/cluster"; import type { Cluster } from "../../common/cluster/cluster";
import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable"; import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable";
import kubeAuthProxyCertificateInjectable from "../kube-auth-proxy/kube-auth-proxy-certificate.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 { export interface KubeAuthProxyServer {
getApiTarget(isLongRunningRequest?: boolean): Promise<ServerOptions>; getApiTarget(isLongRunningRequest?: boolean): Promise<ServerOptions>;

View File

@ -7,11 +7,12 @@ import { getInjectable } from "@ogre-tools/injectable";
import loggerInjectable from "../../common/logger.injectable"; import loggerInjectable from "../../common/logger.injectable";
import type { KubeApiResource } from "../../common/rbac"; import type { KubeApiResource } from "../../common/rbac";
import type { Cluster } from "../../common/cluster/cluster"; 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 { backoffCaller, withConcurrencyLimit } from "@k8slens/utilities";
import requestKubeApiResourcesForInjectable from "./request-kube-api-resources-for.injectable"; import requestKubeApiResourcesForInjectable from "./request-kube-api-resources-for.injectable";
import type { AsyncResult } from "@k8slens/utilities"; import type { AsyncResult } from "@k8slens/utilities";
import broadcastConnectionUpdateInjectable from "./broadcast-connection-update.injectable"; import broadcastConnectionUpdateInjectable from "./broadcast-connection-update.injectable";
import { byOrderNumber } from "../../common/utils/composable-responsibilities/orderable/orderable";
export type RequestApiResources = (cluster: Cluster) => AsyncResult<KubeApiResource[], Error>; export type RequestApiResources = (cluster: Cluster) => AsyncResult<KubeApiResource[], Error>;
@ -24,7 +25,8 @@ const requestApiResourcesInjectable = getInjectable({
id: "request-api-resources", id: "request-api-resources",
instantiate: (di): RequestApiResources => { instantiate: (di): RequestApiResources => {
const logger = di.inject(loggerInjectable); const logger = di.inject(loggerInjectable);
const apiVersionRequesters = di.injectMany(requestApiVersionsInjectionToken); const apiVersionRequesters = di.injectMany(apiVersionsRequesterInjectionToken)
.sort(byOrderNumber);
const requestKubeApiResourcesFor = di.inject(requestKubeApiResourcesForInjectable); const requestKubeApiResourcesFor = di.inject(requestKubeApiResourcesForInjectable);
return async (...args) => { return async (...args) => {
@ -35,7 +37,7 @@ const requestApiResourcesInjectable = getInjectable({
const groupLists: KubeResourceListGroup[] = []; const groupLists: KubeResourceListGroup[] = [];
for (const apiVersionRequester of apiVersionRequesters) { for (const apiVersionRequester of apiVersionRequesters) {
const result = await backoffCaller(() => apiVersionRequester(cluster), { const result = await backoffCaller(() => apiVersionRequester.request(cluster), {
onIntermediateError: (error, attempt) => { onIntermediateError: (error, attempt) => {
broadcastConnectionUpdate({ broadcastConnectionUpdate({
message: `Failed to list kube API resource kinds, attempt ${attempt}: ${error}`, message: `Failed to list kube API resource kinds, attempt ${attempt}: ${error}`,

View File

@ -5,33 +5,36 @@
import type { V1APIVersions } from "@kubernetes/client-node"; import type { V1APIVersions } from "@kubernetes/client-node";
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import k8sRequestInjectable from "../k8s-request.injectable"; import k8sRequestInjectable from "../k8s-request.injectable";
import { requestApiVersionsInjectionToken } from "./request-api-versions"; import { apiVersionsRequesterInjectionToken } from "./api-versions-requester";
const requestCoreApiVersionsInjectable = getInjectable({ const requestCoreApiVersionsInjectable = getInjectable({
id: "request-core-api-versions", id: "request-core-api-versions",
instantiate: (di) => { instantiate: (di) => {
const k8sRequest = di.inject(k8sRequestInjectable); const k8sRequest = di.inject(k8sRequestInjectable);
return async (cluster) => { return {
try { request: async (cluster) => {
const { versions } = await k8sRequest(cluster, "/api") as V1APIVersions; try {
const { versions } = await k8sRequest(cluster, "/api") as V1APIVersions;
return { return {
callWasSuccessful: true, callWasSuccessful: true,
response: versions.map(version => ({ response: versions.map(version => ({
group: "", group: "",
path: `/api/${version}`, path: `/api/${version}`,
})), })),
}; };
} catch (error) { } catch (error) {
return { return {
callWasSuccessful: false, callWasSuccessful: false,
error: error as Error, error: error as Error,
}; };
} }
},
orderNumber: 10,
}; };
}, },
injectionToken: requestApiVersionsInjectionToken, injectionToken: apiVersionsRequesterInjectionToken,
}); });
export default requestCoreApiVersionsInjectable; export default requestCoreApiVersionsInjectable;

View File

@ -8,7 +8,7 @@ import type { Cluster } from "../../common/cluster/cluster";
import type { KubeApiResource } from "../../common/rbac"; import type { KubeApiResource } from "../../common/rbac";
import type { AsyncResult } from "@k8slens/utilities"; import type { AsyncResult } from "@k8slens/utilities";
import k8sRequestInjectable from "../k8s-request.injectable"; 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<KubeApiResource[], Error>; export type RequestKubeApiResources = (grouping: KubeResourceListGroup) => AsyncResult<KubeApiResource[], Error>;

View File

@ -6,35 +6,40 @@ import type { V1APIGroupList } from "@kubernetes/client-node";
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import { iter } from "@k8slens/utilities"; import { iter } from "@k8slens/utilities";
import k8sRequestInjectable from "../k8s-request.injectable"; import k8sRequestInjectable from "../k8s-request.injectable";
import { requestApiVersionsInjectionToken } from "./request-api-versions"; import { apiVersionsRequesterInjectionToken } from "./api-versions-requester";
const requestNonCoreApiVersionsInjectable = getInjectable({ const requestNonCoreApiVersionsInjectable = getInjectable({
id: "request-non-core-api-versions", id: "request-non-core-api-versions",
instantiate: (di) => { instantiate: (di) => {
const k8sRequest = di.inject(k8sRequestInjectable); const k8sRequest = di.inject(k8sRequestInjectable);
return async (cluster) => { return {
try { request: async (cluster) => {
const { groups } = await k8sRequest(cluster, "/apis") as V1APIGroupList; try {
const { groups } = (await k8sRequest(cluster, "/apis")) as V1APIGroupList;
return { return {
callWasSuccessful: true, callWasSuccessful: true,
response: iter.chain(groups.values()) response: iter.chain(groups.values())
.flatMap(group => group.versions.map(version => ({ .flatMap((group) =>
group: group.name, group.versions.map((version) => ({
path: `/apis/${version.groupVersion}`, group: group.name,
}))) path: `/apis/${version.groupVersion}`,
.collect(v => [...v]), })),
}; )
} catch (error) { .collect((v) => [...v]),
return { };
callWasSuccessful: false, } catch (error) {
error: error as Error, return {
}; callWasSuccessful: false,
} error: error as Error,
};
}
},
orderNumber: 20,
}; };
}, },
injectionToken: requestApiVersionsInjectionToken, injectionToken: apiVersionsRequesterInjectionToken,
}); });
export default requestNonCoreApiVersionsInjectable; export default requestNonCoreApiVersionsInjectable;

View File

@ -10,13 +10,13 @@ import type { DiContainer } from "@ogre-tools/injectable";
import { getDiForUnitTesting } from "../getDiForUnitTesting"; import { getDiForUnitTesting } from "../getDiForUnitTesting";
import type { K8sRequest } from "../k8s-request.injectable"; import type { K8sRequest } from "../k8s-request.injectable";
import k8sRequestInjectable 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"; import requestNonCoreApiVersionsInjectable from "./request-non-core-api-versions.injectable";
describe("requestNonCoreApiVersions", () => { describe("requestNonCoreApiVersions", () => {
let di: DiContainer; let di: DiContainer;
let k8sRequestMock: AsyncFnMock<K8sRequest>; let k8sRequestMock: AsyncFnMock<K8sRequest>;
let requestNonCoreApiVersions: RequestApiVersions; let requestNonCoreApiVersions: ApiVersionsRequester;
beforeEach(() => { beforeEach(() => {
di = getDiForUnitTesting(); di = getDiForUnitTesting();
@ -28,10 +28,10 @@ describe("requestNonCoreApiVersions", () => {
}); });
describe("when called", () => { describe("when called", () => {
let versionsRequest: ReturnType<RequestApiVersions>; let versionsRequest: ReturnType<ApiVersionsRequester["request"]>;
beforeEach(() => { beforeEach(() => {
versionsRequest = requestNonCoreApiVersions({ id: "some-cluster-id" }); versionsRequest = requestNonCoreApiVersions.request({ id: "some-cluster-id" });
}); });
it("should request all api groups", () => { it("should request all api groups", () => {

View File

@ -4,7 +4,7 @@
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import type { KubeAuthProxyDependencies } from "./kube-auth-proxy"; 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 type { Cluster } from "../../common/cluster/cluster";
import spawnInjectable from "../child-process/spawn.injectable"; import spawnInjectable from "../child-process/spawn.injectable";
import kubeAuthProxyCertificateInjectable from "./kube-auth-proxy-certificate.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 getDirnameOfPathInjectable from "../../common/path/get-dirname.injectable";
import broadcastConnectionUpdateInjectable from "../cluster/broadcast-connection-update.injectable"; import broadcastConnectionUpdateInjectable from "../cluster/broadcast-connection-update.injectable";
export interface KubeAuthProxy {
readonly apiPrefix: string;
readonly port: number;
run: () => Promise<void>;
exit: () => void;
}
export type CreateKubeAuthProxy = (cluster: Cluster, env: NodeJS.ProcessEnv) => KubeAuthProxy; export type CreateKubeAuthProxy = (cluster: Cluster, env: NodeJS.ProcessEnv) => KubeAuthProxy;
const createKubeAuthProxyInjectable = getInjectable({ const createKubeAuthProxyInjectable = getInjectable({
@ -33,7 +40,7 @@ const createKubeAuthProxyInjectable = getInjectable({
return (cluster, env) => { return (cluster, env) => {
const clusterUrl = new URL(cluster.apiUrl.get()); const clusterUrl = new URL(cluster.apiUrl.get());
return new KubeAuthProxy({ return new KubeAuthProxyImpl({
...dependencies, ...dependencies,
proxyCert: di.inject(kubeAuthProxyCertificateInjectable, clusterUrl.hostname), proxyCert: di.inject(kubeAuthProxyCertificateInjectable, clusterUrl.hostname),
broadcastConnectionUpdate: di.inject(broadcastConnectionUpdateInjectable, cluster), broadcastConnectionUpdate: di.inject(broadcastConnectionUpdateInjectable, cluster),

View File

@ -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 { WaitUntilPortIsUsed } from "./wait-until-port-is-used/wait-until-port-is-used.injectable";
import type { GetDirnameOfPath } from "../../common/path/get-dirname.injectable"; import type { GetDirnameOfPath } from "../../common/path/get-dirname.injectable";
import type { BroadcastConnectionUpdate } from "../cluster/broadcast-connection-update.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 (?<address>.+)"; const startingServeMatcher = "starting to serve on (?<address>.+)";
const startingServeRegex = Object.assign(TypedRegEx(startingServeMatcher, "i"), { const startingServeRegex = Object.assign(TypedRegEx(startingServeMatcher, "i"), {
@ -33,7 +34,7 @@ export interface KubeAuthProxyDependencies {
broadcastConnectionUpdate: BroadcastConnectionUpdate; broadcastConnectionUpdate: BroadcastConnectionUpdate;
} }
export class KubeAuthProxy { export class KubeAuthProxyImpl implements KubeAuthProxy {
public readonly apiPrefix = `/${randomBytes(8).toString("hex")}`; public readonly apiPrefix = `/${randomBytes(8).toString("hex")}`;
public get port(): number { public get port(): number {

View File

@ -85,7 +85,7 @@ export class KubeconfigManager {
return this.tempFilePath = await this.createProxyKubeconfig(); return this.tempFilePath = await this.createProxyKubeconfig();
} catch (error) { } 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}`);
} }
} }

View File

@ -8,7 +8,11 @@ import { BrowserWindow } from "electron";
const resolveSystemProxyWindowInjectable = getInjectable({ const resolveSystemProxyWindowInjectable = getInjectable({
id: "resolve-system-proxy-window", id: "resolve-system-proxy-window",
instantiate: () => { instantiate: () => {
return new BrowserWindow({ show: false, paintWhenInitiallyHidden: false }); const window = new BrowserWindow({ show: false });
window.hide();
return window;
}, },
causesSideEffects: true, causesSideEffects: true,
}); });

View File

@ -0,0 +1,53 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { reaction } from "mobx";
import { customResourceDefinitionApiInjectionToken } from "../../../common/k8s-api/api-manager/crd-api-token";
import type { CustomResourceDefinition } from "../../../common/k8s-api/endpoints";
import { KubeApi } from "../../../common/k8s-api/kube-api";
import { KubeObject } from "../../../common/k8s-api/kube-object";
import maybeKubeApiInjectable from "../../../common/k8s-api/maybe-kube-api.injectable";
import loggerInjectable from "../../../common/logger.injectable";
import { injectableDifferencingRegistratorWith } from "../../../common/utils/registrator-helper";
import customResourceDefinitionStoreInjectable from "../../components/+custom-resources/definition.store.injectable";
import { beforeClusterFrameStartsSecondInjectionToken } from "../tokens";
const setupAutoCrdApiCreationsInjectable = getInjectable({
id: "setup-auto-crd-api-creations",
instantiate: (di) => ({
run: () => {
const customResourceDefinitionStore = di.inject(customResourceDefinitionStoreInjectable);
const injectableDifferencingRegistrator = injectableDifferencingRegistratorWith(di);
reaction(
() => customResourceDefinitionStore.getItems().map(toCrdApiInjectable),
injectableDifferencingRegistrator,
{
fireImmediately: true,
},
);
},
}),
injectionToken: beforeClusterFrameStartsSecondInjectionToken,
});
export default setupAutoCrdApiCreationsInjectable;
const toCrdApiInjectable = (crd: CustomResourceDefinition) => getInjectable({
id: `default-kube-api-for-custom-resource-definition-${crd.getResourceApiBase()}`,
instantiate: (di) => {
const objectConstructor = class extends KubeObject {
static readonly kind = crd.getResourceKind();
static readonly namespaced = crd.isNamespaced();
static readonly apiBase = crd.getResourceApiBase();
};
return new KubeApi({
logger: di.inject(loggerInjectable),
maybeKubeApi: di.inject(maybeKubeApiInjectable),
}, { objectConstructor });
},
injectionToken: customResourceDefinitionApiInjectionToken,
});

View File

@ -5,71 +5,22 @@
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import autoRegistrationEmitterInjectable from "../../../common/k8s-api/api-manager/auto-registration-emitter.injectable"; import autoRegistrationEmitterInjectable from "../../../common/k8s-api/api-manager/auto-registration-emitter.injectable";
import apiManagerInjectable from "../../../common/k8s-api/api-manager/manager.injectable"; import apiManagerInjectable from "../../../common/k8s-api/api-manager/manager.injectable";
import { CustomResourceStore } from "../../../common/k8s-api/api-manager/resource.store"; import type { KubeApi } from "../../../common/k8s-api/kube-api";
import type { CustomResourceDefinition } from "../../../common/k8s-api/endpoints";
import type { KubeApiDependencies } from "../../../common/k8s-api/kube-api";
import { KubeApi } from "../../../common/k8s-api/kube-api";
import { KubeObject } from "../../../common/k8s-api/kube-object";
import { beforeClusterFrameStartsSecondInjectionToken } from "../tokens"; import { beforeClusterFrameStartsSecondInjectionToken } from "../tokens";
import type { KubeObjectStoreDependencies } from "../../../common/k8s-api/kube-object.store";
import clusterFrameContextForNamespacedResourcesInjectable from "../../cluster-frame-context/for-namespaced-resources.injectable";
import loggerInjectable from "../../../common/logger.injectable";
import maybeKubeApiInjectable from "../../../common/k8s-api/maybe-kube-api.injectable";
const setupAutoRegistrationInjectable = getInjectable({ const setupAutoRegistrationInjectable = getInjectable({
id: "setup-auto-registration", id: "setup-auto-registration",
instantiate: (di) => ({ instantiate: (di) => ({
run: () => { run: () => {
const autoRegistrationEmitter = di.inject(autoRegistrationEmitterInjectable); const autoRegistrationEmitter = di.inject(autoRegistrationEmitterInjectable);
const beforeApiManagerInitializationCrds: CustomResourceDefinition[] = [];
const beforeApiManagerInitializationApis: KubeApi[] = []; const beforeApiManagerInitializationApis: KubeApi[] = [];
const kubeApiDependencies: KubeApiDependencies = {
logger: di.inject(loggerInjectable),
maybeKubeApi: di.inject(maybeKubeApiInjectable),
};
const kubeObjectStoreDependencies: KubeObjectStoreDependencies = {
context: di.inject(clusterFrameContextForNamespacedResourcesInjectable),
logger: di.inject(loggerInjectable),
};
let initialized = false; let initialized = false;
const autoInitCustomResourceStore = (crd: CustomResourceDefinition) => {
const objectConstructor = class extends KubeObject {
static readonly kind = crd.getResourceKind();
static readonly namespaced = crd.isNamespaced();
static readonly apiBase = crd.getResourceApiBase();
};
const api = (() => {
const rawApi = apiManager.getApi(objectConstructor.apiBase);
if (rawApi) {
return rawApi;
}
const api = new KubeApi(kubeApiDependencies, { objectConstructor });
apiManager.registerApi(api);
return api;
})();
if (!apiManager.getStore(api)) {
apiManager.registerStore(new CustomResourceStore(kubeObjectStoreDependencies, api));
}
};
const autoInitKubeApi = (api: KubeApi) => { const autoInitKubeApi = (api: KubeApi) => {
apiManager.registerApi(api); apiManager.registerApi(api);
}; };
autoRegistrationEmitter autoRegistrationEmitter
.on("customResourceDefinition", (crd) => {
if (initialized) {
autoInitCustomResourceStore(crd);
} else {
beforeApiManagerInitializationCrds.push(crd);
}
})
.on("kubeApi", (api) => { .on("kubeApi", (api) => {
if (initialized) { if (initialized) {
autoInitKubeApi(api); autoInitKubeApi(api);
@ -81,7 +32,6 @@ const setupAutoRegistrationInjectable = getInjectable({
// NOTE: this MUST happen after the event emitter listeners are registered // NOTE: this MUST happen after the event emitter listeners are registered
const apiManager = di.inject(apiManagerInjectable); const apiManager = di.inject(apiManagerInjectable);
beforeApiManagerInitializationCrds.forEach(autoInitCustomResourceStore);
beforeApiManagerInitializationApis.forEach(autoInitKubeApi); beforeApiManagerInitializationApis.forEach(autoInitKubeApi);
initialized = true; initialized = true;
}, },

View File

@ -4,8 +4,13 @@
*/ */
import type { RenderResult } from "@testing-library/react"; import type { RenderResult } from "@testing-library/react";
import React from "react"; import React from "react";
import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import { Cluster } from "../../../common/cluster/cluster";
import { HorizontalPodAutoscaler, HpaMetricType } from "../../../common/k8s-api/endpoints"; import { HorizontalPodAutoscaler, HpaMetricType } from "../../../common/k8s-api/endpoints";
import hostedClusterInjectable from "../../cluster-frame-context/hosted-cluster.injectable";
import { getDiForUnitTesting } from "../../getDiForUnitTesting"; import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import storesAndApisCanBeCreatedInjectable from "../../stores-apis-can-be-created.injectable";
import type { DiRender } from "../test-utils/renderFor"; import type { DiRender } from "../test-utils/renderFor";
import { renderFor } from "../test-utils/renderFor"; import { renderFor } from "../test-utils/renderFor";
import { HpaDetails } from "./hpa-details"; import { HpaDetails } from "./hpa-details";
@ -41,6 +46,17 @@ describe("<HpaDetails/>", () => {
beforeEach(() => { beforeEach(() => {
const di = getDiForUnitTesting(); const di = getDiForUnitTesting();
di.override(directoryForUserDataInjectable, () => "/some-user-store-path");
di.override(directoryForKubeConfigsInjectable, () => "/some-kube-configs");
di.override(storesAndApisCanBeCreatedInjectable, () => true);
di.override(hostedClusterInjectable, () => new Cluster({
contextName: "some-context-name",
id: "some-cluster-id",
kubeConfigPath: "/some-path-to-a-kubeconfig",
}, {
clusterServerUrl: "https://localhost:8080",
}));
render = renderFor(di); render = renderFor(di);
}); });

View File

@ -4,7 +4,6 @@
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import assert from "assert"; import assert from "assert";
import autoRegistrationEmitterInjectable from "../../../common/k8s-api/api-manager/auto-registration-emitter.injectable";
import { kubeObjectStoreInjectionToken } from "../../../common/k8s-api/api-manager/kube-object-store-token"; import { kubeObjectStoreInjectionToken } from "../../../common/k8s-api/api-manager/kube-object-store-token";
import customResourceDefinitionApiInjectable from "../../../common/k8s-api/endpoints/custom-resource-definition.api.injectable"; import customResourceDefinitionApiInjectable from "../../../common/k8s-api/endpoints/custom-resource-definition.api.injectable";
import loggerInjectable from "../../../common/logger.injectable"; import loggerInjectable from "../../../common/logger.injectable";
@ -20,7 +19,6 @@ const customResourceDefinitionStoreInjectable = getInjectable({
const api = di.inject(customResourceDefinitionApiInjectable); const api = di.inject(customResourceDefinitionApiInjectable);
return new CustomResourceDefinitionStore({ return new CustomResourceDefinitionStore({
autoRegistration: di.inject(autoRegistrationEmitterInjectable),
context: di.inject(clusterFrameContextForClusterScopedResourcesInjectable), context: di.inject(clusterFrameContextForClusterScopedResourcesInjectable),
logger: di.inject(loggerInjectable), logger: di.inject(loggerInjectable),
}, api); }, api);

View File

@ -3,37 +3,22 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { computed, reaction, makeObservable } from "mobx"; import { computed, makeObservable } from "mobx";
import type { KubeObjectStoreDependencies, KubeObjectStoreOptions } from "../../../common/k8s-api/kube-object.store"; import type { KubeObjectStoreDependencies, KubeObjectStoreOptions } from "../../../common/k8s-api/kube-object.store";
import { KubeObjectStore } from "../../../common/k8s-api/kube-object.store"; import { KubeObjectStore } from "../../../common/k8s-api/kube-object.store";
import type { CustomResourceDefinition, CustomResourceDefinitionApi } from "../../../common/k8s-api/endpoints/custom-resource-definition.api"; import type { CustomResourceDefinition, CustomResourceDefinitionApi } from "../../../common/k8s-api/endpoints/custom-resource-definition.api";
import type { KubeObject } from "../../../common/k8s-api/kube-object"; import type { KubeObject } from "../../../common/k8s-api/kube-object";
import type TypedEventEmitter from "typed-emitter";
import type { LegacyAutoRegistration } from "../../../common/k8s-api/api-manager/auto-registration-emitter.injectable";
import autoBind from "auto-bind"; import autoBind from "auto-bind";
export interface CustomResourceDefinitionStoreDependencies extends KubeObjectStoreDependencies {
readonly autoRegistration: TypedEventEmitter<LegacyAutoRegistration>;
}
export class CustomResourceDefinitionStore extends KubeObjectStore<CustomResourceDefinition, CustomResourceDefinitionApi> { export class CustomResourceDefinitionStore extends KubeObjectStore<CustomResourceDefinition, CustomResourceDefinitionApi> {
constructor( constructor(
protected readonly dependencies: CustomResourceDefinitionStoreDependencies, dependencies: KubeObjectStoreDependencies,
api: CustomResourceDefinitionApi, api: CustomResourceDefinitionApi,
opts?: KubeObjectStoreOptions, opts?: KubeObjectStoreOptions,
) { ) {
super(dependencies, api, opts); super(dependencies, api, opts);
makeObservable(this); makeObservable(this);
autoBind(this); autoBind(this);
reaction(
() => this.getItems(),
crds => {
for (const crd of crds) {
this.dependencies.autoRegistration.emit("customResourceDefinition", crd);
}
},
);
} }
protected sortItems(items: CustomResourceDefinition[]) { protected sortItems(items: CustomResourceDefinition[]) {

View File

@ -51,3 +51,55 @@ exports[`Icon settings given no external registrations for cluster settings menu
</div> </div>
</body> </body>
`; `;
exports[`Icon settings given no registrations for cluster settings component injection token renders 1`] = `
<body>
<div>
<div>
<div
class="file-loader flex flex-row items-center"
>
<div
class="mr-5"
>
<div
class="FilePicker"
>
<label
class="flex gaps align-center"
for="file-upload"
>
<div
class="Avatar rounded"
style="width: 53px; height: 53px; background: rgb(9, 124, 92);"
>
skc
</div>
</label>
<input
accept="image/*"
id="file-upload"
name="FilePicker"
type="file"
/>
</div>
</div>
<i
class="Icon material interactive focusable"
data-testid="icon-for-menu-actions-for-cluster-icon-settings-for-some-entity-id"
id="menu-actions-for-cluster-icon-settings-for-some-entity-id"
tabindex="0"
>
<span
class="icon"
data-icon-name="more_horiz"
>
more_horiz
</span>
</i>
</div>
</div>
</div>
</body>
`;

View File

@ -2,8 +2,7 @@
* Copyright (c) OpenLens Authors. All rights reserved. * Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import type { DiContainer } from "@ogre-tools/injectable";
import { getInjectable } from "@ogre-tools/injectable";
import type { RenderResult } from "@testing-library/react"; import type { RenderResult } from "@testing-library/react";
import React from "react"; import React from "react";
import { KubernetesCluster } from "../../../../common/catalog-entities"; import { KubernetesCluster } from "../../../../common/catalog-entities";
@ -13,13 +12,18 @@ import { renderFor } from "../../test-utils/renderFor";
import { ClusterIconSetting } from "../icon-settings"; import { ClusterIconSetting } from "../icon-settings";
import { screen } from "@testing-library/react"; import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
import { clusterIconSettingsMenuInjectionToken } from "../cluster-settings-menu-injection-token"; import type { ClusterIconSettingComponentProps } from "@k8slens/cluster-settings";
import { clusterIconSettingsComponentInjectionToken, clusterIconSettingsMenuInjectionToken } from "@k8slens/cluster-settings";
import { runInAction } from "mobx"; import { runInAction } from "mobx";
import { getInjectable, type DiContainer } from "@ogre-tools/injectable";
const cluster = new Cluster({ const cluster = new Cluster({
contextName: "some-context", contextName: "some-context",
id: "some-id", id: "some-id",
kubeConfigPath: "/some/path/to/kubeconfig", kubeConfigPath: "/some/path/to/kubeconfig",
preferences: {
clusterName: "some-cluster-name",
},
}, { }, {
clusterServerUrl: "https://localhost:9999", clusterServerUrl: "https://localhost:9999",
}); });
@ -53,6 +57,29 @@ const newMenuItem = getInjectable({
injectionToken: clusterIconSettingsMenuInjectionToken, injectionToken: clusterIconSettingsMenuInjectionToken,
}); });
function CustomSettingsComponent(props: ClusterIconSettingComponentProps) {
return (
<div data-testid="my-react-component">
<span>Test React Component</span>
<span>
Cluster
{props.preferences.clusterName}
</span>
</div>
);
}
const newSettingsReactComponent = getInjectable({
id: "cluster-icon-settings-react-component",
instantiate: () => ({
id: "test-react-component",
Component: CustomSettingsComponent,
}),
injectionToken: clusterIconSettingsComponentInjectionToken,
});
describe("Icon settings", () => { describe("Icon settings", () => {
let rendered: RenderResult; let rendered: RenderResult;
let di: DiContainer; let di: DiContainer;
@ -98,4 +125,30 @@ describe("Icon settings", () => {
expect(rendered.getByText("Hello World")).toBeInTheDocument(); expect(rendered.getByText("Hello World")).toBeInTheDocument();
}); });
}); });
describe("given no registrations for cluster settings component injection token", () => {
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("does not have any external components", async () => {
expect(rendered.queryByTestId("test-react-component")).not.toBeInTheDocument();
});
});
describe("given registration for cluster settings component injection token", () => {
beforeEach(() => {
runInAction(() => {
di.register(newSettingsReactComponent);
});
});
it("renders external component", async () => {
expect(rendered.queryByTestId("my-react-component")).toBeInTheDocument();
});
it("external component has cluster preferences in props", async () => {
expect(rendered.getByText(/some-cluster-name/)).toBeInTheDocument();
});
});
}); });

View File

@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import { clusterIconSettingsMenuInjectionToken } from "./cluster-settings-menu-injection-token"; import { clusterIconSettingsMenuInjectionToken } from "@k8slens/cluster-settings";
const clusterIconSettingsMenuClearItem = getInjectable({ const clusterIconSettingsMenuClearItem = getInjectable({
id: "cluster-icon-settings-menu-clear-item", id: "cluster-icon-settings-menu-clear-item",

View File

@ -1,17 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectionToken } from "@ogre-tools/injectable";
import type { ClusterPreferences } from "../../../common/cluster-types";
export interface ClusterIconMenuItem {
id: string;
title: string;
disabled?: (preferences: ClusterPreferences) => boolean;
onClick: (preferences: ClusterPreferences) => void;
}
export const clusterIconSettingsMenuInjectionToken = getInjectionToken<ClusterIconMenuItem>({
id: "cluster-icon-settings-menu-injection-token",
});

View File

@ -15,8 +15,8 @@ import { FilePicker, OverSizeLimitStyle } from "../file-picker";
import { MenuActions, MenuItem } from "../menu"; import { MenuActions, MenuItem } from "../menu";
import type { ShowNotification } from "../notifications"; import type { ShowNotification } from "../notifications";
import showErrorNotificationInjectable from "../notifications/show-error-notification.injectable"; import showErrorNotificationInjectable from "../notifications/show-error-notification.injectable";
import type { ClusterIconMenuItem } from "./cluster-settings-menu-injection-token"; import { clusterIconSettingsComponentInjectionToken, clusterIconSettingsMenuInjectionToken } from "@k8slens/cluster-settings";
import { clusterIconSettingsMenuInjectionToken } from "./cluster-settings-menu-injection-token"; import type { ClusterIconMenuItem, ClusterIconSettingsComponent } from "@k8slens/cluster-settings";
export interface ClusterIconSettingProps { export interface ClusterIconSettingProps {
cluster: Cluster; cluster: Cluster;
@ -25,6 +25,7 @@ export interface ClusterIconSettingProps {
interface Dependencies { interface Dependencies {
menuItems: IComputedValue<ClusterIconMenuItem[]>; menuItems: IComputedValue<ClusterIconMenuItem[]>;
settingComponents: IComputedValue<ClusterIconSettingsComponent[]>;
showErrorNotification: ShowNotification; showErrorNotification: ShowNotification;
} }
@ -95,6 +96,14 @@ const NonInjectedClusterIconSetting = observer((props: ClusterIconSettingProps &
)} )}
</MenuActions> </MenuActions>
</div> </div>
{props.settingComponents.get().map(item => {
return (
<item.Component
key={item.id}
preferences={cluster.preferences}
/>
);
})}
</div> </div>
); );
}); });
@ -106,6 +115,7 @@ export const ClusterIconSetting = withInjectables<Dependencies, ClusterIconSetti
return { return {
...props, ...props,
menuItems: computedInjectMany(clusterIconSettingsMenuInjectionToken), menuItems: computedInjectMany(clusterIconSettingsMenuInjectionToken),
settingComponents: computedInjectMany(clusterIconSettingsComponentInjectionToken),
showErrorNotification: di.inject(showErrorNotificationInjectable), showErrorNotification: di.inject(showErrorNotificationInjectable),
}; };
}, },

View File

@ -53,7 +53,7 @@ import { applicationWindowInjectionToken } from "../../../main/start-main-applic
import closeAllWindowsInjectable from "../../../main/start-main-application/lens-window/hide-all-windows/close-all-windows.injectable"; import closeAllWindowsInjectable from "../../../main/start-main-application/lens-window/hide-all-windows/close-all-windows.injectable";
import type { LensWindow } from "../../../main/start-main-application/lens-window/application-window/create-lens-window.injectable"; import type { LensWindow } from "../../../main/start-main-application/lens-window/application-window/create-lens-window.injectable";
import type { FakeExtensionOptions } from "./get-extension-fake"; import type { FakeExtensionOptions } from "./get-extension-fake";
import { getExtensionFakeForMain, getExtensionFakeForRenderer } from "./get-extension-fake"; import { getMainExtensionFakeWith, getRendererExtensionFakeWith } from "./get-extension-fake";
import namespaceApiInjectable from "../../../common/k8s-api/endpoints/namespace.api.injectable"; import namespaceApiInjectable from "../../../common/k8s-api/endpoints/namespace.api.injectable";
import { Namespace } from "../../../common/k8s-api/endpoints"; import { Namespace } from "../../../common/k8s-api/endpoints";
import { getOverrideFsWithFakes } from "../../../test-utils/override-fs-with-fakes"; import { getOverrideFsWithFakes } from "../../../test-utils/override-fs-with-fakes";
@ -594,49 +594,28 @@ export const getApplicationBuilder = () => {
}, },
enable: (...extensions) => { enable: (...extensions) => {
builder.afterWindowStart(({ windowDi }) => { builder.afterWindowStart(action(({ windowDi }) => {
const rendererExtensionInstances = extensions.map((options) => extensions
getExtensionFakeForRenderer( .map(getRendererExtensionFakeWith(windowDi))
windowDi, .forEach(enableExtensionFor(windowDi, rendererExtensionsStateInjectable));
options.id, }));
options.name,
options.rendererOptions || {},
),
);
rendererExtensionInstances.forEach( builder.afterApplicationStart(action(({ mainDi }) => {
enableExtensionFor(windowDi, rendererExtensionsStateInjectable), extensions
); .map(getMainExtensionFakeWith(mainDi))
}); .forEach(enableExtensionFor(mainDi, mainExtensionsStateInjectable));
}));
builder.afterApplicationStart(({ mainDi }) => {
const mainExtensionInstances = extensions.map((extension) =>
getExtensionFakeForMain(mainDi, extension.id, extension.name, extension.mainOptions || {}),
);
runInAction(() => {
mainExtensionInstances.forEach(
enableExtensionFor(mainDi, mainExtensionsStateInjectable),
);
});
});
}, },
disable: (...extensions) => { disable: (...extensions) => {
builder.afterWindowStart(({ windowDi }) => { builder.afterWindowStart(({ windowDi }) => {
extensions extensions
.map((ext) => ext.id) .forEach(disableExtensionFor(windowDi, rendererExtensionsStateInjectable));
.forEach(
disableExtensionFor(windowDi, rendererExtensionsStateInjectable),
);
}); });
builder.afterApplicationStart(({ mainDi }) => { builder.afterApplicationStart(({ mainDi }) => {
extensions extensions
.map((ext) => ext.id) .forEach(disableExtensionFor(mainDi, mainExtensionsStateInjectable));
.forEach(
disableExtensionFor(mainDi, mainExtensionsStateInjectable),
);
}); });
}, },
}, },
@ -835,49 +814,29 @@ const selectOptionFor = (builder: ApplicationBuilder, menuId: string) => (labelT
userEvent.click(option); userEvent.click(option);
}; };
const enableExtensionFor = ( function enableExtensionFor(di: DiContainer, stateInjectable: Injectable<ObservableMap<string, any>, any, any>) {
di: DiContainer,
stateInjectable: Injectable<ObservableMap<string, any>, any, any>,
) => {
const extensionState = di.inject(stateInjectable); const extensionState = di.inject(stateInjectable);
const getExtension = (extension: LensExtension) => return (instance: LensExtension) => {
di.inject(extensionInjectable, extension); const extension = di.inject(extensionInjectable, instance);
return (extensionInstance: LensExtension) => { extension.register();
const extension = getExtension(extensionInstance); extensionState.set(instance.id, instance);
runInAction(() => {
extension.register();
extensionState.set(extensionInstance.id, extensionInstance);
});
}; };
}; }
const disableExtensionFor = function disableExtensionFor(di: DiContainer, stateInjectable: Injectable<ObservableMap<string, any>, unknown, void>) {
( return (extension: FakeExtensionOptions) => {
di: DiContainer, const extensionsState = di.inject(stateInjectable);
stateInjectable: Injectable<ObservableMap<string, any>, unknown, void>, const instance = extensionsState.get(extension.id);
) =>
(id: string) => {
const getExtension = (extension: LensExtension) =>
di.inject(extensionInjectable, extension);
const extensionsState = di.inject(stateInjectable); if (!instance) {
throw new Error(`Tried to disable extension with ID "${extension.id}", but it wasn't enabled`);
}
const instance = extensionsState.get(id); const injectable = di.inject(extensionInjectable, instance);
if (!instance) { injectable.deregister();
throw new Error( extensionsState.delete(extension.id);
`Tried to disable extension with ID "${id}", but it wasn't enabled`, };
); }
}
const injectable = getExtension(instance);
runInAction(() => {
injectable.deregister();
extensionsState.delete(id);
});
};

View File

@ -27,7 +27,7 @@ export interface FakeExtensionOptions {
mainOptions?: Partial<LensMainExtension>; mainOptions?: Partial<LensMainExtension>;
} }
export const getExtensionFakeForMain = (di: DiContainer, id: string, name: string, options: Partial<LensMainExtension>) => { export const getMainExtensionFakeWith = (di: DiContainer) => ({ id, name, mainOptions = {}}: FakeExtensionOptions) => {
const instance = new TestExtensionMain({ const instance = new TestExtensionMain({
id, id,
absolutePath: "irrelevant", absolutePath: "irrelevant",
@ -44,7 +44,7 @@ export const getExtensionFakeForMain = (di: DiContainer, id: string, name: strin
manifestPath: "irrelevant", manifestPath: "irrelevant",
}); });
Object.assign(instance, options); Object.assign(instance, mainOptions);
(instance as Writable<LensMainExtension>)[lensExtensionDependencies] = { (instance as Writable<LensMainExtension>)[lensExtensionDependencies] = {
fileSystemProvisionerStore: di.inject(fileSystemProvisionerStoreInjectable), fileSystemProvisionerStore: di.inject(fileSystemProvisionerStoreInjectable),
@ -56,7 +56,7 @@ export const getExtensionFakeForMain = (di: DiContainer, id: string, name: strin
return instance; return instance;
}; };
export const getExtensionFakeForRenderer = (di: DiContainer, id: string, name: string, options: Partial<LensRendererExtension>) => { export const getRendererExtensionFakeWith = (di: DiContainer) => ({ id, name, rendererOptions = {}}: FakeExtensionOptions) => {
const instance = new TestExtensionRenderer({ const instance = new TestExtensionRenderer({
id, id,
absolutePath: "irrelevant", absolutePath: "irrelevant",
@ -73,7 +73,7 @@ export const getExtensionFakeForRenderer = (di: DiContainer, id: string, name: s
manifestPath: "irrelevant", manifestPath: "irrelevant",
}); });
Object.assign(instance, options); Object.assign(instance, rendererOptions);
(instance as Writable<LensRendererExtension>)[lensExtensionDependencies] = { (instance as Writable<LensRendererExtension>)[lensExtensionDependencies] = {
categoryRegistry: di.inject(catalogCategoryRegistryInjectable), categoryRegistry: di.inject(catalogCategoryRegistryInjectable),

View File

@ -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 = <T>(data: Partial<T>): T => data as T;

View File

@ -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> =
T extends (...args: any[]) => Promise<any>
? AsyncFnMock<T>
: T extends (...args: any[]) => any
? jest.MockedFunction<T>
: T;
export type Mocked<T extends object> = {
-readonly [P in keyof T]: GetMockedType<T[P]>;
};

View File

@ -96,7 +96,7 @@
}, },
"build": { "build": {
"npmRebuild": false, "npmRebuild": false,
"electronVersion": "19.1.9", "electronVersion": "22.3.3",
"generateUpdatesFilesForAllChannels": true, "generateUpdatesFilesForAllChannels": true,
"files": [ "files": [
"static/**/*", "static/**/*",
@ -251,7 +251,7 @@
"copy-webpack-plugin": "^11.0.0", "copy-webpack-plugin": "^11.0.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"css-loader": "^6.7.2", "css-loader": "^6.7.2",
"electron": "^19.1.9", "electron": "^22.3.3",
"electron-builder": "^23.6.0", "electron-builder": "^23.6.0",
"electron-notarize": "^0.3.0", "electron-notarize": "^0.3.0",
"esbuild-loader": "^2.20.0", "esbuild-loader": "^2.20.0",

View File

@ -35,7 +35,7 @@
"@k8slens/feature-core": "^6.5.0-alpha.0", "@k8slens/feature-core": "^6.5.0-alpha.0",
"@ogre-tools/injectable": "^15.1.2", "@ogre-tools/injectable": "^15.1.2",
"@ogre-tools/injectable-extension-for-auto-registration": "^15.1.2", "@ogre-tools/injectable-extension-for-auto-registration": "^15.1.2",
"electron": "^19.1.9" "electron": "^22.3.3"
}, },
"devDependencies": { "devDependencies": {
"@async-fn/jest": "^1.6.4", "@async-fn/jest": "^1.6.4",

View File

@ -9,7 +9,7 @@ export interface MessageChannel<Message> {
export type ExtraData = { processId: number; frameId: number }; export type ExtraData = { processId: number; frameId: number };
export type MessageChannelHandler<Channel> = Channel extends MessageChannel<infer Message> export type MessageChannelHandler<Channel> = Channel extends MessageChannel<infer Message>
? (message: Message, data: ExtraData) => void ? (message: Message, data?: ExtraData) => void
: never; : never;
export interface MessageChannelListener<Channel> { export interface MessageChannelListener<Channel> {

View File

@ -38,7 +38,7 @@
"@k8slens/messaging": "^1.0.0-alpha.1", "@k8slens/messaging": "^1.0.0-alpha.1",
"@ogre-tools/injectable": "^15.1.2", "@ogre-tools/injectable": "^15.1.2",
"@ogre-tools/injectable-extension-for-auto-registration": "^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" "lodash": "^4.17.21"
}, },
"devDependencies": { "devDependencies": {

View File

@ -39,7 +39,7 @@
"@k8slens/startable-stoppable": "^1.0.0-alpha.1", "@k8slens/startable-stoppable": "^1.0.0-alpha.1",
"@ogre-tools/injectable": "^15.1.2", "@ogre-tools/injectable": "^15.1.2",
"@ogre-tools/injectable-extension-for-auto-registration": "^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" "lodash": "^4.17.21"
}, },
"devDependencies": { "devDependencies": {

View File

@ -10,7 +10,7 @@ const enlistMessageChannelListenerInjectable = getInjectable({
const ipcRenderer = di.inject(ipcRendererInjectable); const ipcRenderer = di.inject(ipcRendererInjectable);
return ({ channel, handler }) => { return ({ channel, handler }) => {
const nativeCallback = (_: IpcRendererEvent, message: unknown) => { const nativeCallback = (event: IpcRendererEvent, message: unknown) => {
handler(message); handler(message);
}; };

View File

@ -2,6 +2,9 @@
* Copyright (c) OpenLens Authors. All rights reserved. * Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information. * 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);
};