From ad9bafe2a5aea5cf9944492d46d2cecf87b310bf Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Fri, 10 Mar 2023 02:37:39 -0500 Subject: [PATCH] Cleanup 'Cluster' to remove environment specific details (#6951) - requestNamespaceListPermissions is infallable so no need to have the extra try/catch - Refactor isMetricHidden method away from Cluster - Refactor shouldShowResource out of Cluster - Refactor isInLocalKubeconfig out of Cluster - Remove depecrated and unused workspace from Cluster - Refactor out kubectl as a dependency of Cluster - Remove from cluster getter used only once - Split out ClusterConnection from Cluster - Also split out KubeAuthProxyServer from ContextHandler - Rename ContextHandler to PrometheusHandler - Cleanup onNetworkOffline/Online impls within ClusterManager - Remove annotations from ClusterConnection - Remove mobx annotations from Cluster - Rename loadConfigFromFileInjectable - Remove all uses of dead createClusterInjectionToken - Fix type errors related to broadcastConnectionUpdate Signed-off-by: Sebastian Malton --- package-lock.json | 286 ++++++- .../common/__tests__/cluster-store.test.ts | 15 +- .../catalog-entities/kubernetes-cluster.ts | 19 +- .../core/src/common/catalog/catalog-entity.ts | 2 +- .../cluster-store/cluster-store.injectable.ts | 2 - .../src/common/cluster-store/cluster-store.ts | 6 +- packages/core/src/common/cluster-types.ts | 20 +- .../authorization-review.injectable.ts | 55 +- packages/core/src/common/cluster/cluster.ts | 698 +++--------------- .../cluster/create-cluster-injection-token.ts | 13 - .../cluster/list-namespaces.injectable.ts | 26 +- .../cluster/load-kubeconfig.injectable.ts | 36 + .../k8s-api/__tests__/api-manager.test.ts | 6 +- .../kube-api-version-detection.test.ts | 6 +- .../common/k8s-api/__tests__/kube-api.test.ts | 9 +- .../load-config-from-file.injectable.ts | 10 +- .../core/src/common/utils/backoff-caller.ts | 2 +- .../common/utils/replace-observable-object.ts | 18 + .../catalog/opening-entity-details.test.tsx | 7 +- .../delete-cluster-dialog.test.tsx | 19 +- .../delete-channel-listener.injectable.ts | 5 +- .../edit-namespace-from-new-tab.test.tsx | 8 +- ...espace-from-previously-opened-tab.test.tsx | 8 +- .../cluster/workload-overview.test.tsx | 18 +- ...owing-settings-for-correct-entity.test.tsx | 7 +- .../core/src/main/__test__/cluster.test.ts | 76 +- .../src/main/__test__/context-handler.test.ts | 46 +- .../src/main/__test__/kube-auth-proxy.test.ts | 64 +- .../main/__test__/kubeconfig-manager.test.ts | 45 +- .../main/__test__/prometheus-handler.test.ts | 213 ++++++ .../__test__/kubeconfig-sync.test.ts | 40 +- .../compute-diff.injectable.ts | 13 +- ...luster-distribution-detector.injectable.ts | 14 +- .../cluster-id-detector.injectable.ts | 2 +- .../detect-cluster-metadata.test.ts | 7 +- .../core/src/main/cluster-detectors/token.ts | 2 +- .../main/cluster/auth-proxy-url.injectable.ts | 21 + .../broadcast-connection-update.injectable.ts | 29 + .../cluster/cluster-connection.injectable.ts | 423 +++++++++++ .../kube-auth-proxy-server.injectable.ts | 114 +++ .../load-proxy-kubeconfig.injectable.ts | 31 + .../src/main/cluster/manager.injectable.ts | 2 + packages/core/src/main/cluster/manager.ts | 75 +- .../prometheus-handler.injectable.ts | 30 + .../prometheus-handler/prometheus-handler.ts | 129 ++++ .../remove-proxy-kubeconfig.injectable.ts | 23 + .../request-api-resources.injectable.ts | 12 +- .../5.0.0-beta.10.injectable.ts | 6 +- .../5.0.0-beta.13.injectable.ts | 69 +- .../update-entity-metadata.injectable.ts | 7 +- .../cluster/update-entity-metadata.test.ts | 19 +- .../main/cluster/update-entity-spec.test.ts | 6 +- .../main/context-handler/context-handler.ts | 219 ------ .../create-context-handler.injectable.ts | 38 - .../create-cluster.injectable.ts | 49 -- .../setup-ipc-main-handlers.injectable.ts | 2 + .../setup-ipc-main-handlers.ts | 27 +- .../delete-helm-release.injectable.ts | 6 +- .../get-helm-release-history.injectable.ts | 6 +- .../get-helm-release-values.injectable.ts | 6 +- .../get-helm-release.injectable.ts | 8 +- .../install-helm-chart.injectable.ts | 6 +- .../list-helm-releases.injectable.ts | 6 +- .../rollback-helm-release.injectable.ts | 6 +- .../update-helm-release.injectable.ts | 6 +- .../migrations/5.0.0-beta.10.injectable.ts | 13 +- .../create-kube-auth-proxy.injectable.ts | 12 +- .../main/kube-auth-proxy/kube-auth-proxy.ts | 72 +- ...le.ts => kubeconfig-manager.injectable.ts} | 37 +- .../kubeconfig-manager/kubeconfig-manager.ts | 43 +- .../kubectl/apply-all-handler.injectable.ts | 7 +- .../main/kubectl/create-kubectl.injectable.ts | 6 +- .../kubectl/delete-all-handler.injectable.ts | 7 +- .../main/lens-proxy/lens-proxy.injectable.ts | 6 +- .../core/src/main/lens-proxy/lens-proxy.ts | 20 +- .../main/lens-proxy/proxy-functions/index.ts | 1 - .../kube-api-upgrade-request.injectable.ts | 78 ++ .../kube-api-upgrade-request.ts | 66 -- .../create-resource-applier.injectable.ts | 31 +- .../main/resource-applier/resource-applier.ts | 32 +- .../get-service-account-route.injectable.ts | 17 +- .../metrics/add-metrics-route.injectable.ts | 9 +- .../start-port-forward-route.injectable.ts | 6 +- .../create-resource-route.injectable.ts | 22 +- .../patch-resource-route.injectable.ts | 22 +- .../local-shell-session.ts | 6 +- .../local-shell-session/open.injectable.ts | 17 +- .../local-shell-session/techincal.test.ts | 25 +- .../node-shell-session/node-shell-session.ts | 23 +- .../node-shell-session/open.injectable.ts | 18 +- .../src/main/shell-session/shell-session.ts | 14 +- .../get-active-cluster-entity.injectable.ts | 14 +- .../entity/metrics-enabled.injectable.ts | 14 +- ...es-cluster-context-menu-open.injectable.ts | 8 +- .../for-namespaced-resources.injectable.ts | 2 +- .../should-show-resource.injectable.ts | 2 +- .../cluster/create-cluster.injectable.ts | 46 -- .../components/+cluster/cluster-overview.tsx | 46 +- .../__tests__/secret-details.test.tsx | 6 +- .../namespace-select-filter.test.tsx | 6 +- .../+namespaces/namespace-store.test.ts | 6 +- .../__tests__/dialog.test.tsx | 6 +- .../+role-bindings/__tests__/dialog.test.tsx | 6 +- .../daemonset-details.tsx | 4 - .../+workloads-pods/pod-details-container.tsx | 12 +- .../__tests__/cronjob.store.test.ts | 6 +- .../__tests__/daemonset.store.test.ts | 6 +- .../__tests__/deployments.store.test.ts | 6 +- .../components/__tests__/job.store.test.ts | 6 +- .../components/__tests__/pods.store.test.ts | 6 +- .../__tests__/replicaset.store.test.ts | 6 +- .../__tests__/statefulset.store.test.ts | 6 +- .../cluster-manager/cluster-frame-handler.ts | 10 +- .../cluster-manager/cluster-status.tsx | 2 +- .../cluster-manager/cluster-view.tsx | 6 +- .../cluster-local-terminal-settings.test.tsx | 98 ++- .../cluster-settings/kubeconfig.tsx | 6 +- ...l-terminal-setting-presenter.injectable.ts | 48 ++ .../local-terminal-settings.tsx | 119 ++- .../cluster-settings/node-shell-setting.tsx | 9 +- .../is-current-context.tsx | 2 +- .../is-in-local-kubeconfig.injectable.ts | 20 + .../components/delete-cluster-dialog/view.tsx | 22 +- .../kube-object-list-layout.test.tsx | 6 +- .../cluster-name.injectable.ts | 19 + .../dependencies/cluster-name.injectable.ts | 21 - .../dependencies/cluster.injectable.ts | 18 - .../kube-object-menu.test.tsx | 17 +- .../kube-object-menu/kube-object-menu.tsx | 6 +- .../test-utils/get-application-builder.tsx | 26 +- .../cluster-frame/cluster-frame.test.tsx | 12 +- .../init-cluster-frame/init-cluster-frame.ts | 2 +- ...amespaces-forbidden-handler.injectable.tsx | 2 +- 133 files changed, 2544 insertions(+), 1961 deletions(-) delete mode 100644 packages/core/src/common/cluster/create-cluster-injection-token.ts create mode 100644 packages/core/src/common/cluster/load-kubeconfig.injectable.ts create mode 100644 packages/core/src/common/utils/replace-observable-object.ts create mode 100644 packages/core/src/main/__test__/prometheus-handler.test.ts create mode 100644 packages/core/src/main/cluster/auth-proxy-url.injectable.ts create mode 100644 packages/core/src/main/cluster/broadcast-connection-update.injectable.ts create mode 100644 packages/core/src/main/cluster/cluster-connection.injectable.ts create mode 100644 packages/core/src/main/cluster/kube-auth-proxy-server.injectable.ts create mode 100644 packages/core/src/main/cluster/load-proxy-kubeconfig.injectable.ts create mode 100644 packages/core/src/main/cluster/prometheus-handler/prometheus-handler.injectable.ts create mode 100644 packages/core/src/main/cluster/prometheus-handler/prometheus-handler.ts create mode 100644 packages/core/src/main/cluster/remove-proxy-kubeconfig.injectable.ts delete mode 100644 packages/core/src/main/context-handler/context-handler.ts delete mode 100644 packages/core/src/main/context-handler/create-context-handler.injectable.ts delete mode 100644 packages/core/src/main/create-cluster/create-cluster.injectable.ts rename packages/core/src/main/kubeconfig-manager/{create-kubeconfig-manager.injectable.ts => kubeconfig-manager.injectable.ts} (61%) create mode 100644 packages/core/src/main/lens-proxy/proxy-functions/kube-api-upgrade-request.injectable.ts delete mode 100644 packages/core/src/main/lens-proxy/proxy-functions/kube-api-upgrade-request.ts delete mode 100644 packages/core/src/renderer/cluster/create-cluster.injectable.ts create mode 100644 packages/core/src/renderer/components/cluster-settings/local-terminal-setting-presenter.injectable.ts create mode 100644 packages/core/src/renderer/components/delete-cluster-dialog/is-in-local-kubeconfig.injectable.ts create mode 100644 packages/core/src/renderer/components/kube-object-menu/cluster-name.injectable.ts delete mode 100644 packages/core/src/renderer/components/kube-object-menu/dependencies/cluster-name.injectable.ts delete mode 100644 packages/core/src/renderer/components/kube-object-menu/dependencies/cluster.injectable.ts diff --git a/package-lock.json b/package-lock.json index 8eb60c76ad..bcc47f1439 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1698,7 +1698,8 @@ "node_modules/@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", - "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==" + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "dev": true }, "node_modules/@hapi/b64": { "version": "5.0.0", @@ -1929,7 +1930,8 @@ "node_modules/@isaacs/string-locale-compare": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@isaacs/string-locale-compare/-/string-locale-compare-1.1.0.tgz", - "integrity": "sha512-SQ7Kzhh9+D+ZW9MA0zkYv3VXhIDNx+LzM6EJ+/65I3QY+enU6Itte7E5XX7EWrqLW2FN4n06GWzBnPoC3th2aQ==" + "integrity": "sha512-SQ7Kzhh9+D+ZW9MA0zkYv3VXhIDNx+LzM6EJ+/65I3QY+enU6Itte7E5XX7EWrqLW2FN4n06GWzBnPoC3th2aQ==", + "dev": true }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", @@ -3722,6 +3724,7 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/@npmcli/arborist/-/arborist-5.3.0.tgz", "integrity": "sha512-+rZ9zgL1lnbl8Xbb1NQdMjveOMwj4lIYfcDtyJHHi5x4X8jtR6m8SXooJMZy5vmFVZ8w7A2Bnd/oX9eTuU8w5A==", + "dev": true, "dependencies": { "@isaacs/string-locale-compare": "^1.1.0", "@npmcli/installed-package-contents": "^1.0.7", @@ -3769,6 +3772,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-5.2.1.tgz", "integrity": "sha512-xIcQYMnhcx2Nr4JTjsFmwwnr9vldugPy9uVm0o87bjqqWMv9GaqsTeT+i99wTl0mk1uLxJtHxLb8kymqTENQsw==", + "dev": true, "dependencies": { "lru-cache": "^7.5.1" }, @@ -3780,6 +3784,7 @@ "version": "7.16.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.16.1.tgz", "integrity": "sha512-9kkuMZHnLH/8qXARvYSjNvq8S1GYFFzynQTAfKeaJ0sIrR3PUPuu37Z+EiIANiZBvpfTf2B5y8ecDLSMWlLv+w==", + "dev": true, "engines": { "node": ">=12" } @@ -3788,6 +3793,7 @@ "version": "9.1.2", "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-9.1.2.tgz", "integrity": "sha512-pzd9rLEx4TfNJkovvlBSLGhq31gGu2QDexFPWT19yCDh0JgnRhlBLNo5759N0AJmBk+kQ9Y/hXoLnlgFD+ukmg==", + "dev": true, "dependencies": { "hosted-git-info": "^5.0.0", "proc-log": "^2.0.1", @@ -3802,6 +3808,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, "dependencies": { "glob": "^7.1.3" }, @@ -3816,6 +3823,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", + "dev": true, "dependencies": { "@gar/promisify": "^1.1.3", "semver": "^7.3.5" @@ -3828,6 +3836,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-3.0.2.tgz", "integrity": "sha512-CAcd08y3DWBJqJDpfuVL0uijlq5oaXaOJEKHKc4wqrjd00gkvTZB+nFuLn+doOOKddaQS9JfqtNoFCO2LCvA3w==", + "dev": true, "dependencies": { "@npmcli/promise-spawn": "^3.0.0", "lru-cache": "^7.4.4", @@ -3847,6 +3856,7 @@ "version": "7.16.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.16.1.tgz", "integrity": "sha512-9kkuMZHnLH/8qXARvYSjNvq8S1GYFFzynQTAfKeaJ0sIrR3PUPuu37Z+EiIANiZBvpfTf2B5y8ecDLSMWlLv+w==", + "dev": true, "engines": { "node": ">=12" } @@ -3855,6 +3865,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-1.0.7.tgz", "integrity": "sha512-9rufe0wnJusCQoLpV9ZPKIVP55itrM5BxOXs10DmdbRfgWtHy1LDyskbwRnBghuB0PrF7pNPOqREVtpz4HqzKw==", + "dev": true, "dependencies": { "npm-bundled": "^1.1.1", "npm-normalize-package-bin": "^1.0.1" @@ -3870,6 +3881,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/@npmcli/map-workspaces/-/map-workspaces-2.0.4.tgz", "integrity": "sha512-bMo0aAfwhVwqoVM5UzX1DJnlvVvzDCHae821jv48L1EsrYwfOZChlqWYXEtto/+BkBXetPbEWgau++/brh4oVg==", + "dev": true, "dependencies": { "@npmcli/name-from-folder": "^1.0.1", "glob": "^8.0.1", @@ -3884,6 +3896,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, "dependencies": { "balanced-match": "^1.0.0" } @@ -3892,6 +3905,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -3910,6 +3924,7 @@ "version": "5.1.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, "dependencies": { "brace-expansion": "^2.0.1" }, @@ -3921,6 +3936,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/@npmcli/metavuln-calculator/-/metavuln-calculator-3.1.1.tgz", "integrity": "sha512-n69ygIaqAedecLeVH3KnO39M6ZHiJ2dEv5A7DGvcqCB8q17BGUgW8QaanIkbWUo2aYGZqJaOORTLAlIvKjNDKA==", + "dev": true, "dependencies": { "cacache": "^16.0.0", "json-parse-even-better-errors": "^2.3.1", @@ -3936,6 +3952,7 @@ "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", "deprecated": "This functionality has been moved to @npmcli/fs", + "dev": true, "dependencies": { "mkdirp": "^1.0.4", "rimraf": "^3.0.2" @@ -3948,6 +3965,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, "dependencies": { "glob": "^7.1.3" }, @@ -3961,12 +3979,14 @@ "node_modules/@npmcli/name-from-folder": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@npmcli/name-from-folder/-/name-from-folder-1.0.1.tgz", - "integrity": "sha512-qq3oEfcLFwNfEYOQ8HLimRGKlD8WSeGEdtUa7hmzpR8Sa7haL1KVQrvgO6wqMjhWFFVjgtrh1gIxDz+P8sjUaA==" + "integrity": "sha512-qq3oEfcLFwNfEYOQ8HLimRGKlD8WSeGEdtUa7hmzpR8Sa7haL1KVQrvgO6wqMjhWFFVjgtrh1gIxDz+P8sjUaA==", + "dev": true }, "node_modules/@npmcli/node-gyp": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-2.0.0.tgz", "integrity": "sha512-doNI35wIe3bBaEgrlPfdJPaCpUR89pJWep4Hq3aRdh6gKazIVWfs0jHttvSSoq47ZXgC7h73kDsUl8AoIQUB+A==", + "dev": true, "engines": { "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } @@ -3975,6 +3995,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-2.0.0.tgz", "integrity": "sha512-42jnZ6yl16GzjWSH7vtrmWyJDGVa/LXPdpN2rcUWolFjc9ON2N3uz0qdBbQACfmhuJZ2lbKYtmK5qx68ZPLHMA==", + "dev": true, "dependencies": { "json-parse-even-better-errors": "^2.3.1" }, @@ -3986,6 +4007,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-3.0.0.tgz", "integrity": "sha512-s9SgS+p3a9Eohe68cSI3fi+hpcZUmXq5P7w0kMlAsWVtR7XbK3ptkZqKT2cK1zLDObJ3sR+8P59sJE0w/KTL1g==", + "dev": true, "dependencies": { "infer-owner": "^1.0.4" }, @@ -3997,6 +4019,7 @@ "version": "4.1.7", "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-4.1.7.tgz", "integrity": "sha512-WXr/MyM4tpKA4BotB81NccGAv8B48lNH0gRoILucbcAhTQXLCoi6HflMV3KdXubIqvP9SuLsFn68Z7r4jl+ppw==", + "dev": true, "dependencies": { "@npmcli/node-gyp": "^2.0.0", "@npmcli/promise-spawn": "^3.0.0", @@ -4012,6 +4035,7 @@ "version": "7.16.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.16.1.tgz", "integrity": "sha512-9kkuMZHnLH/8qXARvYSjNvq8S1GYFFzynQTAfKeaJ0sIrR3PUPuu37Z+EiIANiZBvpfTf2B5y8ecDLSMWlLv+w==", + "dev": true, "engines": { "node": ">=12" } @@ -4020,6 +4044,7 @@ "version": "10.2.1", "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", + "dev": true, "dependencies": { "agentkeepalive": "^4.2.1", "cacache": "^16.1.0", @@ -4046,6 +4071,7 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -4057,6 +4083,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz", "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", + "dev": true, "dependencies": { "minipass": "^3.1.6", "minipass-sized": "^1.0.3", @@ -4073,6 +4100,7 @@ "version": "9.3.1", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.3.1.tgz", "integrity": "sha512-4Q16ZCqq3g8awk6UplT7AuxQ35XN4R/yf/+wSAwcBUAjg7l58RTactWaP8fIDTi0FzI7YcVLujwExakZlfWkXg==", + "dev": true, "dependencies": { "env-paths": "^2.2.0", "glob": "^7.1.4", @@ -4096,6 +4124,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", + "dev": true, "dependencies": { "abbrev": "^1.0.0" }, @@ -4110,6 +4139,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, "dependencies": { "glob": "^7.1.3" }, @@ -4124,6 +4154,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", + "dev": true, "dependencies": { "agent-base": "^6.0.2", "debug": "^4.3.3", @@ -4136,7 +4167,8 @@ "node_modules/@npmcli/run-script/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true }, "node_modules/@nrwl/cli": { "version": "15.7.2", @@ -6910,7 +6942,8 @@ "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true }, "node_modules/abort-controller": { "version": "3.0.0", @@ -7064,6 +7097,7 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.2.1.tgz", "integrity": "sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA==", + "dev": true, "dependencies": { "debug": "^4.1.0", "depd": "^1.1.2", @@ -7334,7 +7368,8 @@ "node_modules/aproba": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", - "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "dev": true }, "node_modules/arch": { "version": "2.2.0", @@ -7360,6 +7395,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "dev": true, "dependencies": { "delegates": "^1.0.0", "readable-stream": "^3.6.0" @@ -7508,7 +7544,8 @@ "node_modules/asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true }, "node_modules/asar": { "version": "3.2.0", @@ -7997,6 +8034,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/bin-links/-/bin-links-3.0.3.tgz", "integrity": "sha512-zKdnMPWEdh4F5INR07/eBrodC7QrF5JKvqskjz/ZZRXg5YSAZIbn8zGhbhUrElzHBZ2fvEQdOU59RHcTG3GiwA==", + "dev": true, "dependencies": { "cmd-shim": "^5.0.0", "mkdirp-infer-owner": "^2.0.0", @@ -8013,6 +8051,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-2.0.0.tgz", "integrity": "sha512-awzfKUO7v0FscrSpRoogyNm0sajikhBWpU0QMrW09AMi9n1PoKU6WaIqUzuJSQnpciZZmJ/jMZ2Egfmb/9LiWQ==", + "dev": true, "engines": { "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } @@ -8021,6 +8060,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, "dependencies": { "glob": "^7.1.3" }, @@ -8467,6 +8507,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz", "integrity": "sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==", + "dev": true, "dependencies": { "semver": "^7.0.0" } @@ -8501,6 +8542,7 @@ "version": "16.1.3", "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==", + "dev": true, "dependencies": { "@npmcli/fs": "^2.1.0", "@npmcli/move-file": "^2.0.0", @@ -8529,6 +8571,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, "dependencies": { "balanced-match": "^1.0.0" } @@ -8537,6 +8580,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -8555,6 +8599,7 @@ "version": "7.16.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.16.1.tgz", "integrity": "sha512-9kkuMZHnLH/8qXARvYSjNvq8S1GYFFzynQTAfKeaJ0sIrR3PUPuu37Z+EiIANiZBvpfTf2B5y8ecDLSMWlLv+w==", + "dev": true, "engines": { "node": ">=12" } @@ -8563,6 +8608,7 @@ "version": "5.1.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, "dependencies": { "brace-expansion": "^2.0.1" }, @@ -8574,6 +8620,7 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -8585,6 +8632,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, "dependencies": { "glob": "^7.1.3" }, @@ -8599,6 +8647,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -8608,6 +8657,7 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -8627,6 +8677,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -8637,7 +8688,8 @@ "node_modules/cacache/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true }, "node_modules/cacheable-lookup": { "version": "5.0.4", @@ -9162,6 +9214,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/cmd-shim/-/cmd-shim-5.0.0.tgz", "integrity": "sha512-qkCtZ59BidfEwHltnJwkyVZn+XQojdAySM1D1gSeh11Z4pW1Kpolkyo53L5noc0nrxmIvyFwTmJRo4xs7FFLPw==", + "dev": true, "dependencies": { "mkdirp-infer-owner": "^2.0.0" }, @@ -9235,6 +9288,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "dev": true, "bin": { "color-support": "bin.js" } @@ -9279,6 +9333,7 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/columnify/-/columnify-1.6.0.tgz", "integrity": "sha512-lomjuFZKfM6MSAnV9aCZC9sc0qGbmZdfygNv+nCpqVkSKdCxCklLtd16O0EILGkImHw9ZpHkAnHaB+8Zxq5W6Q==", + "dev": true, "dependencies": { "strip-ansi": "^6.0.1", "wcwidth": "^1.0.0" @@ -9324,7 +9379,8 @@ "node_modules/common-ancestor-path": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/common-ancestor-path/-/common-ancestor-path-1.0.1.tgz", - "integrity": "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==" + "integrity": "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==", + "dev": true }, "node_modules/common-path-prefix": { "version": "3.0.0", @@ -9547,7 +9603,8 @@ "node_modules/console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "dev": true }, "node_modules/content-disposition": { "version": "0.5.4", @@ -10259,6 +10316,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz", "integrity": "sha512-syBZ+rnAK3EgMsH2aYEOLUW7mZSY9Gb+0wUMCFsZvcmiz+HigA0LOcq/HoQqVuGG+EKykunc7QG2bzrponfaSw==", + "dev": true, "engines": { "node": "*" } @@ -10567,12 +10625,14 @@ "node_modules/delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "dev": true }, "node_modules/depd": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, "engines": { "node": ">= 0.6" } @@ -10645,6 +10705,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, "dependencies": { "asap": "^2.0.0", "wrappy": "1" @@ -11390,6 +11451,7 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, "optional": true, "dependencies": { "iconv-lite": "^0.6.2" @@ -11466,7 +11528,8 @@ "node_modules/err-code": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", - "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==" + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true }, "node_modules/errno": { "version": "0.1.8", @@ -13879,6 +13942,7 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "dev": true, "dependencies": { "aproba": "^1.0.3 || ^2.0.0", "color-support": "^1.1.3", @@ -14589,7 +14653,8 @@ "node_modules/has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "dev": true }, "node_modules/he": { "version": "1.2.0", @@ -14630,6 +14695,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, "dependencies": { "lru-cache": "^6.0.0" }, @@ -14641,6 +14707,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -14651,7 +14718,8 @@ "node_modules/hosted-git-info/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true }, "node_modules/hpack.js": { "version": "2.1.6", @@ -14960,6 +15028,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dev": true, "dependencies": { "ms": "^2.0.0" } @@ -15062,6 +15131,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-5.0.1.tgz", "integrity": "sha512-yemi4pMf51WKT7khInJqAvsIGzoqYXblnsz0ql8tM+yi1EKYTY1evX4NAbJrLL/Aanr2HyZeluqU+Oi7MGHokw==", + "dev": true, "dependencies": { "minimatch": "^5.0.1" }, @@ -15073,6 +15143,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, "dependencies": { "balanced-match": "^1.0.0" } @@ -15081,6 +15152,7 @@ "version": "5.1.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, "dependencies": { "brace-expansion": "^2.0.1" }, @@ -15185,7 +15257,8 @@ "node_modules/infer-owner": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", - "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==" + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "dev": true }, "node_modules/inflight": { "version": "1.0.6", @@ -15210,6 +15283,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/init-package-json/-/init-package-json-3.0.2.tgz", "integrity": "sha512-YhlQPEjNFqlGdzrBfDNRLhvoSgX7iQRgSxgsNknRQ9ITXFT7UMfVMWhBTOh2Y+25lRnGrv5Xz8yZwQ3ACR6T3A==", + "dev": true, "dependencies": { "npm-package-arg": "^9.0.1", "promzard": "^0.3.0", @@ -15227,6 +15301,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-5.2.1.tgz", "integrity": "sha512-xIcQYMnhcx2Nr4JTjsFmwwnr9vldugPy9uVm0o87bjqqWMv9GaqsTeT+i99wTl0mk1uLxJtHxLb8kymqTENQsw==", + "dev": true, "dependencies": { "lru-cache": "^7.5.1" }, @@ -15238,6 +15313,7 @@ "version": "7.16.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.16.1.tgz", "integrity": "sha512-9kkuMZHnLH/8qXARvYSjNvq8S1GYFFzynQTAfKeaJ0sIrR3PUPuu37Z+EiIANiZBvpfTf2B5y8ecDLSMWlLv+w==", + "dev": true, "engines": { "node": ">=12" } @@ -15246,6 +15322,7 @@ "version": "9.1.2", "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-9.1.2.tgz", "integrity": "sha512-pzd9rLEx4TfNJkovvlBSLGhq31gGu2QDexFPWT19yCDh0JgnRhlBLNo5759N0AJmBk+kQ9Y/hXoLnlgFD+ukmg==", + "dev": true, "dependencies": { "hosted-git-info": "^5.0.0", "proc-log": "^2.0.1", @@ -15322,7 +15399,8 @@ "node_modules/ip": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", - "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==" + "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==", + "dev": true }, "node_modules/ip-regex": { "version": "4.3.0", @@ -15559,7 +15637,8 @@ "node_modules/is-lambda": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", - "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==" + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "dev": true }, "node_modules/is-map": { "version": "2.0.2", @@ -19032,6 +19111,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/json-stringify-nice/-/json-stringify-nice-1.1.4.tgz", "integrity": "sha512-5Z5RFW63yxReJ7vANgW6eZFGWaQvnPE3WNmZoOJrSkGju2etKA2L5rrOa1sm877TVTFt57A80BH1bArcmlLfPw==", + "dev": true, "funding": { "url": "https://github.com/sponsors/isaacs" } @@ -19072,6 +19152,7 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dev": true, "engines": [ "node >= 0.2.0" ] @@ -19281,12 +19362,14 @@ "node_modules/just-diff": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/just-diff/-/just-diff-5.2.0.tgz", - "integrity": "sha512-6ufhP9SHjb7jibNFrNxyFZ6od3g+An6Ai9mhGRvcYe8UJlH0prseN64M+6ZBBUoKYHZsitDP42gAJ8+eVWr3lw==" + "integrity": "sha512-6ufhP9SHjb7jibNFrNxyFZ6od3g+An6Ai9mhGRvcYe8UJlH0prseN64M+6ZBBUoKYHZsitDP42gAJ8+eVWr3lw==", + "dev": true }, "node_modules/just-diff-apply": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/just-diff-apply/-/just-diff-apply-5.5.0.tgz", - "integrity": "sha512-OYTthRfSh55WOItVqwpefPtNt2VdKsq5AnAK6apdtR6yCH8pr0CmSr710J0Mf+WdQy7K/OzMy7K2MgAfdQURDw==" + "integrity": "sha512-OYTthRfSh55WOItVqwpefPtNt2VdKsq5AnAK6apdtR6yCH8pr0CmSr710J0Mf+WdQy7K/OzMy7K2MgAfdQURDw==", + "dev": true }, "node_modules/keyv": { "version": "4.5.2", @@ -19871,6 +19954,7 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/libnpmaccess/-/libnpmaccess-6.0.3.tgz", "integrity": "sha512-4tkfUZprwvih2VUZYMozL7EMKgQ5q9VW2NtRyxWtQWlkLTAWHRklcAvBN49CVqEkhUw7vTX2fNgB5LzgUucgYg==", + "dev": true, "dependencies": { "aproba": "^2.0.0", "minipass": "^3.1.1", @@ -19885,6 +19969,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-5.2.1.tgz", "integrity": "sha512-xIcQYMnhcx2Nr4JTjsFmwwnr9vldugPy9uVm0o87bjqqWMv9GaqsTeT+i99wTl0mk1uLxJtHxLb8kymqTENQsw==", + "dev": true, "dependencies": { "lru-cache": "^7.5.1" }, @@ -19896,6 +19981,7 @@ "version": "7.16.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.16.1.tgz", "integrity": "sha512-9kkuMZHnLH/8qXARvYSjNvq8S1GYFFzynQTAfKeaJ0sIrR3PUPuu37Z+EiIANiZBvpfTf2B5y8ecDLSMWlLv+w==", + "dev": true, "engines": { "node": ">=12" } @@ -19904,6 +19990,7 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -19915,6 +20002,7 @@ "version": "9.1.2", "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-9.1.2.tgz", "integrity": "sha512-pzd9rLEx4TfNJkovvlBSLGhq31gGu2QDexFPWT19yCDh0JgnRhlBLNo5759N0AJmBk+kQ9Y/hXoLnlgFD+ukmg==", + "dev": true, "dependencies": { "hosted-git-info": "^5.0.0", "proc-log": "^2.0.1", @@ -19928,12 +20016,14 @@ "node_modules/libnpmaccess/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true }, "node_modules/libnpmpublish": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/libnpmpublish/-/libnpmpublish-6.0.4.tgz", "integrity": "sha512-lvAEYW8mB8QblL6Q/PI/wMzKNvIrF7Kpujf/4fGS/32a2i3jzUXi04TNyIBcK6dQJ34IgywfaKGh+Jq4HYPFmg==", + "dev": true, "dependencies": { "normalize-package-data": "^4.0.0", "npm-package-arg": "^9.0.1", @@ -19949,6 +20039,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-5.2.1.tgz", "integrity": "sha512-xIcQYMnhcx2Nr4JTjsFmwwnr9vldugPy9uVm0o87bjqqWMv9GaqsTeT+i99wTl0mk1uLxJtHxLb8kymqTENQsw==", + "dev": true, "dependencies": { "lru-cache": "^7.5.1" }, @@ -19960,6 +20051,7 @@ "version": "7.16.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.16.1.tgz", "integrity": "sha512-9kkuMZHnLH/8qXARvYSjNvq8S1GYFFzynQTAfKeaJ0sIrR3PUPuu37Z+EiIANiZBvpfTf2B5y8ecDLSMWlLv+w==", + "dev": true, "engines": { "node": ">=12" } @@ -19968,6 +20060,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-4.0.1.tgz", "integrity": "sha512-EBk5QKKuocMJhB3BILuKhmaPjI8vNRSpIfO9woLC6NyHVkKKdVEdAO1mrT0ZfxNR1lKwCcTkuZfmGIFdizZ8Pg==", + "dev": true, "dependencies": { "hosted-git-info": "^5.0.0", "is-core-module": "^2.8.1", @@ -19982,6 +20075,7 @@ "version": "9.1.2", "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-9.1.2.tgz", "integrity": "sha512-pzd9rLEx4TfNJkovvlBSLGhq31gGu2QDexFPWT19yCDh0JgnRhlBLNo5759N0AJmBk+kQ9Y/hXoLnlgFD+ukmg==", + "dev": true, "dependencies": { "hosted-git-info": "^5.0.0", "proc-log": "^2.0.1", @@ -20308,6 +20402,7 @@ "version": "9.1.0", "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "dev": true, "dependencies": { "agentkeepalive": "^4.1.3", "cacache": "^15.2.0", @@ -20334,6 +20429,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "dev": true, "dependencies": { "@gar/promisify": "^1.0.1", "semver": "^7.3.5" @@ -20344,6 +20440,7 @@ "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", "deprecated": "This functionality has been moved to @npmcli/fs", + "dev": true, "dependencies": { "mkdirp": "^1.0.4", "rimraf": "^3.0.2" @@ -20356,6 +20453,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "dev": true, "engines": { "node": ">= 6" } @@ -20364,6 +20462,7 @@ "version": "15.3.0", "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", + "dev": true, "dependencies": { "@npmcli/fs": "^1.0.0", "@npmcli/move-file": "^1.0.1", @@ -20392,6 +20491,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "dev": true, "dependencies": { "@tootallnate/once": "1", "agent-base": "6", @@ -20405,6 +20505,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -20416,6 +20517,7 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -20427,6 +20529,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, "dependencies": { "glob": "^7.1.3" }, @@ -20441,6 +20544,7 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "dev": true, "dependencies": { "minipass": "^3.1.1" }, @@ -20452,6 +20556,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "dev": true, "dependencies": { "unique-slug": "^2.0.0" } @@ -20460,6 +20565,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "dev": true, "dependencies": { "imurmurhash": "^0.1.4" } @@ -20467,7 +20573,8 @@ "node_modules/make-fetch-happen/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true }, "node_modules/make-plural": { "version": "6.2.2", @@ -21080,6 +21187,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "dev": true, "dependencies": { "minipass": "^3.0.0" }, @@ -21091,6 +21199,7 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -21101,12 +21210,14 @@ "node_modules/minipass-collect/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true }, "node_modules/minipass-fetch": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "dev": true, "dependencies": { "minipass": "^3.1.0", "minipass-sized": "^1.0.3", @@ -21123,6 +21234,7 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -21133,12 +21245,14 @@ "node_modules/minipass-fetch/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true }, "node_modules/minipass-flush": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, "dependencies": { "minipass": "^3.0.0" }, @@ -21150,6 +21264,7 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -21160,12 +21275,14 @@ "node_modules/minipass-flush/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true }, "node_modules/minipass-json-stream": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minipass-json-stream/-/minipass-json-stream-1.0.1.tgz", "integrity": "sha512-ODqY18UZt/I8k+b7rl2AENgbWE8IDYam+undIJONvigAz8KR5GWblsFTEfQs0WODsjbSXWlm+JHEv8Gr6Tfdbg==", + "dev": true, "dependencies": { "jsonparse": "^1.3.1", "minipass": "^3.0.0" @@ -21175,6 +21292,7 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -21185,12 +21303,14 @@ "node_modules/minipass-json-stream/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true }, "node_modules/minipass-pipeline": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, "dependencies": { "minipass": "^3.0.0" }, @@ -21202,6 +21322,7 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -21212,12 +21333,14 @@ "node_modules/minipass-pipeline/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true }, "node_modules/minipass-sized": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, "dependencies": { "minipass": "^3.0.0" }, @@ -21229,6 +21352,7 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -21239,7 +21363,8 @@ "node_modules/minipass-sized/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true }, "node_modules/minizlib": { "version": "2.1.2", @@ -21326,6 +21451,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/mkdirp-infer-owner/-/mkdirp-infer-owner-2.0.0.tgz", "integrity": "sha512-sdqtiFt3lkOaYvTXSRIUjkIdPTcxgv5+fgqYE/5qgwdw12cOrAuzzgzvVExIkH/ul1oeHN3bCLOWSG3XOqbKKw==", + "dev": true, "dependencies": { "chownr": "^2.0.0", "infer-owner": "^1.0.4", @@ -21594,6 +21720,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, "engines": { "node": ">= 0.6" } @@ -21692,6 +21819,7 @@ "version": "8.4.1", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "dev": true, "dependencies": { "env-paths": "^2.2.0", "glob": "^7.1.4", @@ -21726,6 +21854,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, "dependencies": { "glob": "^7.1.3" }, @@ -21845,6 +21974,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dev": true, "dependencies": { "abbrev": "1" }, @@ -22064,6 +22194,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.2.tgz", "integrity": "sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ==", + "dev": true, "dependencies": { "npm-normalize-package-bin": "^1.0.1" } @@ -22096,6 +22227,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-5.0.0.tgz", "integrity": "sha512-65lUsMI8ztHCxFz5ckCEC44DRvEGdZX5usQFriauxHEwt7upv1FKaQEmAtU0YnOAdwuNWCmk64xYiQABNrEyLA==", + "dev": true, "dependencies": { "semver": "^7.1.1" }, @@ -22106,12 +22238,14 @@ "node_modules/npm-normalize-package-bin": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", - "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==" + "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", + "dev": true }, "node_modules/npm-package-arg": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-8.1.1.tgz", "integrity": "sha512-CsP95FhWQDwNqiYS+Q0mZ7FAEDytDZAkNxQqea6IaAFJTAY9Lhhqyl0irU/6PMc7BGfUmnsbHcqxJD7XuVM/rg==", + "dev": true, "dependencies": { "hosted-git-info": "^3.0.6", "semver": "^7.0.0", @@ -22124,12 +22258,14 @@ "node_modules/npm-package-arg/node_modules/builtins": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/builtins/-/builtins-1.0.3.tgz", - "integrity": "sha512-uYBjakWipfaO/bXI7E8rq6kpwHRZK5cNYrUv2OzZSI/FvmdMyXJ2tG9dKcjEC5YHmHpUAwsargWIZNWdxb/bnQ==" + "integrity": "sha512-uYBjakWipfaO/bXI7E8rq6kpwHRZK5cNYrUv2OzZSI/FvmdMyXJ2tG9dKcjEC5YHmHpUAwsargWIZNWdxb/bnQ==", + "dev": true }, "node_modules/npm-package-arg/node_modules/hosted-git-info": { "version": "3.0.8", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-3.0.8.tgz", "integrity": "sha512-aXpmwoOhRBrw6X3j0h5RloK4x1OzsxMPyxqIHyNfSe2pypkVTZFpEiRoSipPEPlMrh0HW/XsjkJ5WgnCirpNUw==", + "dev": true, "dependencies": { "lru-cache": "^6.0.0" }, @@ -22141,6 +22277,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -22152,6 +22289,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz", "integrity": "sha512-M6w37eVCMMouJ9V/sdPGnC5H4uDr73/+xdq0FBLO3TFFX1+7wiUY6Es328NN+y43tmY+doUdN9g9J21vqB7iLw==", + "dev": true, "dependencies": { "builtins": "^1.0.3" } @@ -22159,12 +22297,14 @@ "node_modules/npm-package-arg/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true }, "node_modules/npm-packlist": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-5.1.1.tgz", "integrity": "sha512-UfpSvQ5YKwctmodvPPkK6Fwk603aoVsf8AEbmVKAEECrfvL8SSe1A2YIwrJ6xmTHAITKPwwZsWo7WwEbNk0kxw==", + "dev": true, "dependencies": { "glob": "^8.0.1", "ignore-walk": "^5.0.1", @@ -22182,6 +22322,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, "dependencies": { "balanced-match": "^1.0.0" } @@ -22190,6 +22331,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -22208,6 +22350,7 @@ "version": "5.1.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, "dependencies": { "brace-expansion": "^2.0.1" }, @@ -22219,6 +22362,7 @@ "version": "7.0.2", "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-7.0.2.tgz", "integrity": "sha512-gk37SyRmlIjvTfcYl6RzDbSmS9Y4TOBXfsPnoYqTHARNgWbyDiCSMLUpmALDj4jjcTZpURiEfsSHJj9k7EV4Rw==", + "dev": true, "dependencies": { "npm-install-checks": "^5.0.0", "npm-normalize-package-bin": "^2.0.0", @@ -22233,6 +22377,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-5.2.1.tgz", "integrity": "sha512-xIcQYMnhcx2Nr4JTjsFmwwnr9vldugPy9uVm0o87bjqqWMv9GaqsTeT+i99wTl0mk1uLxJtHxLb8kymqTENQsw==", + "dev": true, "dependencies": { "lru-cache": "^7.5.1" }, @@ -22244,6 +22389,7 @@ "version": "7.16.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.16.1.tgz", "integrity": "sha512-9kkuMZHnLH/8qXARvYSjNvq8S1GYFFzynQTAfKeaJ0sIrR3PUPuu37Z+EiIANiZBvpfTf2B5y8ecDLSMWlLv+w==", + "dev": true, "engines": { "node": ">=12" } @@ -22252,6 +22398,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-2.0.0.tgz", "integrity": "sha512-awzfKUO7v0FscrSpRoogyNm0sajikhBWpU0QMrW09AMi9n1PoKU6WaIqUzuJSQnpciZZmJ/jMZ2Egfmb/9LiWQ==", + "dev": true, "engines": { "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } @@ -22260,6 +22407,7 @@ "version": "9.1.2", "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-9.1.2.tgz", "integrity": "sha512-pzd9rLEx4TfNJkovvlBSLGhq31gGu2QDexFPWT19yCDh0JgnRhlBLNo5759N0AJmBk+kQ9Y/hXoLnlgFD+ukmg==", + "dev": true, "dependencies": { "hosted-git-info": "^5.0.0", "proc-log": "^2.0.1", @@ -22274,6 +22422,7 @@ "version": "13.3.0", "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-13.3.0.tgz", "integrity": "sha512-10LJQ/1+VhKrZjIuY9I/+gQTvumqqlgnsCufoXETHAPFTS3+M+Z5CFhZRDHGavmJ6rOye3UvNga88vl8n1r6gg==", + "dev": true, "dependencies": { "make-fetch-happen": "^10.0.6", "minipass": "^3.1.6", @@ -22291,6 +22440,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-5.2.1.tgz", "integrity": "sha512-xIcQYMnhcx2Nr4JTjsFmwwnr9vldugPy9uVm0o87bjqqWMv9GaqsTeT+i99wTl0mk1uLxJtHxLb8kymqTENQsw==", + "dev": true, "dependencies": { "lru-cache": "^7.5.1" }, @@ -22302,6 +22452,7 @@ "version": "7.16.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.16.1.tgz", "integrity": "sha512-9kkuMZHnLH/8qXARvYSjNvq8S1GYFFzynQTAfKeaJ0sIrR3PUPuu37Z+EiIANiZBvpfTf2B5y8ecDLSMWlLv+w==", + "dev": true, "engines": { "node": ">=12" } @@ -22310,6 +22461,7 @@ "version": "10.2.1", "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", + "dev": true, "dependencies": { "agentkeepalive": "^4.2.1", "cacache": "^16.1.0", @@ -22336,6 +22488,7 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -22347,6 +22500,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz", "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", + "dev": true, "dependencies": { "minipass": "^3.1.6", "minipass-sized": "^1.0.3", @@ -22363,6 +22517,7 @@ "version": "9.1.2", "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-9.1.2.tgz", "integrity": "sha512-pzd9rLEx4TfNJkovvlBSLGhq31gGu2QDexFPWT19yCDh0JgnRhlBLNo5759N0AJmBk+kQ9Y/hXoLnlgFD+ukmg==", + "dev": true, "dependencies": { "hosted-git-info": "^5.0.0", "proc-log": "^2.0.1", @@ -22377,6 +22532,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", + "dev": true, "dependencies": { "agent-base": "^6.0.2", "debug": "^4.3.3", @@ -22389,7 +22545,8 @@ "node_modules/npm-registry-fetch/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true }, "node_modules/npm-run-path": { "version": "2.0.2", @@ -24609,6 +24766,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "dev": true, "dependencies": { "are-we-there-yet": "^3.0.0", "console-control-strings": "^1.1.0", @@ -25417,6 +25575,7 @@ "version": "13.6.1", "resolved": "https://registry.npmjs.org/pacote/-/pacote-13.6.1.tgz", "integrity": "sha512-L+2BI1ougAPsFjXRyBhcKmfT016NscRFLv6Pz5EiNf1CCFJFU0pSKKQwsZTyAQB+sTuUL4TyFyp6J1Ork3dOqw==", + "dev": true, "dependencies": { "@npmcli/git": "^3.0.0", "@npmcli/installed-package-contents": "^1.0.7", @@ -25451,6 +25610,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-5.2.1.tgz", "integrity": "sha512-xIcQYMnhcx2Nr4JTjsFmwwnr9vldugPy9uVm0o87bjqqWMv9GaqsTeT+i99wTl0mk1uLxJtHxLb8kymqTENQsw==", + "dev": true, "dependencies": { "lru-cache": "^7.5.1" }, @@ -25462,6 +25622,7 @@ "version": "7.16.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.16.1.tgz", "integrity": "sha512-9kkuMZHnLH/8qXARvYSjNvq8S1GYFFzynQTAfKeaJ0sIrR3PUPuu37Z+EiIANiZBvpfTf2B5y8ecDLSMWlLv+w==", + "dev": true, "engines": { "node": ">=12" } @@ -25470,6 +25631,7 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -25481,6 +25643,7 @@ "version": "9.1.2", "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-9.1.2.tgz", "integrity": "sha512-pzd9rLEx4TfNJkovvlBSLGhq31gGu2QDexFPWT19yCDh0JgnRhlBLNo5759N0AJmBk+kQ9Y/hXoLnlgFD+ukmg==", + "dev": true, "dependencies": { "hosted-git-info": "^5.0.0", "proc-log": "^2.0.1", @@ -25495,6 +25658,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, "dependencies": { "glob": "^7.1.3" }, @@ -25508,7 +25672,8 @@ "node_modules/pacote/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true }, "node_modules/pako": { "version": "0.2.9", @@ -25540,6 +25705,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/parse-conflict-json/-/parse-conflict-json-2.0.2.tgz", "integrity": "sha512-jDbRGb00TAPFsKWCpZZOT93SxVP9nONOSgES3AevqRq/CHvavEBvKAjxX9p5Y5F0RZLxH9Ufd9+RwtCsa+lFDA==", + "dev": true, "dependencies": { "json-parse-even-better-errors": "^2.3.1", "just-diff": "^5.0.1", @@ -25660,6 +25826,7 @@ "version": "1.6.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.6.1.tgz", "integrity": "sha512-OW+5s+7cw6253Q4E+8qQ/u1fVvcJQCJo/VFD8pje+dbJCF1n5ZRMV2AEHbGp+5Q7jxQIYJxkHopnj6nzdGeZLA==", + "dev": true, "dependencies": { "lru-cache": "^7.14.1", "minipass": "^4.0.2" @@ -25675,6 +25842,7 @@ "version": "7.18.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, "engines": { "node": ">=12" } @@ -26541,6 +26709,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-2.0.1.tgz", "integrity": "sha512-Kcmo2FhfDTXdcbfDH76N7uBYHINxc/8GW7UAVuVP9I+Va3uHSerrnKV6dLooga/gh7GlgzuCCr/eoldnL1muGw==", + "dev": true, "engines": { "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } @@ -26570,6 +26739,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-all-reject-late/-/promise-all-reject-late-1.0.1.tgz", "integrity": "sha512-vuf0Lf0lOxyQREH7GDIOUMLS7kz+gs8i6B+Yi8dC68a2sychGrHTJYghMBD6k7eUcH0H5P73EckCA48xijWqXw==", + "dev": true, "funding": { "url": "https://github.com/sponsors/isaacs" } @@ -26578,6 +26748,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-call-limit/-/promise-call-limit-1.0.1.tgz", "integrity": "sha512-3+hgaa19jzCGLuSCbieeRsu5C2joKfYn8pY6JAuXFRVfF4IO+L7UPpFWNTeWT9pM7uhskvbPPd/oEOktCn317Q==", + "dev": true, "funding": { "url": "https://github.com/sponsors/isaacs" } @@ -26585,12 +26756,14 @@ "node_modules/promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==" + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "dev": true }, "node_modules/promise-retry": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, "dependencies": { "err-code": "^2.0.2", "retry": "^0.12.0" @@ -26615,6 +26788,7 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/promzard/-/promzard-0.3.0.tgz", "integrity": "sha512-JZeYqd7UAcHCwI+sTOeUDYkvEU+1bQ7iE0UT1MgB/tERkAPkesW46MrpIySzODi+owTjZtiF8Ay5j9m60KmMBw==", + "dev": true, "dependencies": { "read": "1" } @@ -27229,6 +27403,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", "integrity": "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==", + "dev": true, "dependencies": { "mute-stream": "~0.0.4" }, @@ -27249,6 +27424,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-3.0.0.tgz", "integrity": "sha512-KQDVjGqhZk92PPNRj9ZEXEuqg8bUobSKRw+q0YQ3TKI5xkce7bUJobL4Z/OtiEbAAv70yEpYIXp4iQ9L8oPVog==", + "dev": true, "engines": { "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } @@ -27282,6 +27458,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-5.0.1.tgz", "integrity": "sha512-MALHuNgYWdGW3gKzuNMuYtcSSZbGQm94fAp16xt8VsYTLBjUSc55bLMKe6gzpWue0Tfi6CBgwCSdDAqutGDhMg==", + "dev": true, "dependencies": { "glob": "^8.0.1", "json-parse-even-better-errors": "^2.3.1", @@ -27296,6 +27473,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-2.0.3.tgz", "integrity": "sha512-W/BKtbL+dUjTuRL2vziuYhp76s5HZ9qQhd/dKfWIZveD0O40453QNyZhC0e63lqZrAQ4jiOapVoeJ7JrszenQQ==", + "dev": true, "dependencies": { "json-parse-even-better-errors": "^2.3.0", "npm-normalize-package-bin": "^1.0.1" @@ -27308,6 +27486,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, "dependencies": { "balanced-match": "^1.0.0" } @@ -27316,6 +27495,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -27334,6 +27514,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-5.2.1.tgz", "integrity": "sha512-xIcQYMnhcx2Nr4JTjsFmwwnr9vldugPy9uVm0o87bjqqWMv9GaqsTeT+i99wTl0mk1uLxJtHxLb8kymqTENQsw==", + "dev": true, "dependencies": { "lru-cache": "^7.5.1" }, @@ -27345,6 +27526,7 @@ "version": "7.16.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.16.1.tgz", "integrity": "sha512-9kkuMZHnLH/8qXARvYSjNvq8S1GYFFzynQTAfKeaJ0sIrR3PUPuu37Z+EiIANiZBvpfTf2B5y8ecDLSMWlLv+w==", + "dev": true, "engines": { "node": ">=12" } @@ -27353,6 +27535,7 @@ "version": "5.1.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, "dependencies": { "brace-expansion": "^2.0.1" }, @@ -27364,6 +27547,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-4.0.1.tgz", "integrity": "sha512-EBk5QKKuocMJhB3BILuKhmaPjI8vNRSpIfO9woLC6NyHVkKKdVEdAO1mrT0ZfxNR1lKwCcTkuZfmGIFdizZ8Pg==", + "dev": true, "dependencies": { "hosted-git-info": "^5.0.0", "is-core-module": "^2.8.1", @@ -27587,6 +27771,7 @@ "resolved": "https://registry.npmjs.org/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz", "integrity": "sha512-asaikDeqAQg7JifRsZn1NJZXo9E+VwlyCfbkZhwyISinqk5zNS6266HS5kah6P0SaQKGF6SkNnZVHUzHFYxYDw==", "deprecated": "This functionality has been moved to @npmcli/fs", + "dev": true, "dependencies": { "debuglog": "^1.0.1", "dezalgo": "^1.0.0", @@ -27946,6 +28131,7 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-4.3.1.tgz", "integrity": "sha512-GfHJHBzFQra23IxDzIdBqhOWfbtdgS1/dCHrDy+yvhpoJY5TdwdT28oWaHWfRpKFDLd3GZnGTx6Mlt4+anbsxQ==", + "dev": true, "dependencies": { "glob": "^9.2.0" }, @@ -27963,6 +28149,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, "dependencies": { "balanced-match": "^1.0.0" } @@ -27971,6 +28158,7 @@ "version": "9.2.1", "resolved": "https://registry.npmjs.org/glob/-/glob-9.2.1.tgz", "integrity": "sha512-Pxxgq3W0HyA3XUvSXcFhRSs+43Jsx0ddxcFrbjxNGkL2Ak5BAUBxLqI5G6ADDeCHLfzzXFhe0b1yYcctGmytMA==", + "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^7.4.1", @@ -27988,6 +28176,7 @@ "version": "7.4.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.2.tgz", "integrity": "sha512-xy4q7wou3vUoC9k1xGTXc+awNdGaGVHtFUaey8tiX4H1QRc04DZ/rmDFwNm2EBsuYEhAZ6SgMmYf3InGY6OauA==", + "dev": true, "dependencies": { "brace-expansion": "^2.0.1" }, @@ -28478,7 +28667,8 @@ "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true }, "node_modules/set-getter": { "version": "0.1.1", @@ -28723,6 +28913,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, "engines": { "node": ">= 6.0.0", "npm": ">= 3.0.0" @@ -28743,6 +28934,7 @@ "version": "2.7.1", "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==", + "dev": true, "dependencies": { "ip": "^2.0.0", "smart-buffer": "^4.2.0" @@ -28756,6 +28948,7 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", + "dev": true, "dependencies": { "agent-base": "^6.0.2", "debug": "^4.3.3", @@ -28857,6 +29050,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "dev": true, "dependencies": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" @@ -28865,12 +29059,14 @@ "node_modules/spdx-exceptions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==" + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true }, "node_modules/spdx-expression-parse": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" @@ -28879,7 +29075,8 @@ "node_modules/spdx-license-ids": { "version": "3.0.12", "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.12.tgz", - "integrity": "sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA==" + "integrity": "sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA==", + "dev": true }, "node_modules/spdy": { "version": "4.0.2", @@ -28975,6 +29172,7 @@ "version": "9.0.1", "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", + "dev": true, "dependencies": { "minipass": "^3.1.1" }, @@ -28986,6 +29184,7 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -28996,7 +29195,8 @@ "node_modules/ssri/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true }, "node_modules/stack-trace": { "version": "0.0.10", @@ -30073,7 +30273,8 @@ "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true }, "node_modules/through": { "version": "2.3.8", @@ -30361,6 +30562,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/treeverse/-/treeverse-2.0.0.tgz", "integrity": "sha512-N5gJCkLu1aXccpOTtqV6ddSEi6ZmGkh3hjmbu1IjcavJK4qyOVQmi0myQKM7z5jVGmD68SJoliaVrMmVObhj6A==", + "dev": true, "engines": { "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } @@ -30787,6 +30989,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==", + "dev": true, "dependencies": { "unique-slug": "^3.0.0" }, @@ -30798,6 +31001,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz", "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==", + "dev": true, "dependencies": { "imurmurhash": "^0.1.4" }, @@ -31028,6 +31232,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, "dependencies": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" @@ -31037,6 +31242,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-4.0.0.tgz", "integrity": "sha512-mzR0L8ZDktZjpX4OB46KT+56MAhl4EIazWP/+G/HPGuvfdaqg4YsCdtOm6U9+LOFyYDoh4dpnpxZRB9MQQns5Q==", + "dev": true, "dependencies": { "builtins": "^5.0.0" }, @@ -31139,7 +31345,8 @@ "node_modules/walk-up-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-1.0.0.tgz", - "integrity": "sha512-hwj/qMDUEjCU5h0xr90KGCf0tg0/LgJbmOWgrWKYlcJZM7XvquvUJZ0G/HMGr7F7OQMOUuPHWP9JpriinkAlkg==" + "integrity": "sha512-hwj/qMDUEjCU5h0xr90KGCf0tg0/LgJbmOWgrWKYlcJZM7XvquvUJZ0G/HMGr7F7OQMOUuPHWP9JpriinkAlkg==", + "dev": true }, "node_modules/walker": { "version": "1.0.8", @@ -31665,6 +31872,7 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dev": true, "dependencies": { "string-width": "^1.0.2 || 2 || 3 || 4" } diff --git a/packages/core/src/common/__tests__/cluster-store.test.ts b/packages/core/src/common/__tests__/cluster-store.test.ts index b30bc8178d..5b052548a5 100644 --- a/packages/core/src/common/__tests__/cluster-store.test.ts +++ b/packages/core/src/common/__tests__/cluster-store.test.ts @@ -8,8 +8,6 @@ import type { GetCustomKubeConfigFilePath } from "../app-paths/get-custom-kube-c import getCustomKubeConfigFilePathInjectable from "../app-paths/get-custom-kube-config-directory/get-custom-kube-config-directory.injectable"; import clusterStoreInjectable from "../cluster-store/cluster-store.injectable"; import type { DiContainer } from "@ogre-tools/injectable"; -import type { CreateCluster } from "../cluster/create-cluster-injection-token"; -import { createClusterInjectionToken } from "../cluster/create-cluster-injection-token"; import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable"; import { getDiForUnitTesting } from "../../main/getDiForUnitTesting"; import assert from "assert"; @@ -27,6 +25,7 @@ import type { WriteFileSync } from "../fs/write-file-sync.injectable"; import writeFileSyncInjectable from "../fs/write-file-sync.injectable"; import type { WriteBufferSync } from "../fs/write-buffer-sync.injectable"; import writeBufferSyncInjectable from "../fs/write-buffer-sync.injectable"; +import { Cluster } from "../cluster/cluster"; // NOTE: this is intended to read the actual file system const testDataIcon = readFileSync("test-data/cluster-store-migration-icon.png"); @@ -58,7 +57,6 @@ users: describe("cluster-store", () => { let di: DiContainer; let clusterStore: ClusterStore; - let createCluster: CreateCluster; let writeJsonSync: WriteJsonSync; let writeFileSync: WriteFileSync; let writeBufferSync: WriteBufferSync; @@ -74,7 +72,6 @@ describe("cluster-store", () => { di.override(kubectlBinaryNameInjectable, () => "kubectl"); di.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64"); di.override(normalizedPlatformInjectable, () => "darwin"); - writeJsonSync = di.inject(writeJsonSyncInjectable); writeFileSync = di.inject(writeFileSyncInjectable); writeBufferSync = di.inject(writeBufferSyncInjectable); @@ -84,7 +81,6 @@ describe("cluster-store", () => { describe("empty config", () => { beforeEach(async () => { - createCluster = di.inject(createClusterInjectionToken); getCustomKubeConfigFilePath = di.inject(getCustomKubeConfigFilePathInjectable); writeJsonSync("/some-directory-for-user-data/lens-cluster-store.json", {}); @@ -94,7 +90,7 @@ describe("cluster-store", () => { describe("with foo cluster added", () => { beforeEach(() => { - const cluster = createCluster({ + const cluster = new Cluster({ id: "foo", contextName: "foo", preferences: { @@ -201,7 +197,6 @@ describe("cluster-store", () => { ], }); - createCluster = di.inject(createClusterInjectionToken); getCustomKubeConfigFilePath = di.inject(getCustomKubeConfigFilePathInjectable); clusterStore = di.inject(clusterStoreInjectable); @@ -256,7 +251,6 @@ describe("cluster-store", () => { ], }); - createCluster = di.inject(createClusterInjectionToken); getCustomKubeConfigFilePath = di.inject(getCustomKubeConfigFilePathInjectable); clusterStore = di.inject(clusterStoreInjectable); @@ -274,7 +268,6 @@ describe("cluster-store", () => { beforeEach(() => { di.override(storeMigrationVersionInjectable, () => "3.6.0"); - createCluster = di.inject(createClusterInjectionToken); getCustomKubeConfigFilePath = di.inject(getCustomKubeConfigFilePathInjectable); writeJsonSync("/some-directory-for-user-data/lens-cluster-store.json", { @@ -302,9 +295,9 @@ describe("cluster-store", () => { }); it("migrates to modern format with kubeconfig in a file", async () => { - const config = clusterStore.clustersList[0].kubeConfigPath; + const configPath = clusterStore.clustersList[0].kubeConfigPath.get(); - expect(readFileSync(config)).toBe(minimalValidKubeConfig); + expect(readFileSync(configPath)).toBe(minimalValidKubeConfig); }); it("migrates to modern format with icon not in file", async () => { diff --git a/packages/core/src/common/catalog-entities/kubernetes-cluster.ts b/packages/core/src/common/catalog-entities/kubernetes-cluster.ts index a9dac5873a..7615c19f3e 100644 --- a/packages/core/src/common/catalog-entities/kubernetes-cluster.ts +++ b/packages/core/src/common/catalog-entities/kubernetes-cluster.ts @@ -13,6 +13,7 @@ import { requestClusterActivation, requestClusterDisconnection } from "../../ren import KubeClusterCategoryIcon from "./icons/kubernetes.svg"; import getClusterByIdInjectable from "../cluster-store/get-by-id.injectable"; import { getLegacyGlobalDiForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; +import clusterConnectionInjectable from "../../main/cluster/cluster-connection.injectable"; export interface KubernetesClusterPrometheusMetrics { address?: { @@ -79,8 +80,15 @@ export class KubernetesCluster< if (app) { const di = getLegacyGlobalDiForExtensionApi(); const getClusterById = di.inject(getClusterByIdInjectable); + const cluster = getClusterById(this.getId()); - await getClusterById(this.getId())?.activate(); + if (!cluster) { + return; + } + + const connectionCluster = di.inject(clusterConnectionInjectable, cluster); + + await connectionCluster.activate(); } else { await requestClusterActivation(this.getId(), false); } @@ -90,8 +98,15 @@ export class KubernetesCluster< if (app) { const di = getLegacyGlobalDiForExtensionApi(); const getClusterById = di.inject(getClusterByIdInjectable); + const cluster = getClusterById(this.getId()); - getClusterById(this.getId())?.disconnect(); + if (!cluster) { + return; + } + + const connectionCluster = di.inject(clusterConnectionInjectable, cluster); + + connectionCluster.disconnect(); } else { await requestClusterDisconnection(this.getId(), false); } diff --git a/packages/core/src/common/catalog/catalog-entity.ts b/packages/core/src/common/catalog/catalog-entity.ts index 3a86da757f..eafe8fcb0c 100644 --- a/packages/core/src/common/catalog/catalog-entity.ts +++ b/packages/core/src/common/catalog/catalog-entity.ts @@ -241,7 +241,7 @@ export interface CatalogEntityMetadata extends EntityMetadataObject { shortName?: string; description?: string; source?: string; - labels: Record; + labels: Partial>; } export interface CatalogEntityStatus { diff --git a/packages/core/src/common/cluster-store/cluster-store.injectable.ts b/packages/core/src/common/cluster-store/cluster-store.injectable.ts index 9712e3fdb0..ddd811c760 100644 --- a/packages/core/src/common/cluster-store/cluster-store.injectable.ts +++ b/packages/core/src/common/cluster-store/cluster-store.injectable.ts @@ -4,7 +4,6 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import { ClusterStore } from "./cluster-store"; -import { createClusterInjectionToken } from "../cluster/create-cluster-injection-token"; import readClusterConfigSyncInjectable from "./read-cluster-config.injectable"; import emitAppEventInjectable from "../app-event-bus/emit-event.injectable"; import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable"; @@ -23,7 +22,6 @@ const clusterStoreInjectable = getInjectable({ id: "cluster-store", instantiate: (di) => new ClusterStore({ - createCluster: di.inject(createClusterInjectionToken), readClusterConfigSync: di.inject(readClusterConfigSyncInjectable), emitAppEvent: di.inject(emitAppEventInjectable), directoryForUserData: di.inject(directoryForUserDataInjectable), diff --git a/packages/core/src/common/cluster-store/cluster-store.ts b/packages/core/src/common/cluster-store/cluster-store.ts index 20929cf77e..8d283411c9 100644 --- a/packages/core/src/common/cluster-store/cluster-store.ts +++ b/packages/core/src/common/cluster-store/cluster-store.ts @@ -10,7 +10,6 @@ import { BaseStore } from "../base-store/base-store"; import { Cluster } from "../cluster/cluster"; import { toJS } from "../utils"; import type { ClusterModel, ClusterId } from "../cluster-types"; -import type { CreateCluster } from "../cluster/create-cluster-injection-token"; import type { ReadClusterConfigSync } from "./read-cluster-config.injectable"; import type { EmitAppEvent } from "../app-event-bus/emit-event.injectable"; @@ -19,7 +18,6 @@ export interface ClusterStoreModel { } interface Dependencies extends BaseStoreDependencies { - createCluster: CreateCluster; readClusterConfigSync: ReadClusterConfigSync; emitAppEvent: EmitAppEvent; } @@ -64,7 +62,7 @@ export class ClusterStore extends BaseStore { const cluster = clusterOrModel instanceof Cluster ? clusterOrModel - : this.dependencies.createCluster( + : new Cluster( clusterOrModel, this.dependencies.readClusterConfigSync(clusterOrModel), ); @@ -87,7 +85,7 @@ export class ClusterStore extends BaseStore { if (cluster) { cluster.updateModel(clusterModel); } else { - cluster = this.dependencies.createCluster( + cluster = new Cluster( clusterModel, this.dependencies.readClusterConfigSync(clusterModel), ); diff --git a/packages/core/src/common/cluster-types.ts b/packages/core/src/common/cluster-types.ts index 3e904a385e..ba01489152 100644 --- a/packages/core/src/common/cluster-types.ts +++ b/packages/core/src/common/cluster-types.ts @@ -39,10 +39,6 @@ export const updateClusterModelChecker = Joi.object({ contextName: Joi.string() .required() .min(1), - workspace: Joi.string() - .optional(), - workspaces: Joi.array() - .items(Joi.string()), preferences: Joi.object(), metadata: Joi.object(), accessibleNamespaces: Joi.array() @@ -70,18 +66,6 @@ export interface ClusterModel { /** Path to cluster kubeconfig */ kubeConfigPath: string; - /** - * Workspace id - * - * @deprecated - */ - workspace?: string; - - /** - * @deprecated this is used only for hotbar migrations from 4.2.X - */ - workspaces?: string[]; - /** User context in kubeconfig */ contextName: string; @@ -97,7 +81,7 @@ export interface ClusterModel { /** * Labels for the catalog entity */ - labels?: Record; + labels?: Partial>; } /** @@ -206,6 +190,6 @@ export interface ClusterState { ready: boolean; isAdmin: boolean; allowedNamespaces: string[]; - allowedResources: string[]; + resourcesToShow: string[]; isGlobalWatchEnabled: boolean; } diff --git a/packages/core/src/common/cluster/authorization-review.injectable.ts b/packages/core/src/common/cluster/authorization-review.injectable.ts index 4c9b83330d..3352423377 100644 --- a/packages/core/src/common/cluster/authorization-review.injectable.ts +++ b/packages/core/src/common/cluster/authorization-review.injectable.ts @@ -6,7 +6,6 @@ import type { KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node"; import { AuthorizationV1Api } from "@kubernetes/client-node"; import { getInjectable } from "@ogre-tools/injectable"; -import type { Logger } from "../logger"; import loggerInjectable from "../logger.injectable"; /** @@ -19,41 +18,33 @@ export type CanI = (resourceAttributes: V1ResourceAttributes) => Promise CanI; +export type CreateAuthorizationReview = (proxyConfig: KubeConfig) => CanI; -interface Dependencies { - logger: Logger; -} - -const authorizationReview = ({ logger }: Dependencies): AuthorizationReview => { - return (proxyConfig) => { - const api = proxyConfig.makeApiClient(AuthorizationV1Api); - - return async (resourceAttributes: V1ResourceAttributes): Promise => { - try { - const { body } = await api.createSelfSubjectAccessReview({ - apiVersion: "authorization.k8s.io/v1", - kind: "SelfSubjectAccessReview", - spec: { resourceAttributes }, - }); - - return body.status?.allowed ?? false; - } catch (error) { - logger.error(`[AUTHORIZATION-REVIEW]: failed to create access review: ${error}`, { resourceAttributes }); - - return false; - } - }; - }; -}; - -const authorizationReviewInjectable = getInjectable({ +const createAuthorizationReviewInjectable = getInjectable({ id: "authorization-review", - instantiate: (di) => { + instantiate: (di): CreateAuthorizationReview => { const logger = di.inject(loggerInjectable); - return authorizationReview({ logger }); + return (proxyConfig) => { + const api = proxyConfig.makeApiClient(AuthorizationV1Api); + + return async (resourceAttributes: V1ResourceAttributes): Promise => { + try { + const { body } = await api.createSelfSubjectAccessReview({ + apiVersion: "authorization.k8s.io/v1", + kind: "SelfSubjectAccessReview", + spec: { resourceAttributes }, + }); + + return body.status?.allowed ?? false; + } catch (error) { + logger.error(`[AUTHORIZATION-REVIEW]: failed to create access review: ${error}`, { resourceAttributes }); + + return false; + } + }; + }; }, }); -export default authorizationReviewInjectable; +export default createAuthorizationReviewInjectable; diff --git a/packages/core/src/common/cluster/cluster.ts b/packages/core/src/common/cluster/cluster.ts index 329d6e2df7..fc66d9aa29 100644 --- a/packages/core/src/common/cluster/cluster.ts +++ b/packages/core/src/common/cluster/cluster.ts @@ -3,164 +3,74 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { action, comparer, computed, makeObservable, observable, reaction, runInAction, when } from "mobx"; -import type { ClusterContextHandler } from "../../main/context-handler/context-handler"; -import type { KubeConfig } from "@kubernetes/client-node"; -import { HttpError } from "@kubernetes/client-node"; -import type { Kubectl } from "../../main/kubectl/kubectl"; -import type { KubeconfigManager } from "../../main/kubeconfig-manager/kubeconfig-manager"; -import type { KubeApiResource, KubeApiResourceDescriptor } from "../rbac"; -import { formatKubeApiResource } from "../rbac"; -import plimit from "p-limit"; -import type { ClusterState, ClusterMetricsResourceType, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel, KubeAuthUpdate, ClusterConfigData } from "../cluster-types"; -import { ClusterMetadataKey, initialNodeShellImage, ClusterStatus, clusterModelIdChecker, updateClusterModelChecker } from "../cluster-types"; -import { disposer, isDefined, isRequestError, toJS } from "../utils"; -import { clusterListNamespaceForbiddenChannel } from "../ipc/cluster"; -import type { CanI } from "./authorization-review.injectable"; -import type { ListNamespaces } from "./list-namespaces.injectable"; -import assert from "assert"; -import type { Logger } from "../logger"; -import type { BroadcastMessage } from "../ipc/broadcast-message.injectable"; -import type { LoadConfigfromFile } from "../kube-helpers/load-config-from-file.injectable"; -import type { CanListResource, RequestNamespaceListPermissions, RequestNamespaceListPermissionsFor } from "./request-namespace-list-permissions.injectable"; -import type { RequestApiResources } from "../../main/cluster/request-api-resources.injectable"; -import type { DetectClusterMetadata } from "../../main/cluster-detectors/detect-cluster-metadata.injectable"; -import type { FalibleOnlyClusterMetadataDetector } from "../../main/cluster-detectors/token"; +import { computed, observable, toJS, runInAction } from "mobx"; +import type { KubeApiResource } from "../rbac"; +import type { ClusterState, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel, ClusterConfigData } from "../cluster-types"; +import { ClusterMetadataKey, clusterModelIdChecker, updateClusterModelChecker } from "../cluster-types"; +import type { IObservableValue } from "mobx"; +import { replaceObservableObject } from "../utils/replace-observable-object"; +import { pick } from "lodash"; -export interface ClusterDependencies { - readonly directoryForKubeConfigs: string; - readonly logger: Logger; - readonly clusterVersionDetector: FalibleOnlyClusterMetadataDetector; - detectClusterMetadata: DetectClusterMetadata; - createKubeconfigManager: (cluster: Cluster) => KubeconfigManager; - createContextHandler: (cluster: Cluster) => ClusterContextHandler; - createKubectl: (clusterVersion: string) => Kubectl; - createAuthorizationReview: (config: KubeConfig) => CanI; - requestApiResources: RequestApiResources; - requestNamespaceListPermissionsFor: RequestNamespaceListPermissionsFor; - createListNamespaces: (config: KubeConfig) => ListNamespaces; - broadcastMessage: BroadcastMessage; - loadConfigfromFile: LoadConfigfromFile; -} - -/** - * Cluster - * - * @beta - */ -export class Cluster implements ClusterModel { - /** Unique id for a cluster */ - public readonly id: ClusterId; - private kubeCtl: Kubectl | undefined; +export class Cluster { /** - * Context handler - * - * @internal + * Unique id for a cluster */ - protected readonly _contextHandler: ClusterContextHandler | undefined; - protected readonly _proxyKubeconfigManager: KubeconfigManager | undefined; - protected readonly eventsDisposer = disposer(); - protected activated = false; - - public get contextHandler() { - // TODO: remove these once main/renderer are seperate classes - assert(this._contextHandler, "contextHandler is only defined in the main environment"); - - return this._contextHandler; - } - - protected get proxyKubeconfigManager() { - // TODO: remove these once main/renderer are seperate classes - assert(this._proxyKubeconfigManager, "proxyKubeconfigManager is only defined in the main environment"); - - return this._proxyKubeconfigManager; - } - - get whenReady() { - return when(() => this.ready); - } + readonly id: ClusterId; /** * Kubeconfig context name - * - * @observable */ - @observable contextName!: string; + readonly contextName = observable.box() as IObservableValue; + /** * Path to kubeconfig - * - * @observable */ - @observable kubeConfigPath!: string; - /** - * @deprecated - */ - @observable workspace?: string; - /** - * @deprecated - */ - @observable workspaces?: string[]; + readonly kubeConfigPath = observable.box() as IObservableValue; + /** * Kubernetes API server URL - * - * @observable */ - @observable apiUrl: string; // cluster server url + readonly apiUrl: IObservableValue; + /** - * Is cluster online - * - * @observable + * Describes if we can detect that cluster is online */ - @observable online = false; // describes if we can detect that cluster is online + readonly online = observable.box(false); + /** - * Can user access cluster resources - * - * @observable + * Describes if user is able to access cluster resources */ - @observable accessible = false; // if user is able to access cluster resources + readonly accessible = observable.box(false); + /** * Is cluster instance in usable state - * - * @observable */ - @observable ready = false; // cluster is in usable state - /** - * Is cluster currently reconnecting - * - * @observable - */ - @observable reconnecting = false; + readonly ready = observable.box(false); + /** * Is cluster disconnected. False if user has selected to connect. - * - * @observable */ - @observable disconnected = true; + readonly disconnected = observable.box(true); + /** * Does user have admin like access - * - * @observable */ - @observable isAdmin = false; + readonly isAdmin = observable.box(false); /** * Global watch-api accessibility , e.g. "/api/v1/services?watch=1" - * - * @observable */ - @observable isGlobalWatchEnabled = false; + readonly isGlobalWatchEnabled = observable.box(false); + /** * Preferences - * - * @observable */ - @observable preferences: ClusterPreferences = {}; + readonly preferences = observable.object({}); + /** * Metadata - * - * @observable */ - @observable metadata: ClusterMetadata = {}; + readonly metadata = observable.object({}); /** * List of allowed namespaces verified via K8S::SelfSubjectAccessReview api @@ -172,73 +82,47 @@ export class Cluster implements ClusterModel { */ readonly accessibleNamespaces = observable.array(); - private readonly knownResources = observable.array(); + /** + * The list of all known resources associated with this cluster + */ + readonly knownResources = observable.array(); - // The formatting of this is `group.name` or `name` (if in core) - private readonly allowedResources = observable.set(); + /** + * The formatting of this is `group.name` or `name` (if in core) + */ + readonly resourcesToShow = observable.set(); /** * Labels for the catalog entity */ - @observable labels: Record = {}; + readonly labels = observable.object>>({}); /** * Is cluster available - * - * @computed */ - @computed get available() { - return this.accessible && !this.disconnected; - } + readonly available = computed(() => this.accessible.get() && !this.disconnected.get()); /** * Cluster name - * - * @computed */ - @computed get name() { - return this.preferences.clusterName || this.contextName; - } + readonly name = computed(() => this.preferences.clusterName || this.contextName.get()); /** * The detected kubernetes distribution */ - @computed get distribution(): string { - return this.metadata[ClusterMetadataKey.DISTRIBUTION]?.toString() || "unknown"; - } + readonly distribution = computed(() => this.metadata[ClusterMetadataKey.DISTRIBUTION]?.toString() || "unknown"); /** * The detected kubernetes version */ - @computed get version(): string { - return this.metadata[ClusterMetadataKey.VERSION]?.toString() || "unknown"; - } + readonly version = computed(() => this.metadata[ClusterMetadataKey.VERSION]?.toString() || "unknown"); /** * Prometheus preferences - * - * @computed - * @internal */ - @computed get prometheusPreferences(): ClusterPrometheusPreferences { - const { prometheus, prometheusProvider } = this.preferences; - - return toJS({ prometheus, prometheusProvider }); - } - - /** - * defaultNamespace preference - * - * @computed - * @internal - */ - @computed get defaultNamespace(): string | undefined { - return this.preferences.defaultNamespace; - } - - constructor(private readonly dependencies: ClusterDependencies, { id, ...model }: ClusterModel, configData: ClusterConfigData) { - makeObservable(this); + readonly prometheusPreferences = computed(() => pick(toJS(this.preferences), "prometheus", "prometheusProvider") as ClusterPrometheusPreferences); + constructor({ id, ...model }: ClusterModel, configData: ClusterConfigData) { const { error } = clusterModelIdChecker.validate({ id }); if (error) { @@ -247,16 +131,7 @@ export class Cluster implements ClusterModel { this.id = id; this.updateModel(model); - this.apiUrl = configData.clusterServerUrl; - - // for the time being, until renderer gets its own cluster type - this._contextHandler = this.dependencies.createContextHandler(this); - this._proxyKubeconfigManager = this.dependencies.createKubeconfigManager(this); - this.dependencies.logger.debug(`[CLUSTER]: Cluster init success`, { - id: this.id, - context: this.contextName, - apiUrl: this.apiUrl, - }); + this.apiUrl = observable.box(configData.clusterServerUrl); } /** @@ -264,7 +139,7 @@ export class Cluster implements ClusterModel { * * @param model */ - @action updateModel(model: UpdateClusterModel) { + updateModel(model: UpdateClusterModel) { // Note: do not assign ID as that should never be updated const { error } = updateClusterModelChecker.validate(model, { allowUnknown: true }); @@ -273,454 +148,83 @@ export class Cluster implements ClusterModel { throw error; } - this.kubeConfigPath = model.kubeConfigPath; - this.contextName = model.contextName; - - if (model.workspace) { - this.workspace = model.workspace; - } - - if (model.workspaces) { - this.workspaces = model.workspaces; - } - - if (model.preferences) { - this.preferences = model.preferences; - } - - if (model.metadata) { - this.metadata = model.metadata; - } - - if (model.accessibleNamespaces) { - this.accessibleNamespaces.replace(model.accessibleNamespaces); - } - - if (model.labels) { - this.labels = model.labels; - } - } - - /** - * @internal - */ - protected bindEvents() { - this.dependencies.logger.info(`[CLUSTER]: bind events`, this.getMeta()); - const refreshTimer = setInterval(() => !this.disconnected && this.refresh(), 30000); // every 30s - const refreshMetadataTimer = setInterval(() => this.available && this.refreshAccessibilityAndMetadata(), 900000); // every 15 minutes - - this.eventsDisposer.push( - reaction( - () => this.prometheusPreferences, - prefs => this.contextHandler.setupPrometheus(prefs), - { equals: comparer.structural }, - ), - () => clearInterval(refreshTimer), - () => clearInterval(refreshMetadataTimer), - reaction(() => this.defaultNamespace, () => this.recreateProxyKubeconfig()), - ); - } - - /** - * @internal - */ - protected async recreateProxyKubeconfig() { - this.dependencies.logger.info("[CLUSTER]: Recreating proxy kubeconfig"); - - try { - await this.proxyKubeconfigManager.clear(); - await this.getProxyKubeconfig(); - } catch (error) { - this.dependencies.logger.error(`[CLUSTER]: failed to recreate proxy kubeconfig`, error); - } - } - - /** - * @param force force activation - * @internal - */ - @action - async activate(force = false) { - if (this.activated && !force) { - return; - } - - this.dependencies.logger.info(`[CLUSTER]: activate`, this.getMeta()); - - if (!this.eventsDisposer.length) { - this.bindEvents(); - } - - if (this.disconnected || !this.accessible) { - try { - this.broadcastConnectUpdate("Starting connection ..."); - await this.reconnect(); - } catch (error) { - this.broadcastConnectUpdate(`Failed to start connection: ${error}`, "error"); - - return; - } - } - - try { - this.broadcastConnectUpdate("Refreshing connection status ..."); - await this.refreshConnectionStatus(); - } catch (error) { - this.broadcastConnectUpdate(`Failed to connection status: ${error}`, "error"); - - return; - } - - if (this.accessible) { - try { - this.broadcastConnectUpdate("Refreshing cluster accessibility ..."); - await this.refreshAccessibility(); - } catch (error) { - this.broadcastConnectUpdate(`Failed to refresh accessibility: ${error}`, "error"); - - return; - } - - // download kubectl in background, so it's not blocking dashboard - this.ensureKubectl() - .catch(error => this.dependencies.logger.warn(`[CLUSTER]: failed to download kubectl for clusterId=${this.id}`, error)); - this.broadcastConnectUpdate("Connected, waiting for view to load ..."); - } - - this.activated = true; - } - - /** - * @internal - */ - async ensureKubectl() { - this.kubeCtl ??= this.dependencies.createKubectl(this.version); - - await this.kubeCtl.ensureKubectl(); - - return this.kubeCtl; - } - - /** - * @internal - */ - @action - async reconnect() { - this.dependencies.logger.info(`[CLUSTER]: reconnect`, this.getMeta()); - await this.contextHandler?.restartServer(); - this.disconnected = false; - } - - /** - * @internal - */ - @action disconnect(): void { - if (this.disconnected) { - return void this.dependencies.logger.debug("[CLUSTER]: already disconnected", { id: this.id }); - } - - this.dependencies.logger.info(`[CLUSTER]: disconnecting`, { id: this.id }); - this.eventsDisposer(); - this.contextHandler?.stopServer(); - this.disconnected = true; - this.online = false; - this.accessible = false; - this.ready = false; - this.activated = false; - this.allowedNamespaces.clear(); - this.dependencies.logger.info(`[CLUSTER]: disconnected`, { id: this.id }); - } - - /** - * @internal - */ - @action - async refresh() { - this.dependencies.logger.info(`[CLUSTER]: refresh`, this.getMeta()); - await this.refreshConnectionStatus(); - } - - /** - * @internal - */ - @action - async refreshAccessibilityAndMetadata() { - await this.refreshAccessibility(); - await this.refreshMetadata(); - } - - /** - * @internal - */ - async refreshMetadata() { - this.dependencies.logger.info(`[CLUSTER]: refreshMetadata`, this.getMeta()); - - const newMetadata = await this.dependencies.detectClusterMetadata(this); - runInAction(() => { - this.metadata = { - ...this.metadata, - ...newMetadata, - }; - }); - } + this.kubeConfigPath.set(model.kubeConfigPath); + this.contextName.set(model.contextName); - /** - * @internal - */ - private async refreshAccessibility(): Promise { - this.dependencies.logger.info(`[CLUSTER]: refreshAccessibility`, this.getMeta()); - const proxyConfig = await this.getProxyKubeconfig(); - const canI = this.dependencies.createAuthorizationReview(proxyConfig); - const requestNamespaceListPermissions = this.dependencies.requestNamespaceListPermissionsFor(proxyConfig); - - this.isAdmin = await canI({ - namespace: "kube-system", - resource: "*", - verb: "create", - }); - this.isGlobalWatchEnabled = await canI({ - verb: "watch", - resource: "*", - }); - this.allowedNamespaces.replace(await this.requestAllowedNamespaces(proxyConfig)); - - const knownResources = await this.dependencies.requestApiResources(this); - - if (knownResources.callWasSuccessful) { - this.knownResources.replace(knownResources.response); - } else if (this.knownResources.length > 0) { - this.dependencies.logger.warn(`[CLUSTER]: failed to list KUBE resources, sticking with previous list`); - } else { - this.dependencies.logger.warn(`[CLUSTER]: failed to list KUBE resources for the first time, blocking connection to cluster...`); - this.broadcastConnectUpdate("Failed to list kube API resources, please reconnect...", "error"); - } - - this.allowedResources.replace(await this.getAllowedResources(requestNamespaceListPermissions)); - this.ready = this.knownResources.length > 0; - this.dependencies.logger.debug(`[CLUSTER]: refreshed accessibility data`, this.getState()); - } - - /** - * @internal - */ - @action - async refreshConnectionStatus() { - const connectionStatus = await this.getConnectionStatus(); - - this.online = connectionStatus > ClusterStatus.Offline; - this.accessible = connectionStatus == ClusterStatus.AccessGranted; - } - - async getKubeconfig(): Promise { - const { config } = await this.dependencies.loadConfigfromFile(this.kubeConfigPath); - - return config; - } - - /** - * @internal - */ - async getProxyKubeconfig(): Promise { - const proxyKCPath = await this.getProxyKubeconfigPath(); - const { config } = await this.dependencies.loadConfigfromFile(proxyKCPath); - - return config; - } - - /** - * @internal - */ - async getProxyKubeconfigPath(): Promise { - return this.proxyKubeconfigManager.getPath(); - } - - protected async getConnectionStatus(): Promise { - try { - const versionData = await this.dependencies.clusterVersionDetector.detect(this); - - this.metadata.version = versionData.value; - - return ClusterStatus.AccessGranted; - } catch (error) { - this.dependencies.logger.error(`[CLUSTER]: Failed to connect to "${this.contextName}": ${error}`); - - if (isRequestError(error)) { - if (error.statusCode) { - if (error.statusCode >= 400 && error.statusCode < 500) { - this.broadcastConnectUpdate("Invalid credentials", "error"); - - return ClusterStatus.AccessDenied; - } - - const message = String(error.error || error.message) || String(error); - - this.broadcastConnectUpdate(message, "error"); - - return ClusterStatus.Offline; - } - - if (error.failed === true) { - if (error.timedOut === true) { - this.broadcastConnectUpdate("Connection timed out", "error"); - - return ClusterStatus.Offline; - } - - this.broadcastConnectUpdate("Failed to fetch credentials", "error"); - - return ClusterStatus.AccessDenied; - } - - const message = String(error.error || error.message) || String(error); - - this.broadcastConnectUpdate(message, "error"); - } else if (error instanceof Error || typeof error === "string") { - this.broadcastConnectUpdate(`${error}`, "error"); - } else { - this.broadcastConnectUpdate("Unknown error has occurred", "error"); + if (model.preferences) { + replaceObservableObject(this.preferences, model.preferences); } - return ClusterStatus.Offline; - } + if (model.metadata) { + replaceObservableObject(this.metadata, model.metadata); + } + + if (model.accessibleNamespaces) { + this.accessibleNamespaces.replace(model.accessibleNamespaces); + } + + if (model.labels) { + replaceObservableObject(this.labels, model.labels); + } + }); } toJSON(): ClusterModel { - return toJS({ + return { id: this.id, - contextName: this.contextName, - kubeConfigPath: this.kubeConfigPath, - workspace: this.workspace, - workspaces: this.workspaces, - preferences: this.preferences, - metadata: this.metadata, - accessibleNamespaces: this.accessibleNamespaces, - labels: this.labels, - }); + contextName: this.contextName.get(), + kubeConfigPath: this.kubeConfigPath.get(), + preferences: toJS(this.preferences), + metadata: toJS(this.metadata), + accessibleNamespaces: this.accessibleNamespaces.toJSON(), + labels: toJS(this.labels), + }; } /** * Serializable cluster-state used for sync btw main <-> renderer */ getState(): ClusterState { - return toJS({ - apiUrl: this.apiUrl, - online: this.online, - ready: this.ready, - disconnected: this.disconnected, - accessible: this.accessible, - isAdmin: this.isAdmin, - allowedNamespaces: this.allowedNamespaces, - allowedResources: [...this.allowedResources], - isGlobalWatchEnabled: this.isGlobalWatchEnabled, - }); + return { + apiUrl: this.apiUrl.get(), + online: this.online.get(), + ready: this.ready.get(), + disconnected: this.disconnected.get(), + accessible: this.accessible.get(), + isAdmin: this.isAdmin.get(), + allowedNamespaces: this.allowedNamespaces.toJSON(), + resourcesToShow: this.resourcesToShow.toJSON(), + isGlobalWatchEnabled: this.isGlobalWatchEnabled.get(), + }; } /** - * @internal * @param state cluster state */ - @action setState(state: ClusterState) { - this.accessible = state.accessible; - this.allowedNamespaces.replace(state.allowedNamespaces); - this.allowedResources.replace(state.allowedResources); - this.apiUrl = state.apiUrl; - this.disconnected = state.disconnected; - this.isAdmin = state.isAdmin; - this.isGlobalWatchEnabled = state.isGlobalWatchEnabled; - this.online = state.online; - this.ready = state.ready; + setState(state: ClusterState) { + runInAction(() => { + this.accessible.set(state.accessible); + this.allowedNamespaces.replace(state.allowedNamespaces); + this.resourcesToShow.replace(state.resourcesToShow); + this.apiUrl.set(state.apiUrl); + this.disconnected.set(state.disconnected); + this.isAdmin.set(state.isAdmin); + this.isGlobalWatchEnabled.set(state.isGlobalWatchEnabled); + this.online.set(state.online); + this.ready.set(state.ready); + }); } // get cluster system meta, e.g. use in "logger" getMeta() { return { id: this.id, - name: this.contextName, - ready: this.ready, - online: this.online, - accessible: this.accessible, - disconnected: this.disconnected, + name: this.contextName.get(), + ready: this.ready.get(), + online: this.online.get(), + accessible: this.accessible.get(), + disconnected: this.disconnected.get(), }; } - - /** - * broadcast an authentication update concerning this cluster - * @internal - */ - broadcastConnectUpdate(message: string, level: KubeAuthUpdate["level"] = "info"): void { - const update: KubeAuthUpdate = { message, level }; - - this.dependencies.logger.debug(`[CLUSTER]: broadcasting connection update`, { ...update, meta: this.getMeta() }); - this.dependencies.broadcastMessage(`cluster:${this.id}:connection-update`, update); - } - - protected async requestAllowedNamespaces(proxyConfig: KubeConfig) { - if (this.accessibleNamespaces.length) { - return this.accessibleNamespaces; - } - - try { - const listNamespaces = this.dependencies.createListNamespaces(proxyConfig); - - return await listNamespaces(); - } catch (error) { - const ctx = proxyConfig.getContextObject(this.contextName); - const namespaceList = [ctx?.namespace].filter(isDefined); - - if (namespaceList.length === 0 && error instanceof HttpError && error.statusCode === 403) { - const { response } = error as HttpError & { response: { body: unknown }}; - - this.dependencies.logger.info("[CLUSTER]: listing namespaces is forbidden, broadcasting", { clusterId: this.id, error: response.body }); - this.dependencies.broadcastMessage(clusterListNamespaceForbiddenChannel, this.id); - } - - return namespaceList; - } - } - - protected async getAllowedResources(requestNamespaceListPermissions: RequestNamespaceListPermissions) { - if (!this.allowedNamespaces.length || !this.knownResources.length) { - return []; - } - - try { - const apiLimit = plimit(5); // 5 concurrent api requests - const canListResourceCheckers = await Promise.all(( - this.allowedNamespaces.map(namespace => apiLimit(() => requestNamespaceListPermissions(namespace))) - )); - const canListNamespacedResource: CanListResource = (resource) => canListResourceCheckers.some(fn => fn(resource)); - - return this.knownResources - .filter(canListNamespacedResource) - .map(formatKubeApiResource); - } catch (error) { - return []; - } - } - - shouldShowResource(resource: KubeApiResourceDescriptor): boolean { - if (this.allowedResources.size === 0) { - // better to show than hide everything - return true; - } - - return this.allowedResources.has(formatKubeApiResource(resource)); - } - - isMetricHidden(resource: ClusterMetricsResourceType): boolean { - return Boolean(this.preferences.hiddenMetrics?.includes(resource)); - } - - get nodeShellImage(): string { - return this.preferences?.nodeShellImage || initialNodeShellImage; - } - - get imagePullSecret(): string | undefined { - return this.preferences?.imagePullSecret; - } - - isInLocalKubeconfig() { - return this.kubeConfigPath.startsWith(this.dependencies.directoryForKubeConfigs); - } } diff --git a/packages/core/src/common/cluster/create-cluster-injection-token.ts b/packages/core/src/common/cluster/create-cluster-injection-token.ts deleted file mode 100644 index a07ce4459f..0000000000 --- a/packages/core/src/common/cluster/create-cluster-injection-token.ts +++ /dev/null @@ -1,13 +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 { ClusterConfigData, ClusterModel } from "../cluster-types"; -import type { Cluster } from "./cluster"; - -export type CreateCluster = (model: ClusterModel, configData: ClusterConfigData) => Cluster; - -export const createClusterInjectionToken = getInjectionToken({ - id: "create-cluster-token", -}); diff --git a/packages/core/src/common/cluster/list-namespaces.injectable.ts b/packages/core/src/common/cluster/list-namespaces.injectable.ts index 468ff3ac2e..54e15c4c59 100644 --- a/packages/core/src/common/cluster/list-namespaces.injectable.ts +++ b/packages/core/src/common/cluster/list-namespaces.injectable.ts @@ -9,21 +9,21 @@ import { isDefined } from "../utils"; export type ListNamespaces = () => Promise; -export function listNamespaces(config: KubeConfig): ListNamespaces { - const coreApi = config.makeApiClient(CoreV1Api); +export type CreateListNamespaces = (config: KubeConfig) => ListNamespaces; - return async () => { - const { body: { items }} = await coreApi.listNamespace(); +const createListNamespacesInjectable = getInjectable({ + id: "create-list-namespaces", + instantiate: (): CreateListNamespaces => (config) => { + const coreApi = config.makeApiClient(CoreV1Api); - return items - .map(ns => ns.metadata?.name) - .filter(isDefined); - }; -} + return async () => { + const { body: { items }} = await coreApi.listNamespace(); -const listNamespacesInjectable = getInjectable({ - id: "list-namespaces", - instantiate: () => listNamespaces, + return items + .map(ns => ns.metadata?.name) + .filter(isDefined); + }; + }, }); -export default listNamespacesInjectable; +export default createListNamespacesInjectable; diff --git a/packages/core/src/common/cluster/load-kubeconfig.injectable.ts b/packages/core/src/common/cluster/load-kubeconfig.injectable.ts new file mode 100644 index 0000000000..4a420dadd3 --- /dev/null +++ b/packages/core/src/common/cluster/load-kubeconfig.injectable.ts @@ -0,0 +1,36 @@ +/** + * 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 { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import type { Cluster } from "./cluster"; +import loadConfigFromFileInjectable from "../kube-helpers/load-config-from-file.injectable"; +import type { ConfigResult } from "../kube-helpers"; + +export interface LoadKubeconfig { + (fullResult?: false): Promise; + (fullResult: true): Promise; +} + +const loadKubeconfigInjectable = getInjectable({ + id: "load-kubeconfig", + instantiate: (di, cluster) => { + const loadConfigFromFile = di.inject(loadConfigFromFileInjectable); + + return (async (fullResult = false) => { + const result = await loadConfigFromFile(cluster.kubeConfigPath.get()); + + if (fullResult) { + return result; + } + + return result.config; + }) as LoadKubeconfig; + }, + lifecycle: lifecycleEnum.keyedSingleton({ + getInstanceKey: (di, cluster: Cluster) => cluster.id, + }), +}); + +export default loadKubeconfigInjectable; diff --git a/packages/core/src/common/k8s-api/__tests__/api-manager.test.ts b/packages/core/src/common/k8s-api/__tests__/api-manager.test.ts index e6092fa417..6ea6327038 100644 --- a/packages/core/src/common/k8s-api/__tests__/api-manager.test.ts +++ b/packages/core/src/common/k8s-api/__tests__/api-manager.test.ts @@ -17,10 +17,10 @@ import { KubeApi } from "../kube-api"; import { KubeObject } from "../kube-object"; import { KubeObjectStore } from "../kube-object.store"; import maybeKubeApiInjectable from "../maybe-kube-api.injectable"; -import { createClusterInjectionToken } from "../../cluster/create-cluster-injection-token"; // eslint-disable-next-line no-restricted-imports import { KubeApi as ExternalKubeApi } from "../../../extensions/common-api/k8s-api"; +import { Cluster } from "../../cluster/cluster"; class TestApi extends KubeApi { protected async checkPreferredVersion() { @@ -43,9 +43,7 @@ describe("ApiManager", () => { di.override(directoryForKubeConfigsInjectable, () => "/some-kube-configs"); di.override(storesAndApisCanBeCreatedInjectable, () => true); - const createCluster = di.inject(createClusterInjectionToken); - - di.override(hostedClusterInjectable, () => createCluster({ + di.override(hostedClusterInjectable, () => new Cluster({ contextName: "some-context-name", id: "some-cluster-id", kubeConfigPath: "/some-path-to-a-kubeconfig", diff --git a/packages/core/src/common/k8s-api/__tests__/kube-api-version-detection.test.ts b/packages/core/src/common/k8s-api/__tests__/kube-api-version-detection.test.ts index da7c46eda3..5848be20c5 100644 --- a/packages/core/src/common/k8s-api/__tests__/kube-api-version-detection.test.ts +++ b/packages/core/src/common/k8s-api/__tests__/kube-api-version-detection.test.ts @@ -22,7 +22,7 @@ import type { DiContainer } from "@ogre-tools/injectable"; import ingressApiInjectable from "../endpoints/ingress.api.injectable"; import loggerInjectable from "../../logger.injectable"; import maybeKubeApiInjectable from "../maybe-kube-api.injectable"; -import { createClusterInjectionToken } from "../../cluster/create-cluster-injection-token"; +import { Cluster } from "../../cluster/cluster"; describe("KubeApi", () => { let fetchMock: AsyncFnMock; @@ -39,9 +39,7 @@ describe("KubeApi", () => { di.override(directoryForKubeConfigsInjectable, () => "/some-kube-configs"); di.override(storesAndApisCanBeCreatedInjectable, () => true); - const createCluster = di.inject(createClusterInjectionToken); - - di.override(hostedClusterInjectable, () => createCluster({ + di.override(hostedClusterInjectable, () => new Cluster({ contextName: "some-context-name", id: "some-cluster-id", kubeConfigPath: "/some-path-to-a-kubeconfig", diff --git a/packages/core/src/common/k8s-api/__tests__/kube-api.test.ts b/packages/core/src/common/k8s-api/__tests__/kube-api.test.ts index e2b5907d85..198ace4bbe 100644 --- a/packages/core/src/common/k8s-api/__tests__/kube-api.test.ts +++ b/packages/core/src/common/k8s-api/__tests__/kube-api.test.ts @@ -35,7 +35,7 @@ import namespaceApiInjectable from "../endpoints/namespace.api.injectable"; // NOTE: this is fine because we are testing something that only exported // eslint-disable-next-line no-restricted-imports import { PodsApi } from "../../../extensions/common-api/k8s-api"; -import { createClusterInjectionToken } from "../../cluster/create-cluster-injection-token"; +import { Cluster } from "../../cluster/cluster"; describe("createKubeApiForRemoteCluster", () => { let createKubeApiForRemoteCluster: CreateKubeApiForRemoteCluster; @@ -48,9 +48,7 @@ describe("createKubeApiForRemoteCluster", () => { di.override(directoryForKubeConfigsInjectable, () => "/some-kube-configs"); di.override(storesAndApisCanBeCreatedInjectable, () => true); - const createCluster = di.inject(createClusterInjectionToken); - - di.override(hostedClusterInjectable, () => createCluster({ + di.override(hostedClusterInjectable, () => new Cluster({ contextName: "some-context-name", id: "some-cluster-id", kubeConfigPath: "/some-path-to-a-kubeconfig", @@ -154,10 +152,9 @@ describe("KubeApi", () => { fetchMock = asyncFn(); di.override(fetchInjectable, () => fetchMock); - const createCluster = di.inject(createClusterInjectionToken); const createKubeJsonApi = di.inject(createKubeJsonApiInjectable); - di.override(hostedClusterInjectable, () => createCluster({ + di.override(hostedClusterInjectable, () => new Cluster({ contextName: "some-context-name", id: "some-cluster-id", kubeConfigPath: "/some-path-to-a-kubeconfig", diff --git a/packages/core/src/common/kube-helpers/load-config-from-file.injectable.ts b/packages/core/src/common/kube-helpers/load-config-from-file.injectable.ts index afa9d3c070..f78b5961da 100644 --- a/packages/core/src/common/kube-helpers/load-config-from-file.injectable.ts +++ b/packages/core/src/common/kube-helpers/load-config-from-file.injectable.ts @@ -8,11 +8,11 @@ import type { ConfigResult } from "../kube-helpers"; import { loadConfigFromString } from "../kube-helpers"; import resolveTildeInjectable from "../path/resolve-tilde.injectable"; -export type LoadConfigfromFile = (filePath: string) => Promise; +export type LoadConfigFromFile = (filePath: string) => Promise; -const loadConfigfromFileInjectable = getInjectable({ - id: "load-configfrom-file", - instantiate: (di): LoadConfigfromFile => { +const loadConfigFromFileInjectable = getInjectable({ + id: "load-config-from-file", + instantiate: (di): LoadConfigFromFile => { const readFile = di.inject(readFileInjectable); const resolveTilde = di.inject(resolveTildeInjectable); @@ -20,4 +20,4 @@ const loadConfigfromFileInjectable = getInjectable({ }, }); -export default loadConfigfromFileInjectable; +export default loadConfigFromFileInjectable; diff --git a/packages/core/src/common/utils/backoff-caller.ts b/packages/core/src/common/utils/backoff-caller.ts index 131565bb09..2d1269a48b 100644 --- a/packages/core/src/common/utils/backoff-caller.ts +++ b/packages/core/src/common/utils/backoff-caller.ts @@ -25,7 +25,7 @@ export interface BackoffCallerOptions { maxAttempts?: number; /** - * In miliseconds + * In milliseconds * @default 1000 */ initialTimeout?: number; diff --git a/packages/core/src/common/utils/replace-observable-object.ts b/packages/core/src/common/utils/replace-observable-object.ts new file mode 100644 index 0000000000..8b88187b6c --- /dev/null +++ b/packages/core/src/common/utils/replace-observable-object.ts @@ -0,0 +1,18 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { runInAction } from "mobx"; + +export function replaceObservableObject(target: Partial>, source: Partial>): void { + runInAction(() => { + for (const key in target) { + if (!(key in source)) { + delete target[key]; + } + } + + Object.assign(target, source); + }); +} diff --git a/packages/core/src/features/catalog/opening-entity-details.test.tsx b/packages/core/src/features/catalog/opening-entity-details.test.tsx index 6cb8c69265..0bfde28a7c 100644 --- a/packages/core/src/features/catalog/opening-entity-details.test.tsx +++ b/packages/core/src/features/catalog/opening-entity-details.test.tsx @@ -7,11 +7,10 @@ import type { DiContainer } from "@ogre-tools/injectable"; import type { RenderResult } from "@testing-library/react"; import { KubernetesCluster, WebLink } from "../../common/catalog-entities"; import getClusterByIdInjectable from "../../common/cluster-store/get-by-id.injectable"; -import type { Cluster } from "../../common/cluster/cluster"; +import { Cluster } from "../../common/cluster/cluster"; import navigateToCatalogInjectable from "../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable"; import { advanceFakeTime, testUsingFakeTime } from "../../common/test-utils/use-fake-time"; import catalogEntityRegistryInjectable from "../../renderer/api/catalog/entity/registry.injectable"; -import createClusterInjectable from "../../renderer/cluster/create-cluster.injectable"; import showEntityDetailsInjectable from "../../renderer/components/+catalog/entity-details/show.injectable"; import { type ApplicationBuilder, getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; @@ -41,8 +40,6 @@ describe("opening catalog entity details panel", () => { testUsingFakeTime(); builder.afterWindowStart((windowDi) => { - const createCluster = windowDi.inject(createClusterInjectable); - clusterEntity = new KubernetesCluster({ metadata: { labels: {}, @@ -85,7 +82,7 @@ describe("opening catalog entity details panel", () => { phase: "available", }, }); - cluster = createCluster({ + cluster = new Cluster({ contextName: clusterEntity.spec.kubeconfigContext, id: clusterEntity.getId(), kubeConfigPath: clusterEntity.spec.kubeconfigPath, diff --git a/packages/core/src/features/cluster/delete-dialog/delete-cluster-dialog.test.tsx b/packages/core/src/features/cluster/delete-dialog/delete-cluster-dialog.test.tsx index 5cbf8267de..000cc69432 100644 --- a/packages/core/src/features/cluster/delete-dialog/delete-cluster-dialog.test.tsx +++ b/packages/core/src/features/cluster/delete-dialog/delete-cluster-dialog.test.tsx @@ -5,16 +5,12 @@ import "@testing-library/jest-dom/extend-expect"; import { KubeConfig } from "@kubernetes/client-node"; import type { RenderResult } from "@testing-library/react"; -import type { CreateCluster } from "../../../common/cluster/create-cluster-injection-token"; -import { createClusterInjectionToken } from "../../../common/cluster/create-cluster-injection-token"; -import createContextHandlerInjectable from "../../../main/context-handler/create-context-handler.injectable"; -import createKubeconfigManagerInjectable from "../../../main/kubeconfig-manager/create-kubeconfig-manager.injectable"; import normalizedPlatformInjectable from "../../../common/vars/normalized-platform.injectable"; import kubectlBinaryNameInjectable from "../../../main/kubectl/binary-name.injectable"; import kubectlDownloadingNormalizedArchInjectable from "../../../main/kubectl/normalized-arch.injectable"; import openDeleteClusterDialogInjectable, { type OpenDeleteClusterDialog } from "../../../renderer/components/delete-cluster-dialog/open.injectable"; import { type ApplicationBuilder, getApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder"; -import type { Cluster } from "../../../common/cluster/cluster"; +import { Cluster } from "../../../common/cluster/cluster"; import navigateToCatalogInjectable from "../../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable"; import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable"; import joinPathsInjectable from "../../../common/path/join-paths.injectable"; @@ -73,7 +69,6 @@ users: describe("Deleting a cluster", () => { let builder: ApplicationBuilder; let openDeleteClusterDialog: OpenDeleteClusterDialog; - let createCluster: CreateCluster; let rendered: RenderResult; let config: KubeConfig; @@ -82,8 +77,6 @@ describe("Deleting a cluster", () => { builder = getApplicationBuilder(); builder.beforeApplicationStart((mainDi) => { - mainDi.override(createContextHandlerInjectable, () => () => undefined as never); - mainDi.override(createKubeconfigManagerInjectable, () => () => undefined as never); mainDi.override(kubectlBinaryNameInjectable, () => "kubectl"); mainDi.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64"); mainDi.override(normalizedPlatformInjectable, () => "darwin"); @@ -94,8 +87,6 @@ describe("Deleting a cluster", () => { }); builder.afterWindowStart(windowDi => { - createCluster = windowDi.inject(createClusterInjectionToken); - const navigateToCatalog = windowDi.inject(navigateToCatalogInjectable); navigateToCatalog(); @@ -111,7 +102,7 @@ describe("Deleting a cluster", () => { beforeEach(() => { config.loadFromString(multiClusterConfig); - currentCluster = createCluster({ + currentCluster = new Cluster({ id: "some-current-context-cluster", contextName: "some-current-context", preferences: { @@ -121,7 +112,7 @@ describe("Deleting a cluster", () => { }, { clusterServerUrl: currentClusterServerUrl, }); - nonCurrentCluster = createCluster({ + nonCurrentCluster = new Cluster({ id: "some-non-current-context-cluster", contextName: "some-non-current-context", preferences: { @@ -199,7 +190,7 @@ describe("Deleting a cluster", () => { const directoryForKubeConfigs = builder.applicationWindow.only.di.inject(directoryForKubeConfigsInjectable); const joinPaths = builder.applicationWindow.only.di.inject(joinPathsInjectable); - currentCluster = createCluster({ + currentCluster = new Cluster({ id: "some-cluster", contextName: "some-context", preferences: { @@ -235,7 +226,7 @@ describe("Deleting a cluster", () => { beforeEach(() => { config.loadFromString(singleClusterConfig); - currentCluster = createCluster({ + currentCluster = new Cluster({ id: "some-cluster", contextName: "some-context", preferences: { diff --git a/packages/core/src/features/cluster/delete-dialog/main/delete-channel-listener.injectable.ts b/packages/core/src/features/cluster/delete-dialog/main/delete-channel-listener.injectable.ts index b2ac54d5e4..5421bc952a 100644 --- a/packages/core/src/features/cluster/delete-dialog/main/delete-channel-listener.injectable.ts +++ b/packages/core/src/features/cluster/delete-dialog/main/delete-channel-listener.injectable.ts @@ -9,6 +9,7 @@ import directoryForLensLocalStorageInjectable from "../../../../common/directory import removePathInjectable from "../../../../common/fs/remove.injectable"; import joinPathsInjectable from "../../../../common/path/join-paths.injectable"; import { noop } from "../../../../common/utils"; +import clusterConnectionInjectable from "../../../../main/cluster/cluster-connection.injectable"; import { getRequestChannelListenerInjectable } from "../../../../main/utils/channel/channel-listeners/listener-tokens"; import { deleteClusterChannel } from "../common/delete-channel"; @@ -31,7 +32,9 @@ const deleteClusterChannelListenerInjectable = getRequestChannelListenerInjectab return; } - cluster.disconnect(); + const clusterConnection = di.inject(clusterConnectionInjectable, cluster); + + clusterConnection.disconnect(); clusterFrames.delete(cluster.id); // Remove from the cluster store as well, this should clear any old settings diff --git a/packages/core/src/features/cluster/namespaces/edit-namespace-from-new-tab.test.tsx b/packages/core/src/features/cluster/namespaces/edit-namespace-from-new-tab.test.tsx index 52796fc314..1780186509 100644 --- a/packages/core/src/features/cluster/namespaces/edit-namespace-from-new-tab.test.tsx +++ b/packages/core/src/features/cluster/namespaces/edit-namespace-from-new-tab.test.tsx @@ -64,9 +64,11 @@ describe("cluster/namespaces - edit namespace from new tab", () => { windowDi.override(callForPatchResourceInjectable, () => callForPatchResourceMock); }); - builder.allowKubeResource({ - apiName: "namespaces", - group: "", + builder.afterWindowStart(() => { + builder.allowKubeResource({ + apiName: "namespaces", + group: "", + }); }); }); diff --git a/packages/core/src/features/cluster/namespaces/edit-namespace-from-previously-opened-tab.test.tsx b/packages/core/src/features/cluster/namespaces/edit-namespace-from-previously-opened-tab.test.tsx index c5bbd15dfc..6ec5447bd6 100644 --- a/packages/core/src/features/cluster/namespaces/edit-namespace-from-previously-opened-tab.test.tsx +++ b/packages/core/src/features/cluster/namespaces/edit-namespace-from-previously-opened-tab.test.tsx @@ -35,9 +35,11 @@ describe("cluster/namespaces - edit namespaces from previously opened tab", () = windowDi.override(callForResourceInjectable, () => callForNamespaceMock); }); - builder.allowKubeResource({ - apiName: "namespaces", - group: "", + builder.afterWindowStart(() => { + builder.allowKubeResource({ + apiName: "namespaces", + group: "", + }); }); }); diff --git a/packages/core/src/features/cluster/workload-overview.test.tsx b/packages/core/src/features/cluster/workload-overview.test.tsx index 725ec04e90..c08dcba0f7 100644 --- a/packages/core/src/features/cluster/workload-overview.test.tsx +++ b/packages/core/src/features/cluster/workload-overview.test.tsx @@ -9,20 +9,24 @@ import { type ApplicationBuilder, getApplicationBuilder } from "../../renderer/c describe("workload overview", () => { let rendered: RenderResult; - let applicationBuilder: ApplicationBuilder; + let builder: ApplicationBuilder; beforeEach(async () => { - applicationBuilder = getApplicationBuilder().setEnvironmentToClusterFrame(); - applicationBuilder.allowKubeResource({ - apiName: "pods", - group: "", + builder = getApplicationBuilder().setEnvironmentToClusterFrame(); + + builder.afterWindowStart(() => { + builder.allowKubeResource({ + apiName: "pods", + group: "", + }); }); - rendered = await applicationBuilder.render(); + + rendered = await builder.render(); }); describe("when navigating to workload overview", () => { beforeEach(() => { - applicationBuilder.navigateWith(navigateToWorkloadsOverviewInjectable); + builder.navigateWith(navigateToWorkloadsOverviewInjectable); }); it("renders", () => { diff --git a/packages/core/src/features/entity-settings/showing-settings-for-correct-entity.test.tsx b/packages/core/src/features/entity-settings/showing-settings-for-correct-entity.test.tsx index aafb4ca720..3a24395b49 100644 --- a/packages/core/src/features/entity-settings/showing-settings-for-correct-entity.test.tsx +++ b/packages/core/src/features/entity-settings/showing-settings-for-correct-entity.test.tsx @@ -7,11 +7,10 @@ import type { DiContainer } from "@ogre-tools/injectable"; import type { RenderResult } from "@testing-library/react"; import { KubernetesCluster, WebLink } from "../../common/catalog-entities"; import getClusterByIdInjectable from "../../common/cluster-store/get-by-id.injectable"; -import type { Cluster } from "../../common/cluster/cluster"; +import { Cluster } from "../../common/cluster/cluster"; import navigateToEntitySettingsInjectable from "../../common/front-end-routing/routes/entity-settings/navigate-to-entity-settings.injectable"; import catalogEntityRegistryInjectable from "../../renderer/api/catalog/entity/registry.injectable"; import { type ApplicationBuilder, getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; -import createClusterInjectable from "../../renderer/cluster/create-cluster.injectable"; describe("Showing correct entity settings", () => { let builder: ApplicationBuilder; @@ -37,8 +36,6 @@ describe("Showing correct entity settings", () => { }); builder.afterWindowStart((windowDi) => { - const createCluster = windowDi.inject(createClusterInjectable); - clusterEntity = new KubernetesCluster({ metadata: { labels: {}, @@ -81,7 +78,7 @@ describe("Showing correct entity settings", () => { phase: "available", }, }); - cluster = createCluster({ + cluster = new Cluster({ contextName: clusterEntity.spec.kubeconfigContext, id: clusterEntity.getId(), kubeConfigPath: clusterEntity.spec.kubeconfigPath, diff --git a/packages/core/src/main/__test__/cluster.test.ts b/packages/core/src/main/__test__/cluster.test.ts index 721188d059..bb495af847 100644 --- a/packages/core/src/main/__test__/cluster.test.ts +++ b/packages/core/src/main/__test__/cluster.test.ts @@ -2,35 +2,29 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ -import broadcastMessageInjectable from "../../common/ipc/broadcast-message.injectable"; -import type { Cluster } from "../../common/cluster/cluster"; +import { Cluster } from "../../common/cluster/cluster"; import { Kubectl } from "../kubectl/kubectl"; import { getDiForUnitTesting } from "../getDiForUnitTesting"; -import type { CreateCluster } from "../../common/cluster/create-cluster-injection-token"; -import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token"; -import authorizationReviewInjectable from "../../common/cluster/authorization-review.injectable"; +import createAuthorizationReviewInjectable from "../../common/cluster/authorization-review.injectable"; import requestNamespaceListPermissionsForInjectable from "../../common/cluster/request-namespace-list-permissions.injectable"; -import listNamespacesInjectable from "../../common/cluster/list-namespaces.injectable"; -import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable"; -import type { ClusterContextHandler } from "../context-handler/context-handler"; -import { parse } from "url"; +import createListNamespacesInjectable from "../../common/cluster/list-namespaces.injectable"; +import prometheusHandlerInjectable from "../cluster/prometheus-handler/prometheus-handler.injectable"; import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; import directoryForTempInjectable from "../../common/app-paths/directory-for-temp/directory-for-temp.injectable"; import normalizedPlatformInjectable from "../../common/vars/normalized-platform.injectable"; import kubectlBinaryNameInjectable from "../kubectl/binary-name.injectable"; import kubectlDownloadingNormalizedArchInjectable from "../kubectl/normalized-arch.injectable"; -import pathExistsSyncInjectable from "../../common/fs/path-exists-sync.injectable"; -import pathExistsInjectable from "../../common/fs/path-exists.injectable"; -import readJsonSyncInjectable from "../../common/fs/read-json-sync.injectable"; -import writeJsonSyncInjectable from "../../common/fs/write-json-sync.injectable"; +import type { ClusterConnection } from "../cluster/cluster-connection.injectable"; +import clusterConnectionInjectable from "../cluster/cluster-connection.injectable"; +import kubeconfigManagerInjectable from "../kubeconfig-manager/kubeconfig-manager.injectable"; +import type { KubeconfigManager } from "../kubeconfig-manager/kubeconfig-manager"; +import broadcastConnectionUpdateInjectable from "../cluster/broadcast-connection-update.injectable"; describe("create clusters", () => { let cluster: Cluster; - let createCluster: CreateCluster; + let clusterConnection: ClusterConnection; beforeEach(() => { - jest.clearAllMocks(); - const di = getDiForUnitTesting(); const clusterServerUrl = "https://192.168.64.3:8443"; @@ -39,65 +33,51 @@ describe("create clusters", () => { di.override(kubectlBinaryNameInjectable, () => "kubectl"); di.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64"); di.override(normalizedPlatformInjectable, () => "darwin"); - di.override(broadcastMessageInjectable, () => async () => {}); - di.override(authorizationReviewInjectable, () => () => () => Promise.resolve(true)); + di.override(broadcastConnectionUpdateInjectable, () => async () => {}); + di.override(createAuthorizationReviewInjectable, () => () => () => Promise.resolve(true)); di.override(requestNamespaceListPermissionsForInjectable, () => () => async () => () => true); - di.override(listNamespacesInjectable, () => () => () => Promise.resolve([ "default" ])); - di.override(createContextHandlerInjectable, () => (cluster) => ({ - restartServer: jest.fn(), - stopServer: jest.fn(), - clusterUrl: parse(cluster.apiUrl), - getApiTarget: jest.fn(), + di.override(createListNamespacesInjectable, () => () => () => Promise.resolve([ "default" ])); + di.override(prometheusHandlerInjectable, () => ({ getPrometheusDetails: jest.fn(), - resolveAuthProxyCa: jest.fn(), - resolveAuthProxyUrl: jest.fn(), setupPrometheus: jest.fn(), - ensureServer: jest.fn(), - } as ClusterContextHandler)); - di.override(pathExistsInjectable, () => () => { throw new Error("tried call pathExists without override"); }); - di.override(pathExistsSyncInjectable, () => () => { throw new Error("tried call pathExistsSync without override"); }); - di.override(readJsonSyncInjectable, () => () => { throw new Error("tried call readJsonSync without override"); }); - di.override(writeJsonSyncInjectable, () => () => { throw new Error("tried call writeJsonSync without override"); }); + })); - createCluster = di.inject(createClusterInjectionToken); + di.override(kubeconfigManagerInjectable, () => ({ + ensurePath: async () => "/some-proxy-kubeconfig-file", + } as Partial as KubeconfigManager)); jest.spyOn(Kubectl.prototype, "ensureKubectl").mockReturnValue(Promise.resolve(true)); - cluster = createCluster({ + cluster = new Cluster({ id: "foo", contextName: "minikube", kubeConfigPath: "minikube-config.yml", }, { clusterServerUrl, }); - }); - afterEach(() => { - cluster.disconnect(); + clusterConnection = di.inject(clusterConnectionInjectable, cluster); }); it("should be able to create a cluster from a cluster model and apiURL should be decoded", () => { - expect(cluster.apiUrl).toBe("https://192.168.64.3:8443"); + expect(cluster.apiUrl.get()).toBe("https://192.168.64.3:8443"); }); it("reconnect should not throw if contextHandler is missing", () => { - expect(() => cluster.reconnect()).not.toThrowError(); + expect(() => clusterConnection.reconnect()).not.toThrowError(); }); it("disconnect should not throw if contextHandler is missing", () => { - expect(() => cluster.disconnect()).not.toThrowError(); + expect(() => clusterConnection.disconnect()).not.toThrowError(); }); it("activating cluster should try to connect to cluster and do a refresh", async () => { - jest.spyOn(cluster, "reconnect"); - jest.spyOn(cluster, "refreshConnectionStatus"); + jest.spyOn(clusterConnection, "reconnect").mockImplementation(async () => {}); + jest.spyOn(clusterConnection, "refreshConnectionStatus").mockImplementation(async () => {}); - await cluster.activate(); + await clusterConnection.activate(); - expect(cluster.reconnect).toBeCalled(); - expect(cluster.refreshConnectionStatus).toBeCalled(); - - cluster.disconnect(); - jest.resetAllMocks(); + expect(clusterConnection.reconnect).toBeCalled(); + expect(clusterConnection.refreshConnectionStatus).toBeCalled(); }); }); diff --git a/packages/core/src/main/__test__/context-handler.test.ts b/packages/core/src/main/__test__/context-handler.test.ts index e6ac1cce8c..61bd12398d 100644 --- a/packages/core/src/main/__test__/context-handler.test.ts +++ b/packages/core/src/main/__test__/context-handler.test.ts @@ -3,16 +3,20 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type { ClusterContextHandler } from "../context-handler/context-handler"; import { getDiForUnitTesting } from "../getDiForUnitTesting"; -import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable"; -import type { Cluster } from "../../common/cluster/cluster"; +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, @@ -41,22 +45,30 @@ const createTestPrometheusProvider = (kind: string, alwaysFail: ServiceResult): }, }); -const clusterStub = { - getProxyKubeconfig: () => ({ - makeApiClient: (): void => undefined, - }), - apiUrl: "http://localhost:81", -} as unknown as Cluster; - describe("ContextHandler", () => { - let createContextHandler: (cluster: Cluster) => ClusterContextHandler; let di: DiContainer; + let cluster: Cluster; beforeEach(() => { di = getDiForUnitTesting(); - di.override(createKubeAuthProxyInjectable, () => ({} as any)); - createContextHandler = di.inject(createContextHandlerInjectable); + di.override(loadProxyKubeconfigInjectable, () => async () => ({ + makeApiClient: () => ({} as any), + } as Partial)); + + di.override(createKubeAuthProxyInjectable, () => () => ({ + run: async () => {}, + } as KubeAuthProxy)); + di.override(directoryForTempInjectable, () => "/some-directory-for-tmp"); + di.inject(lensProxyPortInjectable).set(9968); + + cluster = new Cluster({ + contextName: "some-context-name", + id: "some-cluster-id", + kubeConfigPath: "/some-kubeconfig-path", + }, { + clusterServerUrl: "https://some-website.com", + }); }); describe("getPrometheusService", () => { @@ -76,7 +88,7 @@ describe("ContextHandler", () => { } }); - expect(() => createContextHandler(clusterStub).getPrometheusDetails()).rejects.toThrowError(); + expect(() => di.inject(prometheusHandlerInjectable, cluster).getPrometheusDetails()).rejects.toThrowError(); }); it.each([ @@ -107,7 +119,7 @@ describe("ContextHandler", () => { } }); - const details = await createContextHandler(clusterStub).getPrometheusDetails(); + const details = await di.inject(prometheusHandlerInjectable, cluster).getPrometheusDetails(); expect(details.provider.kind === `id_failure_${failures}`); }); @@ -140,7 +152,7 @@ describe("ContextHandler", () => { } }); - const details = await createContextHandler(clusterStub).getPrometheusDetails(); + const details = await di.inject(prometheusHandlerInjectable, cluster).getPrometheusDetails(); expect(details.provider.kind === "id_failure_0"); }); @@ -183,7 +195,7 @@ describe("ContextHandler", () => { } }); - const details = await createContextHandler(clusterStub).getPrometheusDetails(); + const details = await di.inject(prometheusHandlerInjectable, cluster).getPrometheusDetails(); expect(details.provider.kind === "id_success_0"); }); diff --git a/packages/core/src/main/__test__/kube-auth-proxy.test.ts b/packages/core/src/main/__test__/kube-auth-proxy.test.ts index 5864259712..8fa8a175a5 100644 --- a/packages/core/src/main/__test__/kube-auth-proxy.test.ts +++ b/packages/core/src/main/__test__/kube-auth-proxy.test.ts @@ -4,7 +4,7 @@ */ import waitUntilPortIsUsedInjectable from "../kube-auth-proxy/wait-until-port-is-used/wait-until-port-is-used.injectable"; -import type { 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 { Kubectl } from "../kubectl/kubectl"; @@ -14,8 +14,6 @@ import type { Readable } from "stream"; import { EventEmitter } from "stream"; import { getDiForUnitTesting } from "../getDiForUnitTesting"; import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable"; -import type { CreateCluster } from "../../common/cluster/create-cluster-injection-token"; -import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token"; import spawnInjectable from "../child-process/spawn.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"; @@ -31,7 +29,6 @@ import getBasenameOfPathInjectable from "../../common/path/get-basename.injectab const clusterServerUrl = "https://192.168.64.3:8443"; describe("kube auth proxy tests", () => { - let createCluster: CreateCluster; let createKubeAuthProxy: (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => KubeAuthProxy; let spawnMock: jest.Mock; let waitUntilPortIsUsedMock: jest.Mock; @@ -86,12 +83,11 @@ describe("kube auth proxy tests", () => { di.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64"); di.override(normalizedPlatformInjectable, () => "darwin"); - createCluster = di.inject(createClusterInjectionToken); createKubeAuthProxy = di.inject(createKubeAuthProxyInjectable); }); it("calling exit multiple times shouldn't throw", async () => { - const cluster = createCluster({ + const cluster = new Cluster({ id: "foobar", kubeConfigPath: "minikube-config.yml", contextName: "minikube", @@ -114,8 +110,11 @@ describe("kube auth proxy tests", () => { beforeEach(async () => { mockedCP = mockDeep(); listeners = new EventEmitter(); - const stderr = mockedCP.stderr = mock(); - const stdout = mockedCP.stdout = mock(); + const stderr = mock(); + const stdout = mock(); + + mockedCP.stderr = stderr as any; + mockedCP.stdout = stdout as any; jest.spyOn(Kubectl.prototype, "checkBinary").mockReturnValueOnce(Promise.resolve(true)); jest.spyOn(Kubectl.prototype, "ensureKubectl").mockReturnValueOnce(Promise.resolve(false)); @@ -124,32 +123,32 @@ describe("kube auth proxy tests", () => { return mockedCP; }); - mockedCP.stderr.on.mockImplementation((event: string | symbol, listener: (message: any, sendHandle: any) => void): Readable => { + stderr.on.mockImplementation((event: string | symbol, listener: (message: any, sendHandle: any) => void): Readable => { listeners.on(`stderr/${String(event)}`, listener); return stderr; }); - mockedCP.stderr.off.mockImplementation((event: string | symbol, listener: (message: any, sendHandle: any) => void): Readable => { + stderr.off.mockImplementation((event: string | symbol, listener: (message: any, sendHandle: any) => void): Readable => { listeners.off(`stderr/${String(event)}`, listener); return stderr; }); - mockedCP.stderr.removeListener.mockImplementation((event: string | symbol, listener: (message: any, sendHandle: any) => void): Readable => { + stderr.removeListener.mockImplementation((event: string | symbol, listener: (message: any, sendHandle: any) => void): Readable => { listeners.off(`stderr/${String(event)}`, listener); return stderr; }); - mockedCP.stderr.once.mockImplementation((event: string | symbol, listener: (message: any, sendHandle: any) => void): Readable => { + stderr.once.mockImplementation((event: string | symbol, listener: (message: any, sendHandle: any) => void): Readable => { listeners.once(`stderr/${String(event)}`, listener); return stderr; }); - mockedCP.stderr.removeAllListeners.mockImplementation((event?: string | symbol): Readable => { + stderr.removeAllListeners.mockImplementation((event?: string | symbol): Readable => { listeners.removeAllListeners(event ?? `stderr/${String(event)}`); return stderr; }); - mockedCP.stdout.on.mockImplementation((event: string | symbol, listener: (message: any, sendHandle: any) => void): Readable => { + stdout.on.mockImplementation((event: string | symbol, listener: (message: any, sendHandle: any) => void): Readable => { listeners.on(`stdout/${String(event)}`, listener); if (event === "data") { @@ -158,22 +157,22 @@ describe("kube auth proxy tests", () => { return stdout; }); - mockedCP.stdout.once.mockImplementation((event: string | symbol, listener: (message: any, sendHandle: any) => void): Readable => { + stdout.once.mockImplementation((event: string | symbol, listener: (message: any, sendHandle: any) => void): Readable => { listeners.once(`stdout/${String(event)}`, listener); return stdout; }); - mockedCP.stdout.off.mockImplementation((event: string | symbol, listener: (message: any, sendHandle: any) => void): Readable => { + stdout.off.mockImplementation((event: string | symbol, listener: (message: any, sendHandle: any) => void): Readable => { listeners.off(`stdout/${String(event)}`, listener); return stdout; }); - mockedCP.stdout.removeListener.mockImplementation((event: string | symbol, listener: (message: any, sendHandle: any) => void): Readable => { + stdout.removeListener.mockImplementation((event: string | symbol, listener: (message: any, sendHandle: any) => void): Readable => { listeners.off(`stdout/${String(event)}`, listener); return stdout; }); - mockedCP.stdout.removeAllListeners.mockImplementation((event?: string | symbol): Readable => { + stdout.removeAllListeners.mockImplementation((event?: string | symbol): Readable => { listeners.removeAllListeners(event ?? `stdout/${String(event)}`); return stdout; @@ -185,7 +184,7 @@ describe("kube auth proxy tests", () => { }); waitUntilPortIsUsedMock.mockReturnValueOnce(Promise.resolve()); - const cluster = createCluster({ + const cluster = new Cluster({ id: "foobar", kubeConfigPath: "minikube-config.yml", contextName: "minikube", @@ -194,37 +193,38 @@ describe("kube auth proxy tests", () => { }); proxy = createKubeAuthProxy(cluster, {}); + await proxy.run(); }); - it("should call spawn and broadcast errors", async () => { - await proxy.run(); + it("should call spawn and broadcast errors", () => { listeners.emit("error", { message: "foobarbat" }); expect(broadcastMessageMock).toBeCalledWith("cluster:foobar:connection-update", { message: "foobarbat", level: "error" }); }); - it("should call spawn and broadcast exit", async () => { - await proxy.run(); - listeners.emit("exit", 0); + it("should call spawn and broadcast exit as error when exitCode != 0", () => { + listeners.emit("exit", 1); - expect(broadcastMessageMock).toBeCalledWith("cluster:foobar:connection-update", { message: "proxy exited with code: 0", level: "info" }); + expect(broadcastMessageMock).toBeCalledWith("cluster:foobar:connection-update", { message: "proxy exited with code: 1", level: "error" }); }); - it("should call spawn and broadcast errors from stderr", async () => { - await proxy.run(); + it("should call spawn and broadcast exit as info when exitCode == 0", () => { + listeners.emit("exit", 0); + + expect(broadcastMessageMock).toBeCalledWith("cluster:foobar:connection-update", { message: "proxy exited successfully", level: "info" }); + }); + + it("should call spawn and broadcast errors from stderr", () => { listeners.emit("stderr/data", "an error"); expect(broadcastMessageMock).toBeCalledWith("cluster:foobar:connection-update", { message: "an error", level: "error" }); }); - it("should call spawn and broadcast stdout serving info", async () => { - await proxy.run(); - + it("should call spawn and broadcast stdout serving info", () => { expect(broadcastMessageMock).toBeCalledWith("cluster:foobar:connection-update", { message: "Authentication proxy started", level: "info" }); }); - it("should call spawn and broadcast stdout other info", async () => { - await proxy.run(); + it("should call spawn and broadcast stdout other info", () => { listeners.emit("stdout/data", "some info"); expect(broadcastMessageMock).toBeCalledWith("cluster:foobar:connection-update", { message: "some info", level: "info" }); diff --git a/packages/core/src/main/__test__/kubeconfig-manager.test.ts b/packages/core/src/main/__test__/kubeconfig-manager.test.ts index 4ccb88ba45..1205ba4f8f 100644 --- a/packages/core/src/main/__test__/kubeconfig-manager.test.ts +++ b/packages/core/src/main/__test__/kubeconfig-manager.test.ts @@ -3,14 +3,11 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getDiForUnitTesting } from "../getDiForUnitTesting"; -import { KubeconfigManager } from "../kubeconfig-manager/kubeconfig-manager"; -import type { Cluster } from "../../common/cluster/cluster"; -import createKubeconfigManagerInjectable from "../kubeconfig-manager/create-kubeconfig-manager.injectable"; -import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token"; +import type { KubeconfigManager } from "../kubeconfig-manager/kubeconfig-manager"; +import { Cluster } from "../../common/cluster/cluster"; +import kubeconfigManagerInjectable from "../kubeconfig-manager/kubeconfig-manager.injectable"; import directoryForTempInjectable from "../../common/app-paths/directory-for-temp/directory-for-temp.injectable"; -import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable"; import type { DiContainer } from "@ogre-tools/injectable"; -import { parse } from "url"; import loggerInjectable from "../../common/logger.injectable"; import type { Logger } from "../../common/logger"; import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; @@ -30,12 +27,13 @@ import removePathInjectable from "../../common/fs/remove.injectable"; import pathExistsSyncInjectable from "../../common/fs/path-exists-sync.injectable"; import readJsonSyncInjectable from "../../common/fs/read-json-sync.injectable"; import writeJsonSyncInjectable from "../../common/fs/write-json-sync.injectable"; +import kubeAuthProxyServerInjectable from "../cluster/kube-auth-proxy-server.injectable"; +import lensProxyPortInjectable from "../lens-proxy/lens-proxy-port.injectable"; const clusterServerUrl = "https://192.168.64.3:8443"; describe("kubeconfig manager tests", () => { let clusterFake: Cluster; - let createKubeconfigManager: (cluster: Cluster) => KubeconfigManager; let di: DiContainer; let loggerMock: jest.Mocked; let readFileMock: AsyncFnMock; @@ -56,6 +54,7 @@ describe("kubeconfig manager tests", () => { di.override(pathExistsSyncInjectable, () => () => { throw new Error("tried call pathExistsSync without override"); }); di.override(readJsonSyncInjectable, () => () => { throw new Error("tried call readJsonSync without override"); }); di.override(writeJsonSyncInjectable, () => () => { throw new Error("tried call writeJsonSync without override"); }); + di.inject(lensProxyPortInjectable).set(9191); readFileMock = asyncFn(); di.override(readFileInjectable, () => readFileMock); @@ -78,23 +77,15 @@ describe("kubeconfig manager tests", () => { ensureServerMock = asyncFn(); - di.override(createContextHandlerInjectable, () => (cluster) => ({ - restartServer: jest.fn(), - stopServer: jest.fn(), - clusterUrl: parse(cluster.apiUrl), + di.override(kubeAuthProxyServerInjectable, () => ({ + restart: jest.fn(), + stop: jest.fn(), getApiTarget: jest.fn(), - getPrometheusDetails: jest.fn(), - resolveAuthProxyCa: jest.fn(), - resolveAuthProxyUrl: jest.fn(), - setupPrometheus: jest.fn(), - ensureServer: ensureServerMock, + ensureRunning: ensureServerMock, + ensureAuthProxyUrl: jest.fn(), })); - const createCluster = di.inject(createClusterInjectionToken); - - createKubeconfigManager = di.inject(createKubeconfigManagerInjectable); - - clusterFake = createCluster({ + clusterFake = new Cluster({ id: "foo", contextName: "minikube", kubeConfigPath: "/minikube-config.yml", @@ -102,9 +93,7 @@ describe("kubeconfig manager tests", () => { clusterServerUrl, }); - jest.spyOn(KubeconfigManager.prototype, "resolveProxyUrl", "get").mockReturnValue("https://127.0.0.1:9191/foo"); - - kubeConfManager = createKubeconfigManager(clusterFake); + kubeConfManager = di.inject(kubeconfigManagerInjectable, clusterFake); }); describe("when calling clear", () => { @@ -123,7 +112,7 @@ describe("kubeconfig manager tests", () => { let getPathPromise: Promise; beforeEach(async () => { - getPathPromise = kubeConfManager.getPath(); + getPathPromise = kubeConfManager.ensurePath(); }); it("should not call pathExists()", () => { @@ -174,7 +163,7 @@ describe("kubeconfig manager tests", () => { beforeEach(async () => { await writeFileMock.resolveSpecific( [ - "/some-directory-for-temp/kubeconfig-foo", + "/some-directory-for-temp/kubeconfig-foo", "apiVersion: v1\nkind: Config\npreferences: {}\ncurrent-context: minikube\nclusters:\n - name: minikube\n cluster:\n certificate-authority-data: PGNhLWRhdGE+\n server: https://127.0.0.1:9191/foo\n insecure-skip-tls-verify: false\ncontexts:\n - name: minikube\n context:\n cluster: minikube\n user: proxy\nusers:\n - name: proxy\n user:\n username: lens\n password: fake\n", ], ); @@ -234,7 +223,7 @@ describe("kubeconfig manager tests", () => { let getPathPromise: Promise; beforeEach(async () => { - getPathPromise = kubeConfManager.getPath(); + getPathPromise = kubeConfManager.ensurePath(); }); it("should call pathExists", () => { @@ -303,7 +292,7 @@ describe("kubeconfig manager tests", () => { beforeEach(async () => { await writeFileMock.resolveSpecific( [ - "/some-directory-for-temp/kubeconfig-foo", + "/some-directory-for-temp/kubeconfig-foo", "apiVersion: v1\nkind: Config\npreferences: {}\ncurrent-context: minikube\nclusters:\n - name: minikube\n cluster:\n certificate-authority-data: PGNhLWRhdGE+\n server: https://127.0.0.1:9191/foo\n insecure-skip-tls-verify: false\ncontexts:\n - name: minikube\n context:\n cluster: minikube\n user: proxy\nusers:\n - name: proxy\n user:\n username: lens\n password: fake\n", ], ); diff --git a/packages/core/src/main/__test__/prometheus-handler.test.ts b/packages/core/src/main/__test__/prometheus-handler.test.ts new file mode 100644 index 0000000000..59671553bb --- /dev/null +++ b/packages/core/src/main/__test__/prometheus-handler.test.ts @@ -0,0 +1,213 @@ +/** + * 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 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 loadProxyKubeconfigInjectable from "../cluster/load-proxy-kubeconfig.injectable"; +import { 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, (di, cluster) => async () => { + const res = new KubeConfig(); + + res.addCluster({ + name: "some-cluster-name", + server: cluster.apiUrl.get(), + skipTLSVerify: false, + }); + res.addContext({ + cluster: "some-cluster-name", + name: "some-context-name", + user: "some-user-name", + }); + res.addUser({ + name: "some-user-name", + }); + res.setCurrentContext("some-context-name"); + + return res; + }); + di.override(directoryForTempInjectable, () => "/some-temp-dir"); + di.inject(lensProxyPortInjectable).set(12345); + + cluster = new Cluster({ + contextName: "some-context-name", + id: "some-cluster-id", + kubeConfigPath: "/some/path", + }, { + clusterServerUrl: "http://localhost:81", + }); + }); + + describe("getPrometheusService", () => { + it.each([ + [0], + [1], + [2], + [3], + ])("should throw after %d failure(s)", async (failures) => { + runInAction(() => { + for (let i = 0; i < failures; i += 1) { + di.register(getInjectable({ + id: `test-prometheus-provider-failure-${i}`, + injectionToken: prometheusProviderInjectionToken, + instantiate: () => createTestPrometheusProvider(`id_failure_${i}`, ServiceResult.Failure), + })); + } + }); + + expect(() => di.inject(prometheusHandlerInjectable, cluster).getPrometheusDetails()).rejects.toThrowError(); + }); + + it.each([ + [1, 0], + [1, 1], + [1, 2], + [1, 3], + [2, 0], + [2, 1], + [2, 2], + [2, 3], + ])("should pick the first provider of %d success(es) after %d failure(s)", async (successes, failures) => { + runInAction(() => { + for (let i = 0; i < failures; i += 1) { + di.register(getInjectable({ + id: `test-prometheus-provider-failure-${i}`, + injectionToken: prometheusProviderInjectionToken, + instantiate: () => createTestPrometheusProvider(`id_failure_${i}`, ServiceResult.Failure), + })); + } + + for (let i = 0; i < successes; i += 1) { + di.register(getInjectable({ + id: `test-prometheus-provider-success-${i}`, + injectionToken: prometheusProviderInjectionToken, + instantiate: () => createTestPrometheusProvider(`id_success_${i}`, ServiceResult.Success), + })); + } + }); + + const details = await di.inject(prometheusHandlerInjectable, cluster).getPrometheusDetails(); + + expect(details.provider.kind === `id_failure_${failures}`); + }); + + it.each([ + [1, 0], + [1, 1], + [1, 2], + [1, 3], + [2, 0], + [2, 1], + [2, 2], + [2, 3], + ])("should pick the first provider of %d success(es) before %d failure(s)", async (successes, failures) => { + runInAction(() => { + for (let i = 0; i < failures; i += 1) { + di.register(getInjectable({ + id: `test-prometheus-provider-failure-${i}`, + injectionToken: prometheusProviderInjectionToken, + instantiate: () => createTestPrometheusProvider(`id_failure_${i}`, ServiceResult.Failure), + })); + } + + for (let i = 0; i < successes; i += 1) { + di.register(getInjectable({ + id: `test-prometheus-provider-success-${i}`, + injectionToken: prometheusProviderInjectionToken, + instantiate: () => createTestPrometheusProvider(`id_success_${i}`, ServiceResult.Success), + })); + } + }); + + const details = await di.inject(prometheusHandlerInjectable, cluster).getPrometheusDetails(); + + expect(details.provider.kind === "id_failure_0"); + }); + + it.each([ + [1, 0], + [1, 1], + [1, 2], + [1, 3], + [2, 0], + [2, 1], + [2, 2], + [2, 3], + ])("should pick the first provider of %d success(es) between %d failure(s)", async (successes, failures) => { + const beforeSuccesses = Math.floor(successes / 2); + + runInAction(() => { + for (let i = 0; i < beforeSuccesses; i += 1) { + di.register(getInjectable({ + id: `test-prometheus-provider-success-${i}`, + injectionToken: prometheusProviderInjectionToken, + instantiate: () => createTestPrometheusProvider(`id_success_${i}`, ServiceResult.Success), + })); + } + + for (let i = 0; i < failures; i += 1) { + di.register(getInjectable({ + id: `test-prometheus-provider-failure-${i}`, + injectionToken: prometheusProviderInjectionToken, + instantiate: () => createTestPrometheusProvider(`id_failure_${i}`, ServiceResult.Failure), + })); + } + + for (let i = beforeSuccesses; i < successes; i += 1) { + di.register(getInjectable({ + id: `test-prometheus-provider-success-${i}`, + injectionToken: prometheusProviderInjectionToken, + instantiate: () => createTestPrometheusProvider(`id_success_${i}`, ServiceResult.Success), + })); + } + }); + + const details = await di.inject(prometheusHandlerInjectable, cluster).getPrometheusDetails(); + + expect(details.provider.kind === "id_success_0"); + }); + }); +}); diff --git a/packages/core/src/main/catalog-sources/__test__/kubeconfig-sync.test.ts b/packages/core/src/main/catalog-sources/__test__/kubeconfig-sync.test.ts index 8dbdb87d47..cfeaba0a65 100644 --- a/packages/core/src/main/catalog-sources/__test__/kubeconfig-sync.test.ts +++ b/packages/core/src/main/catalog-sources/__test__/kubeconfig-sync.test.ts @@ -3,7 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { observable, ObservableMap } from "mobx"; +import { observable, ObservableMap, runInAction } from "mobx"; import type { CatalogEntity } from "../../../common/catalog"; import { loadFromOptions } from "../../../common/kube-helpers"; import type { Cluster } from "../../../common/cluster/cluster"; @@ -34,6 +34,8 @@ import pathExistsSyncInjectable from "../../../common/fs/path-exists-sync.inject import pathExistsInjectable from "../../../common/fs/path-exists.injectable"; import readJsonSyncInjectable from "../../../common/fs/read-json-sync.injectable"; import writeJsonSyncInjectable from "../../../common/fs/write-json-sync.injectable"; +import type { KubeconfigManager } from "../../kubeconfig-manager/kubeconfig-manager"; +import kubeconfigManagerInjectable from "../../kubeconfig-manager/kubeconfig-manager.injectable"; describe("kubeconfig-sync.source tests", () => { let computeKubeconfigDiff: ComputeKubeconfigDiff; @@ -52,6 +54,10 @@ describe("kubeconfig-sync.source tests", () => { di.override(readJsonSyncInjectable, () => () => { throw new Error("tried call readJsonSync without override"); }); di.override(writeJsonSyncInjectable, () => () => { throw new Error("tried call writeJsonSync without override"); }); + di.override(kubeconfigManagerInjectable, () => ({ + ensurePath: async () => "/some-proxy-kubeconfig-file", + } as Partial as KubeconfigManager)); + clusters = new Map(); di.override(getClusterByIdInjectable, () => id => clusters.get(id)); @@ -79,7 +85,7 @@ describe("kubeconfig-sync.source tests", () => { const config = loadFromOptions({ clusters: [{ name: "cluster-name", - server: "1.2.3.4", + server: "https://1.2.3.4", skipTLSVerify: false, }], users: [{ @@ -117,7 +123,7 @@ describe("kubeconfig-sync.source tests", () => { clusters: [{ name: "cluster-name", cluster: { - server: "1.2.3.4", + server: "https://1.2.3.4", }, skipTLSVerify: false, }], @@ -149,8 +155,10 @@ describe("kubeconfig-sync.source tests", () => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const c = (iter.first(rootSource.values())!)[0]; - expect(c.kubeConfigPath).toBe("/bar"); - expect(c.contextName).toBe("context-name"); + runInAction(() => { + expect(c.kubeConfigPath.get()).toBe("/bar"); + expect(c.contextName.get()).toBe("context-name"); + }); }); it("should remove a cluster when it is removed from the contents", () => { @@ -158,7 +166,7 @@ describe("kubeconfig-sync.source tests", () => { clusters: [{ name: "cluster-name", cluster: { - server: "1.2.3.4", + server: "https://1.2.3.4", }, skipTLSVerify: false, }], @@ -190,8 +198,8 @@ describe("kubeconfig-sync.source tests", () => { const c = rootSource.values().next().value[0] as Cluster; - expect(c.kubeConfigPath).toBe("/bar"); - expect(c.contextName).toBe("context-name"); + expect(c.kubeConfigPath.get()).toBe("/bar"); + expect(c.contextName.get()).toBe("context-name"); computeKubeconfigDiff("{}", rootSource, filePath); @@ -203,7 +211,7 @@ describe("kubeconfig-sync.source tests", () => { clusters: [{ name: "cluster-name", cluster: { - server: "1.2.3.4", + server: "https://1.2.3.4", }, skipTLSVerify: false, }], @@ -243,15 +251,17 @@ describe("kubeconfig-sync.source tests", () => { { const c = rootSource.values().next().value[0] as Cluster; - expect(c.kubeConfigPath).toBe("/bar"); - expect(["context-name", "context-name-2"].includes(c.contextName)).toBe(true); + runInAction(() => { + expect(c.kubeConfigPath.get()).toBe("/bar"); + expect(["context-name", "context-name-2"].includes(c.contextName.get())).toBe(true); + }); } const newContents = JSON.stringify({ clusters: [{ name: "cluster-name", cluster: { - server: "1.2.3.4", + server: "https://1.2.3.4", }, skipTLSVerify: false, }], @@ -283,8 +293,8 @@ describe("kubeconfig-sync.source tests", () => { { const c = rootSource.values().next().value[0] as Cluster; - expect(c.kubeConfigPath).toBe("/bar"); - expect(c.contextName).toBe("context-name"); + expect(c.kubeConfigPath.get()).toBe("/bar"); + expect(c.contextName.get()).toBe("context-name"); } }); }); @@ -444,7 +454,7 @@ const foobarConfig = JSON.stringify({ clusters: [{ name: "cluster-name", cluster: { - server: "1.2.3.4", + server: "https://1.2.3.4", }, skipTLSVerify: false, }], diff --git a/packages/core/src/main/catalog-sources/kubeconfig-sync/compute-diff.injectable.ts b/packages/core/src/main/catalog-sources/kubeconfig-sync/compute-diff.injectable.ts index cb1e62e6e8..3b81e82382 100644 --- a/packages/core/src/main/catalog-sources/kubeconfig-sync/compute-diff.injectable.ts +++ b/packages/core/src/main/catalog-sources/kubeconfig-sync/compute-diff.injectable.ts @@ -10,13 +10,13 @@ import { homedir } from "os"; import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable"; import type { CatalogEntity } from "../../../common/catalog"; import getClusterByIdInjectable from "../../../common/cluster-store/get-by-id.injectable"; -import type { Cluster } from "../../../common/cluster/cluster"; +import { Cluster } from "../../../common/cluster/cluster"; import { loadConfigFromString } from "../../../common/kube-helpers"; import clustersThatAreBeingDeletedInjectable from "../../cluster/are-being-deleted.injectable"; import { catalogEntityFromCluster } from "../../cluster/manager"; -import createClusterInjectable from "../../create-cluster/create-cluster.injectable"; import configToModelsInjectable from "./config-to-models.injectable"; import kubeconfigSyncLoggerInjectable from "./logger.injectable"; +import clusterConnectionInjectable from "../../cluster/cluster-connection.injectable"; export type ComputeKubeconfigDiff = (contents: string, source: ObservableMap, filePath: string) => void; @@ -24,7 +24,6 @@ const computeKubeconfigDiffInjectable = getInjectable({ id: "compute-kubeconfig-diff", instantiate: (di): ComputeKubeconfigDiff => { const directoryForKubeConfigs = di.inject(directoryForKubeConfigsInjectable); - const createCluster = di.inject(createClusterInjectable); const clustersThatAreBeingDeleted = di.inject(clustersThatAreBeingDeletedInjectable); const configToModels = di.inject(configToModelsInjectable); const logger = di.inject(kubeconfigSyncLoggerInjectable); @@ -51,7 +50,9 @@ const computeKubeconfigDiffInjectable = getInjectable({ // remove from the deleting set, so that if a new context of the same name is added, it isn't marked as deleting clustersThatAreBeingDeleted.delete(value[0].id); - value[0].disconnect(); + const clusterConnection = di.inject(clusterConnectionInjectable, value[0]); + + clusterConnection.disconnect(); source.delete(contextName); logger.debug(`Removed old cluster from sync`, { filePath, contextName }); continue; @@ -71,9 +72,9 @@ const computeKubeconfigDiffInjectable = getInjectable({ // add new clusters to the source try { const clusterId = createHash("md5").update(`${filePath}:${contextName}`).digest("hex"); - const cluster = getClusterById(clusterId) ?? createCluster({ ...model, id: clusterId }, configData); + const cluster = getClusterById(clusterId) ?? new Cluster({ ...model, id: clusterId }, configData); - if (!cluster.apiUrl) { + if (!cluster.apiUrl.get()) { throw new Error("Cluster constructor failed, see above error"); } diff --git a/packages/core/src/main/cluster-detectors/cluster-distribution-detector.injectable.ts b/packages/core/src/main/cluster-detectors/cluster-distribution-detector.injectable.ts index 7a13c62515..44f1c88102 100644 --- a/packages/core/src/main/cluster-detectors/cluster-distribution-detector.injectable.ts +++ b/packages/core/src/main/cluster-detectors/cluster-distribution-detector.injectable.ts @@ -13,18 +13,18 @@ import requestClusterVersionInjectable from "./request-cluster-version.injectabl const isGKE = (version: string) => version.includes("gke"); const isEKS = (version: string) => version.includes("eks"); const isIKS = (version: string) => version.includes("IKS"); -const isAKS = (cluster: Cluster) => cluster.apiUrl.includes("azmk8s.io"); +const isAKS = (cluster: Cluster) => cluster.apiUrl.get().includes("azmk8s.io"); const isMirantis = (version: string) => version.includes("-mirantis-") || version.includes("-docker-"); -const isDigitalOcean = (cluster: Cluster) => cluster.apiUrl.endsWith("k8s.ondigitalocean.com"); -const isMinikube = (cluster: Cluster) => cluster.contextName.startsWith("minikube"); -const isMicrok8s = (cluster: Cluster) => cluster.contextName.startsWith("microk8s"); -const isKind = (cluster: Cluster) => cluster.contextName.startsWith("kubernetes-admin@kind-"); -const isDockerDesktop = (cluster: Cluster) => cluster.contextName === "docker-desktop"; +const isDigitalOcean = (cluster: Cluster) => cluster.apiUrl.get().endsWith("k8s.ondigitalocean.com"); +const isMinikube = (cluster: Cluster) => cluster.contextName.get().startsWith("minikube"); +const isMicrok8s = (cluster: Cluster) => cluster.contextName.get().startsWith("microk8s"); +const isKind = (cluster: Cluster) => cluster.contextName.get().startsWith("kubernetes-admin@kind-"); +const isDockerDesktop = (cluster: Cluster) => cluster.contextName.get() === "docker-desktop"; const isTke = (version: string) => version.includes("-tke."); const isCustom = (version: string) => version.includes("+"); const isVMWare = (version: string) => version.includes("+vmware"); const isRke = (version: string) => version.includes("-rancher"); -const isRancherDesktop = (cluster: Cluster) => cluster.contextName === "rancher-desktop"; +const isRancherDesktop = (cluster: Cluster) => cluster.contextName.get() === "rancher-desktop"; const isK3s = (version: string) => version.includes("+k3s"); const isK0s = (version: string) => version.includes("-k0s") || version.includes("+k0s"); const isAlibaba = (version: string) => version.includes("-aliyun"); diff --git a/packages/core/src/main/cluster-detectors/cluster-id-detector.injectable.ts b/packages/core/src/main/cluster-detectors/cluster-id-detector.injectable.ts index 8d7a90fedc..6699986805 100644 --- a/packages/core/src/main/cluster-detectors/cluster-id-detector.injectable.ts +++ b/packages/core/src/main/cluster-detectors/cluster-id-detector.injectable.ts @@ -28,7 +28,7 @@ const clusterIdDetectorFactoryInjectable = getInjectable({ try { id = await getDefaultNamespaceId(cluster); } catch(_) { - id = cluster.apiUrl; + id = cluster.apiUrl.get(); } const value = createHash("sha256").update(id).digest("hex"); diff --git a/packages/core/src/main/cluster-detectors/detect-cluster-metadata.test.ts b/packages/core/src/main/cluster-detectors/detect-cluster-metadata.test.ts index 496e176afb..9582b386c1 100644 --- a/packages/core/src/main/cluster-detectors/detect-cluster-metadata.test.ts +++ b/packages/core/src/main/cluster-detectors/detect-cluster-metadata.test.ts @@ -7,8 +7,7 @@ import appPathsStateInjectable from "../../common/app-paths/app-paths-state.inje 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 { ClusterMetadataKey } from "../../common/cluster-types"; -import type { Cluster } from "../../common/cluster/cluster"; -import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token"; +import { Cluster } from "../../common/cluster/cluster"; import { getDiForUnitTesting } from "../getDiForUnitTesting"; import clusterDistributionDetectorInjectable from "./cluster-distribution-detector.injectable"; import clusterIdDetectorFactoryInjectable from "./cluster-id-detector.injectable"; @@ -64,9 +63,7 @@ describe("detect-cluster-metadata", () => { detectClusterMetadata = di.inject(detectClusterMetadataInjectable); - const createCluster = di.inject(createClusterInjectionToken); - - cluster = createCluster({ + cluster = new Cluster({ id: "some-id", contextName: "some-context", kubeConfigPath: "minikube-config.yml", diff --git a/packages/core/src/main/cluster-detectors/token.ts b/packages/core/src/main/cluster-detectors/token.ts index fc9031c815..cfc70d8e56 100644 --- a/packages/core/src/main/cluster-detectors/token.ts +++ b/packages/core/src/main/cluster-detectors/token.ts @@ -11,7 +11,7 @@ export interface ClusterDetectionResult { accuracy: number; } -export interface FalibleOnlyClusterMetadataDetector { +export interface FallibleOnlyClusterMetadataDetector { readonly key: string; detect(cluster: Cluster): Promise; } diff --git a/packages/core/src/main/cluster/auth-proxy-url.injectable.ts b/packages/core/src/main/cluster/auth-proxy-url.injectable.ts new file mode 100644 index 0000000000..1ae2a3dca2 --- /dev/null +++ b/packages/core/src/main/cluster/auth-proxy-url.injectable.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import type { Cluster } from "../../common/cluster/cluster"; +import lensProxyPortInjectable from "../lens-proxy/lens-proxy-port.injectable"; + +const kubeAuthProxyUrlInjectable = getInjectable({ + id: "kube-auth-proxy-url", + instantiate: (di, cluster) => { + const lensProxyPort = di.inject(lensProxyPortInjectable); + + return `https://127.0.0.1:${lensProxyPort.get()}/${cluster.id}`; + }, + lifecycle: lifecycleEnum.keyedSingleton({ + getInstanceKey: (di, cluster: Cluster) => cluster.id, + }), +}); + +export default kubeAuthProxyUrlInjectable; diff --git a/packages/core/src/main/cluster/broadcast-connection-update.injectable.ts b/packages/core/src/main/cluster/broadcast-connection-update.injectable.ts new file mode 100644 index 0000000000..51e18f0bfd --- /dev/null +++ b/packages/core/src/main/cluster/broadcast-connection-update.injectable.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import type { KubeAuthUpdate } from "../../common/cluster-types"; +import type { Cluster } from "../../common/cluster/cluster"; +import broadcastMessageInjectable from "../../common/ipc/broadcast-message.injectable"; +import loggerInjectable from "../../common/logger.injectable"; + +export type BroadcastConnectionUpdate = (update: KubeAuthUpdate) => void; + +const broadcastConnectionUpdateInjectable = getInjectable({ + id: "broadcast-connection-update", + instantiate: (di, cluster): BroadcastConnectionUpdate => { + const broadcastMessage = di.inject(broadcastMessageInjectable); + const logger = di.inject(loggerInjectable); + + return (update) => { + logger.debug(`[CLUSTER]: broadcasting connection update`, { ...update, meta: cluster.getMeta() }); + broadcastMessage(`cluster:${cluster.id}:connection-update`, update); + }; + }, + lifecycle: lifecycleEnum.keyedSingleton({ + getInstanceKey: (di, cluster: Cluster) => cluster.id, + }), +}); + +export default broadcastConnectionUpdateInjectable; diff --git a/packages/core/src/main/cluster/cluster-connection.injectable.ts b/packages/core/src/main/cluster/cluster-connection.injectable.ts new file mode 100644 index 0000000000..8154953d78 --- /dev/null +++ b/packages/core/src/main/cluster/cluster-connection.injectable.ts @@ -0,0 +1,423 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { type KubeConfig, HttpError } from "@kubernetes/client-node"; +import { reaction, comparer, runInAction } from "mobx"; +import { ClusterStatus } from "../../common/cluster-types"; +import type { CreateAuthorizationReview } from "../../common/cluster/authorization-review.injectable"; +import type { Cluster } from "../../common/cluster/cluster"; +import type { CreateListNamespaces } from "../../common/cluster/list-namespaces.injectable"; +import type { RequestNamespaceListPermissionsFor, RequestNamespaceListPermissions } from "../../common/cluster/request-namespace-list-permissions.injectable"; +import type { BroadcastMessage } from "../../common/ipc/broadcast-message.injectable"; +import { clusterListNamespaceForbiddenChannel } from "../../common/ipc/cluster"; +import type { Logger } from "../../common/logger"; +import type { KubeApiResource } from "../../common/rbac"; +import { formatKubeApiResource } from "../../common/rbac"; +import { disposer, isDefined, isRequestError } from "../../common/utils"; +import { withConcurrencyLimit } from "../../common/utils/with-concurrency-limit"; +import type { ClusterPrometheusHandler } from "./prometheus-handler/prometheus-handler"; +import type { BroadcastConnectionUpdate } from "./broadcast-connection-update.injectable"; +import type { KubeAuthProxyServer } from "./kube-auth-proxy-server.injectable"; +import type { LoadProxyKubeconfig } from "./load-proxy-kubeconfig.injectable"; +import type { RemoveProxyKubeconfig } from "./remove-proxy-kubeconfig.injectable"; +import type { RequestApiResources } from "./request-api-resources.injectable"; +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import broadcastConnectionUpdateInjectable from "./broadcast-connection-update.injectable"; +import broadcastMessageInjectable from "../../common/ipc/broadcast-message.injectable"; +import createAuthorizationReviewInjectable from "../../common/cluster/authorization-review.injectable"; +import createListNamespacesInjectable from "../../common/cluster/list-namespaces.injectable"; +import kubeAuthProxyServerInjectable from "./kube-auth-proxy-server.injectable"; +import loadProxyKubeconfigInjectable from "./load-proxy-kubeconfig.injectable"; +import loggerInjectable from "../../common/logger.injectable"; +import prometheusHandlerInjectable from "./prometheus-handler/prometheus-handler.injectable"; +import removeProxyKubeconfigInjectable from "./remove-proxy-kubeconfig.injectable"; +import requestApiResourcesInjectable from "./request-api-resources.injectable"; +import requestNamespaceListPermissionsForInjectable from "../../common/cluster/request-namespace-list-permissions.injectable"; +import type { DetectClusterMetadata } from "../cluster-detectors/detect-cluster-metadata.injectable"; +import type { FallibleOnlyClusterMetadataDetector } from "../cluster-detectors/token"; +import clusterVersionDetectorInjectable from "../cluster-detectors/cluster-version-detector.injectable"; +import detectClusterMetadataInjectable from "../cluster-detectors/detect-cluster-metadata.injectable"; +import { replaceObservableObject } from "../../common/utils/replace-observable-object"; + +interface Dependencies { + readonly logger: Logger; + readonly prometheusHandler: ClusterPrometheusHandler; + readonly kubeAuthProxyServer: KubeAuthProxyServer; + readonly clusterVersionDetector: FallibleOnlyClusterMetadataDetector; + createAuthorizationReview: CreateAuthorizationReview; + requestApiResources: RequestApiResources; + requestNamespaceListPermissionsFor: RequestNamespaceListPermissionsFor; + createListNamespaces: CreateListNamespaces; + detectClusterMetadata: DetectClusterMetadata; + broadcastMessage: BroadcastMessage; + broadcastConnectionUpdate: BroadcastConnectionUpdate; + loadProxyKubeconfig: LoadProxyKubeconfig; + removeProxyKubeconfig: RemoveProxyKubeconfig; +} + +export type { ClusterConnection }; + +class ClusterConnection { + protected readonly eventsDisposer = disposer(); + + protected activated = false; + + constructor( + private readonly dependencies: Dependencies, + private readonly cluster: Cluster, + ) {} + + private bindEvents() { + this.dependencies.logger.info(`[CLUSTER]: bind events`, this.cluster.getMeta()); + const refreshTimer = setInterval(() => { + if (!this.cluster.disconnected.get()) { + this.refresh(); + } + }, 30_000); // every 30s + const refreshMetadataTimer = setInterval(() => { + if (this.cluster.available.get()) { + this.refreshAccessibilityAndMetadata(); + } + }, 900000); // every 15 minutes + + this.eventsDisposer.push( + reaction( + () => this.cluster.prometheusPreferences.get(), + preferences => this.dependencies.prometheusHandler.setupPrometheus(preferences), + { equals: comparer.structural }, + ), + () => clearInterval(refreshTimer), + () => clearInterval(refreshMetadataTimer), + reaction(() => this.cluster.preferences.defaultNamespace, () => this.recreateProxyKubeconfig()), + ); + } + + protected async recreateProxyKubeconfig() { + this.dependencies.logger.info("[CLUSTER]: Recreating proxy kubeconfig"); + + try { + await this.dependencies.removeProxyKubeconfig(); + await this.dependencies.loadProxyKubeconfig(); + } catch (error) { + this.dependencies.logger.error(`[CLUSTER]: failed to recreate proxy kubeconfig`, error); + } + } + + /** + * @param force force activation + */ + async activate(force = false) { + if (this.activated && !force) { + return; + } + + this.dependencies.logger.info(`[CLUSTER]: activate`, this.cluster.getMeta()); + + if (!this.eventsDisposer.length) { + this.bindEvents(); + } + + if (this.cluster.disconnected || !this.cluster.accessible.get()) { + try { + this.dependencies.broadcastConnectionUpdate({ + level: "info", + message: "Starting connection ...", + }); + await this.reconnect(); + } catch (error) { + this.dependencies.broadcastConnectionUpdate({ + level: "error", + message: `Failed to start connection: ${error}`, + }); + + return; + } + } + + try { + this.dependencies.broadcastConnectionUpdate({ + level: "info", + message: "Refreshing connection status ...", + }); + await this.refreshConnectionStatus(); + } catch (error) { + this.dependencies.broadcastConnectionUpdate({ + level: "error", + message: `Failed to connection status: ${error}`, + }); + + return; + } + + if (this.cluster.accessible.get()) { + try { + this.dependencies.broadcastConnectionUpdate({ + level: "info", + message: "Refreshing cluster accessibility ...", + }); + await this.refreshAccessibility(); + } catch (error) { + this.dependencies.broadcastConnectionUpdate({ + level: "error", + message: `Failed to refresh accessibility: ${error}`, + }); + + return; + } + this.dependencies.broadcastConnectionUpdate({ + level: "info", + message: "Connected, waiting for view to load ...", + }); + } + + this.activated = true; + } + + async reconnect() { + this.dependencies.logger.info(`[CLUSTER]: reconnect`, this.cluster.getMeta()); + await this.dependencies.kubeAuthProxyServer?.restart(); + + runInAction(() => { + this.cluster.disconnected.set(false); + }); + } + + disconnect() { + if (this.cluster.disconnected) { + return this.dependencies.logger.debug("[CLUSTER]: already disconnected", { id: this.cluster.id }); + } + + runInAction(() => { + this.dependencies.logger.info(`[CLUSTER]: disconnecting`, { id: this.cluster.id }); + this.eventsDisposer(); + this.dependencies.kubeAuthProxyServer?.stop(); + this.cluster.disconnected.set(true); + this.cluster.online.set(false); + this.cluster.accessible.set(false); + this.cluster.ready.set(false); + this.activated = false; + this.cluster.allowedNamespaces.clear(); + this.dependencies.logger.info(`[CLUSTER]: disconnected`, { id: this.cluster.id }); + }); + } + + async refresh() { + this.dependencies.logger.info(`[CLUSTER]: refresh`, this.cluster.getMeta()); + await this.refreshConnectionStatus(); + } + + async refreshAccessibilityAndMetadata() { + await this.refreshAccessibility(); + await this.refreshMetadata(); + } + + async refreshMetadata() { + this.dependencies.logger.info(`[CLUSTER]: refreshMetadata`, this.cluster.getMeta()); + const metadata = await this.dependencies.detectClusterMetadata(this.cluster); + + runInAction(() => { + replaceObservableObject(this.cluster.metadata, metadata); + }); + } + + private async refreshAccessibility(): Promise { + this.dependencies.logger.info(`[CLUSTER]: refreshAccessibility`, this.cluster.getMeta()); + const proxyConfig = await this.dependencies.loadProxyKubeconfig(); + const canI = this.dependencies.createAuthorizationReview(proxyConfig); + const requestNamespaceListPermissions = this.dependencies.requestNamespaceListPermissionsFor(proxyConfig); + + const isAdmin = await canI({ + namespace: "kube-system", + resource: "*", + verb: "create", + }); + const isGlobalWatchEnabled = await canI({ + verb: "watch", + resource: "*", + }); + const allowedNamespaces = await this.requestAllowedNamespaces(proxyConfig); + const knownResources = await (async () => { + const result = await this.dependencies.requestApiResources(this.cluster); + + if (result.callWasSuccessful) { + return result.response; + } + + if (this.cluster.knownResources.length > 0) { + this.dependencies.logger.warn(`[CLUSTER]: failed to list KUBE resources, sticking with previous list`); + + return this.cluster.knownResources; + } + + this.dependencies.logger.warn(`[CLUSTER]: failed to list KUBE resources for the first time, blocking connection to cluster...`); + this.dependencies.broadcastConnectionUpdate({ + level: "error", + message: "Failed to list kube API resources, please reconnect...", + }); + + return []; + })(); + const resourcesToShow = await this.getResourcesToShow(allowedNamespaces, knownResources, requestNamespaceListPermissions); + + runInAction(() => { + this.cluster.isAdmin.set(isAdmin); + this.cluster.isGlobalWatchEnabled.set(isGlobalWatchEnabled); + this.cluster.allowedNamespaces.replace(allowedNamespaces); + this.cluster.knownResources.replace(knownResources); + this.cluster.resourcesToShow.replace(resourcesToShow); + this.cluster.ready.set(this.cluster.knownResources.length > 0); + }); + + this.dependencies.logger.debug(`[CLUSTER]: refreshed accessibility data`, this.cluster.getState()); + } + + async refreshConnectionStatus() { + const connectionStatus = await this.getConnectionStatus(); + + runInAction(() => { + this.cluster.online.set(connectionStatus > ClusterStatus.Offline); + this.cluster.accessible.set(connectionStatus == ClusterStatus.AccessGranted); + }); + } + + protected async getConnectionStatus(): Promise { + try { + const versionData = await this.dependencies.clusterVersionDetector.detect(this.cluster); + + runInAction(() => { + this.cluster.metadata.version = versionData.value; + }); + + return ClusterStatus.AccessGranted; + } catch (error) { + this.dependencies.logger.error(`[CLUSTER]: Failed to connect to "${this.cluster.contextName.get()}": ${error}`); + + if (isRequestError(error)) { + if (error.statusCode) { + if (error.statusCode >= 400 && error.statusCode < 500) { + this.dependencies.broadcastConnectionUpdate({ + level: "error", + message: "Invalid credentials", + }); + + return ClusterStatus.AccessDenied; + } + + const message = String(error.error || error.message) || String(error); + + this.dependencies.broadcastConnectionUpdate({ + level: "error", + message, + }); + + return ClusterStatus.Offline; + } + + if (error.failed === true) { + if (error.timedOut === true) { + this.dependencies.broadcastConnectionUpdate({ + level: "error", + message: "Connection timed out", + }); + + return ClusterStatus.Offline; + } + + this.dependencies.broadcastConnectionUpdate({ + level: "error", + message: "Failed to fetch credentials", + }); + + return ClusterStatus.AccessDenied; + } + + const message = String(error.error || error.message) || String(error); + + this.dependencies.broadcastConnectionUpdate({ + level: "error", + message, + }); + } else if (error instanceof Error || typeof error === "string") { + this.dependencies.broadcastConnectionUpdate({ + level: "error", + message: `${error}`, + }); + } else { + this.dependencies.broadcastConnectionUpdate({ + level: "error", + message: "Unknown error has occurred", + }); + } + + return ClusterStatus.Offline; + } + } + + protected async requestAllowedNamespaces(proxyConfig: KubeConfig) { + if (this.cluster.accessibleNamespaces.length) { + return this.cluster.accessibleNamespaces; + } + + try { + const listNamespaces = this.dependencies.createListNamespaces(proxyConfig); + + return await listNamespaces(); + } catch (error) { + const ctx = proxyConfig.getContextObject(this.cluster.contextName.get()); + const namespaceList = [ctx?.namespace].filter(isDefined); + + if (namespaceList.length === 0 && error instanceof HttpError && error.statusCode === 403) { + const { response } = error as HttpError & { response: Response }; + + this.dependencies.logger.info("[CLUSTER]: listing namespaces is forbidden, broadcasting", { clusterId: this.cluster.id, error: response.body }); + this.dependencies.broadcastMessage(clusterListNamespaceForbiddenChannel, this.cluster.id); + } + + return namespaceList; + } + } + + protected async getResourcesToShow(allowedNamespaces: string[], knownResources: KubeApiResource[], req: RequestNamespaceListPermissions) { + if (!allowedNamespaces.length) { + return []; + } + + const requestNamespaceListPermissions = withConcurrencyLimit(5)(req); + const namespaceListPermissions = allowedNamespaces.map(requestNamespaceListPermissions); + const canListResources = await Promise.all(namespaceListPermissions); + + return knownResources + .filter((resource) => canListResources.some(fn => fn(resource))) + .map(formatKubeApiResource); + } +} + +const clusterConnectionInjectable = getInjectable({ + id: "cluster-connection", + instantiate: (di, cluster) => new ClusterConnection( + { + clusterVersionDetector: di.inject(clusterVersionDetectorInjectable), + kubeAuthProxyServer: di.inject(kubeAuthProxyServerInjectable, cluster), + logger: di.inject(loggerInjectable), + prometheusHandler: di.inject(prometheusHandlerInjectable, cluster), + broadcastConnectionUpdate: di.inject(broadcastConnectionUpdateInjectable, cluster), + broadcastMessage: di.inject(broadcastMessageInjectable), + createAuthorizationReview: di.inject(createAuthorizationReviewInjectable), + createListNamespaces: di.inject(createListNamespacesInjectable), + detectClusterMetadata: di.inject(detectClusterMetadataInjectable), + loadProxyKubeconfig: di.inject(loadProxyKubeconfigInjectable, cluster), + removeProxyKubeconfig: di.inject(removeProxyKubeconfigInjectable, cluster), + requestApiResources: di.inject(requestApiResourcesInjectable), + requestNamespaceListPermissionsFor: di.inject(requestNamespaceListPermissionsForInjectable), + }, + cluster, + ), + lifecycle: lifecycleEnum.keyedSingleton({ + getInstanceKey: (di, cluster: Cluster) => cluster.id, + }), +}); + +export default clusterConnectionInjectable; + diff --git a/packages/core/src/main/cluster/kube-auth-proxy-server.injectable.ts b/packages/core/src/main/cluster/kube-auth-proxy-server.injectable.ts new file mode 100644 index 0000000000..9942c40168 --- /dev/null +++ b/packages/core/src/main/cluster/kube-auth-proxy-server.injectable.ts @@ -0,0 +1,114 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type { ServerOptions } from "http-proxy"; +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import type { Cluster } from "../../common/cluster/cluster"; +import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable"; +import kubeAuthProxyCertificateInjectable from "../kube-auth-proxy/kube-auth-proxy-certificate.injectable"; +import type { KubeAuthProxy } from "../kube-auth-proxy/kube-auth-proxy"; + +export interface KubeAuthProxyServer { + getApiTarget(isLongRunningRequest?: boolean): Promise; + ensureAuthProxyUrl(): Promise; + restart(): Promise; + ensureRunning(): Promise; + stop(): void; +} + +const fourHoursInMs = 4 * 60 * 60 * 1000; +const thirtySecondsInMs = 30 * 1000; + +const kubeAuthProxyServerInjectable = getInjectable({ + id: "kube-auth-proxy-server", + instantiate: (di, cluster): KubeAuthProxyServer => { + const clusterUrl = new URL(cluster.apiUrl.get()); + + const createKubeAuthProxy = di.inject(createKubeAuthProxyInjectable); + const certificate = di.inject(kubeAuthProxyCertificateInjectable, clusterUrl.hostname); + + let kubeAuthProxy: KubeAuthProxy | undefined = undefined; + let apiTarget: ServerOptions | undefined = undefined; + + const ensureServerHelper = async (): Promise => { + if (!kubeAuthProxy) { + const proxyEnv = Object.assign({}, process.env); + + if (cluster.preferences.httpsProxy) { + proxyEnv.HTTPS_PROXY = cluster.preferences.httpsProxy; + } + + kubeAuthProxy = createKubeAuthProxy(cluster, proxyEnv); + } + + await kubeAuthProxy.run(); + + return kubeAuthProxy; + }; + + const newApiTarget = async (timeout: number): Promise => { + const kubeAuthProxy = await ensureServerHelper(); + const headers: Record = {}; + + if (clusterUrl.hostname) { + headers.Host = clusterUrl.hostname; + + // fix current IPv6 inconsistency in url.Parse() and httpProxy. + // with url.Parse the IPv6 Hostname has no Square brackets but httpProxy needs the Square brackets to work. + if (headers.Host.includes(":")) { + headers.Host = `[${headers.Host}]`; + } + } + + return { + target: { + protocol: "https:", + host: "127.0.0.1", + port: kubeAuthProxy.port, + path: kubeAuthProxy.apiPrefix, + ca: certificate.cert, + }, + changeOrigin: true, + timeout, + secure: true, + headers, + }; + }; + + const stopServer = () => { + kubeAuthProxy?.exit(); + kubeAuthProxy = undefined; + apiTarget = undefined; + }; + + return { + getApiTarget: async (isLongRunningRequest = false) => { + if (isLongRunningRequest) { + return newApiTarget(fourHoursInMs); + } + + return apiTarget ??= await newApiTarget(thirtySecondsInMs); + }, + ensureAuthProxyUrl: async () => { + const kubeAuthProxy = await ensureServerHelper(); + + return `https://127.0.0.1:${kubeAuthProxy.port}${kubeAuthProxy.apiPrefix}`; + }, + ensureRunning: async () => { + await ensureServerHelper(); + }, + restart: async () => { + stopServer(); + await ensureServerHelper(); + }, + stop: stopServer, + }; + }, + lifecycle: lifecycleEnum.keyedSingleton({ + getInstanceKey: (di, cluster: Cluster) => cluster.id, + }), +}); + +export default kubeAuthProxyServerInjectable; diff --git a/packages/core/src/main/cluster/load-proxy-kubeconfig.injectable.ts b/packages/core/src/main/cluster/load-proxy-kubeconfig.injectable.ts new file mode 100644 index 0000000000..b5e5856022 --- /dev/null +++ b/packages/core/src/main/cluster/load-proxy-kubeconfig.injectable.ts @@ -0,0 +1,31 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { KubeConfig } from "@kubernetes/client-node"; +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import type { Cluster } from "../../common/cluster/cluster"; +import loadConfigFromFileInjectable from "../../common/kube-helpers/load-config-from-file.injectable"; +import kubeconfigManagerInjectable from "../kubeconfig-manager/kubeconfig-manager.injectable"; + +export type LoadProxyKubeconfig = () => Promise; + +const loadProxyKubeconfigInjectable = getInjectable({ + id: "load-proxy-kubeconfig", + instantiate: (di, cluster) => { + const loadConfigFromFile = di.inject(loadConfigFromFileInjectable); + const proxyKubeconfigManager = di.inject(kubeconfigManagerInjectable, cluster); + + return async () => { + const proxyKubeconfigPath = await proxyKubeconfigManager.ensurePath(); + const { config } = await loadConfigFromFile(proxyKubeconfigPath); + + return config; + }; + }, + lifecycle: lifecycleEnum.keyedSingleton({ + getInstanceKey: (di, cluster: Cluster) => cluster.id, + }), +}); + +export default loadProxyKubeconfigInjectable; diff --git a/packages/core/src/main/cluster/manager.injectable.ts b/packages/core/src/main/cluster/manager.injectable.ts index 28e55a0734..e1567e51b6 100644 --- a/packages/core/src/main/cluster/manager.injectable.ts +++ b/packages/core/src/main/cluster/manager.injectable.ts @@ -7,6 +7,7 @@ import clusterStoreInjectable from "../../common/cluster-store/cluster-store.inj import loggerInjectable from "../../common/logger.injectable"; import catalogEntityRegistryInjectable from "../catalog/entity-registry.injectable"; import clustersThatAreBeingDeletedInjectable from "./are-being-deleted.injectable"; +import clusterConnectionInjectable from "./cluster-connection.injectable"; import { ClusterManager } from "./manager"; import updateEntityMetadataInjectable from "./update-entity-metadata.injectable"; import updateEntitySpecInjectable from "./update-entity-spec.injectable"; @@ -23,6 +24,7 @@ const clusterManagerInjectable = getInjectable({ logger: di.inject(loggerInjectable), updateEntityMetadata: di.inject(updateEntityMetadataInjectable), updateEntitySpec: di.inject(updateEntitySpecInjectable), + getClusterConnection: (cluster) => di.inject(clusterConnectionInjectable, cluster), }), }); diff --git a/packages/core/src/main/cluster/manager.ts b/packages/core/src/main/cluster/manager.ts index 6d84657a79..6dd4b97881 100644 --- a/packages/core/src/main/cluster/manager.ts +++ b/packages/core/src/main/cluster/manager.ts @@ -17,6 +17,7 @@ import type { CatalogEntityRegistry } from "../catalog"; import type { Logger } from "../../common/logger"; import type { UpdateEntityMetadata } from "./update-entity-metadata.injectable"; import type { UpdateEntitySpec } from "./update-entity-spec.injectable"; +import type { ClusterConnection } from "./cluster-connection.injectable"; const logPrefix = "[CLUSTER-MANAGER]:"; @@ -28,8 +29,9 @@ interface Dependencies { readonly clustersThatAreBeingDeleted: ObservableSet; readonly visibleCluster: IObservableValue; readonly logger: Logger; - readonly updateEntityMetadata: UpdateEntityMetadata; - readonly updateEntitySpec: UpdateEntitySpec; + updateEntityMetadata: UpdateEntityMetadata; + updateEntitySpec: UpdateEntitySpec; + getClusterConnection: (cluster: Cluster) => ClusterConnection; } export class ClusterManager { @@ -119,13 +121,13 @@ export class ClusterManager { return LensKubernetesClusterStatus.DISCONNECTED; } - if (cluster.accessible) { + if (cluster.accessible.get()) { this.dependencies.logger.silly(`${logPrefix} setting entity ${entity.getName()} to CONNECTED, reason="cluster is accessible"`); return LensKubernetesClusterStatus.CONNECTED; } - if (!cluster.disconnected) { + if (!cluster.disconnected.get()) { this.dependencies.logger.silly(`${logPrefix} setting entity ${entity.getName()} to CONNECTING, reason="cluster is not disconnected"`); return LensKubernetesClusterStatus.CONNECTING; @@ -174,8 +176,8 @@ export class ClusterManager { } } } else { - cluster.kubeConfigPath = entity.spec.kubeconfigPath; - cluster.contextName = entity.spec.kubeconfigContext; + cluster.kubeConfigPath.set(entity.spec.kubeconfigPath); + cluster.contextName.set(entity.spec.kubeconfigContext); if (entity.spec.accessibleNamespaces) { cluster.accessibleNamespaces.replace(entity.spec.accessibleNamespaces); @@ -202,30 +204,43 @@ export class ClusterManager { } } - protected onNetworkOffline = () => { + protected onNetworkOffline = async () => { this.dependencies.logger.info(`${logPrefix} network is offline`); - this.dependencies.store.clustersList.forEach((cluster) => { - if (!cluster.disconnected) { - cluster.online = false; - cluster.accessible = false; - cluster.refreshConnectionStatus().catch((e) => e); - } - }); + + await Promise.allSettled( + this.dependencies.store.clustersList + .filter(cluster => !cluster.disconnected.get()) + .map(async (cluster) => { + cluster.online.set(false); + cluster.accessible.set(false); + + await this.dependencies + .getClusterConnection(cluster) + .refreshConnectionStatus(); + }), + ); }; - protected onNetworkOnline = () => { + protected onNetworkOnline = async () => { this.dependencies.logger.info(`${logPrefix} network is online`); - this.dependencies.store.clustersList.forEach((cluster) => { - if (!cluster.disconnected) { - cluster.refreshConnectionStatus().catch((e) => e); - } - }); + + await Promise.allSettled( + this.dependencies.store.clustersList + .filter(cluster => !cluster.disconnected.get()) + .map((cluster) => ( + this.dependencies + .getClusterConnection(cluster) + .refreshConnectionStatus() + )), + ); }; stop() { - this.dependencies.store.clusters.forEach((cluster: Cluster) => { - cluster.disconnect(); - }); + for (const cluster of this.dependencies.store.clustersList) { + this.dependencies + .getClusterConnection(cluster) + .disconnect(); + } } } @@ -233,26 +248,26 @@ export function catalogEntityFromCluster(cluster: Cluster) { return new KubernetesCluster({ metadata: { uid: cluster.id, - name: cluster.name, + name: cluster.name.get(), source: "local", labels: { ...cluster.labels, }, - distro: cluster.distribution, - kubeVersion: cluster.version, + distro: cluster.distribution.get(), + kubeVersion: cluster.version.get(), }, spec: { - kubeconfigPath: cluster.kubeConfigPath, - kubeconfigContext: cluster.contextName, + kubeconfigPath: cluster.kubeConfigPath.get(), + kubeconfigContext: cluster.contextName.get(), icon: {}, }, status: { - phase: cluster.disconnected + phase: cluster.disconnected.get() ? LensKubernetesClusterStatus.DISCONNECTED : LensKubernetesClusterStatus.CONNECTED, reason: "", message: "", - active: !cluster.disconnected, + active: !cluster.disconnected.get(), }, }); } diff --git a/packages/core/src/main/cluster/prometheus-handler/prometheus-handler.injectable.ts b/packages/core/src/main/cluster/prometheus-handler/prometheus-handler.injectable.ts new file mode 100644 index 0000000000..bf022b87f9 --- /dev/null +++ b/packages/core/src/main/cluster/prometheus-handler/prometheus-handler.injectable.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import type { Cluster } from "../../../common/cluster/cluster"; +import { createClusterPrometheusHandler } from "./prometheus-handler"; +import getPrometheusProviderByKindInjectable from "../../prometheus/get-by-kind.injectable"; +import prometheusProvidersInjectable from "../../prometheus/providers.injectable"; +import loggerInjectable from "../../../common/logger.injectable"; +import loadProxyKubeconfigInjectable from "../load-proxy-kubeconfig.injectable"; + +const prometheusHandlerInjectable = getInjectable({ + id: "prometheus-handler", + + instantiate: (di, cluster) => createClusterPrometheusHandler( + { + getPrometheusProviderByKind: di.inject(getPrometheusProviderByKindInjectable), + prometheusProviders: di.inject(prometheusProvidersInjectable), + logger: di.inject(loggerInjectable), + loadProxyKubeconfig: di.inject(loadProxyKubeconfigInjectable, cluster), + }, + cluster, + ), + lifecycle: lifecycleEnum.keyedSingleton({ + getInstanceKey: (di, cluster: Cluster) => cluster.id, + }), +}); + +export default prometheusHandlerInjectable; diff --git a/packages/core/src/main/cluster/prometheus-handler/prometheus-handler.ts b/packages/core/src/main/cluster/prometheus-handler/prometheus-handler.ts new file mode 100644 index 0000000000..ddf5cc461a --- /dev/null +++ b/packages/core/src/main/cluster/prometheus-handler/prometheus-handler.ts @@ -0,0 +1,129 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type { PrometheusProvider, PrometheusService } from "../../prometheus/provider"; +import type { ClusterPrometheusPreferences } from "../../../common/cluster-types"; +import type { Cluster } from "../../../common/cluster/cluster"; +import { CoreV1Api } from "@kubernetes/client-node"; +import type { GetPrometheusProviderByKind } from "../../prometheus/get-by-kind.injectable"; +import type { IComputedValue } from "mobx"; +import type { Logger } from "../../../common/logger"; +import type { LoadProxyKubeconfig } from "../load-proxy-kubeconfig.injectable"; + +export interface PrometheusDetails { + prometheusPath: string; + provider: PrometheusProvider; +} + +interface PrometheusServicePreferences { + namespace: string; + service: string; + port: number; + prefix: string; +} + +interface Dependencies { + readonly prometheusProviders: IComputedValue; + readonly logger: Logger; + getPrometheusProviderByKind: GetPrometheusProviderByKind; + loadProxyKubeconfig: LoadProxyKubeconfig; +} + +export interface ClusterPrometheusHandler { + setupPrometheus(preferences?: ClusterPrometheusPreferences): void; + getPrometheusDetails(): Promise; +} + +const ensurePrometheusPath = ({ service, namespace, port }: PrometheusService) => `${namespace}/services/${service}:${port}`; + +export const createClusterPrometheusHandler = (...args: [Dependencies, Cluster]): ClusterPrometheusHandler => { + const [deps, cluster] = args; + const { + getPrometheusProviderByKind, + loadProxyKubeconfig, + logger, + prometheusProviders, + } = deps; + + let prometheusProvider: string | undefined = undefined; + let prometheus: PrometheusServicePreferences | undefined = undefined; + + const setupPrometheus: ClusterPrometheusHandler["setupPrometheus"] = (preferences = {}) => { + prometheusProvider = preferences.prometheusProvider?.type; + prometheus = preferences.prometheus; + }; + + const ensurePrometheusProvider = (service: PrometheusService) => { + if (!prometheusProvider) { + logger.info(`[CONTEXT-HANDLER]: using ${service.kind} as prometheus provider for clusterId=${cluster.id}`); + prometheusProvider = service.kind; + } + + return getPrometheusProviderByKind(prometheusProvider); + }; + + const listPotentialProviders = () => { + if (prometheusProvider) { + const provider = getPrometheusProviderByKind(prometheusProvider); + + if (provider) { + return [provider]; + } + } + + return prometheusProviders.get(); + }; + + const getPrometheusService = async (): Promise => { + setupPrometheus(cluster.preferences); + + if (prometheus && prometheusProvider) { + return { + kind: prometheusProvider, + namespace: prometheus.namespace, + service: prometheus.service, + port: prometheus.port, + }; + } + + const providers = listPotentialProviders(); + const proxyConfig = await loadProxyKubeconfig(); + const apiClient = proxyConfig.makeApiClient(CoreV1Api); + const potentialServices = await Promise.allSettled( + providers.map(provider => provider.getPrometheusService(apiClient)), + ); + const errors = []; + + for (const res of potentialServices) { + switch (res.status) { + case "rejected": + errors.push(res.reason); + break; + + case "fulfilled": + if (res.value) { + return res.value; + } + } + } + + throw new Error("No Prometheus service found", { cause: errors }); + }; + + const getPrometheusDetails: ClusterPrometheusHandler["getPrometheusDetails"] = async () => { + const service = await getPrometheusService(); + const prometheusPath = ensurePrometheusPath(service); + const provider = ensurePrometheusProvider(service); + + return { prometheusPath, provider }; + }; + + setupPrometheus(cluster.preferences); + + return { + setupPrometheus, + getPrometheusDetails, + }; +}; diff --git a/packages/core/src/main/cluster/remove-proxy-kubeconfig.injectable.ts b/packages/core/src/main/cluster/remove-proxy-kubeconfig.injectable.ts new file mode 100644 index 0000000000..053f7ba18d --- /dev/null +++ b/packages/core/src/main/cluster/remove-proxy-kubeconfig.injectable.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import type { Cluster } from "../../common/cluster/cluster"; +import kubeconfigManagerInjectable from "../kubeconfig-manager/kubeconfig-manager.injectable"; + +export type RemoveProxyKubeconfig = () => Promise; + +const removeProxyKubeconfigInjectable = getInjectable({ + id: "remove-proxy-kubeconfig", + instantiate: (di, cluster): RemoveProxyKubeconfig => { + const proxyKubeconfigManager = di.inject(kubeconfigManagerInjectable, cluster); + + return () => proxyKubeconfigManager.clear(); + }, + lifecycle: lifecycleEnum.keyedSingleton({ + getInstanceKey: (di, cluster: Cluster) => cluster.id, + }), +}); + +export default removeProxyKubeconfigInjectable; diff --git a/packages/core/src/main/cluster/request-api-resources.injectable.ts b/packages/core/src/main/cluster/request-api-resources.injectable.ts index 552bd47800..fee8c0e3aa 100644 --- a/packages/core/src/main/cluster/request-api-resources.injectable.ts +++ b/packages/core/src/main/cluster/request-api-resources.injectable.ts @@ -12,6 +12,7 @@ import { withConcurrencyLimit } from "../../common/utils/with-concurrency-limit" import requestKubeApiResourcesForInjectable from "./request-kube-api-resources-for.injectable"; import type { AsyncResult } from "../../common/utils/async-result"; import { backoffCaller } from "../../common/utils/backoff-caller"; +import broadcastConnectionUpdateInjectable from "./broadcast-connection-update.injectable"; export type RequestApiResources = (cluster: Cluster) => Promise>; @@ -29,6 +30,7 @@ const requestApiResourcesInjectable = getInjectable({ return async (...args) => { const [cluster] = args; + const broadcastConnectionUpdate = di.inject(broadcastConnectionUpdateInjectable, cluster); const requestKubeApiResources = withConcurrencyLimit(5)(requestKubeApiResourcesFor(cluster)); const groupLists: KubeResourceListGroup[] = []; @@ -36,7 +38,10 @@ const requestApiResourcesInjectable = getInjectable({ for (const apiVersionRequester of apiVersionRequesters) { const result = await backoffCaller(() => apiVersionRequester(cluster), { onIntermediateError: (error, attempt) => { - cluster.broadcastConnectUpdate(`Failed to list kube API resource kinds, attempt ${attempt}: ${error}`, "warning"); + broadcastConnectionUpdate({ + message: `Failed to list kube API resource kinds, attempt ${attempt}: ${error}`, + level: "warning", + }); logger.warn(`[LIST-API-RESOURCES]: failed to list kube api resources: ${error}`, { attempt, clusterId: cluster.id }); }, }); @@ -56,7 +61,10 @@ const requestApiResourcesInjectable = getInjectable({ for (const result of results) { if (!result.callWasSuccessful) { - cluster.broadcastConnectUpdate(`Kube APIs under "${result.listGroup.path}" may not be displayed`, "warning"); + broadcastConnectionUpdate({ + message: `Kube APIs under "${result.listGroup.path}" may not be displayed`, + level: "warning", + }); continue; } diff --git a/packages/core/src/main/cluster/store-migrations/5.0.0-beta.10.injectable.ts b/packages/core/src/main/cluster/store-migrations/5.0.0-beta.10.injectable.ts index 0199e57c97..fdd1ecdc80 100644 --- a/packages/core/src/main/cluster/store-migrations/5.0.0-beta.10.injectable.ts +++ b/packages/core/src/main/cluster/store-migrations/5.0.0-beta.10.injectable.ts @@ -17,6 +17,10 @@ interface Pre500WorkspaceStoreModel { }[]; } +interface Pre500ClusterModel extends ClusterModel { + workspace?: string; +} + const v500Beta10ClusterStoreMigrationInjectable = getInjectable({ id: "v5.0.0-beta.10-cluster-store-migration", instantiate: (di) => { @@ -35,7 +39,7 @@ const v500Beta10ClusterStoreMigrationInjectable = getInjectable({ workspaces.set(id, name); } - const clusters = (store.get("clusters") ?? []) as ClusterModel[]; + const clusters = (store.get("clusters") ?? []) as Pre500ClusterModel[]; for (const cluster of clusters) { if (cluster.workspace) { diff --git a/packages/core/src/main/cluster/store-migrations/5.0.0-beta.13.injectable.ts b/packages/core/src/main/cluster/store-migrations/5.0.0-beta.13.injectable.ts index d8f0368df4..4ac06fee60 100644 --- a/packages/core/src/main/cluster/store-migrations/5.0.0-beta.13.injectable.ts +++ b/packages/core/src/main/cluster/store-migrations/5.0.0-beta.13.injectable.ts @@ -8,6 +8,15 @@ import { moveSync, removeSync } from "fs-extra"; import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; import { isDefined } from "../../../common/utils"; import joinPathsInjectable from "../../../common/path/join-paths.injectable"; +import { getInjectable } from "@ogre-tools/injectable"; +import loggerInjectable from "../../../common/logger.injectable"; +import { clusterStoreMigrationInjectionToken } from "../../../common/cluster-store/migration-token"; +import { generateNewIdFor } from "../../../common/utils/generate-new-id-for"; + +interface Pre500ClusterModel extends ClusterModel { + workspace?: string; + workspaces?: string[]; +} function mergePrometheusPreferences(left: ClusterPrometheusPreferences, right: ClusterPrometheusPreferences): ClusterPrometheusPreferences { if (left.prometheus && left.prometheusProvider) { @@ -27,24 +36,15 @@ function mergePrometheusPreferences(left: ClusterPrometheusPreferences, right: C return {}; } -function mergePreferences(left: ClusterPreferences, right: ClusterPreferences): ClusterPreferences { - return { - terminalCWD: left.terminalCWD || right.terminalCWD || undefined, - clusterName: left.clusterName || right.clusterName || undefined, - iconOrder: left.iconOrder || right.iconOrder || undefined, - icon: left.icon || right.icon || undefined, - httpsProxy: left.httpsProxy || right.httpsProxy || undefined, - hiddenMetrics: mergeSet(left.hiddenMetrics ?? [], right.hiddenMetrics ?? []), - ...mergePrometheusPreferences(left, right), - }; -} - -function mergeLabels(left: Record, right: Record): Record { - return { - ...right, - ...left, - }; -} +const mergePreferences = (left: ClusterPreferences, right: ClusterPreferences): ClusterPreferences => ({ + terminalCWD: left.terminalCWD || right.terminalCWD || undefined, + clusterName: left.clusterName || right.clusterName || undefined, + iconOrder: left.iconOrder || right.iconOrder || undefined, + icon: left.icon || right.icon || undefined, + httpsProxy: left.httpsProxy || right.httpsProxy || undefined, + hiddenMetrics: mergeSet(left.hiddenMetrics ?? [], right.hiddenMetrics ?? []), + ...mergePrometheusPreferences(left, right), +}); function mergeSet(...iterables: Iterable[]): string[] { const res = new Set(); @@ -60,24 +60,17 @@ function mergeSet(...iterables: Iterable[]): string[] { return [...res]; } -function mergeClusterModel(prev: ClusterModel, right: Omit): ClusterModel { - return { - id: prev.id, - kubeConfigPath: prev.kubeConfigPath, - contextName: prev.contextName, - preferences: mergePreferences(prev.preferences ?? {}, right.preferences ?? {}), - metadata: prev.metadata, - labels: mergeLabels(prev.labels ?? {}, right.labels ?? {}), - accessibleNamespaces: mergeSet(prev.accessibleNamespaces ?? [], right.accessibleNamespaces ?? []), - workspace: prev.workspace || right.workspace, - workspaces: mergeSet([prev.workspace, right.workspace], prev.workspaces ?? [], right.workspaces ?? []), - }; -} - -import { getInjectable } from "@ogre-tools/injectable"; -import loggerInjectable from "../../../common/logger.injectable"; -import { clusterStoreMigrationInjectionToken } from "../../../common/cluster-store/migration-token"; -import { generateNewIdFor } from "../../../common/utils/generate-new-id-for"; +const mergeClusterModel = (prev: Pre500ClusterModel, right: Omit): Pre500ClusterModel => ({ + id: prev.id, + kubeConfigPath: prev.kubeConfigPath, + contextName: prev.contextName, + preferences: mergePreferences(prev.preferences ?? {}, right.preferences ?? {}), + metadata: prev.metadata, + labels: { ...(right.labels ?? {}), ...(prev.labels ?? {}) }, + accessibleNamespaces: mergeSet(prev.accessibleNamespaces ?? [], right.accessibleNamespaces ?? []), + workspace: prev.workspace || right.workspace, + workspaces: mergeSet([prev.workspace, right.workspace], prev.workspaces ?? [], right.workspaces ?? []), +}); const v500Beta13ClusterStoreMigrationInjectable = getInjectable({ id: "v5.0.0-beta.13-cluster-store-migration", @@ -104,8 +97,8 @@ const v500Beta13ClusterStoreMigrationInjectable = getInjectable({ version: "5.0.0-beta.13", run(store) { const folder = joinPaths(userDataPath, "lens-local-storage"); - const oldClusters = (store.get("clusters") ?? []) as ClusterModel[]; - const clusters = new Map(); + const oldClusters = (store.get("clusters") ?? []) as Pre500ClusterModel[]; + const clusters = new Map(); for (const { id: oldId, ...cluster } of oldClusters) { const newId = generateNewIdFor(cluster); diff --git a/packages/core/src/main/cluster/update-entity-metadata.injectable.ts b/packages/core/src/main/cluster/update-entity-metadata.injectable.ts index 102cd9ded0..b236ac1dcf 100644 --- a/packages/core/src/main/cluster/update-entity-metadata.injectable.ts +++ b/packages/core/src/main/cluster/update-entity-metadata.injectable.ts @@ -3,6 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; +import { toJS } from "mobx"; import type { KubernetesCluster } from "../../common/catalog-entities"; import { ClusterMetadataKey } from "../../common/cluster-types"; import type { Cluster } from "../../common/cluster/cluster"; @@ -17,10 +18,10 @@ const updateEntityMetadataInjectable = getInjectable({ return (entity, cluster) => { entity.metadata.labels = { ...entity.metadata.labels, - ...cluster.labels, + ...toJS(cluster.labels), }; - entity.metadata.distro = cluster.distribution; - entity.metadata.kubeVersion = cluster.version; + entity.metadata.distro = cluster.distribution.get(); + entity.metadata.kubeVersion = cluster.version.get(); enumKeys(ClusterMetadataKey).forEach((key) => { const metadataKey = ClusterMetadataKey[key]; diff --git a/packages/core/src/main/cluster/update-entity-metadata.test.ts b/packages/core/src/main/cluster/update-entity-metadata.test.ts index 8583dc2702..13683196bb 100644 --- a/packages/core/src/main/cluster/update-entity-metadata.test.ts +++ b/packages/core/src/main/cluster/update-entity-metadata.test.ts @@ -7,8 +7,8 @@ import appPathsStateInjectable from "../../common/app-paths/app-paths-state.inje import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; import { KubernetesCluster } from "../../common/catalog-entities"; import { ClusterMetadataKey } from "../../common/cluster-types"; -import type { Cluster } from "../../common/cluster/cluster"; -import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token"; +import { Cluster } from "../../common/cluster/cluster"; +import { replaceObservableObject } from "../../common/utils/replace-observable-object"; import { getDiForUnitTesting } from "../getDiForUnitTesting"; import type { UpdateEntityMetadata } from "./update-entity-metadata.injectable"; import updateEntityMetadataInjectable from "./update-entity-metadata.injectable"; @@ -27,11 +27,10 @@ describe("update-entity-metadata", () => { get: () => ({} as AppPaths), set: () => {}, })); - const createCluster = di.inject(createClusterInjectionToken); updateEntityMetadata = di.inject(updateEntityMetadataInjectable); - cluster = createCluster({ + cluster = new Cluster({ id: "some-id", contextName: "some-context", kubeConfigPath: "minikube-config.yml", @@ -50,10 +49,6 @@ describe("update-entity-metadata", () => { }, }; - cluster.metadata = { - ...cluster.metadata, - }; - entity = new KubernetesCluster({ metadata: { uid: "some-uid", @@ -125,9 +120,9 @@ describe("update-entity-metadata", () => { }); it("given cluster has labels, updates entity metadata with labels", () => { - cluster.labels = { + replaceObservableObject(cluster.labels, { "some-label": "some-value", - }; + }); entity.metadata.labels = { "some-other-label": "some-other-value", }; @@ -139,9 +134,9 @@ describe("update-entity-metadata", () => { }); it("given cluster has labels, overwrites entity metadata with cluster labels", () => { - cluster.labels = { + replaceObservableObject(cluster.labels, { "some-label": "some-cluster-value", - }; + }); entity.metadata.labels = { "some-label": "some-entity-value", }; diff --git a/packages/core/src/main/cluster/update-entity-spec.test.ts b/packages/core/src/main/cluster/update-entity-spec.test.ts index 60be847ebe..1cd9ed4f96 100644 --- a/packages/core/src/main/cluster/update-entity-spec.test.ts +++ b/packages/core/src/main/cluster/update-entity-spec.test.ts @@ -6,8 +6,7 @@ import type { AppPaths } from "../../common/app-paths/app-path-injection-token"; import appPathsStateInjectable from "../../common/app-paths/app-paths-state.injectable"; import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; import { KubernetesCluster } from "../../common/catalog-entities"; -import type { Cluster } from "../../common/cluster/cluster"; -import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token"; +import { Cluster } from "../../common/cluster/cluster"; import { getDiForUnitTesting } from "../getDiForUnitTesting"; import type { UpdateEntitySpec } from "./update-entity-spec.injectable"; import updateEntitySpecInjectable from "./update-entity-spec.injectable"; @@ -25,11 +24,10 @@ describe("update-entity-spec", () => { get: () => ({} as AppPaths), set: () => {}, })); - const createCluster = di.inject(createClusterInjectionToken); updateEntitySpec = di.inject(updateEntitySpecInjectable); - cluster = createCluster({ + cluster = new Cluster({ id: "some-id", contextName: "some-context", kubeConfigPath: "minikube-config.yml", diff --git a/packages/core/src/main/context-handler/context-handler.ts b/packages/core/src/main/context-handler/context-handler.ts deleted file mode 100644 index 7d40bfcd00..0000000000 --- a/packages/core/src/main/context-handler/context-handler.ts +++ /dev/null @@ -1,219 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import type { PrometheusProvider, PrometheusService } from "../prometheus/provider"; -import type { ClusterPrometheusPreferences } from "../../common/cluster-types"; -import type { Cluster } from "../../common/cluster/cluster"; -import type httpProxy from "http-proxy"; -import type { UrlWithStringQuery } from "url"; -import url from "url"; -import { CoreV1Api } from "@kubernetes/client-node"; -import type { KubeAuthProxy } from "../kube-auth-proxy/kube-auth-proxy"; -import type { CreateKubeAuthProxy } from "../kube-auth-proxy/create-kube-auth-proxy.injectable"; -import type { GetPrometheusProviderByKind } from "../prometheus/get-by-kind.injectable"; -import type { IComputedValue } from "mobx"; -import type { Logger } from "../../common/logger"; - -export interface PrometheusDetails { - prometheusPath: string; - provider: PrometheusProvider; -} - -interface PrometheusServicePreferences { - namespace: string; - service: string; - port: number; - prefix: string; -} - -export interface ContextHandlerDependencies { - createKubeAuthProxy: CreateKubeAuthProxy; - getPrometheusProviderByKind: GetPrometheusProviderByKind; - readonly authProxyCa: string; - readonly prometheusProviders: IComputedValue; - readonly logger: Logger; -} - -export interface ClusterContextHandler { - readonly clusterUrl: UrlWithStringQuery; - setupPrometheus(preferences?: ClusterPrometheusPreferences): void; - getPrometheusDetails(): Promise; - resolveAuthProxyUrl(): Promise; - resolveAuthProxyCa(): string; - getApiTarget(isLongRunningRequest?: boolean): Promise; - restartServer(): Promise; - ensureServer(): Promise; - stopServer(): void; -} - -export class ContextHandler implements ClusterContextHandler { - public readonly clusterUrl: UrlWithStringQuery; - protected kubeAuthProxy?: KubeAuthProxy; - protected apiTarget?: httpProxy.ServerOptions; - protected prometheusProvider?: string; - protected prometheus?: PrometheusServicePreferences; - - constructor(private readonly dependencies: ContextHandlerDependencies, protected readonly cluster: Cluster) { - this.clusterUrl = url.parse(cluster.apiUrl); - this.setupPrometheus(cluster.preferences); - } - - public setupPrometheus(preferences: ClusterPrometheusPreferences = {}) { - this.prometheusProvider = preferences.prometheusProvider?.type; - this.prometheus = preferences.prometheus; - } - - public async getPrometheusDetails(): Promise { - const service = await this.getPrometheusService(); - const prometheusPath = this.ensurePrometheusPath(service); - const provider = this.ensurePrometheusProvider(service); - - return { prometheusPath, provider }; - } - - protected ensurePrometheusPath({ service, namespace, port }: PrometheusService): string { - return `${namespace}/services/${service}:${port}`; - } - - protected ensurePrometheusProvider(service: PrometheusService): PrometheusProvider { - if (!this.prometheusProvider) { - this.dependencies.logger.info(`[CONTEXT-HANDLER]: using ${service.kind} as prometheus provider for clusterId=${this.cluster.id}`); - this.prometheusProvider = service.kind; - } - - return this.dependencies.getPrometheusProviderByKind(this.prometheusProvider); - } - - protected listPotentialProviders(): PrometheusProvider[] { - const provider = this.prometheusProvider && this.dependencies.getPrometheusProviderByKind(this.prometheusProvider); - - if (provider) { - return [provider]; - } - - return this.dependencies.prometheusProviders.get(); - } - - protected async getPrometheusService(): Promise { - this.setupPrometheus(this.cluster.preferences); - - if (this.prometheus && this.prometheusProvider) { - return { - kind: this.prometheusProvider, - namespace: this.prometheus.namespace, - service: this.prometheus.service, - port: this.prometheus.port, - }; - } - - const providers = this.listPotentialProviders(); - const proxyConfig = await this.cluster.getProxyKubeconfig(); - const apiClient = proxyConfig.makeApiClient(CoreV1Api); - const potentialServices = await Promise.allSettled( - providers.map(provider => provider.getPrometheusService(apiClient)), - ); - const errors = []; - - for (const res of potentialServices) { - switch (res.status) { - case "rejected": - errors.push(res.reason); - break; - - case "fulfilled": - if (res.value) { - return res.value; - } - } - } - - throw new Error("No Prometheus service found", { cause: errors }); - } - - async resolveAuthProxyUrl(): Promise { - const kubeAuthProxy = await this.ensureServerHelper(); - - return `https://127.0.0.1:${kubeAuthProxy.port}${kubeAuthProxy.apiPrefix}`; - } - - resolveAuthProxyCa() { - return this.dependencies.authProxyCa; - } - - async getApiTarget(isLongRunningRequest = false): Promise { - const timeout = isLongRunningRequest ? 4 * 60 * 60_000 : 30_000; // 4 hours for long running request, 30 seconds for the rest - - if (isLongRunningRequest) { - return this.newApiTarget(timeout); - } - - return this.apiTarget ??= await this.newApiTarget(timeout); - } - - protected async newApiTarget(timeout: number): Promise { - const kubeAuthProxy = await this.ensureServerHelper(); - const headers: Record = {}; - - if (this.clusterUrl.hostname) { - headers.Host = this.clusterUrl.hostname; - - // fix current IPv6 inconsistency in url.Parse() and httpProxy. - // with url.Parse the IPv6 Hostname has no Square brackets but httpProxy needs the Square brackets to work. - if (headers.Host.includes(":")) { - headers.Host = `[${headers.Host}]`; - } - } - - return { - target: { - protocol: "https:", - host: "127.0.0.1", - port: kubeAuthProxy.port, - path: kubeAuthProxy.apiPrefix, - ca: this.resolveAuthProxyCa(), - }, - changeOrigin: true, - timeout, - secure: true, - headers, - }; - } - - protected async ensureServerHelper(): Promise { - if (!this.kubeAuthProxy) { - const proxyEnv = Object.assign({}, process.env); - - if (this.cluster.preferences.httpsProxy) { - proxyEnv.HTTPS_PROXY = this.cluster.preferences.httpsProxy; - } - this.kubeAuthProxy = this.dependencies.createKubeAuthProxy(this.cluster, proxyEnv); - await this.kubeAuthProxy.run(); - - return this.kubeAuthProxy; - } - - await this.kubeAuthProxy.whenReady; - - return this.kubeAuthProxy; - } - - async ensureServer(): Promise { - await this.ensureServerHelper(); - } - - async restartServer(): Promise { - this.stopServer(); - - await this.ensureServerHelper(); - } - - stopServer() { - this.prometheus = undefined; - this.prometheusProvider = undefined; - this.kubeAuthProxy?.exit(); - this.kubeAuthProxy = undefined; - this.apiTarget = undefined; - } -} diff --git a/packages/core/src/main/context-handler/create-context-handler.injectable.ts b/packages/core/src/main/context-handler/create-context-handler.injectable.ts deleted file mode 100644 index 1567721ac4..0000000000 --- a/packages/core/src/main/context-handler/create-context-handler.injectable.ts +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable } from "@ogre-tools/injectable"; -import type { Cluster } from "../../common/cluster/cluster"; -import type { ClusterContextHandler, ContextHandlerDependencies } from "./context-handler"; -import { ContextHandler } from "./context-handler"; -import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable"; -import kubeAuthProxyCertificateInjectable from "../kube-auth-proxy/kube-auth-proxy-certificate.injectable"; -import URLParse from "url-parse"; -import getPrometheusProviderByKindInjectable from "../prometheus/get-by-kind.injectable"; -import prometheusProvidersInjectable from "../prometheus/providers.injectable"; -import loggerInjectable from "../../common/logger.injectable"; - -const createContextHandlerInjectable = getInjectable({ - id: "create-context-handler", - - instantiate: (di) => { - const dependencies: Omit = { - createKubeAuthProxy: di.inject(createKubeAuthProxyInjectable), - getPrometheusProviderByKind: di.inject(getPrometheusProviderByKindInjectable), - prometheusProviders: di.inject(prometheusProvidersInjectable), - logger: di.inject(loggerInjectable), - }; - - return (cluster: Cluster): ClusterContextHandler => { - const clusterUrl = new URLParse(cluster.apiUrl); - - return new ContextHandler({ - ...dependencies, - authProxyCa: di.inject(kubeAuthProxyCertificateInjectable, clusterUrl.hostname).cert, - }, cluster); - }; - }, -}); - -export default createContextHandlerInjectable; diff --git a/packages/core/src/main/create-cluster/create-cluster.injectable.ts b/packages/core/src/main/create-cluster/create-cluster.injectable.ts deleted file mode 100644 index 79eee5a151..0000000000 --- a/packages/core/src/main/create-cluster/create-cluster.injectable.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable } from "@ogre-tools/injectable"; -import type { ClusterDependencies } from "../../common/cluster/cluster"; -import { Cluster } from "../../common/cluster/cluster"; -import directoryForKubeConfigsInjectable from "../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable"; -import createKubeconfigManagerInjectable from "../kubeconfig-manager/create-kubeconfig-manager.injectable"; -import createKubectlInjectable from "../kubectl/create-kubectl.injectable"; -import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable"; -import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token"; -import authorizationReviewInjectable from "../../common/cluster/authorization-review.injectable"; -import listNamespacesInjectable from "../../common/cluster/list-namespaces.injectable"; -import createListApiResourcesInjectable from "../cluster/request-api-resources.injectable"; -import loggerInjectable from "../../common/logger.injectable"; -import broadcastMessageInjectable from "../../common/ipc/broadcast-message.injectable"; -import loadConfigfromFileInjectable from "../../common/kube-helpers/load-config-from-file.injectable"; -import requestNamespaceListPermissionsForInjectable from "../../common/cluster/request-namespace-list-permissions.injectable"; -import detectClusterMetadataInjectable from "../cluster-detectors/detect-cluster-metadata.injectable"; -import clusterVersionDetectorInjectable from "../cluster-detectors/cluster-version-detector.injectable"; - -const createClusterInjectable = getInjectable({ - id: "create-cluster", - - instantiate: (di) => { - const dependencies: ClusterDependencies = { - directoryForKubeConfigs: di.inject(directoryForKubeConfigsInjectable), - logger: di.inject(loggerInjectable), - clusterVersionDetector: di.inject(clusterVersionDetectorInjectable), - createKubeconfigManager: di.inject(createKubeconfigManagerInjectable), - createKubectl: di.inject(createKubectlInjectable), - createContextHandler: di.inject(createContextHandlerInjectable), - createAuthorizationReview: di.inject(authorizationReviewInjectable), - requestNamespaceListPermissionsFor: di.inject(requestNamespaceListPermissionsForInjectable), - requestApiResources: di.inject(createListApiResourcesInjectable), - createListNamespaces: di.inject(listNamespacesInjectable), - broadcastMessage: di.inject(broadcastMessageInjectable), - loadConfigfromFile: di.inject(loadConfigfromFileInjectable), - detectClusterMetadata: di.inject(detectClusterMetadataInjectable), - }; - - return (model, configData) => new Cluster(dependencies, model, configData); - }, - - injectionToken: createClusterInjectionToken, -}); - -export default createClusterInjectable; diff --git a/packages/core/src/main/electron-app/runnables/setup-ipc-main-handlers/setup-ipc-main-handlers.injectable.ts b/packages/core/src/main/electron-app/runnables/setup-ipc-main-handlers/setup-ipc-main-handlers.injectable.ts index 69d6f4405b..903389a018 100644 --- a/packages/core/src/main/electron-app/runnables/setup-ipc-main-handlers/setup-ipc-main-handlers.injectable.ts +++ b/packages/core/src/main/electron-app/runnables/setup-ipc-main-handlers/setup-ipc-main-handlers.injectable.ts @@ -11,6 +11,7 @@ import applicationMenuItemCompositeInjectable from "../../../../features/applica import emitAppEventInjectable from "../../../../common/app-event-bus/emit-event.injectable"; import getClusterByIdInjectable from "../../../../common/cluster-store/get-by-id.injectable"; import pushCatalogToRendererInjectable from "../../../catalog-sync-to-renderer/push-catalog-to-renderer.injectable"; +import clusterConnectionInjectable from "../../../cluster/cluster-connection.injectable"; const setupIpcMainHandlersInjectable = getInjectable({ id: "setup-ipc-main-handlers", @@ -34,6 +35,7 @@ const setupIpcMainHandlersInjectable = getInjectable({ clusterStore, emitAppEvent, getClusterById, + getClusterConnection: (cluster) => di.inject(clusterConnectionInjectable, cluster), }); }, }; diff --git a/packages/core/src/main/electron-app/runnables/setup-ipc-main-handlers/setup-ipc-main-handlers.ts b/packages/core/src/main/electron-app/runnables/setup-ipc-main-handlers/setup-ipc-main-handlers.ts index 4ae020c42d..ddfc3f2003 100644 --- a/packages/core/src/main/electron-app/runnables/setup-ipc-main-handlers/setup-ipc-main-handlers.ts +++ b/packages/core/src/main/electron-app/runnables/setup-ipc-main-handlers/setup-ipc-main-handlers.ts @@ -18,12 +18,15 @@ import { getApplicationMenuTemplate } from "../../../../features/application-men import type { MenuItemRoot } from "../../../../features/application-menu/main/application-menu-item-composite.injectable"; import type { EmitAppEvent } from "../../../../common/app-event-bus/emit-event.injectable"; import type { GetClusterById } from "../../../../common/cluster-store/get-by-id.injectable"; +import type { Cluster } from "../../../../common/cluster/cluster"; +import type { ClusterConnection } from "../../../cluster/cluster-connection.injectable"; interface Dependencies { applicationMenuItemComposite: IComputedValue>; clusterStore: ClusterStore; emitAppEvent: EmitAppEvent; getClusterById: GetClusterById; pushCatalogToRenderer: () => void; + getClusterConnection: (cluster: Cluster) => ClusterConnection; } export const setupIpcMainHandlers = ({ @@ -32,10 +35,18 @@ export const setupIpcMainHandlers = ({ emitAppEvent, getClusterById, pushCatalogToRenderer, + getClusterConnection, }: Dependencies) => { - ipcMainHandle(clusterActivateHandler, (event, clusterId: ClusterId, force = false) => { - return getClusterById(clusterId) - ?.activate(force); + ipcMainHandle(clusterActivateHandler, async (event, clusterId: ClusterId, force = false) => { + const cluster = getClusterById(clusterId); + + if (!cluster) { + return; + } + + const clusterConnection = getClusterConnection(cluster); + + await clusterConnection.activate(force); }); ipcMainHandle(clusterSetFrameIdHandler, (event: IpcMainInvokeEvent, clusterId: ClusterId) => { @@ -51,10 +62,14 @@ export const setupIpcMainHandlers = ({ emitAppEvent({ name: "cluster", action: "stop" }); const cluster = getClusterById(clusterId); - if (cluster) { - cluster.disconnect(); - clusterFrameMap.delete(cluster.id); + if (!cluster) { + return; } + + const clusterConnection = getClusterConnection(cluster); + + clusterConnection.disconnect(); + clusterFrameMap.delete(cluster.id); }); ipcMainHandle(windowActionHandleChannel, (event, action) => handleWindowAction(action)); diff --git a/packages/core/src/main/helm/helm-service/delete-helm-release.injectable.ts b/packages/core/src/main/helm/helm-service/delete-helm-release.injectable.ts index fa27980355..63244b4583 100644 --- a/packages/core/src/main/helm/helm-service/delete-helm-release.injectable.ts +++ b/packages/core/src/main/helm/helm-service/delete-helm-release.injectable.ts @@ -5,6 +5,7 @@ import { getInjectable } from "@ogre-tools/injectable"; import type { Cluster } from "../../../common/cluster/cluster"; import loggerInjectable from "../../../common/logger.injectable"; +import kubeconfigManagerInjectable from "../../kubeconfig-manager/kubeconfig-manager.injectable"; import type { DeleteHelmReleaseData } from "../delete-helm-release.injectable"; import deleteHelmReleaseInjectable from "../delete-helm-release.injectable"; @@ -16,11 +17,12 @@ const deleteClusterHelmReleaseInjectable = getInjectable({ const deleteHelmRelease = di.inject(deleteHelmReleaseInjectable); return async (cluster: Cluster, data: DeleteHelmReleaseData) => { - const proxyKubeconfig = await cluster.getProxyKubeconfigPath(); + const proxyKubeconfigManager = di.inject(kubeconfigManagerInjectable, cluster); + const proxyKubeconfigPath = await proxyKubeconfigManager.ensurePath(); logger.debug(`[CLUSTER]: Delete helm release`, data); - return deleteHelmRelease(proxyKubeconfig, data); + return deleteHelmRelease(proxyKubeconfigPath, data); }; }, }); diff --git a/packages/core/src/main/helm/helm-service/get-helm-release-history.injectable.ts b/packages/core/src/main/helm/helm-service/get-helm-release-history.injectable.ts index fe4e3448ac..45ca241c89 100644 --- a/packages/core/src/main/helm/helm-service/get-helm-release-history.injectable.ts +++ b/packages/core/src/main/helm/helm-service/get-helm-release-history.injectable.ts @@ -5,6 +5,7 @@ import { getInjectable } from "@ogre-tools/injectable"; import type { Cluster } from "../../../common/cluster/cluster"; import loggerInjectable from "../../../common/logger.injectable"; +import kubeconfigManagerInjectable from "../../kubeconfig-manager/kubeconfig-manager.injectable"; import type { GetHelmReleaseHistoryData } from "../get-helm-release-history.injectable"; import getHelmReleaseHistoryInjectable from "../get-helm-release-history.injectable"; @@ -16,11 +17,12 @@ const getClusterHelmReleaseHistoryInjectable = getInjectable({ const getHelmReleaseHistory = di.inject(getHelmReleaseHistoryInjectable); return async (cluster: Cluster, data: GetHelmReleaseHistoryData) => { - const proxyKubeconfig = await cluster.getProxyKubeconfigPath(); + const proxyKubeconfigManager = di.inject(kubeconfigManagerInjectable, cluster); + const proxyKubeconfigPath = await proxyKubeconfigManager.ensurePath(); logger.debug(`[CLUSTER]: Fetch release history for clusterId=${cluster.id}`, data); - return getHelmReleaseHistory(proxyKubeconfig, data); + return getHelmReleaseHistory(proxyKubeconfigPath, data); }; }, }); diff --git a/packages/core/src/main/helm/helm-service/get-helm-release-values.injectable.ts b/packages/core/src/main/helm/helm-service/get-helm-release-values.injectable.ts index 8070ed366e..41c0d44e8f 100644 --- a/packages/core/src/main/helm/helm-service/get-helm-release-values.injectable.ts +++ b/packages/core/src/main/helm/helm-service/get-helm-release-values.injectable.ts @@ -7,6 +7,7 @@ import loggerInjectable from "../../../common/logger.injectable"; import type { Cluster } from "../../../common/cluster/cluster"; import type { GetHelmReleaseValuesData } from "../get-helm-release-values.injectable"; import getHelmReleaseValuesInjectable from "../get-helm-release-values.injectable"; +import kubeconfigManagerInjectable from "../../kubeconfig-manager/kubeconfig-manager.injectable"; const getClusterHelmReleaseValuesInjectable = getInjectable({ id: "get-cluster-helm-release-values", @@ -16,11 +17,12 @@ const getClusterHelmReleaseValuesInjectable = getInjectable({ const getHelmReleaseValues = di.inject(getHelmReleaseValuesInjectable); return async (cluster: Cluster, data: GetHelmReleaseValuesData) => { - const pathToKubeconfig = await cluster.getProxyKubeconfigPath(); + const proxyKubeconfigManager = di.inject(kubeconfigManagerInjectable, cluster); + const proxyKubeconfigPath = await proxyKubeconfigManager.ensurePath(); logger.debug(`[CLUSTER]: getting helm release values`, data); - return getHelmReleaseValues(pathToKubeconfig, data); + return getHelmReleaseValues(proxyKubeconfigPath, data); }; }, }); diff --git a/packages/core/src/main/helm/helm-service/get-helm-release.injectable.ts b/packages/core/src/main/helm/helm-service/get-helm-release.injectable.ts index 71d3fe6184..6dfed7c6b7 100644 --- a/packages/core/src/main/helm/helm-service/get-helm-release.injectable.ts +++ b/packages/core/src/main/helm/helm-service/get-helm-release.injectable.ts @@ -6,6 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable"; import type { Cluster } from "../../../common/cluster/cluster"; import loggerInjectable from "../../../common/logger.injectable"; import { isObject, json } from "../../../common/utils"; +import kubeconfigManagerInjectable from "../../kubeconfig-manager/kubeconfig-manager.injectable"; import execHelmInjectable from "../exec-helm/exec-helm.injectable"; import getHelmReleaseResourcesInjectable from "./get-helm-release-resources/get-helm-release-resources.injectable"; @@ -18,7 +19,8 @@ const getHelmReleaseInjectable = getInjectable({ const getHelmReleaseResources = di.inject(getHelmReleaseResourcesInjectable); return async (cluster: Cluster, releaseName: string, namespace: string) => { - const kubeconfigPath = await cluster.getProxyKubeconfigPath(); + const proxyKubeconfigManager = di.inject(kubeconfigManagerInjectable, cluster); + const proxyKubeconfigPath = await proxyKubeconfigManager.ensurePath(); logger.debug("Fetch release"); @@ -28,7 +30,7 @@ const getHelmReleaseInjectable = getInjectable({ "--namespace", namespace, "--kubeconfig", - kubeconfigPath, + proxyKubeconfigPath, "--output", "json", ]); @@ -48,7 +50,7 @@ const getHelmReleaseInjectable = getInjectable({ const resourcesResult = await getHelmReleaseResources( releaseName, namespace, - kubeconfigPath, + proxyKubeconfigPath, ); if (!resourcesResult.callWasSuccessful) { diff --git a/packages/core/src/main/helm/helm-service/install-helm-chart.injectable.ts b/packages/core/src/main/helm/helm-service/install-helm-chart.injectable.ts index b81b48cfa6..3500395ad3 100644 --- a/packages/core/src/main/helm/helm-service/install-helm-chart.injectable.ts +++ b/packages/core/src/main/helm/helm-service/install-helm-chart.injectable.ts @@ -5,6 +5,7 @@ import { getInjectable } from "@ogre-tools/injectable"; import type { JsonObject } from "type-fest"; import type { Cluster } from "../../../common/cluster/cluster"; +import kubeconfigManagerInjectable from "../../kubeconfig-manager/kubeconfig-manager.injectable"; import installHelmChartInjectable from "../install-helm-chart.injectable"; export interface InstallChartArgs { @@ -22,11 +23,12 @@ const installClusterHelmChartInjectable = getInjectable({ const installHelmChart = di.inject(installHelmChartInjectable); return async (cluster: Cluster, data: InstallChartArgs) => { - const proxyKubeconfig = await cluster.getProxyKubeconfigPath(); + const proxyKubeconfigManager = di.inject(kubeconfigManagerInjectable, cluster); + const proxyKubeconfigPath = await proxyKubeconfigManager.ensurePath(); return installHelmChart({ ...data, - kubeconfigPath: proxyKubeconfig, + kubeconfigPath: proxyKubeconfigPath, }); }; }, diff --git a/packages/core/src/main/helm/helm-service/list-helm-releases.injectable.ts b/packages/core/src/main/helm/helm-service/list-helm-releases.injectable.ts index 14a27f6ea6..d594c3f756 100644 --- a/packages/core/src/main/helm/helm-service/list-helm-releases.injectable.ts +++ b/packages/core/src/main/helm/helm-service/list-helm-releases.injectable.ts @@ -5,6 +5,7 @@ import { getInjectable } from "@ogre-tools/injectable"; import type { Cluster } from "../../../common/cluster/cluster"; import loggerInjectable from "../../../common/logger.injectable"; +import kubeconfigManagerInjectable from "../../kubeconfig-manager/kubeconfig-manager.injectable"; import listHelmReleasesInjectable from "../list-helm-releases.injectable"; const listClusterHelmReleasesInjectable = getInjectable({ @@ -15,11 +16,12 @@ const listClusterHelmReleasesInjectable = getInjectable({ const listHelmReleases = di.inject(listHelmReleasesInjectable); return async (cluster: Cluster, namespace?: string) => { - const proxyKubeconfig = await cluster.getProxyKubeconfigPath(); + const proxyKubeconfigManager = di.inject(kubeconfigManagerInjectable, cluster); + const proxyKubeconfigPath = await proxyKubeconfigManager.ensurePath(); logger.debug(`[CLUSTER]: listing helm releases for clusterId=${cluster.id}`, { namespace }); - return listHelmReleases(proxyKubeconfig, namespace); + return listHelmReleases(proxyKubeconfigPath, namespace); }; }, }); diff --git a/packages/core/src/main/helm/helm-service/rollback-helm-release.injectable.ts b/packages/core/src/main/helm/helm-service/rollback-helm-release.injectable.ts index a426de8f1c..0be6afccbe 100644 --- a/packages/core/src/main/helm/helm-service/rollback-helm-release.injectable.ts +++ b/packages/core/src/main/helm/helm-service/rollback-helm-release.injectable.ts @@ -5,6 +5,7 @@ import { getInjectable } from "@ogre-tools/injectable"; import type { Cluster } from "../../../common/cluster/cluster"; import loggerInjectable from "../../../common/logger.injectable"; +import kubeconfigManagerInjectable from "../../kubeconfig-manager/kubeconfig-manager.injectable"; import type { RollbackHelmReleaseData } from "../rollback-helm-release.injectable"; import rollbackHelmReleaseInjectable from "../rollback-helm-release.injectable"; @@ -16,11 +17,12 @@ const rollbackClusterHelmReleaseInjectable = getInjectable({ const rollbackHelmRelease = di.inject(rollbackHelmReleaseInjectable); return async (cluster: Cluster, data: RollbackHelmReleaseData) => { - const proxyKubeconfig = await cluster.getProxyKubeconfigPath(); + const proxyKubeconfigManager = di.inject(kubeconfigManagerInjectable, cluster); + const proxyKubeconfigPath = await proxyKubeconfigManager.ensurePath(); logger.debug(`[CLUSTER]: rolling back helm release for clusterId=${cluster.id}`, data); - await rollbackHelmRelease(proxyKubeconfig, data); + await rollbackHelmRelease(proxyKubeconfigPath, data); }; }, }); diff --git a/packages/core/src/main/helm/helm-service/update-helm-release.injectable.ts b/packages/core/src/main/helm/helm-service/update-helm-release.injectable.ts index 274dae8cc4..0df63447b3 100644 --- a/packages/core/src/main/helm/helm-service/update-helm-release.injectable.ts +++ b/packages/core/src/main/helm/helm-service/update-helm-release.injectable.ts @@ -10,6 +10,7 @@ import getHelmReleaseInjectable from "./get-helm-release.injectable"; import writeFileInjectable from "../../../common/fs/write-file.injectable"; import removePathInjectable from "../../../common/fs/remove.injectable"; import execHelmInjectable from "../exec-helm/exec-helm.injectable"; +import kubeconfigManagerInjectable from "../../kubeconfig-manager/kubeconfig-manager.injectable"; export interface UpdateChartArgs { chart: string; @@ -28,7 +29,8 @@ const updateHelmReleaseInjectable = getInjectable({ const execHelm = di.inject(execHelmInjectable); return async (cluster: Cluster, releaseName: string, namespace: string, data: UpdateChartArgs) => { - const proxyKubeconfig = await cluster.getProxyKubeconfigPath(); + const proxyKubeconfigManager = di.inject(kubeconfigManagerInjectable, cluster); + const proxyKubeconfigPath = await proxyKubeconfigManager.ensurePath(); const valuesFilePath = tempy.file({ name: "values.yaml" }); logger.debug(`[HELM]: upgrading "${releaseName}" in "${namespace}" to ${data.version}`); @@ -43,7 +45,7 @@ const updateHelmReleaseInjectable = getInjectable({ "--version", data.version, "--values", valuesFilePath, "--namespace", namespace, - "--kubeconfig", proxyKubeconfig, + "--kubeconfig", proxyKubeconfigPath, ]); if (result.callWasSuccessful === false) { diff --git a/packages/core/src/main/hotbar-store/migrations/5.0.0-beta.10.injectable.ts b/packages/core/src/main/hotbar-store/migrations/5.0.0-beta.10.injectable.ts index ddbda706be..a331ebf106 100644 --- a/packages/core/src/main/hotbar-store/migrations/5.0.0-beta.10.injectable.ts +++ b/packages/core/src/main/hotbar-store/migrations/5.0.0-beta.10.injectable.ts @@ -4,7 +4,6 @@ */ import * as uuid from "uuid"; -import type { ClusterStoreModel } from "../../../common/cluster-store/cluster-store"; import type { Hotbar, HotbarItem } from "../../../common/hotbars/types"; import { defaultHotbarCells, getEmptyHotbar } from "../../../common/hotbars/types"; import { getLegacyGlobalDiForExtensionApi } from "../../../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; @@ -17,6 +16,7 @@ import { hotbarStoreMigrationInjectionToken } from "../../../common/hotbars/migr import readJsonSyncInjectable from "../../../common/fs/read-json-sync.injectable"; import loggerInjectable from "../../../common/logger.injectable"; import { generateNewIdFor } from "../../../common/utils/generate-new-id-for"; +import type { ClusterModel } from "../../../common/cluster-types"; interface Pre500WorkspaceStoreModel { workspaces: { @@ -31,6 +31,15 @@ interface PartialHotbar { items: (null | HotbarItem)[]; } +interface Pre500ClusterModel extends ClusterModel { + workspace?: string; + workspaces?: string[]; +} + +interface Pre500ClusterStoreModel { + clusters?: Pre500ClusterModel[]; +} + const v500Beta10HotbarStoreMigrationInjectable = getInjectable({ id: "v5.0.0-beta.10-hotbar-store-migration", instantiate: (di) => { @@ -59,7 +68,7 @@ const v500Beta10HotbarStoreMigrationInjectable = getInjectable({ try { const workspaceStoreData: Pre500WorkspaceStoreModel = readJsonSync(joinPaths(userDataPath, "lens-workspace-store.json")); - const { clusters = [] }: ClusterStoreModel = readJsonSync(joinPaths(userDataPath, "lens-cluster-store.json")); + const { clusters = [] }: Pre500ClusterStoreModel = readJsonSync(joinPaths(userDataPath, "lens-cluster-store.json")); const workspaceHotbars = new Map(); // mapping from WorkspaceId to HotBar for (const { id, name } of workspaceStoreData.workspaces) { diff --git a/packages/core/src/main/kube-auth-proxy/create-kube-auth-proxy.injectable.ts b/packages/core/src/main/kube-auth-proxy/create-kube-auth-proxy.injectable.ts index e7d66eb8f4..56260347bf 100644 --- a/packages/core/src/main/kube-auth-proxy/create-kube-auth-proxy.injectable.ts +++ b/packages/core/src/main/kube-auth-proxy/create-kube-auth-proxy.injectable.ts @@ -13,14 +13,15 @@ import waitUntilPortIsUsedInjectable from "./wait-until-port-is-used/wait-until- import lensK8sProxyPathInjectable from "./lens-k8s-proxy-path.injectable"; import getPortFromStreamInjectable from "../utils/get-port-from-stream.injectable"; import getDirnameOfPathInjectable from "../../common/path/get-dirname.injectable"; +import broadcastConnectionUpdateInjectable from "../cluster/broadcast-connection-update.injectable"; -export type CreateKubeAuthProxy = (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => KubeAuthProxy; +export type CreateKubeAuthProxy = (cluster: Cluster, env: NodeJS.ProcessEnv) => KubeAuthProxy; const createKubeAuthProxyInjectable = getInjectable({ id: "create-kube-auth-proxy", instantiate: (di): CreateKubeAuthProxy => { - const dependencies: Omit = { + const dependencies: Omit = { proxyBinPath: di.inject(lensK8sProxyPathInjectable), spawn: di.inject(spawnInjectable), logger: di.inject(loggerInjectable), @@ -29,13 +30,14 @@ const createKubeAuthProxyInjectable = getInjectable({ dirname: di.inject(getDirnameOfPathInjectable), }; - return (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => { - const clusterUrl = new URL(cluster.apiUrl); + return (cluster, env) => { + const clusterUrl = new URL(cluster.apiUrl.get()); return new KubeAuthProxy({ ...dependencies, proxyCert: di.inject(kubeAuthProxyCertificateInjectable, clusterUrl.hostname), - }, cluster, environmentVariables); + broadcastConnectionUpdate: di.inject(broadcastConnectionUpdateInjectable, cluster), + }, cluster, env); }; }, }); diff --git a/packages/core/src/main/kube-auth-proxy/kube-auth-proxy.ts b/packages/core/src/main/kube-auth-proxy/kube-auth-proxy.ts index 669780b5b6..9a9b5a249f 100644 --- a/packages/core/src/main/kube-auth-proxy/kube-auth-proxy.ts +++ b/packages/core/src/main/kube-auth-proxy/kube-auth-proxy.ts @@ -7,7 +7,7 @@ import type { ChildProcess } from "child_process"; import { randomBytes } from "crypto"; import type { Cluster } from "../../common/cluster/cluster"; import type { GetPortFromStream } from "../utils/get-port-from-stream.injectable"; -import { makeObservable, observable, when } from "mobx"; +import { observable, when } from "mobx"; import type { SelfSignedCert } from "selfsigned"; import assert from "assert"; import { TypedRegEx } from "typed-regex"; @@ -15,6 +15,7 @@ import type { Spawn } from "../child-process/spawn.injectable"; import type { Logger } from "../../common/logger"; import type { WaitUntilPortIsUsed } from "./wait-until-port-is-used/wait-until-port-is-used.injectable"; import type { GetDirnameOfPath } from "../../common/path/get-dirname.injectable"; +import type { BroadcastConnectionUpdate } from "../cluster/broadcast-connection-update.injectable"; const startingServeMatcher = "starting to serve on (?
.+)"; const startingServeRegex = Object.assign(TypedRegEx(startingServeMatcher, "i"), { @@ -29,6 +30,7 @@ export interface KubeAuthProxyDependencies { waitUntilPortIsUsed: WaitUntilPortIsUsed; getPortFromStream: GetPortFromStream; dirname: GetDirnameOfPath; + broadcastConnectionUpdate: BroadcastConnectionUpdate; } export class KubeAuthProxy { @@ -44,19 +46,17 @@ export class KubeAuthProxy { protected _port?: number; protected proxyProcess?: ChildProcess; - @observable protected ready = false; + protected readonly ready = observable.box(false); - constructor(private readonly dependencies: KubeAuthProxyDependencies, protected readonly cluster: Cluster, protected readonly env: NodeJS.ProcessEnv) { - makeObservable(this); - } - - get whenReady() { - return when(() => this.ready); - } + constructor( + private readonly dependencies: KubeAuthProxyDependencies, + protected readonly cluster: Cluster, + protected readonly env: NodeJS.ProcessEnv, + ) {} public async run(): Promise { if (this.proxyProcess) { - return this.whenReady; + return when(() => this.ready.get()); } const proxyBin = this.dependencies.proxyBinPath; @@ -65,26 +65,42 @@ export class KubeAuthProxy { this.proxyProcess = this.dependencies.spawn(proxyBin, [], { env: { ...this.env, - KUBECONFIG: this.cluster.kubeConfigPath, - KUBECONFIG_CONTEXT: this.cluster.contextName, + KUBECONFIG: this.cluster.kubeConfigPath.get(), + KUBECONFIG_CONTEXT: this.cluster.contextName.get(), API_PREFIX: this.apiPrefix, PROXY_KEY: cert.private, PROXY_CERT: cert.cert, }, - cwd: this.dependencies.dirname(this.cluster.kubeConfigPath), + cwd: this.dependencies.dirname(this.cluster.kubeConfigPath.get()), }); this.proxyProcess.on("error", (error) => { - this.cluster.broadcastConnectUpdate(error.message, "error"); + this.dependencies.broadcastConnectionUpdate({ + level: "error", + message: error.message, + }); this.exit(); }); this.proxyProcess.on("exit", (code) => { - this.cluster.broadcastConnectUpdate(`proxy exited with code: ${code}`, code ? "error" : "info"); + if (code) { + this.dependencies.broadcastConnectionUpdate({ + level: "error", + message: `proxy exited with code: ${code}`, + }); + } else { + this.dependencies.broadcastConnectionUpdate({ + level: "info", + message: "proxy exited successfully", + }); + } this.exit(); }); this.proxyProcess.on("disconnect", () => { - this.cluster.broadcastConnectUpdate("Proxy disconnected communications", "error"); + this.dependencies.broadcastConnectionUpdate({ + level: "error", + message: "Proxy disconnected communications", + }); this.exit(); }); @@ -96,28 +112,40 @@ export class KubeAuthProxy { return; } - this.cluster.broadcastConnectUpdate(data.toString(), "error"); + this.dependencies.broadcastConnectionUpdate({ + level: "error", + message: data.toString(), + }); }); this.proxyProcess.stdout.on("data", (data: Buffer) => { if (typeof this._port === "number") { - this.cluster.broadcastConnectUpdate(data.toString()); + this.dependencies.broadcastConnectionUpdate({ + level: "info", + message: data.toString(), + }); } }); this._port = await this.dependencies.getPortFromStream(this.proxyProcess.stdout, { lineRegex: startingServeRegex, - onFind: () => this.cluster.broadcastConnectUpdate("Authentication proxy started"), + onFind: () => this.dependencies.broadcastConnectionUpdate({ + level: "info", + message: "Authentication proxy started", + }), }); this.dependencies.logger.info(`[KUBE-AUTH-PROXY]: found port=${this._port}`); try { await this.dependencies.waitUntilPortIsUsed(this.port, 500, 10000); - this.ready = true; + this.ready.set(true); } catch (error) { this.dependencies.logger.warn("[KUBE-AUTH-PROXY]: waitUntilUsed failed", error); - this.cluster.broadcastConnectUpdate("Proxy port failed to be used within timelimit, restarting...", "error"); + this.dependencies.broadcastConnectionUpdate({ + level: "error", + message: "Proxy port failed to be used within time limit, restarting...", + }); this.exit(); return this.run(); @@ -125,7 +153,7 @@ export class KubeAuthProxy { } public exit() { - this.ready = false; + this.ready.set(false); if (this.proxyProcess) { this.dependencies.logger.debug("[KUBE-AUTH]: stopping local proxy", this.cluster.getMeta()); diff --git a/packages/core/src/main/kubeconfig-manager/create-kubeconfig-manager.injectable.ts b/packages/core/src/main/kubeconfig-manager/kubeconfig-manager.injectable.ts similarity index 61% rename from packages/core/src/main/kubeconfig-manager/create-kubeconfig-manager.injectable.ts rename to packages/core/src/main/kubeconfig-manager/kubeconfig-manager.injectable.ts index 010ec7174e..6e5f54b24c 100644 --- a/packages/core/src/main/kubeconfig-manager/create-kubeconfig-manager.injectable.ts +++ b/packages/core/src/main/kubeconfig-manager/kubeconfig-manager.injectable.ts @@ -2,44 +2,43 @@ * 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 { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import type { Cluster } from "../../common/cluster/cluster"; import directoryForTempInjectable from "../../common/app-paths/directory-for-temp/directory-for-temp.injectable"; -import type { KubeconfigManagerDependencies } from "./kubeconfig-manager"; import { KubeconfigManager } from "./kubeconfig-manager"; import loggerInjectable from "../../common/logger.injectable"; -import lensProxyPortInjectable from "../lens-proxy/lens-proxy-port.injectable"; import joinPathsInjectable from "../../common/path/join-paths.injectable"; import getDirnameOfPathInjectable from "../../common/path/get-dirname.injectable"; import pathExistsInjectable from "../../common/fs/path-exists.injectable"; import writeFileInjectable from "../../common/fs/write-file.injectable"; import removePathInjectable from "../../common/fs/remove.injectable"; import lensProxyCertificateInjectable from "../../common/certificate/lens-proxy-certificate.injectable"; +import kubeAuthProxyServerInjectable from "../cluster/kube-auth-proxy-server.injectable"; +import kubeAuthProxyUrlInjectable from "../cluster/auth-proxy-url.injectable"; +import loadKubeconfigInjectable from "../../common/cluster/load-kubeconfig.injectable"; -export interface KubeConfigManagerInstantiationParameter { - cluster: Cluster; -} +const kubeconfigManagerInjectable = getInjectable({ + id: "kubeconfig-manager", -export type CreateKubeconfigManager = (cluster: Cluster) => KubeconfigManager; - -const createKubeconfigManagerInjectable = getInjectable({ - id: "create-kubeconfig-manager", - - instantiate: (di): CreateKubeconfigManager => { - const dependencies: KubeconfigManagerDependencies = { + instantiate: (di, cluster) => new KubeconfigManager( + { directoryForTemp: di.inject(directoryForTempInjectable), logger: di.inject(loggerInjectable), - lensProxyPort: di.inject(lensProxyPortInjectable), joinPaths: di.inject(joinPathsInjectable), getDirnameOfPath: di.inject(getDirnameOfPathInjectable), removePath: di.inject(removePathInjectable), pathExists: di.inject(pathExistsInjectable), writeFile: di.inject(writeFileInjectable), certificate: di.inject(lensProxyCertificateInjectable).get(), - }; - - return (cluster) => new KubeconfigManager(dependencies, cluster); - }, + loadKubeconfig: di.inject(loadKubeconfigInjectable, cluster), + kubeAuthProxyServer: di.inject(kubeAuthProxyServerInjectable, cluster), + kubeAuthProxyUrl: di.inject(kubeAuthProxyUrlInjectable, cluster), + }, + cluster, + ), + lifecycle: lifecycleEnum.keyedSingleton({ + getInstanceKey: (di, cluster: Cluster) => cluster.id, + }), }); -export default createKubeconfigManagerInjectable; +export default kubeconfigManagerInjectable; diff --git a/packages/core/src/main/kubeconfig-manager/kubeconfig-manager.ts b/packages/core/src/main/kubeconfig-manager/kubeconfig-manager.ts index 0486521e21..8860ffd3c1 100644 --- a/packages/core/src/main/kubeconfig-manager/kubeconfig-manager.ts +++ b/packages/core/src/main/kubeconfig-manager/kubeconfig-manager.ts @@ -4,8 +4,6 @@ */ import type { KubeConfig } from "@kubernetes/client-node"; -import type { Cluster } from "../../common/cluster/cluster"; -import type { ClusterContextHandler } from "../context-handler/context-handler"; import { dumpConfigYaml } from "../../common/kube-helpers"; import { isErrnoException } from "../../common/utils"; import type { PartialDeep } from "type-fest"; @@ -16,17 +14,22 @@ import type { PathExists } from "../../common/fs/path-exists.injectable"; import type { RemovePath } from "../../common/fs/remove.injectable"; import type { WriteFile } from "../../common/fs/write-file.injectable"; import type { SelfSignedCert } from "selfsigned"; +import type { Cluster } from "../../common/cluster/cluster"; +import type { LoadKubeconfig } from "../../common/cluster/load-kubeconfig.injectable"; +import type { KubeAuthProxyServer } from "../cluster/kube-auth-proxy-server.injectable"; -export interface KubeconfigManagerDependencies { +interface KubeconfigManagerDependencies { readonly directoryForTemp: string; readonly logger: Logger; - readonly lensProxyPort: { get: () => number }; + readonly certificate: SelfSignedCert; + readonly kubeAuthProxyServer: KubeAuthProxyServer; + readonly kubeAuthProxyUrl: string; joinPaths: JoinPaths; getDirnameOfPath: GetDirnameOfPath; pathExists: PathExists; removePath: RemovePath; writeFile: WriteFile; - certificate: SelfSignedCert; + loadKubeconfig: LoadKubeconfig; } export class KubeconfigManager { @@ -38,17 +41,16 @@ export class KubeconfigManager { */ protected tempFilePath: string | null = null; - protected readonly contextHandler: ClusterContextHandler; - - constructor(private readonly dependencies: KubeconfigManagerDependencies, protected cluster: Cluster) { - this.contextHandler = cluster.contextHandler; - } + constructor( + private readonly dependencies: KubeconfigManagerDependencies, + private readonly cluster: Cluster, + ) {} /** * * @returns The path to the temporary kubeconfig */ - async getPath(): Promise { + async ensurePath(): Promise { if (this.tempFilePath === null || !(await this.dependencies.pathExists(this.tempFilePath))) { return await this.ensureFile(); } @@ -79,7 +81,7 @@ export class KubeconfigManager { protected async ensureFile() { try { - await this.contextHandler.ensureServer(); + await this.dependencies.kubeAuthProxyServer.ensureRunning(); return this.tempFilePath = await this.createProxyKubeconfig(); } catch (error) { @@ -87,31 +89,26 @@ export class KubeconfigManager { } } - get resolveProxyUrl() { - return `https://127.0.0.1:${this.dependencies.lensProxyPort.get()}/${this.cluster.id}`; - } - /** * Creates new "temporary" kubeconfig that point to the kubectl-proxy. * This way any user of the config does not need to know anything about the auth etc. details. */ protected async createProxyKubeconfig(): Promise { - const { cluster } = this; - const { contextName, id } = cluster; + const { id, preferences: { defaultNamespace }} = this.cluster; + const contextName = this.cluster.contextName.get(); const tempFile = this.dependencies.joinPaths( this.dependencies.directoryForTemp, `kubeconfig-${id}`, ); - const kubeConfig = await cluster.getKubeconfig(); - const { certificate } = this.dependencies; + const kubeConfig = await this.dependencies.loadKubeconfig(); const proxyConfig: PartialDeep = { currentContext: contextName, clusters: [ { name: contextName, - server: this.resolveProxyUrl, + server: this.dependencies.kubeAuthProxyUrl, skipTLSVerify: false, - caData: Buffer.from(certificate.cert).toString("base64"), + caData: Buffer.from(this.dependencies.certificate.cert).toString("base64"), }, ], users: [ @@ -122,7 +119,7 @@ export class KubeconfigManager { user: "proxy", name: contextName, cluster: contextName, - namespace: cluster.defaultNamespace || kubeConfig.getContextObject(contextName)?.namespace, + namespace: defaultNamespace || kubeConfig.getContextObject(contextName)?.namespace, }, ], }; diff --git a/packages/core/src/main/kubectl/apply-all-handler.injectable.ts b/packages/core/src/main/kubectl/apply-all-handler.injectable.ts index ca9d13577b..7f62ca14fc 100644 --- a/packages/core/src/main/kubectl/apply-all-handler.injectable.ts +++ b/packages/core/src/main/kubectl/apply-all-handler.injectable.ts @@ -5,7 +5,7 @@ import emitAppEventInjectable from "../../common/app-event-bus/emit-event.injectable"; import getClusterByIdInjectable from "../../common/cluster-store/get-by-id.injectable"; import { kubectlApplyAllChannel } from "../../common/kube-helpers/channels"; -import createResourceApplierInjectable from "../resource-applier/create-resource-applier.injectable"; +import resourceApplierInjectable from "../resource-applier/create-resource-applier.injectable"; import { getRequestChannelListenerInjectable } from "../utils/channel/channel-listeners/listener-tokens"; const kubectlApplyAllChannelHandlerInjectable = getRequestChannelListenerInjectable({ @@ -13,7 +13,6 @@ const kubectlApplyAllChannelHandlerInjectable = getRequestChannelListenerInjecta handler: (di) => { const getClusterById = di.inject(getClusterByIdInjectable); const emitAppEvent = di.inject(emitAppEventInjectable); - const createResourceApplier = di.inject(createResourceApplierInjectable); return async ({ clusterId, @@ -30,7 +29,9 @@ const kubectlApplyAllChannelHandlerInjectable = getRequestChannelListenerInjecta }; } - return createResourceApplier(cluster).kubectlApplyAll(resources, extraArgs); + const resourceApplier = di.inject(resourceApplierInjectable, cluster); + + return resourceApplier.kubectlApplyAll(resources, extraArgs); }; }, }); diff --git a/packages/core/src/main/kubectl/create-kubectl.injectable.ts b/packages/core/src/main/kubectl/create-kubectl.injectable.ts index adfce0922f..c3df378e13 100644 --- a/packages/core/src/main/kubectl/create-kubectl.injectable.ts +++ b/packages/core/src/main/kubectl/create-kubectl.injectable.ts @@ -19,10 +19,12 @@ import joinPathsInjectable from "../../common/path/join-paths.injectable"; import getBasenameOfPathInjectable from "../../common/path/get-basename.injectable"; import loggerInjectable from "../../common/logger.injectable"; +export type CreateKubectl = (version: string) => Kubectl; + const createKubectlInjectable = getInjectable({ id: "create-kubectl", - instantiate: (di) => { + instantiate: (di): CreateKubectl => { const dependencies: KubectlDependencies = { userStore: di.inject(userStoreInjectable), directoryForKubectlBinaries: di.inject(directoryForKubectlBinariesInjectable), @@ -39,7 +41,7 @@ const createKubectlInjectable = getInjectable({ getBasenameOfPath: di.inject(getBasenameOfPathInjectable), }; - return (clusterVersion: string) => new Kubectl(dependencies, clusterVersion); + return (version) => new Kubectl(dependencies, version); }, }); diff --git a/packages/core/src/main/kubectl/delete-all-handler.injectable.ts b/packages/core/src/main/kubectl/delete-all-handler.injectable.ts index 00a8db6707..715eb0caf9 100644 --- a/packages/core/src/main/kubectl/delete-all-handler.injectable.ts +++ b/packages/core/src/main/kubectl/delete-all-handler.injectable.ts @@ -5,7 +5,7 @@ import emitAppEventInjectable from "../../common/app-event-bus/emit-event.injectable"; import getClusterByIdInjectable from "../../common/cluster-store/get-by-id.injectable"; import { kubectlDeleteAllChannel } from "../../common/kube-helpers/channels"; -import createResourceApplierInjectable from "../resource-applier/create-resource-applier.injectable"; +import resourceApplierInjectable from "../resource-applier/create-resource-applier.injectable"; import { getRequestChannelListenerInjectable } from "../utils/channel/channel-listeners/listener-tokens"; const kubectlDeleteAllChannelHandlerInjectable = getRequestChannelListenerInjectable({ @@ -13,7 +13,6 @@ const kubectlDeleteAllChannelHandlerInjectable = getRequestChannelListenerInject handler: (di) => { const emitAppEvent = di.inject(emitAppEventInjectable); const getClusterById = di.inject(getClusterByIdInjectable); - const createResourceApplier = di.inject(createResourceApplierInjectable); return async ({ clusterId, @@ -31,7 +30,9 @@ const kubectlDeleteAllChannelHandlerInjectable = getRequestChannelListenerInject }; } - return createResourceApplier(cluster).kubectlDeleteAll(resources, extraArgs); + const resourceApplier = di.inject(resourceApplierInjectable, cluster); + + return resourceApplier.kubectlDeleteAll(resources, extraArgs); }; }, }); diff --git a/packages/core/src/main/lens-proxy/lens-proxy.injectable.ts b/packages/core/src/main/lens-proxy/lens-proxy.injectable.ts index 541ef18cc3..96079563ca 100644 --- a/packages/core/src/main/lens-proxy/lens-proxy.injectable.ts +++ b/packages/core/src/main/lens-proxy/lens-proxy.injectable.ts @@ -4,7 +4,6 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import { LensProxy } from "./lens-proxy"; -import { kubeApiUpgradeRequest } from "./proxy-functions"; import routerInjectable from "../router/router.injectable"; import httpProxy from "http-proxy"; import shellApiRequestInjectable from "./proxy-functions/shell-api-request.injectable"; @@ -14,6 +13,8 @@ import emitAppEventInjectable from "../../common/app-event-bus/emit-event.inject import loggerInjectable from "../../common/logger.injectable"; import lensProxyCertificateInjectable from "../../common/certificate/lens-proxy-certificate.injectable"; import getClusterForRequestInjectable from "./get-cluster-for-request.injectable"; +import kubeAuthProxyServerInjectable from "../cluster/kube-auth-proxy-server.injectable"; +import kubeApiUpgradeRequestInjectable from "./proxy-functions/kube-api-upgrade-request.injectable"; const lensProxyInjectable = getInjectable({ id: "lens-proxy", @@ -21,7 +22,7 @@ const lensProxyInjectable = getInjectable({ instantiate: (di) => new LensProxy({ router: di.inject(routerInjectable), proxy: httpProxy.createProxy(), - kubeApiUpgradeRequest, + kubeApiUpgradeRequest: di.inject(kubeApiUpgradeRequestInjectable), shellApiRequest: di.inject(shellApiRequestInjectable), getClusterForRequest: di.inject(getClusterForRequestInjectable), lensProxyPort: di.inject(lensProxyPortInjectable), @@ -29,6 +30,7 @@ const lensProxyInjectable = getInjectable({ emitAppEvent: di.inject(emitAppEventInjectable), logger: di.inject(loggerInjectable), certificate: di.inject(lensProxyCertificateInjectable).get(), + getKubeAuthProxyServer: (cluster) => di.inject(kubeAuthProxyServerInjectable, cluster), }), }); diff --git a/packages/core/src/main/lens-proxy/lens-proxy.ts b/packages/core/src/main/lens-proxy/lens-proxy.ts index 1a5acdd9f2..629e6a0f12 100644 --- a/packages/core/src/main/lens-proxy/lens-proxy.ts +++ b/packages/core/src/main/lens-proxy/lens-proxy.ts @@ -9,7 +9,6 @@ import type http from "http"; import type httpProxy from "http-proxy"; import { apiPrefix, apiKubePrefix } from "../../common/vars"; import type { Router } from "../router/router"; -import type { ClusterContextHandler } from "../context-handler/context-handler"; import type { Cluster } from "../../common/cluster/cluster"; import type { ProxyApiRequestArgs } from "./proxy-functions"; import { getBoolean } from "../utils/parse-query"; @@ -18,6 +17,7 @@ import type { SetRequired } from "type-fest"; import type { EmitAppEvent } from "../../common/app-event-bus/emit-event.injectable"; import type { Logger } from "../../common/logger"; import type { SelfSignedCert } from "selfsigned"; +import type { KubeAuthProxyServer } from "../cluster/kube-auth-proxy-server.injectable"; export type GetClusterForRequest = (req: http.IncomingMessage) => Cluster | undefined; export type ServerIncomingMessage = SetRequired; @@ -28,6 +28,7 @@ interface Dependencies { shellApiRequest: LensProxyApiRequest; kubeApiUpgradeRequest: LensProxyApiRequest; emitAppEvent: EmitAppEvent; + getKubeAuthProxyServer: (cluster: Cluster) => KubeAuthProxyServer; readonly router: Router; readonly proxy: httpProxy; readonly lensProxyPort: { set: (portNumber: number) => void }; @@ -220,15 +221,6 @@ export class LensProxy { return proxy; } - protected async getProxyTarget(req: http.IncomingMessage, contextHandler: ClusterContextHandler): Promise { - if (req.url?.startsWith(apiKubePrefix)) { - delete req.headers.authorization; - req.url = req.url.replace(apiKubePrefix, ""); - - return contextHandler.getApiTarget(isLongRunningRequest(req.url)); - } - } - protected getRequestId(req: http.IncomingMessage): string { assert(req.headers.host); @@ -238,8 +230,12 @@ export class LensProxy { protected async handleRequest(req: ServerIncomingMessage, res: http.ServerResponse) { const cluster = this.dependencies.getClusterForRequest(req); - if (cluster) { - const proxyTarget = await this.getProxyTarget(req, cluster.contextHandler); + if (cluster && req.url.startsWith(apiKubePrefix)) { + delete req.headers.authorization; + req.url = req.url.replace(apiKubePrefix, ""); + + const kubeAuthProxyServer = this.dependencies.getKubeAuthProxyServer(cluster); + const proxyTarget = await kubeAuthProxyServer.getApiTarget(isLongRunningRequest(req.url)); if (proxyTarget) { return this.dependencies.proxy.web(req, res, proxyTarget); diff --git a/packages/core/src/main/lens-proxy/proxy-functions/index.ts b/packages/core/src/main/lens-proxy/proxy-functions/index.ts index 5d374825ee..72553b2d2e 100644 --- a/packages/core/src/main/lens-proxy/proxy-functions/index.ts +++ b/packages/core/src/main/lens-proxy/proxy-functions/index.ts @@ -2,5 +2,4 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ -export * from "./kube-api-upgrade-request"; export * from "./types"; diff --git a/packages/core/src/main/lens-proxy/proxy-functions/kube-api-upgrade-request.injectable.ts b/packages/core/src/main/lens-proxy/proxy-functions/kube-api-upgrade-request.injectable.ts new file mode 100644 index 0000000000..d6ddfca49a --- /dev/null +++ b/packages/core/src/main/lens-proxy/proxy-functions/kube-api-upgrade-request.injectable.ts @@ -0,0 +1,78 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { chunk } from "lodash"; +import type { ConnectionOptions } from "tls"; +import { connect } from "tls"; +import url, { URL } from "url"; +import { apiKubePrefix } from "../../../common/vars"; +import { getInjectable } from "@ogre-tools/injectable"; +import type { LensProxyApiRequest } from "../lens-proxy"; +import kubeAuthProxyServerInjectable from "../../cluster/kube-auth-proxy-server.injectable"; +import kubeAuthProxyCertificateInjectable from "../../kube-auth-proxy/kube-auth-proxy-certificate.injectable"; + +const skipRawHeaders = new Set(["Host", "Authorization"]); + +const kubeApiUpgradeRequestInjectable = getInjectable({ + id: "kube-api-upgrade-request", + instantiate: (di): LensProxyApiRequest => async ({ req, socket, head, cluster }) => { + const clusterUrl = new URL(cluster.apiUrl.get()); + const kubeAuthProxyServer = di.inject(kubeAuthProxyServerInjectable, cluster); + const kubeAuthProxyCertificate = di.inject(kubeAuthProxyCertificateInjectable, clusterUrl.hostname); + + const proxyUrl = await kubeAuthProxyServer.ensureAuthProxyUrl() + req.url.replace(apiKubePrefix, ""); + const apiUrl = url.parse(cluster.apiUrl.get()); + const pUrl = url.parse(proxyUrl); + const connectOpts: ConnectionOptions = { + port: pUrl.port ? parseInt(pUrl.port) : undefined, + host: pUrl.hostname ?? undefined, + ca: kubeAuthProxyCertificate.cert, + }; + + const proxySocket = connect(connectOpts, () => { + proxySocket.write(`${req.method} ${pUrl.path} HTTP/1.1\r\n`); + proxySocket.write(`Host: ${apiUrl.host}\r\n`); + + for (const [key, value] of chunk(req.rawHeaders, 2)) { + if (skipRawHeaders.has(key)) { + continue; + } + + proxySocket.write(`${key}: ${value}\r\n`); + } + + proxySocket.write("\r\n"); + proxySocket.write(head); + }); + + proxySocket.setKeepAlive(true); + socket.setKeepAlive(true); + proxySocket.setTimeout(0); + socket.setTimeout(0); + + proxySocket.on("data", function (chunk) { + socket.write(chunk); + }); + proxySocket.on("end", function () { + socket.end(); + }); + proxySocket.on("error", function () { + socket.write(`HTTP/${req.httpVersion} 500 Connection error\r\n\r\n`); + socket.end(); + }); + socket.on("data", function (chunk) { + proxySocket.write(chunk); + }); + socket.on("end", function () { + proxySocket.end(); + }); + socket.on("error", function () { + proxySocket.end(); + }); + }, +}); + +export default kubeApiUpgradeRequestInjectable; + diff --git a/packages/core/src/main/lens-proxy/proxy-functions/kube-api-upgrade-request.ts b/packages/core/src/main/lens-proxy/proxy-functions/kube-api-upgrade-request.ts deleted file mode 100644 index ddd8e66261..0000000000 --- a/packages/core/src/main/lens-proxy/proxy-functions/kube-api-upgrade-request.ts +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import { chunk } from "lodash"; -import type { ConnectionOptions } from "tls"; -import { connect } from "tls"; -import url from "url"; -import { apiKubePrefix } from "../../../common/vars"; -import type { ProxyApiRequestArgs } from "./types"; - -const skipRawHeaders = new Set(["Host", "Authorization"]); - -export async function kubeApiUpgradeRequest({ req, socket, head, cluster }: ProxyApiRequestArgs) { - const proxyUrl = await cluster.contextHandler.resolveAuthProxyUrl() + req.url.replace(apiKubePrefix, ""); - const proxyCa = cluster.contextHandler.resolveAuthProxyCa(); - const apiUrl = url.parse(cluster.apiUrl); - const pUrl = url.parse(proxyUrl); - const connectOpts: ConnectionOptions = { - port: pUrl.port ? parseInt(pUrl.port) : undefined, - host: pUrl.hostname ?? undefined, - ca: proxyCa, - }; - - const proxySocket = connect(connectOpts, () => { - proxySocket.write(`${req.method} ${pUrl.path} HTTP/1.1\r\n`); - proxySocket.write(`Host: ${apiUrl.host}\r\n`); - - for (const [key, value] of chunk(req.rawHeaders, 2)) { - if (skipRawHeaders.has(key)) { - continue; - } - - proxySocket.write(`${key}: ${value}\r\n`); - } - - proxySocket.write("\r\n"); - proxySocket.write(head); - }); - - proxySocket.setKeepAlive(true); - socket.setKeepAlive(true); - proxySocket.setTimeout(0); - socket.setTimeout(0); - - proxySocket.on("data", function (chunk) { - socket.write(chunk); - }); - proxySocket.on("end", function () { - socket.end(); - }); - proxySocket.on("error", function () { - socket.write(`HTTP/${req.httpVersion} 500 Connection error\r\n\r\n`); - socket.end(); - }); - socket.on("data", function (chunk) { - proxySocket.write(chunk); - }); - socket.on("end", function () { - proxySocket.end(); - }); - socket.on("error", function () { - proxySocket.end(); - }); -} diff --git a/packages/core/src/main/resource-applier/create-resource-applier.injectable.ts b/packages/core/src/main/resource-applier/create-resource-applier.injectable.ts index 509dce086c..245a3551bf 100644 --- a/packages/core/src/main/resource-applier/create-resource-applier.injectable.ts +++ b/packages/core/src/main/resource-applier/create-resource-applier.injectable.ts @@ -2,33 +2,36 @@ * 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 { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import emitAppEventInjectable from "../../common/app-event-bus/emit-event.injectable"; -import type { Cluster } from "../../common/cluster/cluster"; import removePathInjectable from "../../common/fs/remove.injectable"; import execFileInjectable from "../../common/fs/exec-file.injectable"; import writeFileInjectable from "../../common/fs/write-file.injectable"; import loggerInjectable from "../../common/logger.injectable"; import joinPathsInjectable from "../../common/path/join-paths.injectable"; -import type { ResourceApplierDependencies } from "./resource-applier"; import { ResourceApplier } from "./resource-applier"; +import createKubectlInjectable from "../kubectl/create-kubectl.injectable"; +import kubeconfigManagerInjectable from "../kubeconfig-manager/kubeconfig-manager.injectable"; +import type { Cluster } from "../../common/cluster/cluster"; -export type CreateResourceApplier = (cluster: Cluster) => ResourceApplier; - -const createResourceApplierInjectable = getInjectable({ - id: "create-resource-applier", - instantiate: (di): CreateResourceApplier => { - const deps: ResourceApplierDependencies = { +const resourceApplierInjectable = getInjectable({ + id: "resource-applier", + instantiate: (di, cluster) => new ResourceApplier( + { deleteFile: di.inject(removePathInjectable), emitAppEvent: di.inject(emitAppEventInjectable), execFile: di.inject(execFileInjectable), joinPaths: di.inject(joinPathsInjectable), logger: di.inject(loggerInjectable), writeFile: di.inject(writeFileInjectable), - }; - - return (cluster) => new ResourceApplier(deps, cluster); - }, + createKubectl: di.inject(createKubectlInjectable), + proxyKubeconfigManager: di.inject(kubeconfigManagerInjectable, cluster), + }, + cluster, + ), + lifecycle: lifecycleEnum.keyedSingleton({ + getInstanceKey: (di, cluster: Cluster) => cluster.id, + }), }); -export default createResourceApplierInjectable; +export default resourceApplierInjectable; diff --git a/packages/core/src/main/resource-applier/resource-applier.ts b/packages/core/src/main/resource-applier/resource-applier.ts index a604781fce..1015f204d2 100644 --- a/packages/core/src/main/resource-applier/resource-applier.ts +++ b/packages/core/src/main/resource-applier/resource-applier.ts @@ -15,6 +15,8 @@ import type { RemovePath } from "../../common/fs/remove.injectable"; import type { ExecFile } from "../../common/fs/exec-file.injectable"; import type { JoinPaths } from "../../common/path/join-paths.injectable"; import type { AsyncResult } from "../../common/utils/async-result"; +import type { CreateKubectl } from "../kubectl/create-kubectl.injectable"; +import type { KubeconfigManager } from "../kubeconfig-manager/kubeconfig-manager"; export interface ResourceApplierDependencies { emitAppEvent: EmitAppEvent; @@ -22,11 +24,24 @@ export interface ResourceApplierDependencies { deleteFile: RemovePath; execFile: ExecFile; joinPaths: JoinPaths; + createKubectl: CreateKubectl; + readonly proxyKubeconfigManager: KubeconfigManager; readonly logger: Logger; } export class ResourceApplier { - constructor(protected readonly dependencies: ResourceApplierDependencies, protected readonly cluster: Cluster) {} + constructor( + protected readonly dependencies: ResourceApplierDependencies, + protected readonly cluster: Cluster, + ) {} + + private async getKubectlPath() { + const kubectl = this.dependencies.createKubectl(this.cluster.version.get()); + + await kubectl.ensureKubectl(); + + return kubectl.getPath(); + } /** * Patch a kube resource's manifest, throwing any error that occurs. @@ -38,9 +53,8 @@ export class ResourceApplier { async patch(name: string, kind: string, patch: Patch, ns?: string): Promise { this.dependencies.emitAppEvent({ name: "resource", action: "patch" }); - const kubectl = await this.cluster.ensureKubectl(); - const kubectlPath = await kubectl.getPath(); - const proxyKubeconfigPath = await this.cluster.getProxyKubeconfigPath(); + const kubectlPath = await this.getKubectlPath(); + const proxyKubeconfigPath = await this.dependencies.proxyKubeconfigManager.ensurePath(); const args = [ "--kubeconfig", proxyKubeconfigPath, "patch", @@ -74,9 +88,8 @@ export class ResourceApplier { } protected async kubectlApply(content: string): Promise> { - const kubectl = await this.cluster.ensureKubectl(); - const kubectlPath = await kubectl.getPath(); - const proxyKubeconfigPath = await this.cluster.getProxyKubeconfigPath(); + const kubectlPath = await this.getKubectlPath(); + const proxyKubeconfigPath = await this.dependencies.proxyKubeconfigManager.ensurePath(); const fileName = tempy.file({ name: "resource.yaml" }); const args = [ "apply", @@ -121,9 +134,8 @@ export class ResourceApplier { } protected async kubectlCmdAll(subCmd: string, resources: string[], parentArgs: string[] = []): Promise> { - const kubectl = await this.cluster.ensureKubectl(); - const kubectlPath = await kubectl.getPath(); - const proxyKubeconfigPath = await this.cluster.getProxyKubeconfigPath(); + const kubectlPath = await this.getKubectlPath(); + const proxyKubeconfigPath = await this.dependencies.proxyKubeconfigManager.ensurePath(); const tmpDir = tempy.directory(); await Promise.all(resources.map((resource, index) => this.dependencies.writeFile( diff --git a/packages/core/src/main/routes/kubeconfig-route/get-service-account-route.injectable.ts b/packages/core/src/main/routes/kubeconfig-route/get-service-account-route.injectable.ts index 476ee983a3..b6b77d5a40 100644 --- a/packages/core/src/main/routes/kubeconfig-route/get-service-account-route.injectable.ts +++ b/packages/core/src/main/routes/kubeconfig-route/get-service-account-route.injectable.ts @@ -10,15 +10,18 @@ import type { V1Secret } from "@kubernetes/client-node"; import { CoreV1Api } from "@kubernetes/client-node"; import { clusterRoute } from "../../router/route"; import { dump } from "js-yaml"; +import loadProxyKubeconfigInjectable from "../../cluster/load-proxy-kubeconfig.injectable"; const getServiceAccountRouteInjectable = getRouteInjectable({ id: "get-service-account-route", - instantiate: () => clusterRoute({ + instantiate: (di) => clusterRoute({ method: "get", path: `${apiPrefix}/kubeconfig/service-account/{namespace}/{account}`, })(async ({ params, cluster }) => { - const client = (await cluster.getProxyKubeconfig()).makeApiClient(CoreV1Api); + const loadProxyKubeconfig = di.inject(loadProxyKubeconfigInjectable, cluster); + const proxyKubeconfig = await loadProxyKubeconfig(); + const client = proxyKubeconfig.makeApiClient(CoreV1Api); const secretList = await client.listNamespacedSecret(params.namespace); const secret = secretList.body.items.find(secret => { @@ -64,9 +67,9 @@ function generateKubeConfig(username: string, secret: V1Secret, cluster: Cluster "kind": "Config", "clusters": [ { - "name": cluster.contextName, + "name": cluster.contextName.get(), "cluster": { - "server": cluster.apiUrl, + "server": cluster.apiUrl.get(), "certificate-authority-data": caCrt, }, }, @@ -81,14 +84,14 @@ function generateKubeConfig(username: string, secret: V1Secret, cluster: Cluster ], "contexts": [ { - "name": cluster.contextName, + "name": cluster.contextName.get(), "context": { "user": username, - "cluster": cluster.contextName, + "cluster": cluster.contextName.get(), "namespace": secret.metadata.namespace, }, }, ], - "current-context": cluster.contextName, + "current-context": cluster.contextName.get(), }); } diff --git a/packages/core/src/main/routes/metrics/add-metrics-route.injectable.ts b/packages/core/src/main/routes/metrics/add-metrics-route.injectable.ts index eb59f2840b..22e36da8d4 100644 --- a/packages/core/src/main/routes/metrics/add-metrics-route.injectable.ts +++ b/packages/core/src/main/routes/metrics/add-metrics-route.injectable.ts @@ -14,6 +14,8 @@ import { isRequestError, object } from "../../../common/utils"; import type { GetMetrics } from "../../get-metrics.injectable"; import getMetricsInjectable from "../../get-metrics.injectable"; import loggerInjectable from "../../../common/logger.injectable"; +import prometheusHandlerInjectable from "../../cluster/prometheus-handler/prometheus-handler.injectable"; +import { runInAction } from "mobx"; // This is used for backoff retry tracking. const ATTEMPTS = [false, false, false, false, true]; @@ -66,9 +68,10 @@ const addMetricsRouteInjectable = getRouteInjectable({ })(async ({ cluster, payload, query }) => { const queryParams: Partial> = Object.fromEntries(query.entries()); const prometheusMetadata: ClusterPrometheusMetadata = {}; + const prometheusHandler = di.inject(prometheusHandlerInjectable, cluster); try { - const { prometheusPath, provider } = await cluster.contextHandler.getPrometheusDetails(); + const { prometheusPath, provider } = await prometheusHandler.getPrometheusDetails(); prometheusMetadata.provider = provider?.kind; prometheusMetadata.autoDetected = !cluster.preferences.prometheusProvider?.type; @@ -115,7 +118,9 @@ const addMetricsRouteInjectable = getRouteInjectable({ return { response: {}}; } finally { - cluster.metadata[ClusterMetadataKey.PROMETHEUS] = prometheusMetadata; + runInAction(() => { + cluster.metadata[ClusterMetadataKey.PROMETHEUS] = prometheusMetadata; + }); } }); }, diff --git a/packages/core/src/main/routes/port-forward/start-port-forward-route.injectable.ts b/packages/core/src/main/routes/port-forward/start-port-forward-route.injectable.ts index 0716b81a2f..170f93fef8 100644 --- a/packages/core/src/main/routes/port-forward/start-port-forward-route.injectable.ts +++ b/packages/core/src/main/routes/port-forward/start-port-forward-route.injectable.ts @@ -8,6 +8,7 @@ import { PortForward } from "./functionality/port-forward"; import createPortForwardInjectable from "./functionality/create-port-forward.injectable"; import { clusterRoute } from "../../router/route"; import loggerInjectable from "../../../common/logger.injectable"; +import kubeconfigManagerInjectable from "../../kubeconfig-manager/kubeconfig-manager.injectable"; const startPortForwardRouteInjectable = getRouteInjectable({ id: "start-current-port-forward-route", @@ -24,6 +25,8 @@ const startPortForwardRouteInjectable = getRouteInjectable({ const port = Number(query.get("port")); const forwardPort = Number(query.get("forwardPort")); + const proxyKubeconfigManager = di.inject(kubeconfigManagerInjectable, cluster); + try { let portForward = PortForward.getPortforward({ clusterId: cluster.id, @@ -42,8 +45,9 @@ const startPortForwardRouteInjectable = getRouteInjectable({ const thePort = 0 < forwardPort && forwardPort < 65536 ? forwardPort : 0; + const proxyKubeconfigPath = await proxyKubeconfigManager.ensurePath(); - portForward = createPortForward(await cluster.getProxyKubeconfigPath(), { + portForward = createPortForward(proxyKubeconfigPath, { clusterId: cluster.id, kind: resourceType, namespace, diff --git a/packages/core/src/main/routes/resource-applier/create-resource-route.injectable.ts b/packages/core/src/main/routes/resource-applier/create-resource-route.injectable.ts index 3b68201e9d..a2d8766170 100644 --- a/packages/core/src/main/routes/resource-applier/create-resource-route.injectable.ts +++ b/packages/core/src/main/routes/resource-applier/create-resource-route.injectable.ts @@ -6,22 +6,22 @@ import { getRouteInjectable } from "../../router/router.injectable"; import { apiPrefix } from "../../../common/vars"; import { payloadValidatedClusterRoute } from "../../router/route"; import Joi from "joi"; -import createResourceApplierInjectable from "../../resource-applier/create-resource-applier.injectable"; +import resourceApplierInjectable from "../../resource-applier/create-resource-applier.injectable"; const createResourceRouteInjectable = getRouteInjectable({ id: "create-resource-route", - instantiate: (di) => { - const createResourceApplier = di.inject(createResourceApplierInjectable); + instantiate: (di) => payloadValidatedClusterRoute({ + method: "post", + path: `${apiPrefix}/stack`, + payloadValidator: Joi.string(), + })(async ({ cluster, payload }) => { + const resourceApplier = di.inject(resourceApplierInjectable, cluster); - return payloadValidatedClusterRoute({ - method: "post", - path: `${apiPrefix}/stack`, - payloadValidator: Joi.string(), - })(async ({ cluster, payload }) => ({ - response: await createResourceApplier(cluster).create(payload), - })); - }, + return ({ + response: await resourceApplier.create(payload), + }); + }), }); export default createResourceRouteInjectable; diff --git a/packages/core/src/main/routes/resource-applier/patch-resource-route.injectable.ts b/packages/core/src/main/routes/resource-applier/patch-resource-route.injectable.ts index e962c18607..6cac0436f5 100644 --- a/packages/core/src/main/routes/resource-applier/patch-resource-route.injectable.ts +++ b/packages/core/src/main/routes/resource-applier/patch-resource-route.injectable.ts @@ -7,7 +7,7 @@ import { apiPrefix } from "../../../common/vars"; import { payloadValidatedClusterRoute } from "../../router/route"; import Joi from "joi"; import type { Patch } from "rfc6902"; -import createResourceApplierInjectable from "../../resource-applier/create-resource-applier.injectable"; +import resourceApplierInjectable from "../../resource-applier/create-resource-applier.injectable"; interface PatchResourcePayload { name: string; @@ -40,22 +40,22 @@ const patchResourcePayloadValidator = Joi.object { - const createResourceApplier = di.inject(createResourceApplierInjectable); + instantiate: (di) => payloadValidatedClusterRoute({ + method: "patch", + path: `${apiPrefix}/stack`, + payloadValidator: patchResourcePayloadValidator, + })(async ({ cluster, payload }) => { + const resourceApplier = di.inject(resourceApplierInjectable, cluster); - return payloadValidatedClusterRoute({ - method: "patch", - path: `${apiPrefix}/stack`, - payloadValidator: patchResourcePayloadValidator, - })(async ({ cluster, payload }) => ({ - response: await createResourceApplier(cluster).patch( + return ({ + response: await resourceApplier.patch( payload.name, payload.kind, payload.patch, payload.ns, ), - })); - }, + }); + }), }); export default patchResourceRouteInjectable; diff --git a/packages/core/src/main/shell-session/local-shell-session/local-shell-session.ts b/packages/core/src/main/shell-session/local-shell-session/local-shell-session.ts index 49e6dd059f..567fc1dca7 100644 --- a/packages/core/src/main/shell-session/local-shell-session/local-shell-session.ts +++ b/packages/core/src/main/shell-session/local-shell-session/local-shell-session.ts @@ -52,16 +52,16 @@ export class LocalShellSession extends ShellSession { protected async getShellArgs(shell: string): Promise { const pathFromPreferences = this.dependencies.userStore.kubectlBinariesPath || this.kubectl.getBundledPath(); const kubectlPathDir = this.dependencies.userStore.downloadKubectlBinaries - ? await this.kubectlBinDirP + ? this.dependencies.directoryContainingKubectl : this.dependencies.getDirnameOfPath(pathFromPreferences); switch(this.dependencies.getBasenameOfPath(shell)) { case "powershell.exe": return ["-NoExit", "-command", `& {$Env:PATH="${kubectlPathDir};${this.dependencies.directoryForBinaries};$Env:PATH"}`]; case "bash": - return ["--init-file", this.dependencies.joinPaths(await this.kubectlBinDirP, ".bash_set_path")]; + return ["--init-file", this.dependencies.joinPaths(this.dependencies.directoryContainingKubectl, ".bash_set_path")]; case "fish": - return ["--login", "--init-command", `export PATH="${kubectlPathDir}:${this.dependencies.directoryForBinaries}:$PATH"; export KUBECONFIG="${await this.kubeconfigPathP}"`]; + return ["--login", "--init-command", `export PATH="${kubectlPathDir}:${this.dependencies.directoryForBinaries}:$PATH"; export KUBECONFIG="${await this.dependencies.proxyKubeconfigPath}"`]; case "zsh": return ["--login"]; default: diff --git a/packages/core/src/main/shell-session/local-shell-session/open.injectable.ts b/packages/core/src/main/shell-session/local-shell-session/open.injectable.ts index 084aa2e17c..4d5d54984a 100644 --- a/packages/core/src/main/shell-session/local-shell-session/open.injectable.ts +++ b/packages/core/src/main/shell-session/local-shell-session/open.injectable.ts @@ -24,6 +24,7 @@ import appNameInjectable from "../../../common/vars/app-name.injectable"; import buildVersionInjectable from "../../vars/build-version/build-version.injectable"; import emitAppEventInjectable from "../../../common/app-event-bus/emit-event.injectable"; import statInjectable from "../../../common/fs/stat.injectable"; +import kubeconfigManagerInjectable from "../../kubeconfig-manager/kubeconfig-manager.injectable"; export interface OpenLocalShellSessionArgs { websocket: WebSocket; @@ -38,7 +39,7 @@ const openLocalShellSessionInjectable = getInjectable({ instantiate: (di): OpenLocalShellSession => { const createKubectl = di.inject(createKubectlInjectable); - const dependencies: LocalShellSessionDependencies = { + const dependencies: Omit = { directoryForBinaries: di.inject(directoryForBinariesInjectable), isMac: di.inject(isMacInjectable), isWindows: di.inject(isWindowsInjectable), @@ -57,9 +58,17 @@ const openLocalShellSessionInjectable = getInjectable({ stat: di.inject(statInjectable), }; - return (args) => { - const kubectl = createKubectl(args.cluster.version); - const session = new LocalShellSession(dependencies, { kubectl, ...args }); + return async (args) => { + const kubectl = createKubectl(args.cluster.version.get()); + const kubeconfigManager = di.inject(kubeconfigManagerInjectable, args.cluster); + const proxyKubeconfigPath = await kubeconfigManager.ensurePath(); + const directoryContainingKubectl = await kubectl.binDir(); + + const session = new LocalShellSession({ + ...dependencies, + proxyKubeconfigPath, + directoryContainingKubectl, + }, { kubectl, ...args }); return session.open(); }; diff --git a/packages/core/src/main/shell-session/local-shell-session/techincal.test.ts b/packages/core/src/main/shell-session/local-shell-session/techincal.test.ts index 17d9aa3349..f37e690561 100644 --- a/packages/core/src/main/shell-session/local-shell-session/techincal.test.ts +++ b/packages/core/src/main/shell-session/local-shell-session/techincal.test.ts @@ -5,8 +5,9 @@ import type { DiContainer } from "@ogre-tools/injectable"; import type WebSocket from "ws"; +import directoryForTempInjectable from "../../../common/app-paths/directory-for-temp/directory-for-temp.injectable"; import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; -import type { Cluster } from "../../../common/cluster/cluster"; +import { Cluster } from "../../../common/cluster/cluster"; import pathExistsSyncInjectable from "../../../common/fs/path-exists-sync.injectable"; import pathExistsInjectable from "../../../common/fs/path-exists.injectable"; import readJsonSyncInjectable from "../../../common/fs/read-json-sync.injectable"; @@ -14,8 +15,11 @@ import statInjectable from "../../../common/fs/stat.injectable"; import writeJsonSyncInjectable from "../../../common/fs/write-json-sync.injectable"; import platformInjectable from "../../../common/vars/platform.injectable"; import { getDiForUnitTesting } from "../../getDiForUnitTesting"; +import type { KubeconfigManager } from "../../kubeconfig-manager/kubeconfig-manager"; +import kubeconfigManagerInjectable from "../../kubeconfig-manager/kubeconfig-manager.injectable"; import createKubectlInjectable from "../../kubectl/create-kubectl.injectable"; import type { Kubectl } from "../../kubectl/kubectl"; +import lensProxyPortInjectable from "../../lens-proxy/lens-proxy-port.injectable"; import buildVersionInjectable from "../../vars/build-version/build-version.injectable"; import type { OpenShellSession } from "../create-shell-session.injectable"; import type { SpawnPty } from "../spawn-pty.injectable"; @@ -29,6 +33,7 @@ describe("technical unit tests for local shell sessions", () => { di = getDiForUnitTesting(); di.override(directoryForUserDataInjectable, () => "/some-directory-for-user-data"); + di.override(directoryForTempInjectable, () => "/some-directory-for-tmp"); di.override(buildVersionInjectable, () => ({ get: () => "1.1.1", })); @@ -37,6 +42,7 @@ describe("technical unit tests for local shell sessions", () => { di.override(readJsonSyncInjectable, () => () => { throw new Error("tried call readJsonSync without override"); }); di.override(writeJsonSyncInjectable, () => () => { throw new Error("tried call writeJsonSync without override"); }); di.override(statInjectable, () => () => { throw new Error("tried call stat without override"); }); + di.inject(lensProxyPortInjectable).set(1111); }); describe("when on windows", () => { @@ -54,6 +60,10 @@ describe("technical unit tests for local shell sessions", () => { getBundledPath: () => "/some-bundled-kubectl-path", }) as Partial as Kubectl); + di.override(kubeconfigManagerInjectable, () => ({ + ensurePath: async () => "/some-proxy-kubeconfig-file", + } as Partial as KubeconfigManager)); + openLocalShellSession = di.inject(openLocalShellSessionInjectable); }); @@ -89,11 +99,16 @@ describe("technical unit tests for local shell sessions", () => { once: jest.fn(() => websocket), } as Partial as WebSocket; + const cluster = new Cluster({ + contextName: "some-context-name", + id: "some-cluster-id", + kubeConfigPath: "/some-kube-config-path", + }, { + clusterServerUrl: "https://localhost:9999", + }); + await openLocalShellSession({ - cluster: { - getProxyKubeconfigPath: async () => "/some-proxy-kubeconfig", - preferences: {}, - } as Partial as Cluster, + cluster, tabId: "my-tab-id", websocket, }); diff --git a/packages/core/src/main/shell-session/node-shell-session/node-shell-session.ts b/packages/core/src/main/shell-session/node-shell-session/node-shell-session.ts index 492c70d73d..a9a928adb6 100644 --- a/packages/core/src/main/shell-session/node-shell-session/node-shell-session.ts +++ b/packages/core/src/main/shell-session/node-shell-session/node-shell-session.ts @@ -13,6 +13,8 @@ import { NodeApi } from "../../../common/k8s-api/endpoints"; import { TerminalChannels } from "../../../common/terminal/channels"; import type { CreateKubeJsonApiForCluster } from "../../../common/k8s-api/create-kube-json-api-for-cluster.injectable"; import type { CreateKubeApi } from "../../../common/k8s-api/create-kube-api.injectable"; +import { initialNodeShellImage } from "../../../common/cluster-types"; +import type { LoadProxyKubeconfig } from "../../cluster/load-proxy-kubeconfig.injectable"; export interface NodeShellSessionArgs extends ShellSessionArgs { nodeName: string; @@ -21,6 +23,7 @@ export interface NodeShellSessionArgs extends ShellSessionArgs { export interface NodeShellSessionDependencies extends ShellSessionDependencies { createKubeJsonApiForCluster: CreateKubeJsonApiForCluster; createKubeApi: CreateKubeApi; + loadProxyKubeconfig: LoadProxyKubeconfig; } export class NodeShellSession extends ShellSession { @@ -36,9 +39,8 @@ export class NodeShellSession extends ShellSession { } public async open() { - const kc = await this.cluster.getProxyKubeconfig(); - const coreApi = kc.makeApiClient(CoreV1Api); - const shell = await this.kubectl.getPath(); + const proxyKubeconfig = await this.dependencies.loadProxyKubeconfig(); + const coreApi = proxyKubeconfig.makeApiClient(CoreV1Api); const cleanup = once(() => { coreApi @@ -50,7 +52,7 @@ export class NodeShellSession extends ShellSession { try { await this.createNodeShellPod(coreApi); - await this.waitForRunningPod(kc); + await this.waitForRunningPod(proxyKubeconfig); } catch (error) { cleanup(); @@ -92,13 +94,18 @@ export class NodeShellSession extends ShellSession { break; } - await this.openShellProcess(shell, args, env); + await this.openShellProcess(this.dependencies.directoryContainingKubectl, args, env); } protected createNodeShellPod(coreApi: CoreV1Api) { - const imagePullSecrets = this.cluster.imagePullSecret + const { + imagePullSecret, + nodeShellImage, + } = this.cluster.preferences; + + const imagePullSecrets = imagePullSecret ? [{ - name: this.cluster.imagePullSecret, + name: imagePullSecret, }] : undefined; @@ -121,7 +128,7 @@ export class NodeShellSession extends ShellSession { priorityClassName: "system-node-critical", containers: [{ name: "shell", - image: this.cluster.nodeShellImage, + image: nodeShellImage || initialNodeShellImage, securityContext: { privileged: true, }, diff --git a/packages/core/src/main/shell-session/node-shell-session/open.injectable.ts b/packages/core/src/main/shell-session/node-shell-session/open.injectable.ts index cce3fa5f36..55ec26d362 100644 --- a/packages/core/src/main/shell-session/node-shell-session/open.injectable.ts +++ b/packages/core/src/main/shell-session/node-shell-session/open.injectable.ts @@ -20,6 +20,8 @@ import buildVersionInjectable from "../../vars/build-version/build-version.injec import emitAppEventInjectable from "../../../common/app-event-bus/emit-event.injectable"; import statInjectable from "../../../common/fs/stat.injectable"; import createKubeApiInjectable from "../../../common/k8s-api/create-kube-api.injectable"; +import loadProxyKubeconfigInjectable from "../../cluster/load-proxy-kubeconfig.injectable"; +import kubeconfigManagerInjectable from "../../kubeconfig-manager/kubeconfig-manager.injectable"; export interface NodeShellSessionArgs { websocket: WebSocket; @@ -34,7 +36,7 @@ const openNodeShellSessionInjectable = getInjectable({ id: "open-node-shell-session", instantiate: (di): OpenNodeShellSession => { const createKubectl = di.inject(createKubectlInjectable); - const dependencies: NodeShellSessionDependencies = { + const dependencies: Omit = { isMac: di.inject(isMacInjectable), isWindows: di.inject(isWindowsInjectable), logger: di.inject(loggerInjectable), @@ -50,8 +52,18 @@ const openNodeShellSessionInjectable = getInjectable({ }; return async (args) => { - const kubectl = createKubectl(args.cluster.version); - const session = new NodeShellSession(dependencies, { kubectl, ...args }); + const kubectl = createKubectl(args.cluster.version.get()); + const kubeconfigManager = di.inject(kubeconfigManagerInjectable, args.cluster); + const loadProxyKubeconfig = di.inject(loadProxyKubeconfigInjectable, args.cluster); + const proxyKubeconfigPath = await kubeconfigManager.ensurePath(); + const directoryContainingKubectl = await kubectl.binDir(); + + const session = new NodeShellSession({ + ...dependencies, + loadProxyKubeconfig, + proxyKubeconfigPath, + directoryContainingKubectl, + }, { kubectl, ...args }); return session.open(); }; diff --git a/packages/core/src/main/shell-session/shell-session.ts b/packages/core/src/main/shell-session/shell-session.ts index 196625f42a..d4dc3efac7 100644 --- a/packages/core/src/main/shell-session/shell-session.ts +++ b/packages/core/src/main/shell-session/shell-session.ts @@ -111,6 +111,8 @@ export interface ShellSessionDependencies { readonly userShellSetting: IComputedValue; readonly appName: string; readonly buildVersion: InitializableState; + readonly proxyKubeconfigPath: string; + readonly directoryContainingKubectl: string; computeShellEnvironment: ComputeShellEnvironment; spawnPty: SpawnPty; emitAppEvent: EmitAppEvent; @@ -147,8 +149,6 @@ export abstract class ShellSession { } protected running = false; - protected readonly kubectlBinDirP: Promise; - protected readonly kubeconfigPathP: Promise; protected readonly terminalId: string; protected readonly kubectl: Kubectl; protected readonly websocket: WebSocket; @@ -179,8 +179,6 @@ export abstract class ShellSession { this.kubectl = kubectl; this.websocket = websocket; this.cluster = cluster; - this.kubeconfigPathP = this.cluster.getProxyKubeconfigPath(); - this.kubectlBinDirP = this.kubectl.binDir(); this.terminalId = `${cluster.id}:${terminalId}`; } @@ -297,7 +295,7 @@ export abstract class ShellSession { code !== WebSocketCloseEvent.AbnormalClosure && code !== WebSocketCloseEvent.GoingAway ) - || this.cluster.disconnected + || this.cluster.disconnected.get() ); if (stopShellSession) { @@ -350,7 +348,7 @@ export abstract class ShellSession { })(); const env = clearKubeconfigEnvVars(JSON.parse(JSON.stringify(rawEnv))); - const pathStr = [await this.kubectlBinDirP, ...this.getPathEntries(), env.PATH].join(path.delimiter); + const pathStr = [this.dependencies.directoryContainingKubectl, ...this.getPathEntries(), env.PATH].join(path.delimiter); delete env.DEBUG; // don't pass DEBUG into shells @@ -373,12 +371,12 @@ export abstract class ShellSession { if (path.basename(env.PTYSHELL) === "zsh") { env.OLD_ZDOTDIR = env.ZDOTDIR || env.HOME; - env.ZDOTDIR = await this.kubectlBinDirP; + env.ZDOTDIR = this.dependencies.directoryContainingKubectl; env.DISABLE_AUTO_UPDATE = "true"; } env.PTYPID = process.pid.toString(); - env.KUBECONFIG = await this.kubeconfigPathP; + env.KUBECONFIG = this.dependencies.proxyKubeconfigPath; env.TERM_PROGRAM = this.dependencies.appName; env.TERM_PROGRAM_VERSION = this.dependencies.buildVersion.get(); diff --git a/packages/core/src/renderer/api/catalog/entity/get-active-cluster-entity.injectable.ts b/packages/core/src/renderer/api/catalog/entity/get-active-cluster-entity.injectable.ts index d5a2d1f3f8..b856c15ffd 100644 --- a/packages/core/src/renderer/api/catalog/entity/get-active-cluster-entity.injectable.ts +++ b/packages/core/src/renderer/api/catalog/entity/get-active-cluster-entity.injectable.ts @@ -3,20 +3,18 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; import clusterStoreInjectable from "../../../../common/cluster-store/cluster-store.injectable"; -import type { Cluster } from "../../../../common/cluster/cluster"; import catalogEntityRegistryInjectable from "./registry.injectable"; -export type GetActiveClusterEntity = () => Cluster | undefined; - -const getActiveClusterEntityInjectable = getInjectable({ - id: "get-active-cluster-entity", - instantiate: (di): GetActiveClusterEntity => { +const activeEntityInternalClusterInjectable = getInjectable({ + id: "active-entity-internal-cluster", + instantiate: (di) => { const store = di.inject(clusterStoreInjectable); const entityRegistry = di.inject(catalogEntityRegistryInjectable); - return () => store.getById(entityRegistry.activeEntity?.getId()); + return computed(() => store.getById(entityRegistry.activeEntity?.getId())); }, }); -export default getActiveClusterEntityInjectable; +export default activeEntityInternalClusterInjectable; diff --git a/packages/core/src/renderer/api/catalog/entity/metrics-enabled.injectable.ts b/packages/core/src/renderer/api/catalog/entity/metrics-enabled.injectable.ts index d267132fcd..0954259d98 100644 --- a/packages/core/src/renderer/api/catalog/entity/metrics-enabled.injectable.ts +++ b/packages/core/src/renderer/api/catalog/entity/metrics-enabled.injectable.ts @@ -5,14 +5,22 @@ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import { computed } from "mobx"; import type { ClusterMetricsResourceType } from "../../../../common/cluster-types"; -import getActiveClusterEntityInjectable from "./get-active-cluster-entity.injectable"; +import activeEntityInternalClusterInjectable from "./get-active-cluster-entity.injectable"; const enabledMetricsInjectable = getInjectable({ id: "enabled-metrics", instantiate: (di, kind) => { - const getActiveClusterEntity = di.inject(getActiveClusterEntityInjectable); + const activeEntityInternalCluster = di.inject(activeEntityInternalClusterInjectable); - return computed(() => !getActiveClusterEntity()?.isMetricHidden(kind)); + return computed(() => { + const cluster = activeEntityInternalCluster.get(); + + if (!cluster?.preferences.hiddenMetrics) { + return false; + } + + return cluster.preferences.hiddenMetrics.includes(kind); + }); }, lifecycle: lifecycleEnum.keyedSingleton({ getInstanceKey: (di, kind: ClusterMetricsResourceType) => kind, diff --git a/packages/core/src/renderer/before-frame-starts/runnables/setup-kubernetes-cluster-context-menu-open.injectable.ts b/packages/core/src/renderer/before-frame-starts/runnables/setup-kubernetes-cluster-context-menu-open.injectable.ts index 99a3bfa9f3..6ab67a5286 100644 --- a/packages/core/src/renderer/before-frame-starts/runnables/setup-kubernetes-cluster-context-menu-open.injectable.ts +++ b/packages/core/src/renderer/before-frame-starts/runnables/setup-kubernetes-cluster-context-menu-open.injectable.ts @@ -5,8 +5,7 @@ import { getInjectable } from "@ogre-tools/injectable"; import catalogCategoryRegistryInjectable from "../../../common/catalog/category-registry.injectable"; import getClusterByIdInjectable from "../../../common/cluster-store/get-by-id.injectable"; -import readFileInjectable from "../../../common/fs/read-file.injectable"; -import { loadConfigFromString } from "../../../common/kube-helpers"; +import loadKubeconfigInjectable from "../../../common/cluster/load-kubeconfig.injectable"; import loggerInjectable from "../../../common/logger.injectable"; import openDeleteClusterDialogInjectable from "../../components/delete-cluster-dialog/open.injectable"; import { beforeFrameStartsSecondInjectionToken } from "../tokens"; @@ -18,7 +17,6 @@ const setupKubernetesClusterContextMenuOpenInjectable = getInjectable({ run: () => { const catalogCategoryRegistry = di.inject(catalogCategoryRegistryInjectable); const openDeleteClusterDialog = di.inject(openDeleteClusterDialogInjectable); - const readFile = di.inject(readFileInjectable); const getClusterById = di.inject(getClusterByIdInjectable); const logger = di.inject(loggerInjectable); @@ -37,7 +35,9 @@ const setupKubernetesClusterContextMenuOpenInjectable = getInjectable({ return logger.warn("[KUBERNETES-CLUSTER]: cannot delete cluster, does not exist in store", { clusterId }); } - const result = loadConfigFromString(await readFile(cluster.kubeConfigPath)); + const loadKubeconfig = di.inject(loadKubeconfigInjectable, cluster); + + const result = await loadKubeconfig(true); if (result.error) { logger.error("[KUBERNETES-CLUSTER]: failed to parse kubeconfig file", result.error); diff --git a/packages/core/src/renderer/cluster-frame-context/for-namespaced-resources.injectable.ts b/packages/core/src/renderer/cluster-frame-context/for-namespaced-resources.injectable.ts index f4795f8523..bc8c9f38e8 100644 --- a/packages/core/src/renderer/cluster-frame-context/for-namespaced-resources.injectable.ts +++ b/packages/core/src/renderer/cluster-frame-context/for-namespaced-resources.injectable.ts @@ -55,7 +55,7 @@ const clusterFrameContextForNamespacedResourcesInjectable = getInjectable({ && cluster.accessibleNamespaces.length === 0 && allNamespaces.get().every(ns => namespaces.includes(ns)) ), - isGlobalWatchEnabled: () => cluster.isGlobalWatchEnabled, + isGlobalWatchEnabled: () => cluster.isGlobalWatchEnabled.get(), get allNamespaces() { return allNamespaces.get(); }, diff --git a/packages/core/src/renderer/cluster-frame-context/should-show-resource.injectable.ts b/packages/core/src/renderer/cluster-frame-context/should-show-resource.injectable.ts index 2ec4132045..6258796a0b 100644 --- a/packages/core/src/renderer/cluster-frame-context/should-show-resource.injectable.ts +++ b/packages/core/src/renderer/cluster-frame-context/should-show-resource.injectable.ts @@ -15,7 +15,7 @@ const shouldShowResourceInjectable = getInjectable({ const cluster = di.inject(hostedClusterInjectable); return cluster - ? computed(() => cluster.shouldShowResource(resource)) + ? computed(() => cluster.resourcesToShow.has(formatKubeApiResource(resource))) : computed(() => false); }, injectionToken: shouldShowResourceInjectionToken, diff --git a/packages/core/src/renderer/cluster/create-cluster.injectable.ts b/packages/core/src/renderer/cluster/create-cluster.injectable.ts deleted file mode 100644 index ff8ea38fff..0000000000 --- a/packages/core/src/renderer/cluster/create-cluster.injectable.ts +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable } from "@ogre-tools/injectable"; -import type { ClusterDependencies } from "../../common/cluster/cluster"; -import { Cluster } from "../../common/cluster/cluster"; -import directoryForKubeConfigsInjectable from "../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable"; -import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token"; -import loggerInjectable from "../../common/logger.injectable"; -import broadcastMessageInjectable from "../../common/ipc/broadcast-message.injectable"; -import loadConfigfromFileInjectable from "../../common/kube-helpers/load-config-from-file.injectable"; - -const createClusterInjectable = getInjectable({ - id: "create-cluster", - - instantiate: (di) => { - const dependencies: ClusterDependencies = { - directoryForKubeConfigs: di.inject(directoryForKubeConfigsInjectable), - logger: di.inject(loggerInjectable), - broadcastMessage: di.inject(broadcastMessageInjectable), - loadConfigfromFile: di.inject(loadConfigfromFileInjectable), - - // TODO: Dismantle wrong abstraction - // Note: "as never" to get around strictness in unnatural scenario - createKubeconfigManager: () => undefined as never, - createKubectl: () => { throw new Error("Tried to access back-end feature in front-end.");}, - createContextHandler: () => undefined as never, - createAuthorizationReview: () => { throw new Error("Tried to access back-end feature in front-end."); }, - requestNamespaceListPermissionsFor: () => { throw new Error("Tried to access back-end feature in front-end."); }, - createListNamespaces: () => { throw new Error("Tried to access back-end feature in front-end."); }, - requestApiResources: () => { throw new Error("Tried to access back-end feature in front-end."); }, - detectClusterMetadata: () => { throw new Error("Tried to access back-end feature in front-end."); }, - clusterVersionDetector: { - detect: () => { throw new Error("Tried to access back-end feature in front-end."); }, - key: "irrelavent", - }, - }; - - return (model, configData) => new Cluster(dependencies, model, configData); - }, - - injectionToken: createClusterInjectionToken, -}); - -export default createClusterInjectable; diff --git a/packages/core/src/renderer/components/+cluster/cluster-overview.tsx b/packages/core/src/renderer/components/+cluster/cluster-overview.tsx index f1cb9ad461..904311f09e 100644 --- a/packages/core/src/renderer/components/+cluster/cluster-overview.tsx +++ b/packages/core/src/renderer/components/+cluster/cluster-overview.tsx @@ -6,6 +6,7 @@ import styles from "./cluster-overview.module.scss"; import React from "react"; +import type { IComputedValue } from "mobx"; import { reaction } from "mobx"; import { disposeOnUnmount, observer } from "mobx-react"; import type { NodeStore } from "../+nodes/store"; @@ -22,32 +23,30 @@ import type { EventStore } from "../+events/store"; import { withInjectables } from "@ogre-tools/injectable-react"; import clusterOverviewStoreInjectable from "./cluster-overview-store/cluster-overview-store.injectable"; import type { SubscribeStores } from "../../kube-watch-api/kube-watch-api"; -import type { Cluster } from "../../../common/cluster/cluster"; -import hostedClusterInjectable from "../../cluster-frame-context/hosted-cluster.injectable"; -import assert from "assert"; import subscribeStoresInjectable from "../../kube-watch-api/subscribe-stores.injectable"; import podStoreInjectable from "../+workloads-pods/store.injectable"; import eventStoreInjectable from "../+events/store.injectable"; import nodeStoreInjectable from "../+nodes/store.injectable"; +import enabledMetricsInjectable from "../../api/catalog/entity/metrics-enabled.injectable"; interface Dependencies { subscribeStores: SubscribeStores; clusterOverviewStore: ClusterOverviewStore; - hostedCluster: Cluster; podStore: PodStore; eventStore: EventStore; nodeStore: NodeStore; + clusterMetricsAreVisible: IComputedValue; } @observer class NonInjectedClusterOverview extends React.Component { - private metricPoller = interval(60, () => this.loadMetrics()); - - loadMetrics() { - if (this.props.hostedCluster.available) { - this.props.clusterOverviewStore.loadMetrics(); + private readonly metricPoller = interval(60, async () => { + try { + await this.props.clusterOverviewStore.loadMetrics(); + } catch { + // ignore } - } + }); componentDidMount() { this.metricPoller.start(true); @@ -97,14 +96,13 @@ class NonInjectedClusterOverview extends React.Component { } render() { - const { eventStore, nodeStore, hostedCluster } = this.props; + const { eventStore, nodeStore, clusterMetricsAreVisible } = this.props; const isLoaded = nodeStore.isLoaded && eventStore.isLoaded; - const isMetricHidden = hostedCluster.isMetricHidden(ClusterMetricsResourceType.Cluster); return (
- {this.renderClusterOverview(isLoaded, isMetricHidden)} + {this.renderClusterOverview(isLoaded, clusterMetricsAreVisible.get())}
); @@ -112,18 +110,12 @@ class NonInjectedClusterOverview extends React.Component { } export const ClusterOverview = withInjectables(NonInjectedClusterOverview, { - getProps: (di) => { - const hostedCluster = di.inject(hostedClusterInjectable); - - assert(hostedCluster, "Only allowed to renderer ClusterOverview within cluster frame"); - - return { - subscribeStores: di.inject(subscribeStoresInjectable), - clusterOverviewStore: di.inject(clusterOverviewStoreInjectable), - hostedCluster, - podStore: di.inject(podStoreInjectable), - eventStore: di.inject(eventStoreInjectable), - nodeStore: di.inject(nodeStoreInjectable), - }; - }, + getProps: (di) => ({ + subscribeStores: di.inject(subscribeStoresInjectable), + clusterOverviewStore: di.inject(clusterOverviewStoreInjectable), + clusterMetricsAreVisible: di.inject(enabledMetricsInjectable, ClusterMetricsResourceType.Cluster), + podStore: di.inject(podStoreInjectable), + eventStore: di.inject(eventStoreInjectable), + nodeStore: di.inject(nodeStoreInjectable), + }), }); diff --git a/packages/core/src/renderer/components/+config-secrets/__tests__/secret-details.test.tsx b/packages/core/src/renderer/components/+config-secrets/__tests__/secret-details.test.tsx index ddd5604785..e110056ab0 100644 --- a/packages/core/src/renderer/components/+config-secrets/__tests__/secret-details.test.tsx +++ b/packages/core/src/renderer/components/+config-secrets/__tests__/secret-details.test.tsx @@ -11,8 +11,8 @@ import { renderFor } from "../../test-utils/renderFor"; import storesAndApisCanBeCreatedInjectable from "../../../stores-apis-can-be-created.injectable"; import directoryForUserDataInjectable from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; import hostedClusterInjectable from "../../../cluster-frame-context/hosted-cluster.injectable"; -import createClusterInjectable from "../../../cluster/create-cluster.injectable"; import directoryForKubeConfigsInjectable from "../../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable"; +import { Cluster } from "../../../../common/cluster/cluster"; jest.mock("../../kube-object-meta/kube-object-meta", () => ({ KubeObjectMeta: () => null, @@ -27,9 +27,7 @@ describe("SecretDetails tests", () => { di.override(directoryForKubeConfigsInjectable, () => "/some-kube-configs"); di.override(storesAndApisCanBeCreatedInjectable, () => true); - const createCluster = di.inject(createClusterInjectable); - - di.override(hostedClusterInjectable, () => createCluster({ + di.override(hostedClusterInjectable, () => new Cluster({ contextName: "some-context-name", id: "some-cluster-id", kubeConfigPath: "/some-path-to-a-kubeconfig", diff --git a/packages/core/src/renderer/components/+namespaces/namespace-select-filter.test.tsx b/packages/core/src/renderer/components/+namespaces/namespace-select-filter.test.tsx index f646ab0820..be287a22b3 100644 --- a/packages/core/src/renderer/components/+namespaces/namespace-select-filter.test.tsx +++ b/packages/core/src/renderer/components/+namespaces/namespace-select-filter.test.tsx @@ -11,12 +11,12 @@ import { fireEvent } from "@testing-library/react"; import React from "react"; import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable"; import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; +import { Cluster } from "../../../common/cluster/cluster"; import type { Fetch } from "../../../common/fetch/fetch.injectable"; import fetchInjectable from "../../../common/fetch/fetch.injectable"; import { Namespace } from "../../../common/k8s-api/endpoints"; import { createMockResponseFromString } from "../../../test-utils/mock-responses"; import hostedClusterInjectable from "../../cluster-frame-context/hosted-cluster.injectable"; -import createClusterInjectable from "../../cluster/create-cluster.injectable"; import { getDiForUnitTesting } from "../../getDiForUnitTesting"; import subscribeStoresInjectable from "../../kube-watch-api/subscribe-stores.injectable"; import storesAndApisCanBeCreatedInjectable from "../../stores-apis-can-be-created.injectable"; @@ -58,9 +58,7 @@ describe("", () => { fetchMock = asyncFn(); di.override(fetchInjectable, () => fetchMock); - const createCluster = di.inject(createClusterInjectable); - - di.override(hostedClusterInjectable, () => createCluster({ + di.override(hostedClusterInjectable, () => new Cluster({ contextName: "some-context-name", id: "some-cluster-id", kubeConfigPath: "/some-path-to-a-kubeconfig", diff --git a/packages/core/src/renderer/components/+namespaces/namespace-store.test.ts b/packages/core/src/renderer/components/+namespaces/namespace-store.test.ts index e3e33ce9d7..c4b26e9b8a 100644 --- a/packages/core/src/renderer/components/+namespaces/namespace-store.test.ts +++ b/packages/core/src/renderer/components/+namespaces/namespace-store.test.ts @@ -7,9 +7,9 @@ import type { DiContainer } from "@ogre-tools/injectable"; import { observable } from "mobx"; 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 { Namespace } from "../../../common/k8s-api/endpoints"; import hostedClusterInjectable from "../../cluster-frame-context/hosted-cluster.injectable"; -import createClusterInjectable from "../../cluster/create-cluster.injectable"; import { getDiForUnitTesting } from "../../getDiForUnitTesting"; import storesAndApisCanBeCreatedInjectable from "../../stores-apis-can-be-created.injectable"; import type { NamespaceStore } from "./store"; @@ -111,9 +111,7 @@ describe("NamespaceStore", () => { di.override(directoryForKubeConfigsInjectable, () => "/some-kube-configs"); di.override(storesAndApisCanBeCreatedInjectable, () => true); - const createCluster = di.inject(createClusterInjectable); - - di.override(hostedClusterInjectable, () => createCluster({ + di.override(hostedClusterInjectable, () => new Cluster({ contextName: "some-context-name", id: "some-cluster-id", kubeConfigPath: "/some-path-to-a-kubeconfig", diff --git a/packages/core/src/renderer/components/+user-management/+cluster-role-bindings/__tests__/dialog.test.tsx b/packages/core/src/renderer/components/+user-management/+cluster-role-bindings/__tests__/dialog.test.tsx index d9a007bed1..04cf657606 100644 --- a/packages/core/src/renderer/components/+user-management/+cluster-role-bindings/__tests__/dialog.test.tsx +++ b/packages/core/src/renderer/components/+user-management/+cluster-role-bindings/__tests__/dialog.test.tsx @@ -15,11 +15,11 @@ import storesAndApisCanBeCreatedInjectable from "../../../../stores-apis-can-be- import directoryForUserDataInjectable from "../../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; import directoryForKubeConfigsInjectable from "../../../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable"; import hostedClusterInjectable from "../../../../cluster-frame-context/hosted-cluster.injectable"; -import createClusterInjectable from "../../../../cluster/create-cluster.injectable"; import type { CloseClusterRoleBindingDialog } from "../dialog/close.injectable"; import closeClusterRoleBindingDialogInjectable from "../dialog/close.injectable"; import type { OpenClusterRoleBindingDialog } from "../dialog/open.injectable"; import openClusterRoleBindingDialogInjectable from "../dialog/open.injectable"; +import { Cluster } from "../../../../../common/cluster/cluster"; describe("ClusterRoleBindingDialog tests", () => { let render: DiRender; @@ -36,9 +36,7 @@ describe("ClusterRoleBindingDialog tests", () => { closeClusterRoleBindingDialog = di.inject(closeClusterRoleBindingDialogInjectable); openClusterRoleBindingDialog = di.inject(openClusterRoleBindingDialogInjectable); - const createCluster = di.inject(createClusterInjectable); - - di.override(hostedClusterInjectable, () => createCluster({ + di.override(hostedClusterInjectable, () => new Cluster({ contextName: "some-context-name", id: "some-cluster-id", kubeConfigPath: "/some-path-to-a-kubeconfig", diff --git a/packages/core/src/renderer/components/+user-management/+role-bindings/__tests__/dialog.test.tsx b/packages/core/src/renderer/components/+user-management/+role-bindings/__tests__/dialog.test.tsx index add1ba98af..96f7bb5b68 100644 --- a/packages/core/src/renderer/components/+user-management/+role-bindings/__tests__/dialog.test.tsx +++ b/packages/core/src/renderer/components/+user-management/+role-bindings/__tests__/dialog.test.tsx @@ -15,9 +15,9 @@ import clusterRoleStoreInjectable from "../../+cluster-roles/store.injectable"; import storesAndApisCanBeCreatedInjectable from "../../../../stores-apis-can-be-created.injectable"; import directoryForKubeConfigsInjectable from "../../../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable"; import hostedClusterInjectable from "../../../../cluster-frame-context/hosted-cluster.injectable"; -import createClusterInjectable from "../../../../cluster/create-cluster.injectable"; import type { OpenRoleBindingDialog } from "../dialog/open.injectable"; import openRoleBindingDialogInjectable from "../dialog/open.injectable"; +import { Cluster } from "../../../../../common/cluster/cluster"; describe("RoleBindingDialog tests", () => { let render: DiRender; @@ -32,9 +32,7 @@ describe("RoleBindingDialog tests", () => { openRoleBindingDialog = di.inject(openRoleBindingDialogInjectable); - const createCluster = di.inject(createClusterInjectable); - - di.override(hostedClusterInjectable, () => createCluster({ + di.override(hostedClusterInjectable, () => new Cluster({ contextName: "some-context-name", id: "some-cluster-id", kubeConfigPath: "/some-path-to-a-kubeconfig", diff --git a/packages/core/src/renderer/components/+workloads-daemonsets/daemonset-details.tsx b/packages/core/src/renderer/components/+workloads-daemonsets/daemonset-details.tsx index e159d72ab3..260a2e8333 100644 --- a/packages/core/src/renderer/components/+workloads-daemonsets/daemonset-details.tsx +++ b/packages/core/src/renderer/components/+workloads-daemonsets/daemonset-details.tsx @@ -23,8 +23,6 @@ import type { SubscribeStores } from "../../kube-watch-api/kube-watch-api"; import subscribeStoresInjectable from "../../kube-watch-api/subscribe-stores.injectable"; import daemonSetStoreInjectable from "./store.injectable"; import podStoreInjectable from "../+workloads-pods/store.injectable"; -import getActiveClusterEntityInjectable from "../../api/catalog/entity/get-active-cluster-entity.injectable"; -import requestPodMetricsForDaemonSetsInjectable from "../../../common/k8s-api/endpoints/metrics.api/request-pod-metrics-for-daemon-sets.injectable"; import loggerInjectable from "../../../common/logger.injectable"; export interface DaemonSetDetailsProps extends KubeObjectDetailsProps { @@ -109,8 +107,6 @@ export const DaemonSetDetails = withInjectables; } @observer @@ -84,7 +84,7 @@ class NonInjectedPodDetailsContainer extends React.Component c.name == name); - const isMetricHidden = getActiveClusterEntity()?.isMetricHidden(ClusterMetricsResourceType.Container); + const isMetricHidden = containerMetricsVisible.get(); return (
@@ -217,6 +217,6 @@ export const PodDetailsContainer = withInjectables ({ ...props, portForwardStore: di.inject(portForwardStoreInjectable), - getActiveClusterEntity: di.inject(getActiveClusterEntityInjectable), + containerMetricsVisible: di.inject(enabledMetricsInjectable, ClusterMetricsResourceType.Container), }), }); diff --git a/packages/core/src/renderer/components/__tests__/cronjob.store.test.ts b/packages/core/src/renderer/components/__tests__/cronjob.store.test.ts index b704d24598..4e27c286eb 100644 --- a/packages/core/src/renderer/components/__tests__/cronjob.store.test.ts +++ b/packages/core/src/renderer/components/__tests__/cronjob.store.test.ts @@ -10,7 +10,7 @@ import { getDiForUnitTesting } from "../../getDiForUnitTesting"; import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable"; import hostedClusterInjectable from "../../cluster-frame-context/hosted-cluster.injectable"; -import createClusterInjectable from "../../cluster/create-cluster.injectable"; +import { Cluster } from "../../../common/cluster/cluster"; const scheduledCronJob = new CronJob({ apiVersion: "foo", @@ -127,9 +127,7 @@ describe("CronJob Store tests", () => { di.override(directoryForKubeConfigsInjectable, () => "/some-kube-configs"); di.override(storesAndApisCanBeCreatedInjectable, () => true); - const createCluster = di.inject(createClusterInjectable); - - di.override(hostedClusterInjectable, () => createCluster({ + di.override(hostedClusterInjectable, () => new Cluster({ contextName: "some-context-name", id: "some-cluster-id", kubeConfigPath: "/some-path-to-a-kubeconfig", diff --git a/packages/core/src/renderer/components/__tests__/daemonset.store.test.ts b/packages/core/src/renderer/components/__tests__/daemonset.store.test.ts index 755c666b85..bfd4861f48 100644 --- a/packages/core/src/renderer/components/__tests__/daemonset.store.test.ts +++ b/packages/core/src/renderer/components/__tests__/daemonset.store.test.ts @@ -13,7 +13,7 @@ import { getDiForUnitTesting } from "../../getDiForUnitTesting"; import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable"; import hostedClusterInjectable from "../../cluster-frame-context/hosted-cluster.injectable"; -import createClusterInjectable from "../../cluster/create-cluster.injectable"; +import { Cluster } from "../../../common/cluster/cluster"; const runningDaemonSet = new DaemonSet({ apiVersion: "foo", @@ -144,9 +144,7 @@ describe("DaemonSet Store tests", () => { di.override(directoryForKubeConfigsInjectable, () => "/some-kube-configs"); di.override(storesAndApisCanBeCreatedInjectable, () => true); - const createCluster = di.inject(createClusterInjectable); - - di.override(hostedClusterInjectable, () => createCluster({ + di.override(hostedClusterInjectable, () => new Cluster({ contextName: "some-context-name", id: "some-cluster-id", kubeConfigPath: "/some-path-to-a-kubeconfig", diff --git a/packages/core/src/renderer/components/__tests__/deployments.store.test.ts b/packages/core/src/renderer/components/__tests__/deployments.store.test.ts index 1991c30691..23987ccd37 100644 --- a/packages/core/src/renderer/components/__tests__/deployments.store.test.ts +++ b/packages/core/src/renderer/components/__tests__/deployments.store.test.ts @@ -14,7 +14,7 @@ import { getDiForUnitTesting } from "../../getDiForUnitTesting"; import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable"; import hostedClusterInjectable from "../../cluster-frame-context/hosted-cluster.injectable"; -import createClusterInjectable from "../../cluster/create-cluster.injectable"; +import { Cluster } from "../../../common/cluster/cluster"; const spec: PodSpec = { containers: [{ @@ -216,9 +216,7 @@ describe("Deployment Store tests", () => { di.override(directoryForKubeConfigsInjectable, () => "/some-kube-configs"); di.override(storesAndApisCanBeCreatedInjectable, () => true); - const createCluster = di.inject(createClusterInjectable); - - di.override(hostedClusterInjectable, () => createCluster({ + di.override(hostedClusterInjectable, () => new Cluster({ contextName: "some-context-name", id: "some-cluster-id", kubeConfigPath: "/some-path-to-a-kubeconfig", diff --git a/packages/core/src/renderer/components/__tests__/job.store.test.ts b/packages/core/src/renderer/components/__tests__/job.store.test.ts index 74a25f2b8f..73a5cade8f 100644 --- a/packages/core/src/renderer/components/__tests__/job.store.test.ts +++ b/packages/core/src/renderer/components/__tests__/job.store.test.ts @@ -13,7 +13,7 @@ import { getDiForUnitTesting } from "../../getDiForUnitTesting"; import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable"; import hostedClusterInjectable from "../../cluster-frame-context/hosted-cluster.injectable"; -import createClusterInjectable from "../../cluster/create-cluster.injectable"; +import { Cluster } from "../../../common/cluster/cluster"; const runningJob = new Job({ apiVersion: "foo", @@ -181,9 +181,7 @@ describe("Job Store tests", () => { di.override(directoryForKubeConfigsInjectable, () => "/some-kube-configs"); di.override(storesAndApisCanBeCreatedInjectable, () => true); - const createCluster = di.inject(createClusterInjectable); - - di.override(hostedClusterInjectable, () => createCluster({ + di.override(hostedClusterInjectable, () => new Cluster({ contextName: "some-context-name", id: "some-cluster-id", kubeConfigPath: "/some-path-to-a-kubeconfig", diff --git a/packages/core/src/renderer/components/__tests__/pods.store.test.ts b/packages/core/src/renderer/components/__tests__/pods.store.test.ts index e45697eeae..03a057458d 100644 --- a/packages/core/src/renderer/components/__tests__/pods.store.test.ts +++ b/packages/core/src/renderer/components/__tests__/pods.store.test.ts @@ -11,7 +11,7 @@ import { getDiForUnitTesting } from "../../getDiForUnitTesting"; import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable"; import hostedClusterInjectable from "../../cluster-frame-context/hosted-cluster.injectable"; -import createClusterInjectable from "../../cluster/create-cluster.injectable"; +import { Cluster } from "../../../common/cluster/cluster"; const runningPod = new Pod({ apiVersion: "foo", @@ -127,9 +127,7 @@ describe("Pod Store tests", () => { di.override(directoryForKubeConfigsInjectable, () => "/some-kube-configs"); di.override(storesAndApisCanBeCreatedInjectable, () => true); - const createCluster = di.inject(createClusterInjectable); - - di.override(hostedClusterInjectable, () => createCluster({ + di.override(hostedClusterInjectable, () => new Cluster({ contextName: "some-context-name", id: "some-cluster-id", kubeConfigPath: "/some-path-to-a-kubeconfig", diff --git a/packages/core/src/renderer/components/__tests__/replicaset.store.test.ts b/packages/core/src/renderer/components/__tests__/replicaset.store.test.ts index 99c3dd8067..aea690a507 100644 --- a/packages/core/src/renderer/components/__tests__/replicaset.store.test.ts +++ b/packages/core/src/renderer/components/__tests__/replicaset.store.test.ts @@ -13,7 +13,7 @@ import { getDiForUnitTesting } from "../../getDiForUnitTesting"; import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable"; import hostedClusterInjectable from "../../cluster-frame-context/hosted-cluster.injectable"; -import createClusterInjectable from "../../cluster/create-cluster.injectable"; +import { Cluster } from "../../../common/cluster/cluster"; const runningReplicaSet = new ReplicaSet({ apiVersion: "foo", @@ -144,9 +144,7 @@ describe("ReplicaSet Store tests", () => { di.override(directoryForKubeConfigsInjectable, () => "/some-kube-configs"); di.override(storesAndApisCanBeCreatedInjectable, () => true); - const createCluster = di.inject(createClusterInjectable); - - di.override(hostedClusterInjectable, () => createCluster({ + di.override(hostedClusterInjectable, () => new Cluster({ contextName: "some-context-name", id: "some-cluster-id", kubeConfigPath: "/some-path-to-a-kubeconfig", diff --git a/packages/core/src/renderer/components/__tests__/statefulset.store.test.ts b/packages/core/src/renderer/components/__tests__/statefulset.store.test.ts index 92d8f94aed..9e7ada1aa1 100644 --- a/packages/core/src/renderer/components/__tests__/statefulset.store.test.ts +++ b/packages/core/src/renderer/components/__tests__/statefulset.store.test.ts @@ -13,7 +13,7 @@ import { getDiForUnitTesting } from "../../getDiForUnitTesting"; import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable"; import hostedClusterInjectable from "../../cluster-frame-context/hosted-cluster.injectable"; -import createClusterInjectable from "../../cluster/create-cluster.injectable"; +import { Cluster } from "../../../common/cluster/cluster"; const runningStatefulSet = new StatefulSet({ apiVersion: "foo", @@ -144,9 +144,7 @@ describe("StatefulSet Store tests", () => { di.override(directoryForKubeConfigsInjectable, () => "/some-kube-configs"); di.override(storesAndApisCanBeCreatedInjectable, () => true); - const createCluster = di.inject(createClusterInjectable); - - di.override(hostedClusterInjectable, () => createCluster({ + di.override(hostedClusterInjectable, () => new Cluster({ contextName: "some-context-name", id: "some-cluster-id", kubeConfigPath: "/some-path-to-a-kubeconfig", diff --git a/packages/core/src/renderer/components/cluster-manager/cluster-frame-handler.ts b/packages/core/src/renderer/components/cluster-manager/cluster-frame-handler.ts index f1b169915f..ef526f9683 100644 --- a/packages/core/src/renderer/components/cluster-manager/cluster-frame-handler.ts +++ b/packages/core/src/renderer/components/cluster-manager/cluster-frame-handler.ts @@ -56,7 +56,7 @@ export class ClusterFrameHandler { const iframe = document.createElement("iframe"); iframe.id = `cluster-frame-${cluster.id}`; - iframe.name = cluster.contextName; + iframe.name = cluster.contextName.get(); iframe.setAttribute("src", getClusterFrameUrl(clusterId)); iframe.addEventListener("load", action(() => { this.dependencies.logger.info(`[LENS-VIEW]: frame for clusterId=${clusterId} has loaded`); @@ -71,19 +71,19 @@ export class ClusterFrameHandler { this.dependencies.logger.info(`[LENS-VIEW]: waiting cluster to be ready, clusterId=${clusterId}`); const dispose = when( - () => cluster.ready, + () => cluster.ready.get(), () => this.dependencies.logger.info(`[LENS-VIEW]: cluster is ready, clusterId=${clusterId}`), ); when( // cluster.disconnect is set to `false` when the cluster starts to connect - () => !cluster.disconnected, + () => !cluster.disconnected.get(), () => { when( () => { const cluster = this.dependencies.getClusterById(clusterId); - return Boolean(!cluster || (cluster.disconnected && this.views.get(clusterId)?.isLoaded)); + return Boolean(!cluster || (cluster.disconnected.get() && this.views.get(clusterId)?.isLoaded)); }, () => { this.dependencies.logger.info(`[LENS-VIEW]: remove dashboard, clusterId=${clusterId}`); @@ -126,7 +126,7 @@ export class ClusterFrameHandler { () => { const view = this.views.get(clusterId); - if (cluster.available && cluster.ready && view?.isLoaded) { + if (cluster.available.get() && cluster.ready.get() && view?.isLoaded) { return view; } diff --git a/packages/core/src/renderer/components/cluster-manager/cluster-status.tsx b/packages/core/src/renderer/components/cluster-manager/cluster-status.tsx index 940218ecad..d16fb1dd56 100644 --- a/packages/core/src/renderer/components/cluster-manager/cluster-status.tsx +++ b/packages/core/src/renderer/components/cluster-manager/cluster-status.tsx @@ -158,7 +158,7 @@ class NonInjectedClusterStatus extends React.Component
-

{this.entity?.getName() ?? this.cluster.name}

+

{this.entity?.getName() ?? this.cluster.name.get()}

{this.renderStatusIcon()} {this.renderAuthenticationOutput()} {this.renderReconnectionHelp()} diff --git a/packages/core/src/renderer/components/cluster-manager/cluster-view.tsx b/packages/core/src/renderer/components/cluster-manager/cluster-view.tsx index 23cd98fed2..60170e1741 100644 --- a/packages/core/src/renderer/components/cluster-manager/cluster-view.tsx +++ b/packages/core/src/renderer/components/cluster-manager/cluster-view.tsx @@ -53,7 +53,11 @@ class NonInjectedClusterView extends React.Component { @computed get isReady(): boolean { const { cluster } = this; - return (cluster?.ready && cluster?.available && this.isViewLoaded.get()) ?? false; + if (!cluster) { + return false; + } + + return cluster.ready.get() && cluster.available.get() && this.isViewLoaded.get(); } componentDidMount() { diff --git a/packages/core/src/renderer/components/cluster-settings/__tests__/cluster-local-terminal-settings.test.tsx b/packages/core/src/renderer/components/cluster-settings/__tests__/cluster-local-terminal-settings.test.tsx index 50fba67427..ae807bab0a 100644 --- a/packages/core/src/renderer/components/cluster-settings/__tests__/cluster-local-terminal-settings.test.tsx +++ b/packages/core/src/renderer/components/cluster-settings/__tests__/cluster-local-terminal-settings.test.tsx @@ -8,17 +8,19 @@ import { waitFor } from "@testing-library/react"; import { ClusterLocalTerminalSetting } from "../local-terminal-settings"; import userEvent from "@testing-library/user-event"; import type { Stats } from "fs"; -import type { Cluster } from "../../../../common/cluster/cluster"; +import { Cluster } from "../../../../common/cluster/cluster"; import { getDiForUnitTesting } from "../../../getDiForUnitTesting"; import type { DiRender } from "../../test-utils/renderFor"; import { renderFor } from "../../test-utils/renderFor"; import showErrorNotificationInjectable from "../../notifications/show-error-notification.injectable"; import statInjectable from "../../../../common/fs/stat.injectable"; +import loadKubeconfigInjectable from "../../../../common/cluster/load-kubeconfig.injectable"; describe("ClusterLocalTerminalSettings", () => { let render: DiRender; let showErrorNotificationMock: jest.Mock; let statMock: jest.Mock; + let loadKubeconfigMock: jest.Mock; beforeEach(() => { const di = getDiForUnitTesting(); @@ -34,27 +36,28 @@ describe("ClusterLocalTerminalSettings", () => { () => showErrorNotificationMock, ); + loadKubeconfigMock = jest.fn(); + di.override(loadKubeconfigInjectable, () => loadKubeconfigMock); + render = renderFor(di); - - jest.resetAllMocks(); - }); - - it("should render without errors", () => { - const dom = render(); - - expect(dom.container).toBeInstanceOf(HTMLElement); }); it("should render the current settings", async () => { - const cluster = { + loadKubeconfigMock.mockImplementation(() => ({ + getContextObject: () => ({}), + })); + + const cluster = new Cluster({ + contextName: "some-context-name", + id: "some-cluster-id", + kubeConfigPath: "/some/path", preferences: { terminalCWD: "/foobar", defaultNamespace: "kube-system", }, - getKubeconfig: jest.fn(() => ({ - getContextObject: jest.fn(() => ({})), - })), - } as unknown as Cluster; + }, { + clusterServerUrl: "https://localhost:12345", + }); const dom = render(); expect(await dom.findByDisplayValue("/foobar")).toBeDefined(); @@ -62,16 +65,21 @@ describe("ClusterLocalTerminalSettings", () => { }); it("should change placeholder for 'Default Namespace' to be the namespace from the kubeconfig", async () => { - const cluster = { + loadKubeconfigMock.mockImplementation(() => ({ + getContextObject: () => ({ namespace: "blat" }), + })); + + const cluster = new Cluster({ + contextName: "some-context-name", + id: "some-cluster-id", + kubeConfigPath: "/some/path", preferences: { terminalCWD: "/foobar", }, - getKubeconfig: jest.fn(() => ({ - getContextObject: jest.fn(() => ({ - namespace: "blat", - })), - })), - } as unknown as Cluster; + }, { + clusterServerUrl: "https://localhost:12345", + }); + const dom = render(); expect(await dom.findByDisplayValue("/foobar")).toBeDefined(); @@ -79,14 +87,20 @@ describe("ClusterLocalTerminalSettings", () => { }); it("should save the new default namespace after clicking away", async () => { - const cluster = { + loadKubeconfigMock.mockImplementation(() => ({ + getContextObject: () => ({}), + })); + + const cluster = new Cluster({ + contextName: "some-context-name", + id: "some-cluster-id", + kubeConfigPath: "/some/path", preferences: { terminalCWD: "/foobar", }, - getKubeconfig: jest.fn(() => ({ - getContextObject: jest.fn(() => ({})), - })), - } as unknown as Cluster; + }, { + clusterServerUrl: "https://localhost:12345", + }); const dom = render(); const dn = await dom.findByTestId("default-namespace"); @@ -107,11 +121,17 @@ describe("ClusterLocalTerminalSettings", () => { } as Stats; }); - const cluster = { - getKubeconfig: jest.fn(() => ({ - getContextObject: jest.fn(() => ({})), - })), - } as unknown as Cluster; + loadKubeconfigMock.mockImplementation(() => ({ + getContextObject: () => ({}), + })); + + const cluster = new Cluster({ + contextName: "some-context-name", + id: "some-cluster-id", + kubeConfigPath: "/some/path", + }, { + clusterServerUrl: "https://localhost:12345", + }); const dom = render(); const dn = await dom.findByTestId("working-directory"); @@ -133,11 +153,17 @@ describe("ClusterLocalTerminalSettings", () => { } as Stats; }); - const cluster = { - getKubeconfig: jest.fn(() => ({ - getContextObject: jest.fn(() => ({})), - })), - } as unknown as Cluster; + loadKubeconfigMock.mockImplementation(() => ({ + getContextObject: () => ({}), + })); + + const cluster = new Cluster({ + contextName: "some-context-name", + id: "some-cluster-id", + kubeConfigPath: "/some/path", + }, { + clusterServerUrl: "https://localhost:12345", + }); const dom = render(); const dn = await dom.findByTestId("working-directory"); diff --git a/packages/core/src/renderer/components/cluster-settings/kubeconfig.tsx b/packages/core/src/renderer/components/cluster-settings/kubeconfig.tsx index 251185ba89..f17390a588 100644 --- a/packages/core/src/renderer/components/cluster-settings/kubeconfig.tsx +++ b/packages/core/src/renderer/components/cluster-settings/kubeconfig.tsx @@ -19,7 +19,7 @@ export class ClusterKubeconfig extends React.Component { openKubeconfig = () => { const { cluster } = this.props; - shell.showItemInFolder(cluster.kubeConfigPath); + shell.showItemInFolder(cluster.kubeConfigPath.get()); }; render() { @@ -27,7 +27,9 @@ export class ClusterKubeconfig extends React.Component { - {this.props.cluster.kubeConfigPath} + + {this.props.cluster.kubeConfigPath.get()} + ); diff --git a/packages/core/src/renderer/components/cluster-settings/local-terminal-setting-presenter.injectable.ts b/packages/core/src/renderer/components/cluster-settings/local-terminal-setting-presenter.injectable.ts new file mode 100644 index 0000000000..caa5021dac --- /dev/null +++ b/packages/core/src/renderer/components/cluster-settings/local-terminal-setting-presenter.injectable.ts @@ -0,0 +1,48 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { observable } from "mobx"; +import type { Cluster } from "../../../common/cluster/cluster"; +import loadKubeconfigInjectable from "../../../common/cluster/load-kubeconfig.injectable"; + +export interface LocalTerminalSettingPresenter { + readonly directory: { + get: () => string; + set: (value: string) => void; + }; + readonly defaultNamespace: { + get: () => string; + set: (value: string) => void; + }; + readonly placeholderDefaultNamespace: string; +} + +const localTerminalSettingPresenterInjectable = getInjectable({ + id: "local-terminal-setting-presenter", + instantiate: async (di, cluster: Cluster): Promise => { + const loadKubeconfig = di.inject(loadKubeconfigInjectable, cluster); + + const kubeconfig = await loadKubeconfig(); + + const directory = observable.box(cluster.preferences.terminalCWD || ""); + const defaultNamespace = observable.box(cluster.preferences.defaultNamespace || ""); + const placeholderDefaultNamespace = kubeconfig.getContextObject(cluster.contextName.get())?.namespace || "default"; + + return { + directory: { + get: () => directory.get(), + set: (value) => directory.set(value), + }, + defaultNamespace: { + get: () => defaultNamespace.get(), + set: (value) => defaultNamespace.set(value), + }, + placeholderDefaultNamespace, + }; + }, + lifecycle: lifecycleEnum.transient, +}); + +export default localTerminalSettingPresenterInjectable; diff --git a/packages/core/src/renderer/components/cluster-settings/local-terminal-settings.tsx b/packages/core/src/renderer/components/cluster-settings/local-terminal-settings.tsx index 64dde94c00..4e10831ac1 100644 --- a/packages/core/src/renderer/components/cluster-settings/local-terminal-settings.tsx +++ b/packages/core/src/renderer/components/cluster-settings/local-terminal-settings.tsx @@ -3,7 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import React, { useEffect, useState } from "react"; +import React from "react"; import { observer } from "mobx-react"; import type { Cluster } from "../../../common/cluster/cluster"; import { Input } from "../input"; @@ -20,6 +20,10 @@ import Gutter from "../gutter/gutter"; import isWindowsInjectable from "../../../common/vars/is-windows.injectable"; import type { OpenPathPickingDialog } from "../../../features/path-picking-dialog/renderer/pick-paths.injectable"; import openPathPickingDialogInjectable from "../../../features/path-picking-dialog/renderer/pick-paths.injectable"; +import type { LocalTerminalSettingPresenter } from "./local-terminal-setting-presenter.injectable"; +import localTerminalSettingPresenterInjectable from "./local-terminal-setting-presenter.injectable"; +import { Spinner } from "../spinner"; +import { action, runInAction } from "mobx"; export interface ClusterLocalTerminalSettingProps { cluster: Cluster; @@ -30,70 +34,57 @@ interface Dependencies { resolveTilde: ResolveTilde; openPathPickingDialog: OpenPathPickingDialog; isWindows: boolean; + presenter: LocalTerminalSettingPresenter; } -const NonInjectedClusterLocalTerminalSetting = observer(({ - cluster, - showErrorNotification, - validateDirectory, - resolveTilde, - isWindows, - openPathPickingDialog, -}: Dependencies & ClusterLocalTerminalSettingProps) => { - if (!cluster) { - return null; - } - - const [directory, setDirectory] = useState(cluster.preferences?.terminalCWD || ""); - const [defaultNamespace, setDefaultNamespaces] = useState(cluster.preferences?.defaultNamespace || ""); - const [placeholderDefaultNamespace, setPlaceholderDefaultNamespace] = useState("default"); - - useEffect(() => { - (async () => { - const kubeconfig = await cluster.getKubeconfig(); - const { namespace } = kubeconfig.getContextObject(cluster.contextName) ?? {}; - - if (namespace) { - setPlaceholderDefaultNamespace(namespace); - } - })(); - setDirectory(cluster.preferences?.terminalCWD || ""); - setDefaultNamespaces(cluster.preferences?.defaultNamespace || ""); - }, [cluster]); - +const NonInjectedClusterLocalTerminalSetting = observer((props: Dependencies & ClusterLocalTerminalSettingProps) => { + const { + cluster, + showErrorNotification, + validateDirectory, + resolveTilde, + isWindows, + openPathPickingDialog, + presenter, + } = props; const commitDirectory = async (directory: string) => { - cluster.preferences ??= {}; - if (!directory) { - cluster.preferences.terminalCWD = undefined; - } else { - const dir = resolveTilde(directory); - const result = await validateDirectory(dir); + runInAction(() => { + cluster.preferences.terminalCWD = undefined; + }); - if (!result.callWasSuccessful) { - showErrorNotification( - <> - Terminal Working Directory -

- {"Your changes were not saved because "} - {result.error} -

- , - ); - } else { - cluster.preferences.terminalCWD = dir; - setDirectory(dir); - } + return; } + + const dir = resolveTilde(directory); + const result = await validateDirectory(dir); + + if (result.callWasSuccessful) { + runInAction(() => { + cluster.preferences.terminalCWD = dir; + presenter.directory.set(dir); + }); + + return; + } + + showErrorNotification( + <> + Terminal Working Directory +

+ {"Your changes were not saved because "} + {result.error} +

+ , + ); }; - const commitDefaultNamespace = () => { - cluster.preferences ??= {}; - cluster.preferences.defaultNamespace = defaultNamespace || undefined; - }; + const commitDefaultNamespace = action(() => { + cluster.preferences.defaultNamespace = presenter.defaultNamespace.get() || undefined; + }); const setAndCommitDirectory = (newPath: string) => { - setDirectory(newPath); + presenter.directory.set(newPath); commitDirectory(newPath); }; @@ -112,15 +103,15 @@ const NonInjectedClusterLocalTerminalSetting = observer(({ commitDirectory(directory)} + onChange={value => presenter.directory.set(value)} + onBlur={() => commitDirectory(presenter.directory.get())} placeholder={isWindows ? "$USERPROFILE" : "$HOME"} iconRight={( <> { - directory && ( + presenter.directory.get() && ( presenter.defaultNamespace.set(value)} onBlur={commitDefaultNamespace} - placeholder={placeholderDefaultNamespace} + placeholder={presenter.placeholderDefaultNamespace} /> Default namespace used for kubectl. @@ -165,12 +156,14 @@ const NonInjectedClusterLocalTerminalSetting = observer(({ }); export const ClusterLocalTerminalSetting = withInjectables(NonInjectedClusterLocalTerminalSetting, { - getProps: (di, props) => ({ + getPlaceholder: () => , + getProps: async (di, props) => ({ ...props, showErrorNotification: di.inject(showErrorNotificationInjectable), validateDirectory: di.inject(validateDirectoryInjectable), resolveTilde: di.inject(resolveTildeInjectable), isWindows: di.inject(isWindowsInjectable), openPathPickingDialog: di.inject(openPathPickingDialogInjectable), + presenter: await di.inject(localTerminalSettingPresenterInjectable, props.cluster), }), }); diff --git a/packages/core/src/renderer/components/cluster-settings/node-shell-setting.tsx b/packages/core/src/renderer/components/cluster-settings/node-shell-setting.tsx index 858ae9d20e..841d04299e 100644 --- a/packages/core/src/renderer/components/cluster-settings/node-shell-setting.tsx +++ b/packages/core/src/renderer/components/cluster-settings/node-shell-setting.tsx @@ -4,7 +4,7 @@ */ import type { Cluster } from "../../../common/cluster/cluster"; -import { makeObservable, observable } from "mobx"; +import { makeObservable, observable, runInAction } from "mobx"; import { SubTitle } from "../layout/sub-title"; import React from "react"; import { Input } from "../input/input"; @@ -28,9 +28,10 @@ export class ClusterNodeShellSetting extends React.Component { + this.props.cluster.preferences.nodeShellImage = this.nodeShellImage || undefined; + this.props.cluster.preferences.imagePullSecret = this.imagePullSecret || undefined; + }); } render() { diff --git a/packages/core/src/renderer/components/delete-cluster-dialog/is-current-context.tsx b/packages/core/src/renderer/components/delete-cluster-dialog/is-current-context.tsx index e0eebcea63..cbd18ad358 100644 --- a/packages/core/src/renderer/components/delete-cluster-dialog/is-current-context.tsx +++ b/packages/core/src/renderer/components/delete-cluster-dialog/is-current-context.tsx @@ -6,5 +6,5 @@ import type { KubeConfig } from "@kubernetes/client-node"; import type { Cluster } from "../../../common/cluster/cluster"; export function isCurrentContext(config: KubeConfig, cluster: Cluster) { - return config.currentContext == cluster.contextName; + return config.currentContext == cluster.contextName.get(); } diff --git a/packages/core/src/renderer/components/delete-cluster-dialog/is-in-local-kubeconfig.injectable.ts b/packages/core/src/renderer/components/delete-cluster-dialog/is-in-local-kubeconfig.injectable.ts new file mode 100644 index 0000000000..65e43fbb85 --- /dev/null +++ b/packages/core/src/renderer/components/delete-cluster-dialog/is-in-local-kubeconfig.injectable.ts @@ -0,0 +1,20 @@ +/** + * 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 directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable"; +import type { Cluster } from "../../../common/cluster/cluster"; + +export type IsInLocalKubeconfig = (cluster: Cluster) => boolean; + +const isInLocalKubeconfigInjectable = getInjectable({ + id: "is-in-local-kubeconfig", + instantiate: (di): IsInLocalKubeconfig => { + const directoryForKubeConfigs = di.inject(directoryForKubeConfigsInjectable); + + return cluster => cluster.kubeConfigPath.get().startsWith(directoryForKubeConfigs); + }, +}); + +export default isInLocalKubeconfigInjectable; diff --git a/packages/core/src/renderer/components/delete-cluster-dialog/view.tsx b/packages/core/src/renderer/components/delete-cluster-dialog/view.tsx index 78b06c7fe2..b1548354fd 100644 --- a/packages/core/src/renderer/components/delete-cluster-dialog/view.tsx +++ b/packages/core/src/renderer/components/delete-cluster-dialog/view.tsx @@ -30,6 +30,8 @@ import type { SaveKubeconfig } from "./save-kubeconfig.injectable"; import saveKubeconfigInjectable from "./save-kubeconfig.injectable"; import showErrorNotificationInjectable from "../notifications/show-error-notification.injectable"; import { isCurrentContext } from "./is-current-context"; +import type { IsInLocalKubeconfig } from "./is-in-local-kubeconfig.injectable"; +import isInLocalKubeconfigInjectable from "./is-in-local-kubeconfig.injectable"; interface Dependencies { state: IObservableValue; @@ -39,6 +41,7 @@ interface Dependencies { requestClearClusterAsDeleting: RequestClearClusterAsDeleting; showErrorNotification: ShowNotification; saveKubeconfig: SaveKubeconfig; + isInLocalKubeconfig: IsInLocalKubeconfig; } @observer @@ -52,7 +55,7 @@ class NonInjectedDeleteClusterDialog extends React.Component { this.props.state.set({ ...state, config: Object.assign(config, { - contexts: config.contexts.filter(item => item.name !== state.cluster.contextName), + contexts: config.contexts.filter(item => item.name !== state.cluster.contextName.get()), currentContext: newCurrentContext && showContextSwitch ? newCurrentContext : config.currentContext, @@ -61,7 +64,7 @@ class NonInjectedDeleteClusterDialog extends React.Component { }); try { - await this.props.saveKubeconfig(config, cluster.kubeConfigPath); + await this.props.saveKubeconfig(config, cluster.kubeConfigPath.get()); this.props.hotbarStore.removeAllHotbarItems(cluster.id); await this.props.requestDeleteCluster(cluster.id); } catch(error) { @@ -73,7 +76,7 @@ class NonInjectedDeleteClusterDialog extends React.Component { } shouldDeleteBeDisabled({ cluster, config, newCurrentContext, showContextSwitch }: DeleteClusterDialogState): boolean { - const noContextsAvailable = config.contexts.filter(context => context.name !== cluster.contextName).length == 0; + const noContextsAvailable = config.contexts.filter(context => context.name !== cluster.contextName.get()).length == 0; const newContextNotSelected = newCurrentContext === ""; if (noContextsAvailable) { @@ -92,7 +95,7 @@ class NonInjectedDeleteClusterDialog extends React.Component { const selectOptions = config .contexts - .filter(context => context.name !== cluster.contextName) + .filter(context => context.name !== cluster.contextName.get()) .map(context => ({ value: context.name, label: context.name, @@ -121,7 +124,7 @@ class NonInjectedDeleteClusterDialog extends React.Component { } renderDeleteMessage({ cluster }: DeleteClusterDialogState) { - if (cluster.isInLocalKubeconfig()) { + if (this.props.isInLocalKubeconfig(cluster)) { return (
{"Delete the "} @@ -141,7 +144,7 @@ class NonInjectedDeleteClusterDialog extends React.Component { {" context from "} - {cluster.kubeConfigPath} + {cluster.kubeConfigPath.get()} ?
@@ -149,7 +152,7 @@ class NonInjectedDeleteClusterDialog extends React.Component { } getWarningMessage({ cluster, config }: DeleteClusterDialogState) { - if (cluster.isInLocalKubeconfig()) { + if (this.props.isInLocalKubeconfig(cluster)) { return (

Are you sure you want to delete it? It can be re-added through the copy/paste mechanism. @@ -157,7 +160,7 @@ class NonInjectedDeleteClusterDialog extends React.Component { ); } - const contexts = config.contexts.filter(context => context.name !== cluster.contextName); + const contexts = config.contexts.filter(context => context.name !== cluster.contextName.get()); if (!contexts.length) { return ( @@ -191,7 +194,7 @@ class NonInjectedDeleteClusterDialog extends React.Component { renderContents(state: DeleteClusterDialogState) { const { config, cluster, showContextSwitch } = state; - const contexts = state.config.contexts.filter(context => context.name !== state.cluster.contextName); + const contexts = state.config.contexts.filter(context => context.name !== state.cluster.contextName.get()); const disableDelete = this.shouldDeleteBeDisabled(state); const currentContext = isCurrentContext(config, cluster); @@ -271,5 +274,6 @@ export const DeleteClusterDialog = withInjectables(NonInjectedDele requestDeleteCluster: di.inject(requestDeleteClusterInjectable), saveKubeconfig: di.inject(saveKubeconfigInjectable), showErrorNotification: di.inject(showErrorNotificationInjectable), + isInLocalKubeconfig: di.inject(isInLocalKubeconfigInjectable), }), }); diff --git a/packages/core/src/renderer/components/kube-object-list-layout/kube-object-list-layout.test.tsx b/packages/core/src/renderer/components/kube-object-list-layout/kube-object-list-layout.test.tsx index fa06a6d2a0..74f31ff3dd 100644 --- a/packages/core/src/renderer/components/kube-object-list-layout/kube-object-list-layout.test.tsx +++ b/packages/core/src/renderer/components/kube-object-list-layout/kube-object-list-layout.test.tsx @@ -20,7 +20,7 @@ import directoryForUserDataInjectable from "../../../common/app-paths/directory- import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable"; import hostedClusterInjectable from "../../cluster-frame-context/hosted-cluster.injectable"; import type { PodStore } from "../+workloads-pods/store"; -import { createClusterInjectionToken } from "../../../common/cluster/create-cluster-injection-token"; +import { Cluster } from "../../../common/cluster/cluster"; describe("kube-object-list-layout", () => { let di: DiContainer; @@ -34,9 +34,7 @@ describe("kube-object-list-layout", () => { di.override(directoryForKubeConfigsInjectable, () => "/some-kube-configs"); di.override(storesAndApisCanBeCreatedInjectable, () => true); - const createCluster = di.inject(createClusterInjectionToken); - - di.override(hostedClusterInjectable, () => createCluster({ + di.override(hostedClusterInjectable, () => new Cluster({ contextName: "some-context-name", id: "some-cluster-id", kubeConfigPath: "/some-path-to-a-kubeconfig", diff --git a/packages/core/src/renderer/components/kube-object-menu/cluster-name.injectable.ts b/packages/core/src/renderer/components/kube-object-menu/cluster-name.injectable.ts new file mode 100644 index 0000000000..f66d0458b0 --- /dev/null +++ b/packages/core/src/renderer/components/kube-object-menu/cluster-name.injectable.ts @@ -0,0 +1,19 @@ +/** + * 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 { computed } from "mobx"; +import activeEntityInternalClusterInjectable from "../../api/catalog/entity/get-active-cluster-entity.injectable"; + +const clusterNameInjectable = getInjectable({ + id: "cluster-name", + instantiate: (di) => { + const activeEntityInternalCluster = di.inject(activeEntityInternalClusterInjectable); + + return computed(() => activeEntityInternalCluster.get()?.name.get()); + }, +}); + +export default clusterNameInjectable; diff --git a/packages/core/src/renderer/components/kube-object-menu/dependencies/cluster-name.injectable.ts b/packages/core/src/renderer/components/kube-object-menu/dependencies/cluster-name.injectable.ts deleted file mode 100644 index ad7c5c0e80..0000000000 --- a/packages/core/src/renderer/components/kube-object-menu/dependencies/cluster-name.injectable.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; -import clusterInjectable from "./cluster.injectable"; - -const clusterNameInjectable = getInjectable({ - id: "cluster-name", - - instantiate: (di) => { - const cluster = di.inject(clusterInjectable); - - return cluster?.name; - }, - - lifecycle: lifecycleEnum.transient, -}); - -export default clusterNameInjectable; diff --git a/packages/core/src/renderer/components/kube-object-menu/dependencies/cluster.injectable.ts b/packages/core/src/renderer/components/kube-object-menu/dependencies/cluster.injectable.ts deleted file mode 100644 index 5a2fb7c086..0000000000 --- a/packages/core/src/renderer/components/kube-object-menu/dependencies/cluster.injectable.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; -import getActiveClusterEntityInjectable from "../../../api/catalog/entity/get-active-cluster-entity.injectable"; - -const clusterInjectable = getInjectable({ - id: "cluster", - instantiate: (di) => { - const getActiveClusterEntity = di.inject(getActiveClusterEntityInjectable); - - return getActiveClusterEntity(); - }, - lifecycle: lifecycleEnum.transient, -}); - -export default clusterInjectable; diff --git a/packages/core/src/renderer/components/kube-object-menu/kube-object-menu.test.tsx b/packages/core/src/renderer/components/kube-object-menu/kube-object-menu.test.tsx index 56841c2136..1741811a49 100644 --- a/packages/core/src/renderer/components/kube-object-menu/kube-object-menu.test.tsx +++ b/packages/core/src/renderer/components/kube-object-menu/kube-object-menu.test.tsx @@ -15,7 +15,6 @@ import type { AsyncFnMock } from "@async-fn/jest"; import asyncFn from "@async-fn/jest"; import { getDiForUnitTesting } from "../../getDiForUnitTesting"; import { computed, runInAction } from "mobx"; -import clusterInjectable from "./dependencies/cluster.injectable"; import type { DiRender } from "../test-utils/renderFor"; import { renderFor } from "../test-utils/renderFor"; import type { Cluster } from "../../../common/cluster/cluster"; @@ -25,6 +24,7 @@ import { KubeObjectMenu } from "./index"; import createEditResourceTabInjectable from "../dock/edit-resource/edit-resource-tab.injectable"; import hideDetailsInjectable from "../kube-detail-params/hide-details.injectable"; import { kubeObjectMenuItemInjectionToken } from "./kube-object-menu-item-injection-token"; +import activeEntityInternalClusterInjectable from "../../api/catalog/entity/get-active-cluster-entity.injectable"; // TODO: make `animated={false}` not required to make tests deterministic describe("kube-object-menu", () => { @@ -45,11 +45,10 @@ describe("kube-object-menu", () => { render = renderFor(di); di.override( - clusterInjectable, - () => - ({ - name: "Some name", - } as Cluster), + activeEntityInternalClusterInjectable, + () => computed(() => ({ + name: computed(() => "Some name"), + } as Cluster)), ); di.override( @@ -66,7 +65,11 @@ describe("kube-object-menu", () => { }); it("given no cluster, does not crash", () => { - di.override(clusterInjectable, () => null); + + di.override( + activeEntityInternalClusterInjectable, + () => computed(() => undefined), + ); expect(() => { render(); diff --git a/packages/core/src/renderer/components/kube-object-menu/kube-object-menu.tsx b/packages/core/src/renderer/components/kube-object-menu/kube-object-menu.tsx index eb0dc025b9..e3fe090150 100644 --- a/packages/core/src/renderer/components/kube-object-menu/kube-object-menu.tsx +++ b/packages/core/src/renderer/components/kube-object-menu/kube-object-menu.tsx @@ -11,7 +11,7 @@ import { MenuItem, MenuActions } from "../menu"; import identity from "lodash/identity"; import type { ApiManager } from "../../../common/k8s-api/api-manager"; import { withInjectables } from "@ogre-tools/injectable-react"; -import clusterNameInjectable from "./dependencies/cluster-name.injectable"; +import clusterNameInjectable from "./cluster-name.injectable"; import createEditResourceTabInjectable from "../dock/edit-resource/edit-resource-tab.injectable"; import kubeObjectMenuItemsInjectable from "./kube-object-menu-items.injectable"; import apiManagerInjectable from "../../../common/k8s-api/api-manager/manager.injectable"; @@ -38,7 +38,7 @@ export interface KubeObjectMenuProps extends Men interface Dependencies { apiManager: ApiManager; kubeObjectMenuItems: IComputedValue; - clusterName: string | undefined; + clusterName: IComputedValue; hideDetails: HideDetails; createEditResourceTab: (kubeObject: KubeObject) => void; onContextMenuOpen: OnKubeObjectContextMenuOpen; @@ -65,7 +65,7 @@ class NonInjectedKubeObjectMenu extends React.Component {`Remove ${object.kind} `} {breadcrumb} {" from "} - {this.props.clusterName} + {this.props.clusterName.get()} ?

); diff --git a/packages/core/src/renderer/components/test-utils/get-application-builder.tsx b/packages/core/src/renderer/components/test-utils/get-application-builder.tsx index 4ba9c42778..358a72b857 100644 --- a/packages/core/src/renderer/components/test-utils/get-application-builder.tsx +++ b/packages/core/src/renderer/components/test-utils/get-application-builder.tsx @@ -22,7 +22,7 @@ import navigateToPreferencesInjectable from "../../../features/preferences/commo import type { NavigateToHelmCharts } from "../../../common/front-end-routing/routes/cluster/helm/charts/navigate-to-helm-charts.injectable"; import navigateToHelmChartsInjectable from "../../../common/front-end-routing/routes/cluster/helm/charts/navigate-to-helm-charts.injectable"; import hostedClusterInjectable from "../../cluster-frame-context/hosted-cluster.injectable"; -import type { Cluster } from "../../../common/cluster/cluster"; +import { Cluster } from "../../../common/cluster/cluster"; import type { NamespaceStore } from "../+namespaces/store"; import historyInjectable from "../../navigation/history.injectable"; import type { MinimalTrayMenuItem } from "../../../main/tray/electron-tray/electron-tray.injectable"; @@ -228,8 +228,6 @@ export const getApplicationBuilder = () => { }, })); - const allowedResourcesState = observable.set(); - const windowHelpers = new Map RenderResult }>(); const createElectronWindowFake: CreateElectronWindow = (configuration) => { @@ -526,19 +524,20 @@ export const getApplicationBuilder = () => { environment = environments.clusterFrame; builder.beforeWindowStart((windowDi) => { - const clusterStub = { + const cluster = new Cluster({ id: "some-cluster-id", - accessibleNamespaces: observable.array(), - allowedNamespaces: observable.array(), - shouldShowResource: (kind) => allowedResourcesState.has(formatKubeApiResource(kind)), - } as Partial as Cluster; + contextName: "some-context-name", + kubeConfigPath: "/some-path-to-kube-config", + }, { + clusterServerUrl: "https://localhost:12345", + }); windowDi.override(activeKubernetesClusterInjectable, () => - computed(() => catalogEntityFromCluster(clusterStub)), + computed(() => catalogEntityFromCluster(cluster)), ); - windowDi.override(hostedClusterIdInjectable, () => clusterStub.id); - windowDi.override(hostedClusterInjectable, () => clusterStub); + windowDi.override(hostedClusterIdInjectable, () => cluster.id); + windowDi.override(hostedClusterInjectable, () => cluster); // TODO: Figure out a way to remove this stub. windowDi.override(namespaceStoreInjectable, () => ({ @@ -645,8 +644,11 @@ export const getApplicationBuilder = () => { allowKubeResource: (resource) => { environment.onAllowKubeResource(); + const windowDi = builder.applicationWindow.only.di; + const cluster = windowDi.inject(hostedClusterInjectable); + runInAction(() => { - allowedResourcesState.add(formatKubeApiResource(resource)); + cluster?.resourcesToShow.add(formatKubeApiResource(resource)); }); return builder; diff --git a/packages/core/src/renderer/frames/cluster-frame/cluster-frame.test.tsx b/packages/core/src/renderer/frames/cluster-frame/cluster-frame.test.tsx index 138f6141e0..f451d24923 100644 --- a/packages/core/src/renderer/frames/cluster-frame/cluster-frame.test.tsx +++ b/packages/core/src/renderer/frames/cluster-frame/cluster-frame.test.tsx @@ -14,8 +14,7 @@ import { DefaultProps } from "../../mui-base-theme"; import { ClusterFrame } from "./cluster-frame"; import historyInjectable from "../../navigation/history.injectable"; import { computed } from "mobx"; -import type { Cluster } from "../../../common/cluster/cluster"; -import createClusterInjectable from "../../cluster/create-cluster.injectable"; +import { Cluster } from "../../../common/cluster/cluster"; import subscribeStoresInjectable from "../../kube-watch-api/subscribe-stores.injectable"; import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; import storesAndApisCanBeCreatedInjectable from "../../stores-apis-can-be-created.injectable"; @@ -49,9 +48,7 @@ describe("", () => { testUsingFakeTime("2000-01-01 12:00:00am"); - const createCluster = di.inject(createClusterInjectable); - - cluster = createCluster( + cluster = new Cluster( { contextName: "my-cluster", id: "123456", @@ -68,8 +65,7 @@ describe("", () => { describe("given cluster with list nodes and namespaces permissions", () => { beforeEach(() => { - // TODO: replace with not using private info - (cluster as unknown as { readonly allowedResources: Cluster["allowedResources"] }).allowedResources.replace(["nodes", "namespaces"]); + cluster.resourcesToShow.replace(["nodes", "namespaces"]); }); it("renders", () => { @@ -110,7 +106,7 @@ describe("", () => { describe("given cluster without list nodes, but with namespaces permissions", () => { beforeEach(() => { - (cluster as unknown as { readonly allowedResources: Cluster["allowedResources"] }).allowedResources.replace(["namespaces"]); + cluster.resourcesToShow.replace(["namespaces"]); }); it("renders", () => { diff --git a/packages/core/src/renderer/frames/cluster-frame/init-cluster-frame/init-cluster-frame.ts b/packages/core/src/renderer/frames/cluster-frame/init-cluster-frame/init-cluster-frame.ts index 109ae0f0bc..9bd0a26a3c 100644 --- a/packages/core/src/renderer/frames/cluster-frame/init-cluster-frame/init-cluster-frame.ts +++ b/packages/core/src/renderer/frames/cluster-frame/init-cluster-frame/init-cluster-frame.ts @@ -40,7 +40,7 @@ export const initClusterFrame = ({ ); await requestSetClusterFrameId(hostedCluster.id); - await hostedCluster.whenReady; // cluster.activate() is done at this point + await when(() => hostedCluster.ready.get()); // cluster.activate() is done at this point catalogEntityRegistry.activeEntity = hostedCluster.id; diff --git a/packages/core/src/renderer/ipc/list-namespaces-forbidden-handler.injectable.tsx b/packages/core/src/renderer/ipc/list-namespaces-forbidden-handler.injectable.tsx index 75fb51fe68..4acf8182b1 100644 --- a/packages/core/src/renderer/ipc/list-namespaces-forbidden-handler.injectable.tsx +++ b/packages/core/src/renderer/ipc/list-namespaces-forbidden-handler.injectable.tsx @@ -55,7 +55,7 @@ const listNamespacesForbiddenHandlerInjectable = getInjectable({ Add Accessible Namespaces

{"Cluster "} - {getClusterById(clusterId)?.name ?? ""} + {getClusterById(clusterId)?.name.get() ?? ""} {" does not have permissions to list namespaces. Please add the namespaces you have access to."}