From 76066c5ebfaabbfd95631c3aed41ed6ea6a1da34 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Wed, 5 Oct 2022 08:10:36 -0400 Subject: [PATCH] Making apiBase injectable (#6022) * Making apiBase injectable Signed-off-by: Sebastian Malton * Convert all of Helm functions to be DI Signed-off-by: Sebastian Malton * Make PortForward's use of apiBase fully injectable Signed-off-by: Sebastian Malton * Convert all metric requests to be injectable Signed-off-by: Sebastian Malton * Replace resource applier with injectables Signed-off-by: Sebastian Malton * Switch KubeJsonApi.forCluster to be injectable but do not use Signed-off-by: Sebastian Malton * Convert the rest of shell sessions to be DI-ed - This is a prerequesit for using the new createKubeJsonApiForClusterInjectable Signed-off-by: Sebastian Malton * Use new createKubeJsonApiForClusterInjectable for openNodeShellSession Signed-off-by: Sebastian Malton * Make KubeconfigDialog injectable Signed-off-by: Sebastian Malton * Remove jest-fetch-mock and make fetch injectable Signed-off-by: Sebastian Malton * Fix tests with new global override Signed-off-by: Sebastian Malton * Add new injectable for create KubeJsonApi and JsonApi instances Signed-off-by: Sebastian Malton * Fix showing-details-for-helm-release behavioural tests - Remove HelmChartStore in favour of all injectables - Create a model for UpgradeChartDockTab Signed-off-by: Sebastian Malton * Fix show details and updating helm releases tests Signed-off-by: Sebastian Malton * Fix residual typing issues related to metrics Signed-off-by: Sebastian Malton * Fix crash on load due to circular dependency Signed-off-by: Sebastian Malton * Fix create resource tab not working Signed-off-by: Sebastian Malton * Remove legacy apiBase global Signed-off-by: Sebastian Malton * Introduce and use isDebuggingInjectable Signed-off-by: Sebastian Malton * Introduce and use windowLocationInjectable Signed-off-by: Sebastian Malton * Remove global legacy apiKube Signed-off-by: Sebastian Malton * Improve injectable filenames compared to the injectables inside Signed-off-by: Sebastian Malton * Remove modifying input in requestActivePortForwardInjectable Signed-off-by: Sebastian Malton * Introduce and use get(Milli)SecondsFromUnixEpochInjectable Signed-off-by: Sebastian Malton * Switch to non-reactive way of gettting possible helm release versions Signed-off-by: Sebastian Malton * Fix typo Signed-off-by: Sebastian Malton * Fix bug in KubeApi constructor Signed-off-by: Sebastian Malton * Convert all KubeApi related tests to use asyncFn Signed-off-by: Sebastian Malton * Fix unit tests after introducing new injectables that have side effects Signed-off-by: Sebastian Malton * Fix bad rebase causing tests to fail Signed-off-by: Sebastian Malton * Improve expects for multiple field values Signed-off-by: Sebastian Malton * Fix crash will looking up api refs Signed-off-by: Sebastian Malton * Fix breaking change on KubeApi.list Signed-off-by: Sebastian Malton * Better fix for formatting urls Signed-off-by: Sebastian Malton * Remove injectable for time since we should just use useMockTime Signed-off-by: Sebastian Malton * Add happy path behavioural tests for upgrade chart tab Signed-off-by: Sebastian Malton * Remove debug message Signed-off-by: Sebastian Malton * Update snapshots Signed-off-by: Sebastian Malton * fix showing-details-for-helm-release tests Signed-off-by: Sebastian Malton * Fix installing-helm-chart-from-new-tab tests Signed-off-by: Sebastian Malton * Fix tests relating to hosted cluster id Signed-off-by: Sebastian Malton * Update snapshots to recent changes in master Co-authored-by: Janne Savolainen Signed-off-by: Iku-turso * Reupdated upgrade chart new tab test snapshots Signed-off-by: Sebastian Malton * Fix flakiness in unit test when using Signed-off-by: Sebastian Malton * Fix flakiness and improve tests for DeleteClusterDialog Signed-off-by: Sebastian Malton * Fix kubeconfig-sync tests Signed-off-by: Sebastian Malton * Fix tests by removing mockFs and making everything injectable Signed-off-by: Sebastian Malton * Fix build issues Signed-off-by: Sebastian Malton * Fix getElectronAppPathInjectable override not returning absolute paths - Also fixes the listing-active-helm-repos-in-prefs tests Signed-off-by: Sebastian Malton * Replace all uses of getAbsolutePath with joinPaths as it is more correct and less confusing Signed-off-by: Sebastian Malton * Fix opening application window tests by making override properly absolute Signed-off-by: Sebastian Malton * Update snapshots relating no longer using getAbsolutePath Signed-off-by: Sebastian Malton * Fix and add behavioural tests for RenderDelay Signed-off-by: Sebastian Malton * Fix extension discovery tests Signed-off-by: Sebastian Malton * Fix test flakiness because of path side effects, propagate uses to as many places Signed-off-by: Sebastian Malton * Fix extension-discovery tests Signed-off-by: Sebastian Malton * Add global override to fix some tests Signed-off-by: Sebastian Malton * Rewrite and fix implementation of KubeconfigManager and its tests Signed-off-by: Sebastian Malton * Fix tests by global override pathExists Signed-off-by: Sebastian Malton * Fix unit tests failing on windows by using injectable verions of path functions Signed-off-by: Sebastian Malton * Attempt to fix test timeout by using runInAction Signed-off-by: Sebastian Malton * Update snapshots after rebase Signed-off-by: Sebastian Malton * Update snapshots after rebase Signed-off-by: Sebastian Malton * Fix tests after rebase Signed-off-by: Sebastian Malton * Fix setupIpcMainHandlers usage Signed-off-by: Sebastian Malton * Update snapshots Signed-off-by: Sebastian Malton Signed-off-by: Sebastian Malton Signed-off-by: Iku-turso Co-authored-by: Iku-turso --- __mocks__/windowMock.ts | 19 - package.json | 3 +- .../directory-for-binaries.injectable.ts | 6 +- .../directory-for-kube-configs.injectable.ts | 9 +- ...rectory-for-kubectl-binaries.injectable.ts | 7 +- ...custom-kube-config-directory.injectable.ts | 7 +- .../catalog/filtered-categories.injectable.ts | 18 + src/common/cluster/cluster.ts | 7 +- ...ctory-for-lens-local-storage.injectable.ts | 6 +- .../fetch.global-override-for-injectable.ts | 11 + src/common/fetch/fetch.injectable.ts | 17 + ...ess-path.global-override-for-injectable.ts | 11 + src/common/fs/access-path.injectable.ts | 27 + .../fs/copy.global-override-for-injectable.ts | 11 + src/common/fs/copy.injectable.ts | 16 + ...ete-file.global-override-for-injectable.ts | 11 + src/common/fs/delete-file.injectable.ts | 15 + ...sure-dir.global-override-for-injectable.ts | 11 + src/common/fs/ensure-dir.injectable.ts | 6 +- ...ract-tar.global-override-for-injectable.ts | 11 + src/common/fs/extract-tar.injectable.ts | 26 + .../lstat.global-override-for-injectable.ts | 11 + src/common/fs/lstat.injectable.ts | 16 + .../fs/move.global-override-for-injectable.ts | 11 + src/common/fs/move.injectable.ts | 16 + ...h-exists.global-override-for-injectable.ts | 11 + ...irectory.global-override-for-injectable.ts | 11 + src/common/fs/read-directory.injectable.ts | 37 + src/common/fs/read-file.injectable.ts | 9 +- ...ove-path.global-override-for-injectable.ts | 11 + ...njectable.ts => remove-path.injectable.ts} | 10 +- ...ite-file.global-override-for-injectable.ts | 11 + src/common/fs/write-file.injectable.ts | 16 +- src/common/fs/write-json-file.injectable.ts | 32 +- src/common/ipc/cluster.ts | 3 - src/common/k8s-api/__tests__/kube-api.test.ts | 1906 ++++-- src/common/k8s-api/api-base.ts | 39 +- src/common/k8s-api/api-kube.ts | 3 - .../k8s-api/create-json-api.injectable.ts | 26 + .../create-kube-api-for-cluster.injectable.ts | 63 + ...-kube-api-for-remote-cluster.injectable.ts | 104 + ...te-kube-json-api-for-cluster.injectable.ts | 36 + .../create-kube-json-api.injectable.ts | 26 + src/common/k8s-api/endpoints/cluster.api.ts | 41 - .../k8s-api/endpoints/daemon-set.api.ts | 19 - .../k8s-api/endpoints/deployment.api.ts | 20 +- .../k8s-api/endpoints/helm-charts.api.ts | 65 +- .../request-charts.injectable.ts | 34 + .../request-readme.injectable.ts | 24 + .../request-values.injectable.ts | 24 + .../request-versions.injectable.ts | 31 + .../k8s-api/endpoints/helm-releases.api.ts | 53 +- ...guration.global-override-for-injectable.ts | 11 + .../request-configuration.injectable.ts | 29 + .../request-create.injectable.ts | 42 + .../request-delete.injectable.ts | 22 + .../request-details.injectable.ts | 41 + .../request-history.injectable.ts | 31 + .../request-releases.injectable.ts | 24 + .../request-rollback.injectable.ts | 27 + .../request-update.injectable.ts | 49 + .../request-values.injectable.ts | 22 + src/common/k8s-api/endpoints/ingress.api.ts | 22 - src/common/k8s-api/endpoints/job.api.ts | 20 +- src/common/k8s-api/endpoints/metrics.api.ts | 91 +- ...luster-metrics-by-node-names.injectable.ts | 63 + .../request-ingress-metrics.injectable.ts | 38 + ...equest-metrics-for-all-nodes.injectable.ts | 44 + .../metrics.api/request-metrics.injectable.ts | 73 + ...sistent-volume-claim-metrics.injectable.ts | 35 + ...-pod-metrics-for-daemon-sets.injectable.ts | 46 + ...-pod-metrics-for-deployments.injectable.ts | 46 + ...request-pod-metrics-for-jobs.injectable.ts | 46 + ...pod-metrics-for-replica-sets.injectable.ts | 46 + ...od-metrics-for-stateful-sets.injectable.ts | 47 + ...est-pod-metrics-in-namespace.injectable.ts | 44 + .../request-pod-metrics.injectable.ts | 54 + .../request-providers.injectable.ts | 26 + src/common/k8s-api/endpoints/namespace.api.ts | 18 - src/common/k8s-api/endpoints/node.api.ts | 28 - .../endpoints/persistent-volume-claim.api.ts | 18 - src/common/k8s-api/endpoints/pod.api.ts | 37 - .../k8s-api/endpoints/replica-set.api.ts | 19 - .../k8s-api/endpoints/resource-applier.api.ts | 38 - .../request-patch.injectable.ts | 30 + .../request-update.injectable.ts | 20 + .../k8s-api/endpoints/stateful-set.api.ts | 19 - src/common/k8s-api/index.ts | 7 - src/common/k8s-api/json-api.ts | 17 +- src/common/k8s-api/kube-api.ts | 249 +- src/common/k8s-api/kube-json-api.ts | 16 - src/common/k8s-api/kube-object.ts | 34 +- ...location.global-override-for-injectable.ts | 12 + .../k8s-api/window-location.injectable.ts | 17 + src/common/kube-helpers.ts | 19 +- .../load-config-from-file.injectable.ts | 23 + ...ory-path.global-override-for-injectable.ts | 9 + .../os/home-directory-path.injectable.ts | 14 + ...ory-path.global-override-for-injectable.ts | 9 + .../os/temp-directory-path.injectable.ts | 14 + ...ute-path.global-override-for-injectable.ts | 10 + ...basename.global-override-for-injectable.ts | 10 + src/common/path/get-basename.injectable.ts | 16 + ...-dirname.global-override-for-injectable.ts | 10 + src/common/path/get-dirname.injectable.ts | 16 + ...ive-path.global-override-for-injectable.ts | 10 + .../path/get-relative-path.injectable.ts | 16 + .../path/is-logical-child-path.injectable.ts | 51 + ...in-paths.global-override-for-injectable.ts | 10 + .../parse.global-override-for-injectable.ts | 10 + src/common/path/parse.injectable.ts | 14 + src/common/path/resolve-path.injectable.ts | 21 + src/common/path/resolve-tilde.injectable.ts | 31 + ...eparator.global-override-for-injectable.ts | 10 + src/common/path/separator.injectable.ts | 14 + .../test-utils/get-absolute-path-fake.ts | 17 - src/common/test-utils/join-paths-fake.ts | 7 - .../file-name-migration.injectable.ts | 7 +- src/common/user-store/user-store.ts | 7 - src/common/utils/__tests__/paths.test.ts | 33 +- src/common/utils/buildUrl.ts | 21 + .../request-from-channel-injection-token.ts | 2 +- .../utils/date/get-current-date-time.ts | 4 + src/common/utils/index.ts | 1 - src/common/utils/paths.ts | 55 - src/common/utils/sort-compare.ts | 79 +- src/common/utils/tar.ts | 12 +- src/common/vars.ts | 3 + .../base-bundled-binaries-dir.injectable.ts | 6 +- .../vars/bundled-resources-dir.injectable.ts | 6 +- ...ebugging.global-override-for-injectable.ts | 9 + src/common/vars/is-debugging.injectable.ts | 13 + .../vars/static-files-directory.injectable.ts | 6 +- src/extensions/common-api/k8s-api.ts | 26 +- .../extension-discovery.injectable.ts | 26 + .../extension-discovery.test.ts | 37 +- .../extension-discovery.ts | 143 +- .../extension-loader.injectable.ts | 6 + .../extension-loader/extension-loader.ts | 28 +- ...directory-for-extension-data.injectable.ts | 6 +- .../order-of-sidebar-items.test.tsx.snap | 10 +- ...-and-tab-navigation-for-core.test.tsx.snap | 35 +- ...ab-navigation-for-extensions.test.tsx.snap | 40 +- .../visibility-of-sidebar-items.test.tsx.snap | 10 +- .../workload-overview.test.tsx.snap | 5 +- .../delete-cluster-dialog.test.tsx.snap | 2192 +++++++ .../clear-as-deleting-channel.injectable.ts | 20 + .../common/delete-channel.injectable.ts | 20 + .../set-as-deleting-channel.injectable.ts | 20 + .../delete-cluster-dialog.test.tsx | 284 + ...as-deleteing-channel-handler.injectable.ts | 23 + .../main/delete-channel-handler.injectable.ts | 56 + ...as-deleteing-channel-handler.injectable.ts | 23 + .../request-clear-as-deleting.injectable.ts | 22 + .../renderer/request-delete.injectable.ts | 22 + .../request-set-as-deleting.injectable.ts | 22 + ...when-cluster-is-not-relevant.test.tsx.snap | 15 +- ...when-cluster-is-not-relevant.test.tsx.snap | 15 +- ...when-cluster-is-not-relevant.test.tsx.snap | 15 +- ...when-cluster-is-not-relevant.test.tsx.snap | 15 +- ...how-status-for-a-kube-object.test.tsx.snap | 30 +- ...when-cluster-is-not-relevant.test.tsx.snap | 15 +- .../edit-namespace-from-new-tab.test.tsx.snap | 80 +- ...e-from-previously-opened-tab.test.tsx.snap | 10 +- ...debar-and-tab-navigation-for-core.test.tsx | 6 +- ...and-tab-navigation-for-extensions.test.tsx | 12 +- ...when-cluster-is-not-relevant.test.tsx.snap | 15 +- ...elm-repository-in-preferences.test.ts.snap | 52 +- ...tory-from-list-in-preferences.test.ts.snap | 40 +- ...m-repositories-in-preferences.test.ts.snap | 40 +- ...ive-repository-in-preferences.test.ts.snap | 16 +- ...tom-helm-repository-in-preferences.test.ts | 3 + ...lling-helm-chart-from-new-tab.test.ts.snap | 100 +- ...rt-from-previously-opened-tab.test.ts.snap | 10 +- ...tab-for-installing-helm-chart.test.ts.snap | 15 +- ...installing-helm-chart-from-new-tab.test.ts | 193 +- ...m-chart-from-previously-opened-tab.test.ts | 57 +- ...dock-tab-for-installing-helm-chart.test.ts | 91 +- ...e-helm-repositories-in-preferences.test.ts | 4 +- .../upgrade-chart-new-tab.test.ts.snap | 5785 +++++++++++++++++ .../upgrade-chart-new-tab.test.ts | 268 + ...wing-details-for-helm-release.test.ts.snap | 203 +- .../showing-details-for-helm-release.test.ts | 415 +- .../__snapshots__/download-logs.test.tsx.snap | 10 +- ...ion-to-kubernetes-preferences.test.ts.snap | 4 +- ...ning-application-window-using-tray.test.ts | 4 +- src/jest.setup.ts | 4 - src/main/__test__/kubeconfig-manager.test.ts | 293 +- .../get-electron-app-path.test.ts | 3 - .../create-cluster.injectable.ts | 2 + .../setup-ipc-main-handlers.injectable.ts | 13 - .../setup-ipc-main-handlers.ts | 46 +- src/main/get-metrics.injectable.ts | 4 +- src/main/getDiForUnitTesting.ts | 12 +- src/main/helm/__tests__/helm-service.test.ts | 14 +- src/main/helm/helm-binary-path.injectable.ts | 6 +- src/main/helm/helm-chart-manager.ts | 6 +- ...t-readme.global-override-for-injectable.ts | 11 + ...ts => get-helm-chart-readme.injectable.ts} | 13 +- .../get-helm-chart-versions.injectable.ts | 31 + src/main/k8s/api-base.injectable.ts | 33 + .../create-kube-auth-proxy.injectable.ts | 5 +- .../create-kubeconfig-manager.injectable.ts | 10 + .../kubeconfig-manager/kubeconfig-manager.ts | 36 +- .../kubectl/bundled-binary-path.injectable.ts | 14 +- src/main/kubectl/create-kubectl.injectable.ts | 6 + src/main/kubectl/kubectl.ts | 23 +- .../shell-api-request.injectable.ts | 4 +- .../shell-api-request/shell-api-request.ts | 20 +- src/main/resource-applier.ts | 21 +- ...able.ts => get-readme-route.injectable.ts} | 14 +- ...able.ts => get-values-route.injectable.ts} | 6 +- .../charts/get-versions-route.injectable.ts | 25 + ...injectable.ts => list-route.injectable.ts} | 6 +- .../get-service-account-route.injectable.ts | 35 +- .../metrics/add-metrics-route.injectable.ts | 4 +- ...ts => create-resource-route.injectable.ts} | 14 +- .../routes/static-file-route.injectable.ts | 10 +- .../create-shell-session.injectable.ts | 31 +- .../local-shell-session.injectable.ts | 33 - .../local-shell-session.ts | 44 +- .../local-shell-session/open.injectable.ts | 55 + .../node-shell-session.injectable.ts | 32 - .../node-shell-session/node-shell-session.ts | 36 +- .../node-shell-session/open.injectable.ts | 42 + .../modify-terminal-shell-env.injectable.ts | 49 + .../terminal-shell-env-modifiers.ts | 42 - .../terminal-shell-env-modify.injectable.ts | 21 - src/main/shell-session/shell-session.ts | 50 +- .../splash-window/splash-window.injectable.ts | 6 +- .../get-tray-icon-path.injectable.ts | 15 +- src/migrations/cluster-store/3.6.0-beta.1.ts | 11 +- src/migrations/cluster-store/5.0.0-beta.10.ts | 5 +- src/migrations/cluster-store/5.0.0-beta.13.ts | 35 +- src/migrations/hotbar-store/5.0.0-beta.10.ts | 7 +- src/migrations/user-store/5.0.3-beta.1.ts | 23 +- src/renderer/api/index.ts | 6 - src/renderer/api/on-api-error.ts | 17 - .../api/setup-on-api-errors.injectable.ts | 24 - .../components/+add-cluster/add-cluster.tsx | 12 +- .../components/+catalog/catalog-menu.tsx | 86 +- .../+cluster/cluster-metric-switchers.tsx | 2 +- .../components/+cluster/cluster-metrics.tsx | 4 +- .../cluster-overview-store.injectable.ts | 2 + .../cluster-overview-store.ts | 28 +- .../+cluster/cluster-pie-charts.tsx | 39 +- .../+extensions/__tests__/extensions.test.tsx | 37 +- .../attempt-install-by-info.injectable.tsx | 197 +- .../attempt-install/attempt-install.tsx | 36 +- ...ate-temp-files-and-validate.injectable.tsx | 96 +- .../create-temp-files-and-validate.tsx | 94 - .../get-extension-dest-folder.injectable.ts | 15 +- .../get-extension-dest-folder.ts | 16 - .../unpack-extension.injectable.tsx | 128 +- .../unpack-extension/unpack-extension.tsx | 106 - .../attempt-installs.injectable.ts | 23 +- .../attempt-installs/attempt-installs.ts | 28 - .../components/+extensions/extensions.tsx | 10 +- ...nstall-extension-from-input.injectable.tsx | 82 + .../install-from-input.injectable.ts | 23 - .../install-from-input/install-from-input.tsx | 77 - .../read-file-notify.injectable.ts | 38 + .../read-file-notify/read-file-notify.ts | 23 - ...eadme-of-selected-helm-chart.injectable.ts | 6 +- .../call-for-helm-chart-readme.injectable.ts | 29 - ...sions-of-selected-helm-chart.injectable.ts | 6 +- ...call-for-helm-chart-versions.injectable.ts | 28 - .../get-char-details.injectable.ts | 17 - .../helm-chart-store.injectable.ts | 13 - .../+helm-charts/helm-chart.store.ts | 108 - .../components/+helm-charts/helm-charts.tsx | 42 +- .../call-for-helm-charts.injectable.ts | 17 - .../helm-charts/helm-charts.injectable.ts | 6 +- ...ersions-of-chart-for-release.injectable.ts | 36 + .../request-versions.injectable.ts | 28 + .../helm-charts/versions.injectable.ts | 29 + .../+helm-charts/helm-charts/versions.ts | 12 + ...releases.global-override-for-injectable.ts | 15 - .../call-for-helm-releases.injectable.ts | 24 - ...call-for-create-helm-release.injectable.ts | 44 - .../create-release.injectable.ts | 8 +- .../delete-release.injectable.ts | 10 +- .../+helm-releases/dialog/dialog.tsx | 28 +- ...guration.global-override-for-injectable.ts | 15 - ...r-helm-release-configuration.injectable.ts | 29 - ...-details.global-override-for-injectable.ts | 15 - ...all-for-helm-release-details.injectable.ts | 45 - .../call-for-helm-release.injectable.ts | 51 - .../release-details-model.injectable.tsx | 60 +- ...equest-detailed-helm-release.injectable.ts | 51 + .../+helm-releases/release-menu.tsx | 9 +- .../+helm-releases/releases.injectable.ts | 97 +- .../rollback-release.injectable.ts | 11 +- .../to-helm-release.injectable.ts | 90 + ...e-update.global-override-for-injectable.ts | 15 - ...call-for-helm-release-update.injectable.ts | 50 - .../update-release.injectable.ts | 8 +- .../+namespaces/namespace-details.tsx | 10 +- .../+network-ingresses/ingress-details.tsx | 24 +- src/renderer/components/+nodes/details.tsx | 13 +- src/renderer/components/+nodes/route.tsx | 50 +- .../volume-claim-details.tsx | 41 +- .../service-account-menu.tsx | 23 +- .../daemonset-details.tsx | 12 +- .../deployment-details.tsx | 13 +- .../+workloads-jobs/job-details.tsx | 12 +- .../+workloads-pods/pod-details-container.tsx | 2 +- .../+workloads-pods/pod-details.tsx | 43 +- .../replicaset-details.tsx | 13 +- .../statefulset-details.tsx | 13 +- src/renderer/components/animate/animate.tsx | 171 +- .../default-enter-duration.injectable.ts | 12 + .../default-leave-duration.injectable.ts | 12 + .../request-animation-frame.injectable.ts | 5 +- .../cluster-local-terminal-settings.tsx | 31 +- .../components/cluster-prometheus-setting.tsx | 9 +- .../__tests__/delete-cluster-dialog.test.tsx | 275 - ...luster-frame-child-component.injectable.ts | 2 - .../delete-cluster-dialog/save-config.ts | 21 - ...beconfig.global-override-for-injectable.ts | 11 + .../save-kubeconfig.injectable.ts | 35 + .../components/delete-cluster-dialog/view.tsx | 54 +- src/renderer/components/dialog/dialog.tsx | 6 +- .../lens-templates.injectable.ts | 54 +- .../user-templates.injectable.ts | 181 +- .../components/dock/create-resource/view.tsx | 11 +- src/renderer/components/dock/dock-tab.tsx | 1 + .../call-for-helm-chart-values.injectable.ts | 20 - .../install-chart-model.injectable.tsx | 41 +- .../create-upgrade-chart-tab.injectable.ts | 10 +- .../dock/upgrade-chart/store.injectable.ts | 12 +- .../components/dock/upgrade-chart/store.ts | 55 - .../dock/upgrade-chart/tab-data.injectable.ts | 21 + .../upgrade-chart-model.injectable.ts | 129 + .../components/dock/upgrade-chart/view.tsx | 205 +- .../kubeconfig-dialog/kubeconfig-dialog.tsx | 148 +- ...e-account-kube-config-dialog.injectable.ts | 31 + .../kubeconfig-dialog/open.injectable.ts | 40 + .../kubeconfig-dialog/state.injectable.ts | 14 + src/renderer/components/menu/menu.tsx | 23 +- .../__snapshots__/render-delay.test.tsx.snap | 37 + .../__tests__/render-delay.test.tsx | 105 +- ...callback.global-override-for-injectable.ts | 9 + .../cancel-idle-callback.injectable.ts | 15 + .../idle-callback-timeout.injectable.ts | 12 + .../components/render-delay/render-delay.tsx | 78 +- ...callback.global-override-for-injectable.ts | 13 + .../request-idle-callback.injectable.ts | 15 + .../resource-metrics-text.tsx | 18 +- .../resource-metrics/resource-metrics.tsx | 7 +- .../test-utils/get-application-builder.tsx | 81 +- .../create-cluster.injectable.ts | 2 + .../__snapshots__/cluster-frame.test.tsx.snap | 15 +- .../cluster-frame/cluster-frame.test.tsx | 3 + src/renderer/getDiForUnitTesting.tsx | 14 +- src/renderer/ipc/index.ts | 14 +- ...amespaces-forbidden-handler.injectable.tsx | 6 +- src/renderer/k8s/api-base.injectable.ts | 36 + src/renderer/k8s/api-kube.injectable.ts | 27 +- .../port-forward-store.injectable.ts | 14 +- .../port-forward-store/port-forward-store.ts | 39 +- .../request-active-port-forward.injectable.ts | 44 + .../create-storage.injectable.ts | 4 +- .../utils/create-storage/create-storage.ts | 14 +- src/test-utils/override-fs-with-fakes.ts | 8 +- types/dom.d.ts | 5 - yarn.lock | 46 +- 367 files changed, 16701 insertions(+), 5302 deletions(-) delete mode 100644 __mocks__/windowMock.ts create mode 100644 src/common/catalog/filtered-categories.injectable.ts create mode 100644 src/common/fetch/fetch.global-override-for-injectable.ts create mode 100644 src/common/fetch/fetch.injectable.ts create mode 100644 src/common/fs/access-path.global-override-for-injectable.ts create mode 100644 src/common/fs/access-path.injectable.ts create mode 100644 src/common/fs/copy.global-override-for-injectable.ts create mode 100644 src/common/fs/copy.injectable.ts create mode 100644 src/common/fs/delete-file.global-override-for-injectable.ts create mode 100644 src/common/fs/delete-file.injectable.ts create mode 100644 src/common/fs/ensure-dir.global-override-for-injectable.ts create mode 100644 src/common/fs/extract-tar.global-override-for-injectable.ts create mode 100644 src/common/fs/extract-tar.injectable.ts create mode 100644 src/common/fs/lstat.global-override-for-injectable.ts create mode 100644 src/common/fs/lstat.injectable.ts create mode 100644 src/common/fs/move.global-override-for-injectable.ts create mode 100644 src/common/fs/move.injectable.ts create mode 100644 src/common/fs/path-exists.global-override-for-injectable.ts create mode 100644 src/common/fs/read-directory.global-override-for-injectable.ts create mode 100644 src/common/fs/read-directory.injectable.ts create mode 100644 src/common/fs/remove-path.global-override-for-injectable.ts rename src/common/fs/{read-dir.injectable.ts => remove-path.injectable.ts} (52%) create mode 100644 src/common/fs/write-file.global-override-for-injectable.ts create mode 100644 src/common/k8s-api/create-json-api.injectable.ts create mode 100644 src/common/k8s-api/create-kube-api-for-cluster.injectable.ts create mode 100644 src/common/k8s-api/create-kube-api-for-remote-cluster.injectable.ts create mode 100644 src/common/k8s-api/create-kube-json-api-for-cluster.injectable.ts create mode 100644 src/common/k8s-api/create-kube-json-api.injectable.ts create mode 100644 src/common/k8s-api/endpoints/helm-charts.api/request-charts.injectable.ts create mode 100644 src/common/k8s-api/endpoints/helm-charts.api/request-readme.injectable.ts create mode 100644 src/common/k8s-api/endpoints/helm-charts.api/request-values.injectable.ts create mode 100644 src/common/k8s-api/endpoints/helm-charts.api/request-versions.injectable.ts create mode 100644 src/common/k8s-api/endpoints/helm-releases.api/request-configuration.global-override-for-injectable.ts create mode 100644 src/common/k8s-api/endpoints/helm-releases.api/request-configuration.injectable.ts create mode 100644 src/common/k8s-api/endpoints/helm-releases.api/request-create.injectable.ts create mode 100644 src/common/k8s-api/endpoints/helm-releases.api/request-delete.injectable.ts create mode 100644 src/common/k8s-api/endpoints/helm-releases.api/request-details.injectable.ts create mode 100644 src/common/k8s-api/endpoints/helm-releases.api/request-history.injectable.ts create mode 100644 src/common/k8s-api/endpoints/helm-releases.api/request-releases.injectable.ts create mode 100644 src/common/k8s-api/endpoints/helm-releases.api/request-rollback.injectable.ts create mode 100644 src/common/k8s-api/endpoints/helm-releases.api/request-update.injectable.ts create mode 100644 src/common/k8s-api/endpoints/helm-releases.api/request-values.injectable.ts create mode 100644 src/common/k8s-api/endpoints/metrics.api/request-cluster-metrics-by-node-names.injectable.ts create mode 100644 src/common/k8s-api/endpoints/metrics.api/request-ingress-metrics.injectable.ts create mode 100644 src/common/k8s-api/endpoints/metrics.api/request-metrics-for-all-nodes.injectable.ts create mode 100644 src/common/k8s-api/endpoints/metrics.api/request-metrics.injectable.ts create mode 100644 src/common/k8s-api/endpoints/metrics.api/request-persistent-volume-claim-metrics.injectable.ts create mode 100644 src/common/k8s-api/endpoints/metrics.api/request-pod-metrics-for-daemon-sets.injectable.ts create mode 100644 src/common/k8s-api/endpoints/metrics.api/request-pod-metrics-for-deployments.injectable.ts create mode 100644 src/common/k8s-api/endpoints/metrics.api/request-pod-metrics-for-jobs.injectable.ts create mode 100644 src/common/k8s-api/endpoints/metrics.api/request-pod-metrics-for-replica-sets.injectable.ts create mode 100644 src/common/k8s-api/endpoints/metrics.api/request-pod-metrics-for-stateful-sets.injectable.ts create mode 100644 src/common/k8s-api/endpoints/metrics.api/request-pod-metrics-in-namespace.injectable.ts create mode 100644 src/common/k8s-api/endpoints/metrics.api/request-pod-metrics.injectable.ts create mode 100644 src/common/k8s-api/endpoints/metrics.api/request-providers.injectable.ts delete mode 100644 src/common/k8s-api/endpoints/resource-applier.api.ts create mode 100644 src/common/k8s-api/endpoints/resource-applier.api/request-patch.injectable.ts create mode 100644 src/common/k8s-api/endpoints/resource-applier.api/request-update.injectable.ts delete mode 100644 src/common/k8s-api/index.ts create mode 100644 src/common/k8s-api/window-location.global-override-for-injectable.ts create mode 100644 src/common/k8s-api/window-location.injectable.ts create mode 100644 src/common/kube-helpers/load-config-from-file.injectable.ts create mode 100644 src/common/os/home-directory-path.global-override-for-injectable.ts create mode 100644 src/common/os/home-directory-path.injectable.ts create mode 100644 src/common/os/temp-directory-path.global-override-for-injectable.ts create mode 100644 src/common/os/temp-directory-path.injectable.ts create mode 100644 src/common/path/get-absolute-path.global-override-for-injectable.ts create mode 100644 src/common/path/get-basename.global-override-for-injectable.ts create mode 100644 src/common/path/get-basename.injectable.ts create mode 100644 src/common/path/get-dirname.global-override-for-injectable.ts create mode 100644 src/common/path/get-dirname.injectable.ts create mode 100644 src/common/path/get-relative-path.global-override-for-injectable.ts create mode 100644 src/common/path/get-relative-path.injectable.ts create mode 100644 src/common/path/is-logical-child-path.injectable.ts create mode 100644 src/common/path/join-paths.global-override-for-injectable.ts create mode 100644 src/common/path/parse.global-override-for-injectable.ts create mode 100644 src/common/path/parse.injectable.ts create mode 100644 src/common/path/resolve-path.injectable.ts create mode 100644 src/common/path/resolve-tilde.injectable.ts create mode 100644 src/common/path/separator.global-override-for-injectable.ts create mode 100644 src/common/path/separator.injectable.ts delete mode 100644 src/common/test-utils/get-absolute-path-fake.ts delete mode 100644 src/common/test-utils/join-paths-fake.ts delete mode 100644 src/common/utils/paths.ts create mode 100644 src/common/vars/is-debugging.global-override-for-injectable.ts create mode 100644 src/common/vars/is-debugging.injectable.ts create mode 100644 src/features/cluster/delete-dialog/__snapshots__/delete-cluster-dialog.test.tsx.snap create mode 100644 src/features/cluster/delete-dialog/common/clear-as-deleting-channel.injectable.ts create mode 100644 src/features/cluster/delete-dialog/common/delete-channel.injectable.ts create mode 100644 src/features/cluster/delete-dialog/common/set-as-deleting-channel.injectable.ts create mode 100644 src/features/cluster/delete-dialog/delete-cluster-dialog.test.tsx create mode 100644 src/features/cluster/delete-dialog/main/clear-as-deleteing-channel-handler.injectable.ts create mode 100644 src/features/cluster/delete-dialog/main/delete-channel-handler.injectable.ts create mode 100644 src/features/cluster/delete-dialog/main/set-as-deleteing-channel-handler.injectable.ts create mode 100644 src/features/cluster/delete-dialog/renderer/request-clear-as-deleting.injectable.ts create mode 100644 src/features/cluster/delete-dialog/renderer/request-delete.injectable.ts create mode 100644 src/features/cluster/delete-dialog/renderer/request-set-as-deleting.injectable.ts create mode 100644 src/features/helm-charts/upgrade-chart/__snapshots__/upgrade-chart-new-tab.test.ts.snap create mode 100644 src/features/helm-charts/upgrade-chart/upgrade-chart-new-tab.test.ts create mode 100644 src/main/helm/helm-service/get-helm-chart-readme.global-override-for-injectable.ts rename src/main/helm/helm-service/{get-helm-chart.injectable.ts => get-helm-chart-readme.injectable.ts} (73%) create mode 100644 src/main/helm/helm-service/get-helm-chart-versions.injectable.ts create mode 100644 src/main/k8s/api-base.injectable.ts rename src/main/routes/helm/charts/{get-chart-route.injectable.ts => get-readme-route.injectable.ts} (58%) rename src/main/routes/helm/charts/{get-chart-values-route.injectable.ts => get-values-route.injectable.ts} (84%) create mode 100644 src/main/routes/helm/charts/get-versions-route.injectable.ts rename src/main/routes/helm/charts/{list-charts-route.injectable.ts => list-route.injectable.ts} (82%) rename src/main/routes/resource-applier/{apply-resource-route.injectable.ts => create-resource-route.injectable.ts} (53%) delete mode 100644 src/main/shell-session/local-shell-session/local-shell-session.injectable.ts create mode 100644 src/main/shell-session/local-shell-session/open.injectable.ts delete mode 100644 src/main/shell-session/node-shell-session/node-shell-session.injectable.ts create mode 100644 src/main/shell-session/node-shell-session/open.injectable.ts create mode 100644 src/main/shell-session/shell-env-modifier/modify-terminal-shell-env.injectable.ts delete mode 100644 src/main/shell-session/shell-env-modifier/terminal-shell-env-modifiers.ts delete mode 100644 src/main/shell-session/shell-env-modifier/terminal-shell-env-modify.injectable.ts delete mode 100644 src/renderer/api/index.ts delete mode 100644 src/renderer/api/on-api-error.ts delete mode 100644 src/renderer/api/setup-on-api-errors.injectable.ts delete mode 100644 src/renderer/components/+extensions/attempt-install/create-temp-files-and-validate/create-temp-files-and-validate.tsx delete mode 100644 src/renderer/components/+extensions/attempt-install/get-extension-dest-folder/get-extension-dest-folder.ts delete mode 100644 src/renderer/components/+extensions/attempt-install/unpack-extension/unpack-extension.tsx delete mode 100644 src/renderer/components/+extensions/attempt-installs/attempt-installs.ts create mode 100644 src/renderer/components/+extensions/install-extension-from-input.injectable.tsx delete mode 100644 src/renderer/components/+extensions/install-from-input/install-from-input.injectable.ts delete mode 100644 src/renderer/components/+extensions/install-from-input/install-from-input.tsx create mode 100644 src/renderer/components/+extensions/read-file-notify/read-file-notify.injectable.ts delete mode 100644 src/renderer/components/+extensions/read-file-notify/read-file-notify.ts delete mode 100644 src/renderer/components/+helm-charts/details/readme/call-for-helm-chart-readme.injectable.ts delete mode 100644 src/renderer/components/+helm-charts/details/versions/call-for-helm-chart-versions.injectable.ts delete mode 100644 src/renderer/components/+helm-charts/get-char-details.injectable.ts delete mode 100644 src/renderer/components/+helm-charts/helm-chart-store.injectable.ts delete mode 100644 src/renderer/components/+helm-charts/helm-chart.store.ts delete mode 100644 src/renderer/components/+helm-charts/helm-charts/call-for-helm-charts.injectable.ts create mode 100644 src/renderer/components/+helm-charts/helm-charts/request-versions-of-chart-for-release.injectable.ts create mode 100644 src/renderer/components/+helm-charts/helm-charts/request-versions.injectable.ts create mode 100644 src/renderer/components/+helm-charts/helm-charts/versions.injectable.ts create mode 100644 src/renderer/components/+helm-charts/helm-charts/versions.ts delete mode 100644 src/renderer/components/+helm-releases/call-for-helm-releases/call-for-helm-releases.global-override-for-injectable.ts delete mode 100644 src/renderer/components/+helm-releases/call-for-helm-releases/call-for-helm-releases.injectable.ts delete mode 100644 src/renderer/components/+helm-releases/create-release/call-for-create-helm-release.injectable.ts delete mode 100644 src/renderer/components/+helm-releases/release-details/release-details-model/call-for-helm-release-configuration/call-for-helm-release-configuration.global-override-for-injectable.ts delete mode 100644 src/renderer/components/+helm-releases/release-details/release-details-model/call-for-helm-release-configuration/call-for-helm-release-configuration.injectable.ts delete mode 100644 src/renderer/components/+helm-releases/release-details/release-details-model/call-for-helm-release/call-for-helm-release-details/call-for-helm-release-details.global-override-for-injectable.ts delete mode 100644 src/renderer/components/+helm-releases/release-details/release-details-model/call-for-helm-release/call-for-helm-release-details/call-for-helm-release-details.injectable.ts delete mode 100644 src/renderer/components/+helm-releases/release-details/release-details-model/call-for-helm-release/call-for-helm-release.injectable.ts create mode 100644 src/renderer/components/+helm-releases/release-details/release-details-model/request-detailed-helm-release.injectable.ts create mode 100644 src/renderer/components/+helm-releases/to-helm-release.injectable.ts delete mode 100644 src/renderer/components/+helm-releases/update-release/call-for-helm-release-update/call-for-helm-release-update.global-override-for-injectable.ts delete mode 100644 src/renderer/components/+helm-releases/update-release/call-for-helm-release-update/call-for-helm-release-update.injectable.ts create mode 100644 src/renderer/components/animate/default-enter-duration.injectable.ts create mode 100644 src/renderer/components/animate/default-leave-duration.injectable.ts delete mode 100644 src/renderer/components/delete-cluster-dialog/__tests__/delete-cluster-dialog.test.tsx delete mode 100644 src/renderer/components/delete-cluster-dialog/save-config.ts create mode 100644 src/renderer/components/delete-cluster-dialog/save-kubeconfig.global-override-for-injectable.ts create mode 100644 src/renderer/components/delete-cluster-dialog/save-kubeconfig.injectable.ts delete mode 100644 src/renderer/components/dock/install-chart/chart-data/call-for-helm-chart-values.injectable.ts delete mode 100644 src/renderer/components/dock/upgrade-chart/store.ts create mode 100644 src/renderer/components/dock/upgrade-chart/tab-data.injectable.ts create mode 100644 src/renderer/components/dock/upgrade-chart/upgrade-chart-model.injectable.ts create mode 100644 src/renderer/components/kubeconfig-dialog/open-service-account-kube-config-dialog.injectable.ts create mode 100644 src/renderer/components/kubeconfig-dialog/open.injectable.ts create mode 100644 src/renderer/components/kubeconfig-dialog/state.injectable.ts create mode 100644 src/renderer/components/render-delay/__tests__/__snapshots__/render-delay.test.tsx.snap create mode 100644 src/renderer/components/render-delay/cancel-idle-callback.global-override-for-injectable.ts create mode 100644 src/renderer/components/render-delay/cancel-idle-callback.injectable.ts create mode 100644 src/renderer/components/render-delay/idle-callback-timeout.injectable.ts create mode 100644 src/renderer/components/render-delay/request-idle-callback.global-override-for-injectable.ts create mode 100644 src/renderer/components/render-delay/request-idle-callback.injectable.ts create mode 100644 src/renderer/k8s/api-base.injectable.ts create mode 100644 src/renderer/port-forward/port-forward-store/request-active-port-forward.injectable.ts diff --git a/__mocks__/windowMock.ts b/__mocks__/windowMock.ts deleted file mode 100644 index bcc3da05a6..0000000000 --- a/__mocks__/windowMock.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -/** - * Mock the global window variable - */ -export function mockWindow() { - Object.defineProperty(window, "requestIdleCallback", { - writable: true, - value: jest.fn().mockImplementation(callback => callback()), - }); - - Object.defineProperty(window, "cancelIdleCallback", { - writable: true, - value: jest.fn(), - }); -} diff --git a/package.json b/package.json index 49fa584d7c..26ba378293 100644 --- a/package.json +++ b/package.json @@ -351,7 +351,7 @@ "@types/semver": "^7.3.12", "@types/sharp": "^0.31.0", "@types/spdy": "^3.4.5", - "@types/tar": "^4.0.5", + "@types/tar": "^6.1.2", "@types/tar-stream": "^2.2.2", "@types/tcp-port-used": "^1.0.1", "@types/tempy": "^0.3.0", @@ -396,7 +396,6 @@ "jest": "^28.1.3", "jest-canvas-mock": "^2.3.1", "jest-environment-jsdom": "^28.1.3", - "jest-fetch-mock": "^3.0.3", "jest-mock-extended": "^2.0.9", "make-plural": "^6.2.2", "mini-css-extract-plugin": "^2.6.1", diff --git a/src/common/app-paths/directory-for-binaries/directory-for-binaries.injectable.ts b/src/common/app-paths/directory-for-binaries/directory-for-binaries.injectable.ts index 5f6b14f5ba..f8a55b041c 100644 --- a/src/common/app-paths/directory-for-binaries/directory-for-binaries.injectable.ts +++ b/src/common/app-paths/directory-for-binaries/directory-for-binaries.injectable.ts @@ -4,16 +4,16 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import directoryForUserDataInjectable from "../directory-for-user-data/directory-for-user-data.injectable"; -import getAbsolutePathInjectable from "../../path/get-absolute-path.injectable"; +import joinPathsInjectable from "../../path/join-paths.injectable"; const directoryForBinariesInjectable = getInjectable({ id: "directory-for-binaries", instantiate: (di) => { - const getAbsolutePath = di.inject(getAbsolutePathInjectable); + const joinPaths = di.inject(joinPathsInjectable); const directoryForUserData = di.inject(directoryForUserDataInjectable); - return getAbsolutePath(directoryForUserData, "binaries"); + return joinPaths(directoryForUserData, "binaries"); }, }); diff --git a/src/common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable.ts b/src/common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable.ts index f371860f18..d49029572e 100644 --- a/src/common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable.ts +++ b/src/common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable.ts @@ -4,19 +4,16 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import directoryForUserDataInjectable from "../directory-for-user-data/directory-for-user-data.injectable"; -import getAbsolutePathInjectable from "../../path/get-absolute-path.injectable"; +import joinPathsInjectable from "../../path/join-paths.injectable"; const directoryForKubeConfigsInjectable = getInjectable({ id: "directory-for-kube-configs", instantiate: (di) => { - const getAbsolutePath = di.inject(getAbsolutePathInjectable); + const joinPaths = di.inject(joinPathsInjectable); const directoryForUserData = di.inject(directoryForUserDataInjectable); - return getAbsolutePath( - directoryForUserData, - "kubeconfigs", - ); + return joinPaths(directoryForUserData, "kubeconfigs"); }, }); diff --git a/src/common/app-paths/directory-for-kubectl-binaries/directory-for-kubectl-binaries.injectable.ts b/src/common/app-paths/directory-for-kubectl-binaries/directory-for-kubectl-binaries.injectable.ts index 4b6ce56601..c1ef1b23f5 100644 --- a/src/common/app-paths/directory-for-kubectl-binaries/directory-for-kubectl-binaries.injectable.ts +++ b/src/common/app-paths/directory-for-kubectl-binaries/directory-for-kubectl-binaries.injectable.ts @@ -4,17 +4,16 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import directoryForBinariesInjectable from "../directory-for-binaries/directory-for-binaries.injectable"; -import getAbsolutePathInjectable from "../../path/get-absolute-path.injectable"; +import joinPathsInjectable from "../../path/join-paths.injectable"; const directoryForKubectlBinariesInjectable = getInjectable({ id: "directory-for-kubectl-binaries", instantiate: (di) => { - const getAbsolutePath = di.inject(getAbsolutePathInjectable); + const joinPaths = di.inject(joinPathsInjectable); const directoryForBinaries = di.inject(directoryForBinariesInjectable); - - return getAbsolutePath(directoryForBinaries, "kubectl"); + return joinPaths(directoryForBinaries, "kubectl"); }, }); diff --git a/src/common/app-paths/get-custom-kube-config-directory/get-custom-kube-config-directory.injectable.ts b/src/common/app-paths/get-custom-kube-config-directory/get-custom-kube-config-directory.injectable.ts index 6f22aa32f2..5d53e506bd 100644 --- a/src/common/app-paths/get-custom-kube-config-directory/get-custom-kube-config-directory.injectable.ts +++ b/src/common/app-paths/get-custom-kube-config-directory/get-custom-kube-config-directory.injectable.ts @@ -4,17 +4,16 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import directoryForKubeConfigsInjectable from "../directory-for-kube-configs/directory-for-kube-configs.injectable"; -import getAbsolutePathInjectable from "../../path/get-absolute-path.injectable"; +import joinPathsInjectable from "../../path/join-paths.injectable"; const getCustomKubeConfigDirectoryInjectable = getInjectable({ id: "get-custom-kube-config-directory", instantiate: (di) => { const directoryForKubeConfigs = di.inject(directoryForKubeConfigsInjectable); - const getAbsolutePath = di.inject(getAbsolutePathInjectable); + const joinPaths = di.inject(joinPathsInjectable); - return (directoryName: string) => - getAbsolutePath(directoryForKubeConfigs, directoryName); + return (directoryName: string) => joinPaths(directoryForKubeConfigs, directoryName); }, }); diff --git a/src/common/catalog/filtered-categories.injectable.ts b/src/common/catalog/filtered-categories.injectable.ts new file mode 100644 index 0000000000..c84d527ff2 --- /dev/null +++ b/src/common/catalog/filtered-categories.injectable.ts @@ -0,0 +1,18 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import catalogCategoryRegistryInjectable from "./category-registry.injectable"; + +const filteredCategoriesInjectable = getInjectable({ + id: "filtered-categories", + instantiate: (di) => { + const registry = di.inject(catalogCategoryRegistryInjectable); + + return computed(() => [...registry.filteredItems]); + }, +}); + +export default filteredCategoriesInjectable; diff --git a/src/common/cluster/cluster.ts b/src/common/cluster/cluster.ts index 9b0eed76aa..b0d7fbfbc3 100644 --- a/src/common/cluster/cluster.ts +++ b/src/common/cluster/cluster.ts @@ -9,7 +9,6 @@ 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 { loadConfigFromFile } from "../kube-helpers"; import type { KubeApiResource, KubeResource } from "../rbac"; import { apiResourceRecord, apiResources } from "../rbac"; import type { VersionDetector } from "../../main/cluster-detectors/version-detector"; @@ -25,6 +24,7 @@ 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"; export interface ClusterDependencies { readonly directoryForKubeConfigs: string; @@ -37,6 +37,7 @@ export interface ClusterDependencies { createListNamespaces: (config: KubeConfig) => ListNamespaces; createVersionDetector: (cluster: Cluster) => VersionDetector; broadcastMessage: BroadcastMessage; + loadConfigfromFile: LoadConfigfromFile; } /** @@ -500,7 +501,7 @@ export class Cluster implements ClusterModel, ClusterState { } async getKubeconfig(): Promise { - const { config } = await loadConfigFromFile(this.kubeConfigPath); + const { config } = await this.dependencies.loadConfigfromFile(this.kubeConfigPath); return config; } @@ -510,7 +511,7 @@ export class Cluster implements ClusterModel, ClusterState { */ async getProxyKubeconfig(): Promise { const proxyKCPath = await this.getProxyKubeconfigPath(); - const { config } = await loadConfigFromFile(proxyKCPath); + const { config } = await this.dependencies.loadConfigfromFile(proxyKCPath); return config; } diff --git a/src/common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable.ts b/src/common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable.ts index 4bb5cb2495..11a8f3f7de 100644 --- a/src/common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable.ts +++ b/src/common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable.ts @@ -4,16 +4,16 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable"; -import getAbsolutePathInjectable from "../path/get-absolute-path.injectable"; +import joinPathsInjectable from "../path/join-paths.injectable"; const directoryForLensLocalStorageInjectable = getInjectable({ id: "directory-for-lens-local-storage", instantiate: (di) => { - const getAbsolutePath = di.inject(getAbsolutePathInjectable); + const joinPaths = di.inject(joinPathsInjectable); const directoryForUserData = di.inject(directoryForUserDataInjectable); - return getAbsolutePath( + return joinPaths( directoryForUserData, "lens-local-storage", ); diff --git a/src/common/fetch/fetch.global-override-for-injectable.ts b/src/common/fetch/fetch.global-override-for-injectable.ts new file mode 100644 index 0000000000..cd6160641c --- /dev/null +++ b/src/common/fetch/fetch.global-override-for-injectable.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getGlobalOverride } from "../test-utils/get-global-override"; +import fetchInjectable from "./fetch.injectable"; + +export default getGlobalOverride(fetchInjectable, () => () => { + throw new Error("tried to fetch a resource without override in test"); +}); diff --git a/src/common/fetch/fetch.injectable.ts b/src/common/fetch/fetch.injectable.ts new file mode 100644 index 0000000000..c6d2a7e1af --- /dev/null +++ b/src/common/fetch/fetch.injectable.ts @@ -0,0 +1,17 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import type { RequestInfo, RequestInit, Response } from "node-fetch"; +import fetch from "node-fetch"; + +export type Fetch = (url: RequestInfo, init?: RequestInit) => Promise; + +const fetchInjectable = getInjectable({ + id: "fetch", + instantiate: (): Fetch => fetch, + causesSideEffects: true, +}); + +export default fetchInjectable; diff --git a/src/common/fs/access-path.global-override-for-injectable.ts b/src/common/fs/access-path.global-override-for-injectable.ts new file mode 100644 index 0000000000..747d839682 --- /dev/null +++ b/src/common/fs/access-path.global-override-for-injectable.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getGlobalOverride } from "../test-utils/get-global-override"; +import accessPathInjectable from "./access-path.injectable"; + +export default getGlobalOverride(accessPathInjectable, () => async () => { + throw new Error("tried to verify path access without override"); +}); diff --git a/src/common/fs/access-path.injectable.ts b/src/common/fs/access-path.injectable.ts new file mode 100644 index 0000000000..0504de9d6f --- /dev/null +++ b/src/common/fs/access-path.injectable.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import fsInjectable from "./fs.injectable"; + +export type AccessPath = (path: string, mode?: number) => Promise; + +const accessPathInjectable = getInjectable({ + id: "access-path", + instantiate: (di): AccessPath => { + const { access } = di.inject(fsInjectable); + + return async (path, mode) => { + try { + await access(path, mode); + + return true; + } catch { + return false; + } + }; + }, +}); + +export default accessPathInjectable; diff --git a/src/common/fs/copy.global-override-for-injectable.ts b/src/common/fs/copy.global-override-for-injectable.ts new file mode 100644 index 0000000000..b6d899d2c4 --- /dev/null +++ b/src/common/fs/copy.global-override-for-injectable.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getGlobalOverride } from "../test-utils/get-global-override"; +import copyInjectable from "./copy.injectable"; + +export default getGlobalOverride(copyInjectable, () => async () => { + throw new Error("tried to copy filepaths without override"); +}); diff --git a/src/common/fs/copy.injectable.ts b/src/common/fs/copy.injectable.ts new file mode 100644 index 0000000000..6a64ee3751 --- /dev/null +++ b/src/common/fs/copy.injectable.ts @@ -0,0 +1,16 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import type { CopyOptions } from "fs-extra"; +import fsInjectable from "./fs.injectable"; + +export type Copy = (src: string, dest: string, options?: CopyOptions | undefined) => Promise; + +const copyInjectable = getInjectable({ + id: "copy", + instantiate: (di): Copy => di.inject(fsInjectable).copy, +}); + +export default copyInjectable; diff --git a/src/common/fs/delete-file.global-override-for-injectable.ts b/src/common/fs/delete-file.global-override-for-injectable.ts new file mode 100644 index 0000000000..c03dca88dc --- /dev/null +++ b/src/common/fs/delete-file.global-override-for-injectable.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getGlobalOverride } from "../test-utils/get-global-override"; +import deleteFileInjectable from "./delete-file.injectable"; + +export default getGlobalOverride(deleteFileInjectable, () => async () => { + throw new Error("tried to delete file without override"); +}); diff --git a/src/common/fs/delete-file.injectable.ts b/src/common/fs/delete-file.injectable.ts new file mode 100644 index 0000000000..57aba5b379 --- /dev/null +++ b/src/common/fs/delete-file.injectable.ts @@ -0,0 +1,15 @@ +/** + * 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 fsInjectable from "./fs.injectable"; + +export type DeleteFile = (filePath: string) => Promise; + +const deleteFileInjectable = getInjectable({ + id: "delete-file", + instantiate: (di): DeleteFile => di.inject(fsInjectable).unlink, +}); + +export default deleteFileInjectable; diff --git a/src/common/fs/ensure-dir.global-override-for-injectable.ts b/src/common/fs/ensure-dir.global-override-for-injectable.ts new file mode 100644 index 0000000000..4dd098b163 --- /dev/null +++ b/src/common/fs/ensure-dir.global-override-for-injectable.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getGlobalOverride } from "../test-utils/get-global-override"; +import ensureDirInjectable from "./ensure-dir.injectable"; + +export default getGlobalOverride(ensureDirInjectable, () => async () => { + throw new Error("tried to ensure directory without override"); +}); diff --git a/src/common/fs/ensure-dir.injectable.ts b/src/common/fs/ensure-dir.injectable.ts index 88410ceee2..78ec4d91dc 100644 --- a/src/common/fs/ensure-dir.injectable.ts +++ b/src/common/fs/ensure-dir.injectable.ts @@ -5,14 +5,14 @@ import { getInjectable } from "@ogre-tools/injectable"; import fsInjectable from "./fs.injectable"; +export type EnsureDirectory = (dirPath: string) => Promise; + const ensureDirInjectable = getInjectable({ id: "ensure-dir", // TODO: Remove usages of ensureDir from business logic. // TODO: Read, Write, Watch etc. operations should do this internally. - instantiate: (di) => di.inject(fsInjectable).ensureDir, - - causesSideEffects: true, + instantiate: (di): EnsureDirectory => di.inject(fsInjectable).ensureDir, }); export default ensureDirInjectable; diff --git a/src/common/fs/extract-tar.global-override-for-injectable.ts b/src/common/fs/extract-tar.global-override-for-injectable.ts new file mode 100644 index 0000000000..02a46c1d6b --- /dev/null +++ b/src/common/fs/extract-tar.global-override-for-injectable.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getGlobalOverride } from "../test-utils/get-global-override"; +import extractTarInjectable from "./extract-tar.injectable"; + +export default getGlobalOverride(extractTarInjectable, () => async () => { + throw new Error("tried to extract a tar file without override"); +}); diff --git a/src/common/fs/extract-tar.injectable.ts b/src/common/fs/extract-tar.injectable.ts new file mode 100644 index 0000000000..410512139e --- /dev/null +++ b/src/common/fs/extract-tar.injectable.ts @@ -0,0 +1,26 @@ +/** + * 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 { ExtractOptions } from "tar"; +import { extract } from "tar"; +import getDirnameOfPathInjectable from "../path/get-dirname.injectable"; + +export type ExtractTar = (filePath: string, opts?: ExtractOptions) => Promise; + +const extractTarInjectable = getInjectable({ + id: "extract-tar", + instantiate: (di): ExtractTar => { + const getDirnameOfPath = di.inject(getDirnameOfPathInjectable); + + return (filePath, opts = {}) => extract({ + file: filePath, + cwd: getDirnameOfPath(filePath), + ...opts, + }); + }, + causesSideEffects: true, +}); + +export default extractTarInjectable; diff --git a/src/common/fs/lstat.global-override-for-injectable.ts b/src/common/fs/lstat.global-override-for-injectable.ts new file mode 100644 index 0000000000..9c9f3d4933 --- /dev/null +++ b/src/common/fs/lstat.global-override-for-injectable.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getGlobalOverride } from "../test-utils/get-global-override"; +import lstatInjectable from "./lstat.injectable"; + +export default getGlobalOverride(lstatInjectable, () => async () => { + throw new Error("tried to lstat a filepath without override"); +}); diff --git a/src/common/fs/lstat.injectable.ts b/src/common/fs/lstat.injectable.ts new file mode 100644 index 0000000000..50c1d4ad12 --- /dev/null +++ b/src/common/fs/lstat.injectable.ts @@ -0,0 +1,16 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import type { Stats } from "fs"; +import fsInjectable from "./fs.injectable"; + +export type LStat = (path: string) => Promise; + +const lstatInjectable = getInjectable({ + id: "lstat", + instantiate: (di): LStat => di.inject(fsInjectable).lstat, +}); + +export default lstatInjectable; diff --git a/src/common/fs/move.global-override-for-injectable.ts b/src/common/fs/move.global-override-for-injectable.ts new file mode 100644 index 0000000000..c39907ee6e --- /dev/null +++ b/src/common/fs/move.global-override-for-injectable.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getGlobalOverride } from "../test-utils/get-global-override"; +import moveInjectable from "./move.injectable"; + +export default getGlobalOverride(moveInjectable, () => async () => { + throw new Error("tried to move without override"); +}); diff --git a/src/common/fs/move.injectable.ts b/src/common/fs/move.injectable.ts new file mode 100644 index 0000000000..ff11120d80 --- /dev/null +++ b/src/common/fs/move.injectable.ts @@ -0,0 +1,16 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import type { MoveOptions } from "fs-extra"; +import fsInjectable from "./fs.injectable"; + +export type Move = (src: string, dest: string, options?: MoveOptions) => Promise; + +const moveInjectable = getInjectable({ + id: "move", + instantiate: (di): Move => di.inject(fsInjectable).move, +}); + +export default moveInjectable; diff --git a/src/common/fs/path-exists.global-override-for-injectable.ts b/src/common/fs/path-exists.global-override-for-injectable.ts new file mode 100644 index 0000000000..1b9b96c8dd --- /dev/null +++ b/src/common/fs/path-exists.global-override-for-injectable.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getGlobalOverride } from "../test-utils/get-global-override"; +import pathExistsInjectable from "./path-exists.injectable"; + +export default getGlobalOverride(pathExistsInjectable, () => async () => { + throw new Error("Tried to check if a path exists without override"); +}); diff --git a/src/common/fs/read-directory.global-override-for-injectable.ts b/src/common/fs/read-directory.global-override-for-injectable.ts new file mode 100644 index 0000000000..57c83ceffb --- /dev/null +++ b/src/common/fs/read-directory.global-override-for-injectable.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getGlobalOverride } from "../test-utils/get-global-override"; +import readDirectoryInjectable from "./read-directory.injectable"; + +export default getGlobalOverride(readDirectoryInjectable, () => async () => { + throw new Error("tried to read a directory's content without override"); +}); diff --git a/src/common/fs/read-directory.injectable.ts b/src/common/fs/read-directory.injectable.ts new file mode 100644 index 0000000000..57632bd4d7 --- /dev/null +++ b/src/common/fs/read-directory.injectable.ts @@ -0,0 +1,37 @@ +/** + * 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 { Dirent } from "fs"; +import fsInjectable from "./fs.injectable"; + +export interface ReadDirectory { + ( + path: string, + options: "buffer" | { encoding: "buffer"; withFileTypes?: false | undefined } + ): Promise; + ( + path: string, + options?: + | { encoding: BufferEncoding | string | null; withFileTypes?: false | undefined } + | BufferEncoding + | string + | null, + ): Promise; + ( + path: string, + options?: { encoding?: BufferEncoding | string | null | undefined; withFileTypes?: false | undefined }, + ): Promise; + ( + path: string, + options: { encoding?: BufferEncoding | string | null | undefined; withFileTypes: true }, + ): Promise; +} + +const readDirectoryInjectable = getInjectable({ + id: "read-directory", + instantiate: (di): ReadDirectory => di.inject(fsInjectable).readdir, +}); + +export default readDirectoryInjectable; diff --git a/src/common/fs/read-file.injectable.ts b/src/common/fs/read-file.injectable.ts index b0a2b7233e..0dc539e1b1 100644 --- a/src/common/fs/read-file.injectable.ts +++ b/src/common/fs/read-file.injectable.ts @@ -5,11 +5,16 @@ import { getInjectable } from "@ogre-tools/injectable"; import fsInjectable from "./fs.injectable"; +export type ReadFile = (filePath: string) => Promise; + const readFileInjectable = getInjectable({ id: "read-file", - instantiate: (di) => (filePath: string) => - di.inject(fsInjectable).readFile(filePath, "utf-8"), + instantiate: (di): ReadFile => { + const { readFile } = di.inject(fsInjectable); + + return (filePath) => readFile(filePath, "utf-8"); + }, }); export default readFileInjectable; diff --git a/src/common/fs/remove-path.global-override-for-injectable.ts b/src/common/fs/remove-path.global-override-for-injectable.ts new file mode 100644 index 0000000000..5b9720a837 --- /dev/null +++ b/src/common/fs/remove-path.global-override-for-injectable.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getGlobalOverride } from "../test-utils/get-global-override"; +import removePathInjectable from "./remove-path.injectable"; + +export default getGlobalOverride(removePathInjectable, () => async () => { + throw new Error("tried to remove a path without override"); +}); diff --git a/src/common/fs/read-dir.injectable.ts b/src/common/fs/remove-path.injectable.ts similarity index 52% rename from src/common/fs/read-dir.injectable.ts rename to src/common/fs/remove-path.injectable.ts index 2c7b59d9b2..02c8da0e1e 100644 --- a/src/common/fs/read-dir.injectable.ts +++ b/src/common/fs/remove-path.injectable.ts @@ -5,9 +5,11 @@ import { getInjectable } from "@ogre-tools/injectable"; import fsInjectable from "./fs.injectable"; -const readDirInjectable = getInjectable({ - id: "read-dir", - instantiate: (di) => di.inject(fsInjectable).readdir, +export type RemovePath = (path: string) => Promise; + +const removePathInjectable = getInjectable({ + id: "remove-path", + instantiate: (di): RemovePath => di.inject(fsInjectable).remove, }); -export default readDirInjectable; +export default removePathInjectable; diff --git a/src/common/fs/write-file.global-override-for-injectable.ts b/src/common/fs/write-file.global-override-for-injectable.ts new file mode 100644 index 0000000000..c8b7ef8e45 --- /dev/null +++ b/src/common/fs/write-file.global-override-for-injectable.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getGlobalOverride } from "../test-utils/get-global-override"; +import writeFileInjectable from "./write-file.injectable"; + +export default getGlobalOverride(writeFileInjectable, () => async () => { + throw new Error("tried to write file without override"); +}); diff --git a/src/common/fs/write-file.injectable.ts b/src/common/fs/write-file.injectable.ts index 70dcb76373..faa5285ca1 100644 --- a/src/common/fs/write-file.injectable.ts +++ b/src/common/fs/write-file.injectable.ts @@ -3,20 +3,28 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import path from "path"; +import type { WriteFileOptions } from "fs-extra"; +import getDirnameOfPathInjectable from "../path/get-dirname.injectable"; import fsInjectable from "./fs.injectable"; +export type WriteFile = (filePath: string, content: string | Buffer, opts?: WriteFileOptions) => Promise; + const writeFileInjectable = getInjectable({ id: "write-file", - instantiate: (di) => { + instantiate: (di): WriteFile => { const { writeFile, ensureDir } = di.inject(fsInjectable); + const getDirnameOfPath = di.inject(getDirnameOfPathInjectable); - return async (filePath: string, content: string | Buffer) => { - await ensureDir(path.dirname(filePath), { mode: 0o755 }); + return async (filePath, content, opts) => { + await ensureDir(getDirnameOfPath(filePath), { + mode: 0o755, + ...(opts ?? {}), + }); await writeFile(filePath, content, { encoding: "utf-8", + ...(opts ?? {}), }); }; }, diff --git a/src/common/fs/write-json-file.injectable.ts b/src/common/fs/write-json-file.injectable.ts index 6d05e01a7c..a7079d7f84 100644 --- a/src/common/fs/write-json-file.injectable.ts +++ b/src/common/fs/write-json-file.injectable.ts @@ -3,37 +3,27 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import type { EnsureOptions, WriteOptions } from "fs-extra"; -import path from "path"; import type { JsonValue } from "type-fest"; +import getDirnameOfPathInjectable from "../path/get-dirname.injectable"; import fsInjectable from "./fs.injectable"; export type WriteJson = (filePath: string, contents: JsonValue) => Promise; -interface Dependencies { - writeJson: (file: string, object: any, options?: WriteOptions | BufferEncoding | string) => Promise; - ensureDir: (dir: string, options?: EnsureOptions | number) => Promise; -} - -const writeJsonFile = ({ writeJson, ensureDir }: Dependencies): WriteJson => async (filePath, content) => { - await ensureDir(path.dirname(filePath), { mode: 0o755 }); - - await writeJson(filePath, content, { - encoding: "utf-8", - spaces: 2, - }); -}; - const writeJsonFileInjectable = getInjectable({ id: "write-json-file", - instantiate: (di) => { + instantiate: (di): WriteJson => { const { writeJson, ensureDir } = di.inject(fsInjectable); + const getDirnameOfPath = di.inject(getDirnameOfPathInjectable); - return writeJsonFile({ - writeJson, - ensureDir, - }); + return async (filePath, content) => { + await ensureDir(getDirnameOfPath(filePath), { mode: 0o755 }); + + await writeJson(filePath, content, { + encoding: "utf-8", + spaces: 2, + }); + }; }, }); diff --git a/src/common/ipc/cluster.ts b/src/common/ipc/cluster.ts index 9f69ff42d5..c5bec1f59f 100644 --- a/src/common/ipc/cluster.ts +++ b/src/common/ipc/cluster.ts @@ -8,9 +8,6 @@ export const clusterSetFrameIdHandler = "cluster:set-frame-id"; export const clusterVisibilityHandler = "cluster:visibility"; export const clusterRefreshHandler = "cluster:refresh"; export const clusterDisconnectHandler = "cluster:disconnect"; -export const clusterDeleteHandler = "cluster:delete"; -export const clusterSetDeletingHandler = "cluster:deleting:set"; -export const clusterClearDeletingHandler = "cluster:deleting:clear"; export const clusterKubectlApplyAllHandler = "cluster:kubectl-apply-all"; export const clusterKubectlDeleteAllHandler = "cluster:kubectl-delete-all"; export const clusterStates = "cluster:states"; diff --git a/src/common/k8s-api/__tests__/kube-api.test.ts b/src/common/k8s-api/__tests__/kube-api.test.ts index 05da1f89d5..d76518be00 100644 --- a/src/common/k8s-api/__tests__/kube-api.test.ts +++ b/src/common/k8s-api/__tests__/kube-api.test.ts @@ -2,37 +2,43 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ - -import { forRemoteCluster, KubeApi } from "../kube-api"; -import { KubeJsonApi } from "../kube-json-api"; -import { KubeObject } from "../kube-object"; -import { delay } from "../../utils/delay"; +import type { KubeApiWatchCallback } from "../kube-api"; +import { KubeApi } from "../kube-api"; +import type { KubeJsonApi, KubeJsonApiData } from "../kube-json-api"; import { PassThrough } from "stream"; -import { ApiManager } from "../api-manager"; -import type { FetchMock } from "jest-fetch-mock/types"; -import { DeploymentApi, Ingress, IngressApi, Pod, PodApi } from "../endpoints"; +import type { ApiManager } from "../api-manager"; +import { Deployment, DeploymentApi, Ingress, IngressApi, NamespaceApi, Pod, PodApi } from "../endpoints"; import { getDiForUnitTesting } from "../../../renderer/getDiForUnitTesting"; import apiManagerInjectable from "../api-manager/manager.injectable"; import autoRegistrationInjectable from "../api-manager/auto-registration.injectable"; +import type { Fetch } from "../../fetch/fetch.injectable"; +import fetchInjectable from "../../fetch/fetch.injectable"; +import type { CreateKubeApiForRemoteCluster } from "../create-kube-api-for-remote-cluster.injectable"; +import createKubeApiForRemoteClusterInjectable from "../create-kube-api-for-remote-cluster.injectable"; +import { Response } from "node-fetch"; +import type { AsyncFnMock } from "@async-fn/jest"; +import asyncFn from "@async-fn/jest"; +import { flushPromises } from "../../test-utils/flush-promises"; +import createKubeJsonApiInjectable from "../create-kube-json-api.injectable"; +import type { IKubeWatchEvent } from "../kube-watch-event"; +import type { KubeJsonApiDataFor } from "../kube-object"; import AbortController from "abort-controller"; -jest.mock("../api-manager"); - -const mockFetch = fetch as FetchMock; - -describe("forRemoteCluster", () => { - let apiManager: jest.Mocked; +describe("createKubeApiForRemoteCluster", () => { + let createKubeApiForRemoteCluster: CreateKubeApiForRemoteCluster; + let fetchMock: AsyncFnMock; beforeEach(() => { const di = getDiForUnitTesting({ doGeneralOverrides: true }); - apiManager = new ApiManager() as jest.Mocked; + fetchMock = asyncFn(); + di.override(fetchInjectable, () => fetchMock); - di.override(apiManagerInjectable, () => apiManager); + createKubeApiForRemoteCluster = di.inject(createKubeApiForRemoteClusterInjectable); }); it("builds api client for KubeObject", async () => { - const api = forRemoteCluster({ + const api = createKubeApiForRemoteCluster({ cluster: { server: "https://127.0.0.1:6443", }, @@ -44,232 +50,523 @@ describe("forRemoteCluster", () => { expect(api).toBeInstanceOf(KubeApi); }); - it("builds api client for given KubeApi", async () => { - const api = forRemoteCluster({ - cluster: { - server: "https://127.0.0.1:6443", - }, - user: { - token: "daa", - }, - }, Pod, PodApi); + describe("when building for remote cluster with specific constructor", () => { + let api: PodApi; - expect(api).toBeInstanceOf(PodApi); - }); - - it("calls right api endpoint", async () => { - const api = forRemoteCluster({ - cluster: { - server: "https://127.0.0.1:6443", - }, - user: { - token: "daa", - }, - }, Pod); - - mockFetch.mockResponse(async (request: any) => { - expect(request.url).toEqual("https://127.0.0.1:6443/api/v1/pods"); - - return { - body: "hello", - }; + beforeEach(() => { + api = createKubeApiForRemoteCluster({ + cluster: { + server: "https://127.0.0.1:6443", + }, + user: { + token: "daa", + }, + }, Pod, PodApi); }); - expect.hasAssertions(); + it("uses the constructor", () => { + expect(api).toBeInstanceOf(PodApi); + }); - await api.list(); + describe("when calling list without namespace", () => { + let listRequest: Promise; + + beforeEach(async () => { + listRequest = api.list(); + + // This is required because of how JS promises work + await flushPromises(); + }); + + it("should request pods from default namespace", () => { + expect(fetchMock.mock.lastCall).toMatchObject([ + "https://127.0.0.1:6443/api/v1/pods", + { + headers: { + "content-type": "application/json", + }, + method: "get", + }, + ]); + }); + + describe("when request resolves with data", () => { + beforeEach(async () => { + await fetchMock.resolveSpecific( + ["https://127.0.0.1:6443/api/v1/pods"], + new Response(JSON.stringify({ + kind: "PodList", + apiVersion: "v1", + metadata:{ + resourceVersion: "452899", + }, + items: [], + })), + ); + }); + + it("resolves the list call", async () => { + expect(await listRequest).toEqual([]); + }); + }); + }); }); }); describe("KubeApi", () => { let request: KubeJsonApi; - let apiManager: jest.Mocked; + let registerApiSpy: jest.SpiedFunction; + let fetchMock: AsyncFnMock; beforeEach(() => { const di = getDiForUnitTesting({ doGeneralOverrides: true }); - request = new KubeJsonApi({ + fetchMock = asyncFn(); + di.override(fetchInjectable, () => fetchMock); + + const createKubeJsonApi = di.inject(createKubeJsonApiInjectable); + + request = createKubeJsonApi({ serverAddress: `http://127.0.0.1:9999`, apiBase: "/api-kube", }); - apiManager = new ApiManager() as jest.Mocked; + registerApiSpy = jest.spyOn(di.inject(apiManagerInjectable), "registerApi"); - di.override(apiManagerInjectable, () => apiManager); di.inject(autoRegistrationInjectable); }); - it("uses url from apiBase if apiBase contains the resource", async () => { - mockFetch.mockResponse(async (request: any) => { - if (request.url === "http://127.0.0.1:9999/api-kube/apis/networking.k8s.io/v1") { - return { - body: JSON.stringify({ - resources: [{ - name: "ingresses", - }], - }), - }; - } else if (request.url === "http://127.0.0.1:9999/api-kube/apis/extensions/v1beta1") { - // Even if the old API contains ingresses, KubeApi should prefer the apiBase url - return { - body: JSON.stringify({ - resources: [{ - name: "ingresses", - }], - }), - }; - } else { - return { - body: JSON.stringify({ - resources: [], - }), - }; - } - }); + describe("on first call to IngressApi.get()", () => { + let ingressApi: IngressApi; + let getCall: Promise; - const apiBase = "/apis/networking.k8s.io/v1/ingresses"; - const fallbackApiBase = "/apis/extensions/v1beta1/ingresses"; - const kubeApi = new IngressApi({ - request, - objectConstructor: Ingress, - apiBase, - fallbackApiBases: [fallbackApiBase], - checkPreferredVersion: true, - }); - - await kubeApi.get({ - name: "foo", - namespace: "default", - }); - expect(kubeApi.apiPrefix).toEqual("/apis"); - expect(kubeApi.apiGroup).toEqual("networking.k8s.io"); - }); - - it("uses url from fallbackApiBases if apiBase lacks the resource", async () => { - mockFetch.mockResponse(async (request: any) => { - if (request.url === "http://127.0.0.1:9999/api-kube/apis/networking.k8s.io/v1") { - return { - body: JSON.stringify({ - resources: [], - }), - }; - } else if (request.url === "http://127.0.0.1:9999/api-kube/apis/extensions/v1beta1") { - return { - body: JSON.stringify({ - resources: [{ - name: "ingresses", - }], - }), - }; - } else { - return { - body: JSON.stringify({ - resources: [], - }), - }; - } - }); - - const apiBase = "apis/networking.k8s.io/v1/ingresses"; - const fallbackApiBase = "/apis/extensions/v1beta1/ingresses"; - const kubeApi = new IngressApi({ - request, - objectConstructor: Object.assign(KubeObject, { apiBase }), - kind: "Ingress", - fallbackApiBases: [fallbackApiBase], - checkPreferredVersion: true, - }); - - await kubeApi.get({ - name: "foo", - namespace: "default", - }); - expect(kubeApi.apiPrefix).toEqual("/apis"); - expect(kubeApi.apiGroup).toEqual("extensions"); - }); - - describe("checkPreferredVersion", () => { - it("registers with apiManager if checkPreferredVersion changes apiVersionPreferred", async () => { - expect.hasAssertions(); - - const api = new IngressApi({ + beforeEach(async () => { + ingressApi = new IngressApi({ + request, objectConstructor: Ingress, - checkPreferredVersion: true, + apiBase: "/apis/networking.k8s.io/v1/ingresses", fallbackApiBases: ["/apis/extensions/v1beta1/ingresses"], - request: { - get: jest.fn() - .mockImplementation((path: string) => { - switch (path) { - case "/apis/networking.k8s.io/v1": - throw new Error("no"); - case "/apis/extensions/v1beta1": - return { - resources: [ - { - name: "ingresses", - }, - ], - }; - case "/apis/extensions": - return { - preferredVersion: { - version: "v1beta1", - }, - }; - default: - throw new Error("unknown path"); - } - }), - } as Partial as KubeJsonApi, + checkPreferredVersion: true, + }); + getCall = ingressApi.get({ + name: "foo", + namespace: "default", }); - await (api as any).checkPreferredVersion(); - - expect(api.apiVersionPreferred).toBe("v1beta1"); - expect(apiManager.registerApi).toBeCalledWith(api); + // This is needed because of how JS promises work + await flushPromises(); }); - it("registers with apiManager if checkPreferredVersion changes apiVersionPreferred with non-grouped apis", async () => { - expect.hasAssertions(); + it("requests resources from the versioned api group from the initial apiBase", () => { + expect(fetchMock.mock.lastCall).toMatchObject([ + "http://127.0.0.1:9999/api-kube/apis/networking.k8s.io/v1", + { + headers: { + "content-type": "application/json", + }, + method: "get", + }, + ]); + }); - const api = new PodApi({ - objectConstructor: Pod, - checkPreferredVersion: true, - fallbackApiBases: ["/api/v1beta1/pods"], - request: { - get: jest.fn() - .mockImplementation((path: string) => { - switch (path) { - case "/api/v1": - throw new Error("no"); - case "/api/v1beta1": - return { - resources: [ - { - name: "pods", - }, - ], - }; - case "/api": - return { - preferredVersion: { - version: "v1beta1", - }, - }; - default: - throw new Error("unknown path"); - } - }), - } as Partial as KubeJsonApi, + describe("when resource request fufills with a resource", () => { + beforeEach(async () => { + await fetchMock.resolveSpecific( + ["http://127.0.0.1:9999/api-kube/apis/networking.k8s.io/v1"], + new Response(JSON.stringify({ + resources: [{ + name: "ingresses", + }], + })), + ); }); - await (api as any).checkPreferredVersion(); + it("requests the perferred version of that api group", () => { + expect(fetchMock.mock.lastCall).toMatchObject([ + "http://127.0.0.1:9999/api-kube/apis/networking.k8s.io", + { + headers: { + "content-type": "application/json", + }, + method: "get", + }, + ]); + }); - expect(api.apiVersionPreferred).toBe("v1beta1"); - expect(apiManager.registerApi).toBeCalledWith(api); + describe("when the preferred version resolves with v1", () => { + beforeEach(async () => { + await fetchMock.resolveSpecific( + ["http://127.0.0.1:9999/api-kube/apis/networking.k8s.io"], + new Response(JSON.stringify({ + preferredVersion: { + version: "v1", + }, + })), + ); + }); + + it("makes the request to get the resource", () => { + expect(fetchMock.mock.lastCall).toMatchObject([ + "http://127.0.0.1:9999/api-kube/apis/networking.k8s.io/v1/namespaces/default/ingresses/foo", + { + headers: { + "content-type": "application/json", + }, + method: "get", + }, + ]); + }); + + it("sets fields in the api instance", () => { + expect(ingressApi).toEqual(expect.objectContaining({ + apiVersionPreferred: "v1", + apiPrefix: "/apis", + apiGroup: "networking.k8s.io", + })); + }); + + it("registers the api with the changes info", () => { + expect(registerApiSpy).toBeCalledWith(ingressApi); + }); + + describe("when the request resolves with no data", () => { + let result: Ingress | null; + + beforeEach(async () => { + await fetchMock.resolveSpecific( + ["http://127.0.0.1:9999/api-kube/apis/networking.k8s.io/v1/namespaces/default/ingresses/foo"], + new Response(JSON.stringify({})), + ); + result = await getCall; + }); + + it("results in the get call resolving to null", () => { + expect(result).toBeNull(); + }); + + describe("on the second call to IngressApi.get()", () => { + let getCall: Promise; + + beforeEach(async () => { + getCall = ingressApi.get({ + name: "foo1", + namespace: "default", + }); + + // This is needed because of how JS promises work + await flushPromises(); + }); + + it("makes the request to get the resource", () => { + expect(fetchMock.mock.lastCall).toMatchObject([ + "http://127.0.0.1:9999/api-kube/apis/networking.k8s.io/v1/namespaces/default/ingresses/foo1", + { + headers: { + "content-type": "application/json", + }, + method: "get", + }, + ]); + }); + + describe("when the request resolves with no data", () => { + let result: Ingress | null; + + beforeEach(async () => { + await fetchMock.resolveSpecific( + ["http://127.0.0.1:9999/api-kube/apis/networking.k8s.io/v1/namespaces/default/ingresses/foo1"], + new Response(JSON.stringify({})), + ); + result = await getCall; + }); + + it("results in the get call resolving to null", () => { + expect(result).toBeNull(); + }); + }); + }); + }); + + describe("when the request resolves with data", () => { + let result: Ingress | null; + + beforeEach(async () => { + await fetchMock.resolveSpecific( + ["http://127.0.0.1:9999/api-kube/apis/networking.k8s.io/v1/namespaces/default/ingresses/foo"], + new Response(JSON.stringify({ + apiVersion: "v1", + kind: "Ingress", + metadata: { + name: "foo", + namespace: "default", + resourceVersion: "1", + uid: "12345", + }, + })), + ); + result = await getCall; + }); + + it("results in the get call resolving to an instance", () => { + expect(result).toBeInstanceOf(Ingress); + }); + + describe("on the second call to IngressApi.get()", () => { + let getCall: Promise; + + beforeEach(async () => { + getCall = ingressApi.get({ + name: "foo1", + namespace: "default", + }); + + // This is needed because of how JS promises work + await flushPromises(); + }); + + it("makes the request to get the resource", () => { + expect(fetchMock.mock.lastCall).toMatchObject([ + "http://127.0.0.1:9999/api-kube/apis/networking.k8s.io/v1/namespaces/default/ingresses/foo1", + { + headers: { + "content-type": "application/json", + }, + method: "get", + }, + ]); + }); + + describe("when the request resolves with no data", () => { + let result: Ingress | null; + + beforeEach(async () => { + await fetchMock.resolveSpecific( + ["http://127.0.0.1:9999/api-kube/apis/networking.k8s.io/v1/namespaces/default/ingresses/foo1"], + new Response(JSON.stringify({})), + ); + result = await getCall; + }); + + it("results in the get call resolving to null", () => { + expect(result).toBeNull(); + }); + }); + }); + }); + }); + }); + + describe("when resource request fufills with no resource", () => { + beforeEach(async () => { + await fetchMock.resolveSpecific( + ["http://127.0.0.1:9999/api-kube/apis/networking.k8s.io/v1"], + new Response(JSON.stringify({ + resources: [], + })), + ); + }); + + it("requests the resources from the base api url from the fallback api", () => { + expect(fetchMock.mock.lastCall).toMatchObject([ + "http://127.0.0.1:9999/api-kube/apis/extensions/v1beta1", + { + headers: { + "content-type": "application/json", + }, + method: "get", + }, + ]); + }); + + describe("when resource request fufills with a resource", () => { + beforeEach(async () => { + await fetchMock.resolveSpecific( + ["http://127.0.0.1:9999/api-kube/apis/extensions/v1beta1"], + new Response(JSON.stringify({ + resources: [{ + name: "ingresses", + }], + })), + ); + }); + + it("requests the preferred version for that api group", () => { + expect(fetchMock.mock.lastCall).toMatchObject([ + "http://127.0.0.1:9999/api-kube/apis/extensions", + { + headers: { + "content-type": "application/json", + }, + method: "get", + }, + ]); + }); + + describe("when the preferred version request resolves to v1beta1", () => { + beforeEach(async () => { + await fetchMock.resolveSpecific( + ["http://127.0.0.1:9999/api-kube/apis/extensions"], + new Response(JSON.stringify({ + preferredVersion: { + version: "v1beta1", + }, + })), + ); + }); + + it("makes the request to get the resource", () => { + expect(fetchMock.mock.lastCall).toMatchObject([ + "http://127.0.0.1:9999/api-kube/apis/extensions/v1beta1/namespaces/default/ingresses/foo", + { + headers: { + "content-type": "application/json", + }, + method: "get", + }, + ]); + }); + + it("sets fields in the api instance", () => { + expect(ingressApi).toEqual(expect.objectContaining({ + apiVersionPreferred: "v1beta1", + apiPrefix: "/apis", + apiGroup: "extensions", + })); + }); + + it("registers the api with the changes info", () => { + expect(registerApiSpy).toBeCalledWith(ingressApi); + }); + + describe("when the request resolves with no data", () => { + let result: Ingress | null; + + beforeEach(async () => { + await fetchMock.resolveSpecific( + ["http://127.0.0.1:9999/api-kube/apis/extensions/v1beta1/namespaces/default/ingresses/foo"], + new Response(JSON.stringify({})), + ); + result = await getCall; + }); + + it("results in the get call resolving to null", () => { + expect(result).toBeNull(); + }); + + describe("on the second call to IngressApi.get()", () => { + let getCall: Promise; + + beforeEach(async () => { + getCall = ingressApi.get({ + name: "foo1", + namespace: "default", + }); + + // This is needed because of how JS promises work + await flushPromises(); + }); + + it("makes the request to get the resource", () => { + expect(fetchMock.mock.lastCall).toMatchObject([ + "http://127.0.0.1:9999/api-kube/apis/extensions/v1beta1/namespaces/default/ingresses/foo1", + { + headers: { + "content-type": "application/json", + }, + method: "get", + }, + ]); + }); + + describe("when the request resolves with no data", () => { + let result: Ingress | null; + + beforeEach(async () => { + await fetchMock.resolveSpecific( + ["http://127.0.0.1:9999/api-kube/apis/extensions/v1beta1/namespaces/default/ingresses/foo1"], + new Response(JSON.stringify({})), + ); + result = await getCall; + }); + + it("results in the get call resolving to null", () => { + expect(result).toBeNull(); + }); + }); + }); + }); + + describe("when the request resolves with data", () => { + let result: Ingress | null; + + beforeEach(async () => { + await fetchMock.resolveSpecific( + ["http://127.0.0.1:9999/api-kube/apis/extensions/v1beta1/namespaces/default/ingresses/foo"], + new Response(JSON.stringify({ + apiVersion: "v1beta1", + kind: "Ingress", + metadata: { + name: "foo", + namespace: "default", + resourceVersion: "1", + uid: "12345", + }, + })), + ); + result = await getCall; + }); + + it("results in the get call resolving to an instance", () => { + expect(result).toBeInstanceOf(Ingress); + }); + + describe("on the second call to IngressApi.get()", () => { + let getCall: Promise; + + beforeEach(async () => { + getCall = ingressApi.get({ + name: "foo1", + namespace: "default", + }); + + // This is needed because of how JS promises work + await flushPromises(); + }); + + it("makes the request to get the resource", () => { + expect(fetchMock.mock.lastCall).toMatchObject([ + "http://127.0.0.1:9999/api-kube/apis/extensions/v1beta1/namespaces/default/ingresses/foo1", + { + headers: { + "content-type": "application/json", + }, + method: "get", + }, + ]); + }); + + describe("when the request resolves with no data", () => { + let result: Ingress | null; + + beforeEach(async () => { + await fetchMock.resolveSpecific( + ["http://127.0.0.1:9999/api-kube/apis/extensions/v1beta1/namespaces/default/ingresses/foo1"], + new Response(JSON.stringify({})), + ); + result = await getCall; + }); + + it("results in the get call resolving to null", () => { + expect(result).toBeNull(); + }); + }); + }); + }); + }); + }); }); }); - describe("patch", () => { + describe("patching deployments", () => { let api: DeploymentApi; beforeEach(() => { @@ -278,140 +575,377 @@ describe("KubeApi", () => { }); }); - it("sends strategic patch by default", async () => { - expect.hasAssertions(); + describe("when patching a resource without providing a strategy", () => { + let patchRequest: Promise; - mockFetch.mockResponse(async request => { - expect(request.method).toEqual("PATCH"); - expect(request.headers.get("content-type")).toMatch("strategic-merge-patch"); - expect(request.body?.toString()).toEqual(JSON.stringify({ spec: { replicas: 2 }})); + beforeEach(async () => { + patchRequest = api.patch({ name: "test", namespace: "default" }, { + spec: { replicas: 2 }, + }); - return {}; + // This is needed because of how JS promises work + await flushPromises(); }); - await api.patch({ name: "test", namespace: "default" }, { - spec: { replicas: 2 }, + it("requests a patch using strategic merge", () => { + expect(fetchMock.mock.lastCall).toMatchObject([ + "http://127.0.0.1:9999/api-kube/apis/apps/v1/namespaces/default/deployments/test", + { + headers: { + "content-type": "application/strategic-merge-patch+json", + }, + method: "patch", + body: JSON.stringify({ spec: { replicas: 2 }}), + }, + ]); + }); + + describe("when the patch request resolves with data", () => { + beforeEach(async () => { + await fetchMock.resolveSpecific( + ["http://127.0.0.1:9999/api-kube/apis/apps/v1/namespaces/default/deployments/test"], + new Response(JSON.stringify({ + apiVersion: "v1", + kind: "Deployment", + metadata: { + name: "test", + namespace: "default", + resourceVersion: "1", + uid: "12345", + }, + spec: { + replicas: 2, + }, + })), + ); + }); + + it("resolves the patch call", async () => { + expect(await patchRequest).toBeInstanceOf(Deployment); + }); }); }); - it("allows to use merge patch", async () => { - expect.hasAssertions(); + describe("when patching a resource using json patch", () => { + let patchRequest: Promise; - mockFetch.mockResponse(async request => { - expect(request.method).toEqual("PATCH"); - expect(request.headers.get("content-type")).toMatch("merge-patch"); - expect(request.body?.toString()).toEqual(JSON.stringify({ spec: { replicas: 2 }})); + beforeEach(async () => { + patchRequest = api.patch({ name: "test", namespace: "default" }, [ + { op: "replace", path: "/spec/replicas", value: 2 }, + ], "json"); - return {}; + // This is needed because of how JS promises work + await flushPromises(); }); - await api.patch({ name: "test", namespace: "default" }, { - spec: { replicas: 2 }, - }, "merge"); + it("requests a patch using json merge", () => { + expect(fetchMock.mock.lastCall).toMatchObject([ + "http://127.0.0.1:9999/api-kube/apis/apps/v1/namespaces/default/deployments/test", + { + headers: { + "content-type": "application/json-patch+json", + }, + method: "patch", + body: JSON.stringify([ + { op: "replace", path: "/spec/replicas", value: 2 }, + ]), + }, + ]); + }); + + describe("when the patch request resolves with data", () => { + beforeEach(async () => { + await fetchMock.resolveSpecific( + ["http://127.0.0.1:9999/api-kube/apis/apps/v1/namespaces/default/deployments/test"], + new Response(JSON.stringify({ + apiVersion: "v1", + kind: "Deployment", + metadata: { + name: "test", + namespace: "default", + resourceVersion: "1", + uid: "12345", + }, + spec: { + replicas: 2, + }, + })), + ); + }); + + it("resolves the patch call", async () => { + expect(await patchRequest).toBeInstanceOf(Deployment); + }); + }); }); - it("allows to use json patch", async () => { - expect.hasAssertions(); + describe("when patching a resource using merge patch", () => { + let patchRequest: Promise; - mockFetch.mockResponse(async request => { - expect(request.method).toEqual("PATCH"); - expect(request.headers.get("content-type")).toMatch("json-patch"); - expect(request.body?.toString()).toEqual(JSON.stringify([{ op: "replace", path: "/spec/replicas", value: 2 }])); + beforeEach(async () => { + patchRequest = api.patch( + { name: "test", namespace: "default" }, + { metadata: { annotations: { provisioned: "True" }}}, + "merge", + ); - return {}; + // This is needed because of how JS promises work + await flushPromises(); }); - await api.patch({ name: "test", namespace: "default" }, [ - { op: "replace", path: "/spec/replicas", value: 2 }, - ], "json"); - }); - - it("allows deep partial patch", async () => { - expect.hasAssertions(); - - mockFetch.mockResponse(async request => { - expect(request.method).toEqual("PATCH"); - expect(request.headers.get("content-type")).toMatch("merge-patch"); - expect(request.body?.toString()).toEqual(JSON.stringify({ metadata: { annotations: { provisioned: "true" }}})); - - return {}; + it("requests a patch using json merge", () => { + expect(fetchMock.mock.lastCall).toMatchObject([ + "http://127.0.0.1:9999/api-kube/apis/apps/v1/namespaces/default/deployments/test", + { + headers: { + "content-type": "application/merge-patch+json", + }, + method: "patch", + body: JSON.stringify({ metadata: { annotations: { provisioned: "True" }}}), + }, + ]); }); - await api.patch( - { name: "test", namespace: "default" }, - { metadata: { annotations: { provisioned: "true" }}}, - "merge", - ); + describe("when the patch request resolves with data", () => { + beforeEach(async () => { + await fetchMock.resolveSpecific( + ["http://127.0.0.1:9999/api-kube/apis/apps/v1/namespaces/default/deployments/test"], + new Response(JSON.stringify({ + apiVersion: "v1", + kind: "Deployment", + metadata: { + name: "test", + namespace: "default", + resourceVersion: "1", + uid: "12345", + annotations: { + provisioned: "True", + }, + }, + })), + ); + }); + + it("resolves the patch call", async () => { + expect(await patchRequest).toBeInstanceOf(Deployment); + }); + }); }); }); - describe("delete", () => { + describe("deleting pods (namespace scoped resource)", () => { let api: PodApi; beforeEach(() => { api = new PodApi({ request, - objectConstructor: Pod, }); }); - it("sends correct request with empty namespace", async () => { - expect.hasAssertions(); - mockFetch.mockResponse(async request => { - expect(request.method).toEqual("DELETE"); - expect(request.url).toEqual("http://127.0.0.1:9999/api-kube/api/v1/pods/foo?propagationPolicy=Background"); + describe("when deleting by just name", () => { + let deleteRequest: Promise; - return {}; + beforeEach(async () => { + deleteRequest = api.delete({ name: "foo" }); + + // This is required for how JS promises work + await flushPromises(); }); - await api.delete({ name: "foo", namespace: "" }); + it("requests deleting pod in default namespace", () => { + expect(fetchMock.mock.lastCall).toMatchObject([ + "http://127.0.0.1:9999/api-kube/api/v1/namespaces/default/pods/foo?propagationPolicy=Background", + { + headers: { + "content-type": "application/json", + }, + method: "delete", + }, + ]); + }); + + describe("when request resolves", () => { + beforeEach(async () => { + fetchMock.resolveSpecific( + ["http://127.0.0.1:9999/api-kube/api/v1/namespaces/default/pods/foo?propagationPolicy=Background"], + new Response("{}"), + ); + }); + + it("resolves the call", async () => { + expect(await deleteRequest).toBeDefined(); + }); + }); }); - it("sends correct request without namespace", async () => { - expect.hasAssertions(); - mockFetch.mockResponse(async request => { - expect(request.method).toEqual("DELETE"); - expect(request.url).toEqual("http://127.0.0.1:9999/api-kube/api/v1/namespaces/default/pods/foo?propagationPolicy=Background"); + describe("when deleting by name and empty namespace", () => { + let deleteRequest: Promise; - return {}; + beforeEach(async () => { + deleteRequest = api.delete({ name: "foo", namespace: "" }); + + // This is required for how JS promises work + await flushPromises(); }); - await api.delete({ name: "foo" }); + it("requests deleting pod in default namespace", () => { + expect(fetchMock.mock.lastCall).toMatchObject([ + "http://127.0.0.1:9999/api-kube/api/v1/namespaces/default/pods/foo?propagationPolicy=Background", + { + headers: { + "content-type": "application/json", + }, + method: "delete", + }, + ]); + }); + + describe("when request resolves", () => { + beforeEach(async () => { + fetchMock.resolveSpecific( + ["http://127.0.0.1:9999/api-kube/api/v1/namespaces/default/pods/foo?propagationPolicy=Background"], + new Response("{}"), + ); + }); + + it("resolves the call", async () => { + expect(await deleteRequest).toBeDefined(); + }); + }); }); - it("sends correct request with namespace", async () => { - expect.hasAssertions(); - mockFetch.mockResponse(async request => { - expect(request.method).toEqual("DELETE"); - expect(request.url).toEqual("http://127.0.0.1:9999/api-kube/api/v1/namespaces/kube-system/pods/foo?propagationPolicy=Background"); + describe("when deleting by name and namespace", () => { + let deleteRequest: Promise; - return {}; + beforeEach(async () => { + deleteRequest = api.delete({ name: "foo", namespace: "test" }); + + // This is required for how JS promises work + await flushPromises(); }); - await api.delete({ name: "foo", namespace: "kube-system" }); - }); - - it("allows to change propagationPolicy", async () => { - expect.hasAssertions(); - mockFetch.mockResponse(async request => { - expect(request.method).toEqual("DELETE"); - expect(request.url).toMatch("propagationPolicy=Orphan"); - - return {}; + it("requests deleting pod in given namespace", () => { + expect(fetchMock.mock.lastCall).toMatchObject([ + "http://127.0.0.1:9999/api-kube/api/v1/namespaces/test/pods/foo?propagationPolicy=Background", + { + headers: { + "content-type": "application/json", + }, + method: "delete", + }, + ]); }); - await api.delete({ name: "foo", namespace: "default", propagationPolicy: "Orphan" }); + describe("when request resolves", () => { + beforeEach(async () => { + fetchMock.resolveSpecific( + ["http://127.0.0.1:9999/api-kube/api/v1/namespaces/test/pods/foo?propagationPolicy=Background"], + new Response("{}"), + ); + }); + + it("resolves the call", async () => { + expect(await deleteRequest).toBeDefined(); + }); + }); }); }); - describe("watch", () => { + describe("deleting namespaces (cluser scoped resource)", () => { + let api: NamespaceApi; + + beforeEach(() => { + api = new NamespaceApi({ + request, + }); + }); + + describe("when deleting by just name", () => { + let deleteRequest: Promise; + + beforeEach(async () => { + deleteRequest = api.delete({ name: "foo" }); + + // This is required for how JS promises work + await flushPromises(); + }); + + it("requests deleting Namespace without namespace", () => { + expect(fetchMock.mock.lastCall).toMatchObject([ + "http://127.0.0.1:9999/api-kube/api/v1/namespaces/foo?propagationPolicy=Background", + { + headers: { + "content-type": "application/json", + }, + method: "delete", + }, + ]); + }); + + describe("when request resolves", () => { + beforeEach(async () => { + fetchMock.resolveSpecific( + ["http://127.0.0.1:9999/api-kube/api/v1/namespaces/foo?propagationPolicy=Background"], + new Response("{}"), + ); + }); + + it("resolves the call", async () => { + expect(await deleteRequest).toBeDefined(); + }); + }); + }); + + describe("when deleting by name and empty namespace", () => { + let deleteRequest: Promise; + + beforeEach(async () => { + deleteRequest = api.delete({ name: "foo", namespace: "" }); + + // This is required for how JS promises work + await flushPromises(); + }); + + it("requests deleting Namespace without namespace", () => { + expect(fetchMock.mock.lastCall).toMatchObject([ + "http://127.0.0.1:9999/api-kube/api/v1/namespaces/foo?propagationPolicy=Background", + { + headers: { + "content-type": "application/json", + }, + method: "delete", + }, + ]); + }); + + describe("when request resolves", () => { + beforeEach(async () => { + fetchMock.resolveSpecific( + ["http://127.0.0.1:9999/api-kube/api/v1/namespaces/foo?propagationPolicy=Background"], + new Response("{}"), + ); + }); + + it("resolves the call", async () => { + expect(await deleteRequest).toBeDefined(); + }); + }); + }); + + describe("when deleting by name and namespace", () => { + it("rejects request", () => { + expect(api.delete({ name: "foo", namespace: "test" })).rejects.toBeDefined(); + }); + }); + }); + + describe("watching pods", () => { let api: PodApi; let stream: PassThrough; beforeEach(() => { api = new PodApi({ request, - objectConstructor: Pod, }); stream = new PassThrough(); }); @@ -421,184 +955,341 @@ describe("KubeApi", () => { stream.destroy(); }); - it("sends a valid watch request", () => { - const spy = jest.spyOn(request, "getResponse"); + describe("when watching in a namespace", () => { + let stopWatch: () => void; + let callback: jest.MockedFunction; - mockFetch.mockResponse(async () => { - return { - // needed for https://github.com/jefflau/jest-fetch-mock/issues/218 - body: stream as unknown as string, - }; + beforeEach(async () => { + callback = jest.fn(); + stopWatch = api.watch({ + namespace: "kube-system", + callback, + }); + + await flushPromises(); }); - api.watch({ namespace: "kube-system" }); - expect(spy).toHaveBeenCalledWith("/api/v1/namespaces/kube-system/pods?watch=1&resourceVersion=", expect.anything(), expect.anything()); + it("requests the watch", () => { + expect(fetchMock.mock.lastCall).toMatchObject([ + "http://127.0.0.1:9999/api-kube/api/v1/namespaces/kube-system/pods?watch=1&resourceVersion=", + { + headers: { + "content-type": "application/json", + }, + method: "get", + }, + ]); + }); + + describe("when the request resolves with a stream", () => { + beforeEach(async () => { + await fetchMock.resolveSpecific( + ([url, init]) => { + const isMatch = url === "http://127.0.0.1:9999/api-kube/api/v1/namespaces/kube-system/pods?watch=1&resourceVersion="; + + if (isMatch) { + init?.signal?.addEventListener("abort", () => { + stream.destroy(); + }); + } + + return isMatch; + }, + new Response(stream), + ); + }); + + describe("when some data comes back on the stream", () => { + beforeEach(() => { + stream.emit("data", `${JSON.stringify({ + type: "ADDED", + object: { + apiVersion: "v1", + kind: "Pod", + metadata: { + name: "foobar", + namespace: "kube-system", + resourceVersion: "1", + uid: "123456", + }, + }, + } as IKubeWatchEvent>)}\n`); + }); + + it("calls the callback with the data", () => { + expect(callback).toBeCalledWith( + { + type: "ADDED", + object: { + apiVersion: "v1", + kind: "Pod", + metadata: { + name: "foobar", + namespace: "kube-system", + resourceVersion: "1", + selfLink: "/api/v1/namespaces/kube-system/pods/foobar", + uid: "123456", + }, + }, + }, + null, + ); + }); + + describe("when stopping the watch", () => { + beforeEach(() => { + stopWatch(); + }); + + it("closes the stream", () => { + expect(stream.destroyed).toBe(true); + }); + }); + }); + }); }); - it("sends timeout as a query parameter", async () => { - const spy = jest.spyOn(request, "getResponse"); + describe("when watching in a namespace with an abort controller provided", () => { + let callback: jest.MockedFunction; + let abortController: AbortController; - mockFetch.mockResponse(async () => { - return { - // needed for https://github.com/jefflau/jest-fetch-mock/issues/218 - body: stream as unknown as string, - }; + beforeEach(async () => { + callback = jest.fn(); + abortController = new AbortController(); + api.watch({ + namespace: "kube-system", + callback, + abortController, + }); + + await flushPromises(); }); - api.watch({ namespace: "kube-system", timeout: 60 }); - expect(spy).toHaveBeenCalledWith("/api/v1/namespaces/kube-system/pods?watch=1&resourceVersion=", { query: { timeoutSeconds: 60 }}, expect.anything()); + it("requests the watch", () => { + expect(fetchMock.mock.lastCall).toMatchObject([ + "http://127.0.0.1:9999/api-kube/api/v1/namespaces/kube-system/pods?watch=1&resourceVersion=", + { + headers: { + "content-type": "application/json", + }, + method: "get", + }, + ]); + }); + + describe("when the request resolves with a stream", () => { + beforeEach(async () => { + await fetchMock.resolveSpecific( + ([url, init]) => { + const isMatch = url === "http://127.0.0.1:9999/api-kube/api/v1/namespaces/kube-system/pods?watch=1&resourceVersion="; + + if (isMatch) { + init?.signal?.addEventListener("abort", () => { + stream.destroy(); + }); + } + + return isMatch; + }, + new Response(stream), + ); + }); + + describe("when some data comes back on the stream", () => { + beforeEach(() => { + stream.emit("data", `${JSON.stringify({ + type: "ADDED", + object: { + apiVersion: "v1", + kind: "Pod", + metadata: { + name: "foobar", + namespace: "kube-system", + resourceVersion: "1", + uid: "123456", + }, + }, + } as IKubeWatchEvent>)}\n`); + }); + + it("calls the callback with the data", () => { + expect(callback).toBeCalledWith( + { + type: "ADDED", + object: { + apiVersion: "v1", + kind: "Pod", + metadata: { + name: "foobar", + namespace: "kube-system", + resourceVersion: "1", + selfLink: "/api/v1/namespaces/kube-system/pods/foobar", + uid: "123456", + }, + }, + }, + null, + ); + }); + + describe("when stopping the watch via the controller", () => { + beforeEach(() => { + abortController.abort(); + }); + + it("closes the stream", () => { + expect(stream.destroyed).toBe(true); + }); + }); + }); + }); }); - it("aborts watch using abortController", (done) => { - const spy = jest.spyOn(request, "getResponse"); + describe("when watching in a namespace with a timeout", () => { + let stopWatch: () => void; + let callback: jest.MockedFunction; - mockFetch.mockResponse(async request => { - request.signal.addEventListener("abort", () => { - done(); - }); - - return { - // needed for https://github.com/jefflau/jest-fetch-mock/issues/218 - body: stream as unknown as string, - }; - }); - - const abortController = new AbortController(); - - api.watch({ - namespace: "kube-system", - timeout: 60, - abortController, - }); - - expect(spy).toHaveBeenCalledWith("/api/v1/namespaces/kube-system/pods?watch=1&resourceVersion=", { query: { timeoutSeconds: 60 }}, expect.anything()); - delay(100).then(() => abortController.abort()); - }); - - describe("retries", () => { - it("if request ended", (done) => { - const spy = jest.spyOn(request, "getResponse"); - - jest.spyOn(stream, "on").mockImplementation((event: string | symbol, callback: Function) => { - // End the request in 100ms. - if (event === "end") { - setTimeout(() => { - callback(); - }, 100); - } - - return stream; - }); - - // we need to mock using jest as jest-fetch-mock doesn't support mocking the body completely - jest.spyOn(global, "fetch").mockImplementation(async () => { - return { - ok: true, - body: stream as never, - } as Partial as Response; - }); - - api.watch({ + beforeEach(async () => { + callback = jest.fn(); + stopWatch = api.watch({ namespace: "kube-system", + callback, + timeout: 60, }); - expect(spy).toHaveBeenCalledTimes(1); - - setTimeout(() => { - expect(spy).toHaveBeenCalledTimes(2); - done(); - }, 2000); + await flushPromises(); }); - it("if request not closed after timeout", (done) => { - const spy = jest.spyOn(request, "getResponse"); - - mockFetch.mockResponse(async () => { - return { - // needed for https://github.com/jefflau/jest-fetch-mock/issues/218 - body: stream as unknown as string, - }; - }); - - const timeoutSeconds = 1; - - api.watch({ - namespace: "kube-system", - timeout: timeoutSeconds, - }); - - expect(spy).toHaveBeenCalledTimes(1); - - setTimeout(() => { - expect(spy).toHaveBeenCalledTimes(2); - done(); - }, timeoutSeconds * 1000 * 1.2); + it("requests the watch", () => { + expect(fetchMock.mock.lastCall).toMatchObject([ + "http://127.0.0.1:9999/api-kube/api/v1/namespaces/kube-system/pods?watch=1&resourceVersion=&timeoutSeconds=60", + { + headers: { + "content-type": "application/json", + }, + method: "get", + }, + ]); }); - it("retries only once if request ends and timeout is set", (done) => { - const spy = jest.spyOn(request, "getResponse"); + describe("when the request resolves with a stream", () => { + beforeEach(async () => { + await fetchMock.resolveSpecific( + ([url, init]) => { + const isMatch = url === "http://127.0.0.1:9999/api-kube/api/v1/namespaces/kube-system/pods?watch=1&resourceVersion=&timeoutSeconds=60"; - jest.spyOn(stream, "on").mockImplementation((event: string | symbol, callback: Function) => { - // End the request in 100ms. - if (event === "end") { - setTimeout(() => { - callback(); - }, 100); - } + if (isMatch) { + init?.signal?.addEventListener("abort", () => { + stream.destroy(); + }); + } - return stream; + return isMatch; + }, + new Response(stream), + ); }); - // we need to mock using jest as jest-fetch-mock doesn't support mocking the body completely - jest.spyOn(global, "fetch").mockImplementation(async () => { - return { - ok: true, - body: stream as never, - } as Partial as Response; + describe("when some data comes back on the stream", () => { + beforeEach(() => { + stream.emit("data", `${JSON.stringify({ + type: "ADDED", + object: { + apiVersion: "v1", + kind: "Pod", + metadata: { + name: "foobar", + namespace: "kube-system", + resourceVersion: "1", + uid: "123456", + }, + }, + } as IKubeWatchEvent>)}\n`); + }); + + it("calls the callback with the data", () => { + expect(callback).toBeCalledWith( + { + type: "ADDED", + object: { + apiVersion: "v1", + kind: "Pod", + metadata: { + name: "foobar", + namespace: "kube-system", + resourceVersion: "1", + selfLink: "/api/v1/namespaces/kube-system/pods/foobar", + uid: "123456", + }, + }, + }, + null, + ); + }); + + describe("when stopping the watch", () => { + beforeEach(() => { + stopWatch(); + }); + + it("closes the stream", () => { + expect(stream.destroyed).toBe(true); + }); + }); + + describe("when the watch ends", () => { + beforeEach(() => { + stream.end(); + }); + + it("requests a new watch", () => { + expect(fetchMock.mock.lastCall).toMatchObject([ + "http://127.0.0.1:9999/api-kube/api/v1/namespaces/kube-system/pods?watch=1&resourceVersion=&timeoutSeconds=60", + { + headers: { + "content-type": "application/json", + }, + method: "get", + }, + ]); + }); + + describe("when stopping the watch", () => { + beforeEach(() => { + stopWatch(); + }); + + it("closes the stream", () => { + expect(stream.destroyed).toBe(true); + }); + }); + }); }); - - const timeoutSeconds = 0.5; - - api.watch({ - namespace: "kube-system", - timeout: timeoutSeconds, - }); - - expect(spy).toHaveBeenCalledTimes(1); - - setTimeout(() => { - expect(spy).toHaveBeenCalledTimes(2); - done(); - }, 2000); - }); - - afterEach(() => { - jest.clearAllMocks(); }); }); }); - describe("create", () => { + describe("creating pods", () => { let api: PodApi; beforeEach(() => { api = new PodApi({ request, - objectConstructor: Pod, }); }); - it("should add kind and apiVersion", async () => { - expect.hasAssertions(); + describe("when creating a pod", () => { + let createRequest: Promise; - mockFetch.mockResponse(async request => { - expect(request.method).toEqual("POST"); - expect(JSON.parse(String(request.body))).toEqual({ - kind: "Pod", - apiVersion: "v1", + beforeEach(async () => { + createRequest = api.create({ + name: "foobar", + namespace: "default", + }, { metadata: { - name: "foobar", - namespace: "default", + labels: { + foo: "bar", + }, }, spec: { containers: [ @@ -617,101 +1308,324 @@ describe("KubeApi", () => { }, }); - return {}; + // This is required because of how JS promises work + await flushPromises(); }); - await api.create({ - name: "foobar", - namespace: "default", - }, { - spec: { - containers: [ - { - name: "web", - image: "nginx", - ports: [ - { - name: "web", - containerPort: 80, - protocol: "TCP", + it("should request to create a pod with full descriptor", () => { + expect(fetchMock.mock.lastCall).toMatchObject([ + "http://127.0.0.1:9999/api-kube/api/v1/namespaces/default/pods", + { + headers: { + "content-type": "application/json", + }, + method: "post", + body: JSON.stringify({ + metadata: { + labels: { + foo: "bar", }, - ], - }, - ], - }, - }); - }); - - it("doesn't override metadata.labels", async () => { - expect.hasAssertions(); - - mockFetch.mockResponse(async request => { - expect(request.method).toEqual("POST"); - expect(JSON.parse(String(request.body))).toEqual({ - kind: "Pod", - apiVersion: "v1", - metadata: { - name: "foobar", - namespace: "default", - labels: { - foo: "bar", - }, + name: "foobar", + namespace: "default", + }, + spec: { + containers: [{ + name: "web", + image: "nginx", + ports: [{ + name: "web", + containerPort: 80, + protocol: "TCP", + }], + }], + }, + kind: "Pod", + apiVersion: "v1", + }), }, + ]); + }); + + describe("when request resolves with data", () => { + beforeEach(async () => { + await fetchMock.resolveSpecific( + ["http://127.0.0.1:9999/api-kube/api/v1/namespaces/default/pods"], + new Response(JSON.stringify({ + kind: "Pod", + apiVersion: "v1", + metadata: { + name: "foobar", + namespace: "default", + labels: { + foo: "bar", + }, + resourceVersion: "1", + uid: "123456798", + }, + spec: { + containers: [{ + name: "web", + image: "nginx", + ports: [{ + name: "web", + containerPort: 80, + protocol: "TCP", + }], + }], + }, + })), + ); }); - return {}; - }); - - await api.create({ - name: "foobar", - namespace: "default", - }, { - metadata: { - labels: { - foo: "bar", - }, - }, + it("call should resolve in a Pod instance", async () => { + expect(await createRequest).toBeInstanceOf(Pod); + }); }); }); }); - describe("update", () => { + describe("updating pods", () => { let api: PodApi; beforeEach(() => { api = new PodApi({ request, - objectConstructor: Pod, }); }); - it("doesn't override metadata.labels", async () => { - expect.hasAssertions(); + describe("when updating a pod", () => { + let updateRequest: Promise; - mockFetch.mockResponse(async request => { - expect(request.method).toEqual("PUT"); - expect(JSON.parse(String(request.body))).toEqual({ + beforeEach(async () => { + updateRequest = api.update({ + name: "foobar", + namespace: "default", + }, { + kind: "Pod", + apiVersion: "v1", metadata: { - name: "foobar", - namespace: "default", labels: { foo: "bar", }, }, + spec: { + containers: [{ + name: "web", + image: "nginx", + ports: [{ + name: "web", + containerPort: 80, + protocol: "TCP", + }], + }], + }, }); - return {}; + await flushPromises(); }); - await api.update({ - name: "foobar", - namespace: "default", - }, { - metadata: { - labels: { - foo: "bar", + it("should request that the pod is updated", () => { + expect(fetchMock.mock.lastCall).toMatchObject([ + "http://127.0.0.1:9999/api-kube/api/v1/namespaces/default/pods/foobar", + { + headers: { + "content-type": "application/json", + }, + method: "put", + body: JSON.stringify({ + kind: "Pod", + apiVersion: "v1", + metadata: { + labels: { + foo: "bar", + }, + name: "foobar", + namespace: "default", + }, + spec: { + containers: [{ + name: "web", + image: "nginx", + ports: [{ + name: "web", + containerPort: 80, + protocol: "TCP", + }], + }], + }, + }), }, - }, + ]); + }); + + describe("when the request resolves with data", () => { + beforeEach(async () => { + await fetchMock.resolveSpecific( + ["http://127.0.0.1:9999/api-kube/api/v1/namespaces/default/pods/foobar"], + new Response(JSON.stringify({ + kind: "Pod", + apiVersion: "v1", + metadata: { + name: "foobar", + namespace: "default", + labels: { + foo: "bar", + }, + resourceVersion: "1", + uid: "123456798", + }, + spec: { + containers: [{ + name: "web", + image: "nginx", + ports: [{ + name: "web", + containerPort: 80, + protocol: "TCP", + }], + }], + }, + })), + ); + }); + + it("the call should resolve to a Pod", async () => { + expect(await updateRequest).toBeInstanceOf(Pod); + }); + }); + }); + }); + + describe("listing pods", () => { + let api: PodApi; + + beforeEach(() => { + api = new PodApi({ + request, + }); + }); + + describe("when listing pods with no descriptor", () => { + let listRequest: Promise; + + beforeEach(async () => { + listRequest = api.list(); + + await flushPromises(); + }); + + it("should request that the pods from all namespaces", () => { + expect(fetchMock.mock.lastCall).toMatchObject([ + "http://127.0.0.1:9999/api-kube/api/v1/pods", + { + headers: { + "content-type": "application/json", + }, + method: "get", + }, + ]); + }); + + describe("when the request resolves with empty data", () => { + beforeEach(async () => { + await fetchMock.resolveSpecific( + ["http://127.0.0.1:9999/api-kube/api/v1/pods"], + new Response(JSON.stringify({ + kind: "PodList", + apiVersion: "v1", + metadata: {}, + items: [], + })), + ); + }); + + it("the call should resolve to an empty list", async () => { + expect(await listRequest).toEqual([]); + }); + }); + }); + + describe("when listing pods with descriptor with namespace=''", () => { + let listRequest: Promise; + + beforeEach(async () => { + listRequest = api.list({ + namespace: "", + }); + + await flushPromises(); + }); + + it("should request that the pods from all namespaces", () => { + expect(fetchMock.mock.lastCall).toMatchObject([ + "http://127.0.0.1:9999/api-kube/api/v1/pods", + { + headers: { + "content-type": "application/json", + }, + method: "get", + }, + ]); + }); + + describe("when the request resolves with empty data", () => { + beforeEach(async () => { + await fetchMock.resolveSpecific( + ["http://127.0.0.1:9999/api-kube/api/v1/pods"], + new Response(JSON.stringify({ + kind: "PodList", + apiVersion: "v1", + metadata: {}, + items: [], + })), + ); + }); + + it("the call should resolve to an empty list", async () => { + expect(await listRequest).toEqual([]); + }); + }); + }); + + describe("when listing pods with descriptor with namespace='default'", () => { + let listRequest: Promise; + + beforeEach(async () => { + listRequest = api.list({ + namespace: "default", + }); + + await flushPromises(); + }); + + it("should request that the pods from just the default namespace", () => { + expect(fetchMock.mock.lastCall).toMatchObject([ + "http://127.0.0.1:9999/api-kube/api/v1/namespaces/default/pods", + { + headers: { + "content-type": "application/json", + }, + method: "get", + }, + ]); + }); + + describe("when the request resolves with empty data", () => { + beforeEach(async () => { + await fetchMock.resolveSpecific( + ["http://127.0.0.1:9999/api-kube/api/v1/namespaces/default/pods"], + new Response(JSON.stringify({ + kind: "PodList", + apiVersion: "v1", + metadata: {}, + items: [], + })), + ); + }); + + it("the call should resolve to an empty list", async () => { + expect(await listRequest).toEqual([]); + }); }); }); }); diff --git a/src/common/k8s-api/api-base.ts b/src/common/k8s-api/api-base.ts index e511dd8454..9544e4f60c 100644 --- a/src/common/k8s-api/api-base.ts +++ b/src/common/k8s-api/api-base.ts @@ -3,38 +3,9 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { JsonApi } from "./json-api"; -import { apiPrefix, isDebugging, isDevelopment } from "../vars"; -import { appEventBus } from "../app-event-bus/event-bus"; +import type { JsonApi } from "./json-api"; +import { getInjectionToken } from "@ogre-tools/injectable"; -export let apiBase: JsonApi; - -if (typeof window === "undefined") { - appEventBus.addListener((event) => { - if (event.name !== "lens-proxy" && event.action !== "listen") return; - - const params = event.params as { port?: number }; - - if (!params.port) return; - - apiBase = new JsonApi({ - serverAddress: `http://127.0.0.1:${params.port}`, - apiBase: apiPrefix, - debug: isDevelopment || isDebugging, - }, { - headers: { - "Host": `localhost:${params.port}`, - }, - }); - }); -} else { - apiBase = new JsonApi({ - serverAddress: `http://127.0.0.1:${window.location.port}`, - apiBase: apiPrefix, - debug: isDevelopment || isDebugging, - }, { - headers: { - "Host": window.location.host, - }, - }); -} +export const apiBaseInjectionToken = getInjectionToken({ + id: "api-base-token", +}); diff --git a/src/common/k8s-api/api-kube.ts b/src/common/k8s-api/api-kube.ts index 4154ec2f00..58aa95208a 100644 --- a/src/common/k8s-api/api-kube.ts +++ b/src/common/k8s-api/api-kube.ts @@ -4,11 +4,8 @@ */ import { getInjectionToken } from "@ogre-tools/injectable"; -import { asLegacyGlobalForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api"; import type { KubeJsonApi } from "./kube-json-api"; export const apiKubeInjectionToken = getInjectionToken({ id: "api-kube-injection-token", }); - -export const apiKube = asLegacyGlobalForExtensionApi(apiKubeInjectionToken); diff --git a/src/common/k8s-api/create-json-api.injectable.ts b/src/common/k8s-api/create-json-api.injectable.ts new file mode 100644 index 0000000000..aa05a5c157 --- /dev/null +++ b/src/common/k8s-api/create-json-api.injectable.ts @@ -0,0 +1,26 @@ +/** + * 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 { RequestInit } from "node-fetch"; +import fetchInjectable from "../fetch/fetch.injectable"; +import loggerInjectable from "../logger.injectable"; +import type { JsonApiConfig, JsonApiData, JsonApiDependencies, JsonApiParams } from "./json-api"; +import { JsonApi } from "./json-api"; + +export type CreateJsonApi = = JsonApiParams>(config: JsonApiConfig, reqInit?: RequestInit) => JsonApi; + +const createJsonApiInjectable = getInjectable({ + id: "create-json-api", + instantiate: (di): CreateJsonApi => { + const deps: JsonApiDependencies = { + fetch: di.inject(fetchInjectable), + logger: di.inject(loggerInjectable), + }; + + return (config, reqInit) => new JsonApi(deps, config, reqInit); + }, +}); + +export default createJsonApiInjectable; diff --git a/src/common/k8s-api/create-kube-api-for-cluster.injectable.ts b/src/common/k8s-api/create-kube-api-for-cluster.injectable.ts new file mode 100644 index 0000000000..e7509dcbc7 --- /dev/null +++ b/src/common/k8s-api/create-kube-api-for-cluster.injectable.ts @@ -0,0 +1,63 @@ +/** + * 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 { apiKubePrefix } from "../vars"; +import isDevelopmentInjectable from "../vars/is-development.injectable"; +import { apiBaseInjectionToken } from "./api-base"; +import createKubeJsonApiInjectable from "./create-kube-json-api.injectable"; +import type { KubeApiOptions } from "./kube-api"; +import { KubeApi } from "./kube-api"; +import type { KubeJsonApiDataFor, KubeObject, KubeObjectConstructor } from "./kube-object"; + +export interface CreateKubeApiForLocalClusterConfig { + metadata: { + uid: string; + }; +} + +export interface CreateKubeApiForCluster { + , Data extends KubeJsonApiDataFor>( + cluster: CreateKubeApiForLocalClusterConfig, + kubeClass: KubeObjectConstructor, + apiClass: new (apiOpts: KubeApiOptions) => Api + ): Api; + >( + cluster: CreateKubeApiForLocalClusterConfig, + kubeClass: KubeObjectConstructor, + apiClass?: new (apiOpts: KubeApiOptions) => KubeApi + ): KubeApi; +} + +const createKubeApiForClusterInjectable = getInjectable({ + id: "create-kube-api-for-cluster", + instantiate: (di): CreateKubeApiForCluster => { + const apiBase = di.inject(apiBaseInjectionToken); + const isDevelopment = di.inject(isDevelopmentInjectable); + const createKubeJsonApi = di.inject(createKubeJsonApiInjectable); + + return ( + cluster: CreateKubeApiForLocalClusterConfig, + kubeClass: KubeObjectConstructor>, + apiClass = KubeApi, + ) => ( + new apiClass({ + objectConstructor: kubeClass, + request: createKubeJsonApi( + { + serverAddress: apiBase.config.serverAddress, + apiBase: apiKubePrefix, + debug: isDevelopment, + }, { + headers: { + "Host": `${cluster.metadata.uid}.localhost:${new URL(apiBase.config.serverAddress).port}`, + }, + }, + ), + }) + ); + }, +}); + +export default createKubeApiForClusterInjectable; diff --git a/src/common/k8s-api/create-kube-api-for-remote-cluster.injectable.ts b/src/common/k8s-api/create-kube-api-for-remote-cluster.injectable.ts new file mode 100644 index 0000000000..d11b7ae34b --- /dev/null +++ b/src/common/k8s-api/create-kube-api-for-remote-cluster.injectable.ts @@ -0,0 +1,104 @@ +/** + * 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 { AgentOptions } from "https"; +import { Agent } from "https"; +import type { RequestInit } from "node-fetch"; +import isDevelopmentInjectable from "../vars/is-development.injectable"; +import createKubeJsonApiInjectable from "./create-kube-json-api.injectable"; +import type { KubeApiOptions } from "./kube-api"; +import { KubeApi } from "./kube-api"; +import type { KubeJsonApiDataFor, KubeObject, KubeObjectConstructor } from "./kube-object"; + +export interface CreateKubeApiForRemoteClusterConfig { + cluster: { + server: string; + caData?: string; + skipTLSVerify?: boolean; + }; + user: { + token?: string | (() => Promise); + clientCertificateData?: string; + clientKeyData?: string; + }; + /** + * Custom instance of https.agent to use for the requests + * + * @remarks the custom agent replaced default agent, options skipTLSVerify, + * clientCertificateData, clientKeyData and caData are ignored. + */ + agent?: Agent; +} + +export interface CreateKubeApiForRemoteCluster { + , Data extends KubeJsonApiDataFor>( + config: CreateKubeApiForRemoteClusterConfig, + kubeClass: KubeObjectConstructor, + apiClass: new (apiOpts: KubeApiOptions) => Api, + ): Api; + >( + config: CreateKubeApiForRemoteClusterConfig, + kubeClass: KubeObjectConstructor, + apiClass?: new (apiOpts: KubeApiOptions) => KubeApi, + ): KubeApi; +} + +const createKubeApiForRemoteClusterInjectable = getInjectable({ + id: "create-kube-api-for-remote-cluster", + instantiate: (di): CreateKubeApiForRemoteCluster => { + const isDevelopment = di.inject(isDevelopmentInjectable); + const createKubeJsonApi = di.inject(createKubeJsonApiInjectable); + + return (config: CreateKubeApiForRemoteClusterConfig, kubeClass: KubeObjectConstructor>, apiClass = KubeApi) => { + const reqInit: RequestInit = {}; + const agentOptions: AgentOptions = {}; + + if (config.cluster.skipTLSVerify === true) { + agentOptions.rejectUnauthorized = false; + } + + if (config.user.clientCertificateData) { + agentOptions.cert = config.user.clientCertificateData; + } + + if (config.user.clientKeyData) { + agentOptions.key = config.user.clientKeyData; + } + + if (config.cluster.caData) { + agentOptions.ca = config.cluster.caData; + } + + if (Object.keys(agentOptions).length > 0) { + reqInit.agent = new Agent(agentOptions); + } + + if (config.agent) { + reqInit.agent = config.agent; + } + + const token = config.user.token; + const request = createKubeJsonApi({ + serverAddress: config.cluster.server, + apiBase: "", + debug: isDevelopment, + ...(token ? { + getRequestOptions: async () => ({ + headers: { + "Authorization": `Bearer ${typeof token === "function" ? await token() : token}`, + }, + }), + } : {}), + }, reqInit); + + return new apiClass({ + objectConstructor: kubeClass, + request, + }); + }; + }, +}); + +export default createKubeApiForRemoteClusterInjectable; diff --git a/src/common/k8s-api/create-kube-json-api-for-cluster.injectable.ts b/src/common/k8s-api/create-kube-json-api-for-cluster.injectable.ts new file mode 100644 index 0000000000..9901731c94 --- /dev/null +++ b/src/common/k8s-api/create-kube-json-api-for-cluster.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 { getInjectable } from "@ogre-tools/injectable"; +import { apiKubePrefix } from "../vars"; +import isDebuggingInjectable from "../vars/is-debugging.injectable"; +import { apiBaseInjectionToken } from "./api-base"; +import createKubeJsonApiInjectable from "./create-kube-json-api.injectable"; +import type { KubeJsonApi } from "./kube-json-api"; + +export type CreateKubeJsonApiForCluster = (clusterId: string) => KubeJsonApi; + +const createKubeJsonApiForClusterInjectable = getInjectable({ + id: "create-kube-json-api-for-cluster", + instantiate: (di): CreateKubeJsonApiForCluster => { + const apiBase = di.inject(apiBaseInjectionToken); + const createKubeJsonApi = di.inject(createKubeJsonApiInjectable); + const isDebugging = di.inject(isDebuggingInjectable); + + return (clusterId) => createKubeJsonApi( + { + serverAddress: apiBase.config.serverAddress, + apiBase: apiKubePrefix, + debug: isDebugging, + }, + { + headers: { + "Host": `${clusterId}.localhost:${new URL(apiBase.config.serverAddress).port}`, + }, + }, + ); + }, +}); + +export default createKubeJsonApiForClusterInjectable; diff --git a/src/common/k8s-api/create-kube-json-api.injectable.ts b/src/common/k8s-api/create-kube-json-api.injectable.ts new file mode 100644 index 0000000000..93de5a0d21 --- /dev/null +++ b/src/common/k8s-api/create-kube-json-api.injectable.ts @@ -0,0 +1,26 @@ +/** + * 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 { RequestInit } from "node-fetch"; +import fetchInjectable from "../fetch/fetch.injectable"; +import loggerInjectable from "../logger.injectable"; +import type { JsonApiConfig, JsonApiDependencies } from "./json-api"; +import { KubeJsonApi } from "./kube-json-api"; + +export type CreateKubeJsonApi = (config: JsonApiConfig, reqInit?: RequestInit) => KubeJsonApi; + +const createKubeJsonApiInjectable = getInjectable({ + id: "create-kube-json-api", + instantiate: (di): CreateKubeJsonApi => { + const dependencies: JsonApiDependencies = { + fetch: di.inject(fetchInjectable), + logger: di.inject(loggerInjectable), + }; + + return (config, reqInit) => new KubeJsonApi(dependencies, config, reqInit); + }, +}); + +export default createKubeJsonApiInjectable; diff --git a/src/common/k8s-api/endpoints/cluster.api.ts b/src/common/k8s-api/endpoints/cluster.api.ts index 082f0adb09..aae4c239ad 100644 --- a/src/common/k8s-api/endpoints/cluster.api.ts +++ b/src/common/k8s-api/endpoints/cluster.api.ts @@ -3,8 +3,6 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type { MetricData, IMetricsReqParams } from "./metrics.api"; -import { metricsApi } from "./metrics.api"; import { KubeObject } from "../kube-object"; import type { DerivedKubeApiOptions, IgnoredKubeApiOptions } from "../kube-api"; import { KubeApi } from "../kube-api"; @@ -28,30 +26,6 @@ export class ClusterApi extends KubeApi { } } -export function getMetricsByNodeNames(nodeNames: string[], params?: IMetricsReqParams): Promise { - const nodes = nodeNames.join("|"); - const opts = { category: "cluster", nodes }; - - return metricsApi.getMetrics({ - memoryUsage: opts, - workloadMemoryUsage: opts, - memoryRequests: opts, - memoryLimits: opts, - memoryCapacity: opts, - memoryAllocatableCapacity: opts, - cpuUsage: opts, - cpuRequests: opts, - cpuLimits: opts, - cpuCapacity: opts, - cpuAllocatableCapacity: opts, - podUsage: opts, - podCapacity: opts, - podAllocatableCapacity: opts, - fsSize: opts, - fsUsage: opts, - }, params); -} - export enum ClusterStatus { ACTIVE = "Active", CREATING = "Creating", @@ -59,21 +33,6 @@ export enum ClusterStatus { ERROR = "Error", } -export interface ClusterMetricData extends Partial> { - memoryUsage: MetricData; - memoryRequests: MetricData; - memoryLimits: MetricData; - memoryCapacity: MetricData; - cpuUsage: MetricData; - cpuRequests: MetricData; - cpuLimits: MetricData; - cpuCapacity: MetricData; - podUsage: MetricData; - podCapacity: MetricData; - fsSize: MetricData; - fsUsage: MetricData; -} - export interface Cluster { spec: { clusterNetwork?: { diff --git a/src/common/k8s-api/endpoints/daemon-set.api.ts b/src/common/k8s-api/endpoints/daemon-set.api.ts index f023652306..49719b04f9 100644 --- a/src/common/k8s-api/endpoints/daemon-set.api.ts +++ b/src/common/k8s-api/endpoints/daemon-set.api.ts @@ -5,8 +5,6 @@ import type { DerivedKubeApiOptions, IgnoredKubeApiOptions } from "../kube-api"; import { KubeApi } from "../kube-api"; -import { metricsApi } from "./metrics.api"; -import type { PodMetricData } from "./pod.api"; import type { KubeObjectStatus, LabelSelector, NamespaceScopedMetadata } from "../kube-object"; import { KubeObject } from "../kube-object"; import type { PodTemplateSpec } from "./types/pod-template-spec"; @@ -90,20 +88,3 @@ export class DaemonSetApi extends KubeApi { }); } } - -export function getMetricsForDaemonSets(daemonsets: DaemonSet[], namespace: string, selector = ""): Promise { - const podSelector = daemonsets.map(daemonset => `${daemonset.getName()}-[[:alnum:]]{5}`).join("|"); - const opts = { category: "pods", pods: podSelector, namespace, selector }; - - return metricsApi.getMetrics({ - cpuUsage: opts, - memoryUsage: opts, - fsUsage: opts, - fsWrites: opts, - fsReads: opts, - networkReceive: opts, - networkTransmit: opts, - }, { - namespace, - }); -} diff --git a/src/common/k8s-api/endpoints/deployment.api.ts b/src/common/k8s-api/endpoints/deployment.api.ts index f67f26f70f..318ea8563f 100644 --- a/src/common/k8s-api/endpoints/deployment.api.ts +++ b/src/common/k8s-api/endpoints/deployment.api.ts @@ -7,8 +7,7 @@ import moment from "moment"; import type { DerivedKubeApiOptions } from "../kube-api"; import { KubeApi } from "../kube-api"; -import { metricsApi } from "./metrics.api"; -import type { PodMetricData, PodSpec } from "./pod.api"; +import type { PodSpec } from "./pod.api"; import type { KubeObjectStatus, LabelSelector, NamespaceScopedMetadata } from "../kube-object"; import { KubeObject } from "../kube-object"; import { hasTypedProperty, isNumber, isObject } from "../../utils"; @@ -70,23 +69,6 @@ export class DeploymentApi extends KubeApi { } } -export function getMetricsForDeployments(deployments: Deployment[], namespace: string, selector = ""): Promise { - const podSelector = deployments.map(deployment => `${deployment.getName()}-[[:alnum:]]{9,}-[[:alnum:]]{5}`).join("|"); - const opts = { category: "pods", pods: podSelector, namespace, selector }; - - return metricsApi.getMetrics({ - cpuUsage: opts, - memoryUsage: opts, - fsUsage: opts, - fsWrites: opts, - fsReads: opts, - networkReceive: opts, - networkTransmit: opts, - }, { - namespace, - }); -} - export interface DeploymentSpec { replicas: number; selector: LabelSelector; diff --git a/src/common/k8s-api/endpoints/helm-charts.api.ts b/src/common/k8s-api/endpoints/helm-charts.api.ts index f4ba0cf45f..26da740830 100644 --- a/src/common/k8s-api/endpoints/helm-charts.api.ts +++ b/src/common/k8s-api/endpoints/helm-charts.api.ts @@ -3,72 +3,9 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { compile } from "path-to-regexp"; -import { apiBase } from "../index"; -import { stringify } from "querystring"; -import type { RequestInit } from "node-fetch"; -import { autoBind, bifurcateArray, isDefined } from "../../utils"; +import { autoBind, bifurcateArray } from "../../utils"; import Joi from "joi"; -export type RepoHelmChartList = Record; - -export interface IHelmChartDetails { - readme: string; - versions: HelmChart[]; -} - -const endpoint = compile(`/v2/charts/:repo?/:name?`) as (params?: { - repo?: string; - name?: string; -}) => string; - -/** - * Get a list of all helm charts from all saved helm repos - */ -export async function listCharts(): Promise { - const data = await apiBase.get>(endpoint()); - - return Object - .values(data) - .reduce((allCharts, repoCharts) => allCharts.concat(Object.values(repoCharts)), new Array()) - .map(([chart]) => HelmChart.create(chart, { onError: "log" })) - .filter(isDefined); -} - -export interface GetChartDetailsOptions { - version?: string; - reqInit?: RequestInit; -} - -/** - * Get the readme and all versions of a chart - * @param repo The repo to get from - * @param name The name of the chart to request the data of - * @param options.version The version of the chart's readme to get, default latest - * @param options.reqInit A way for passing in an abort controller or other browser request options - */ -export async function getChartDetails(repo: string, name: string, { version, reqInit }: GetChartDetailsOptions = {}): Promise { - const path = endpoint({ repo, name }); - - const { readme, ...data } = await apiBase.get(`${path}?${stringify({ version })}`, undefined, reqInit); - const versions = data.versions.map(version => HelmChart.create(version, { onError: "log" })).filter(isDefined); - - return { - readme, - versions, - }; -} - -/** - * Get chart values related to a specific repos' version of a chart - * @param repo The repo to get from - * @param name The name of the chart to request the data of - * @param version The version to get the values from - */ -export async function getChartValues(repo: string, name: string, version: string): Promise { - return apiBase.get(`/v2/charts/${repo}/${name}/values?${stringify({ version })}`); -} - export interface RawHelmChart { apiVersion: string; name: string; diff --git a/src/common/k8s-api/endpoints/helm-charts.api/request-charts.injectable.ts b/src/common/k8s-api/endpoints/helm-charts.api/request-charts.injectable.ts new file mode 100644 index 0000000000..89de5f9d17 --- /dev/null +++ b/src/common/k8s-api/endpoints/helm-charts.api/request-charts.injectable.ts @@ -0,0 +1,34 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { apiBaseInjectionToken } from "../../api-base"; +import type { RawHelmChart } from "../helm-charts.api"; +import { HelmChart } from "../helm-charts.api"; +import { isDefined } from "../../../utils"; + +export type RequestHelmCharts = () => Promise; +export type RepoHelmChartList = Record; + +/** + * Get a list of all helm charts from all saved helm repos + */ +const requestHelmChartsInjectable = getInjectable({ + id: "request-helm-charts", + instantiate: (di) => { + const apiBase = di.inject(apiBaseInjectionToken); + + return async () => { + const data = await apiBase.get>("/v2/charts"); + + return Object + .values(data) + .reduce((allCharts, repoCharts) => allCharts.concat(Object.values(repoCharts)), new Array()) + .map(([chart]) => HelmChart.create(chart, { onError: "log" })) + .filter(isDefined); + }; + }, +}); + +export default requestHelmChartsInjectable; diff --git a/src/common/k8s-api/endpoints/helm-charts.api/request-readme.injectable.ts b/src/common/k8s-api/endpoints/helm-charts.api/request-readme.injectable.ts new file mode 100644 index 0000000000..c6815c4b93 --- /dev/null +++ b/src/common/k8s-api/endpoints/helm-charts.api/request-readme.injectable.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { urlBuilderFor } from "../../../utils/buildUrl"; +import { apiBaseInjectionToken } from "../../api-base"; + +const requestReadmeEndpoint = urlBuilderFor("/v2/charts/:repo/:name/readme"); + +export type RequestHelmChartReadme = (repo: string, name: string, version?: string) => Promise; + +const requestHelmChartReadmeInjectable = getInjectable({ + id: "request-helm-chart-readme", + instantiate: (di): RequestHelmChartReadme => { + const apiBase = di.inject(apiBaseInjectionToken); + + return (repo, name, version) => ( + apiBase.get(requestReadmeEndpoint.compile({ name, repo }, { version })) + ); + }, +}); + +export default requestHelmChartReadmeInjectable; diff --git a/src/common/k8s-api/endpoints/helm-charts.api/request-values.injectable.ts b/src/common/k8s-api/endpoints/helm-charts.api/request-values.injectable.ts new file mode 100644 index 0000000000..ec927fc37a --- /dev/null +++ b/src/common/k8s-api/endpoints/helm-charts.api/request-values.injectable.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { urlBuilderFor } from "../../../utils/buildUrl"; +import { apiBaseInjectionToken } from "../../api-base"; + +const requestValuesEndpoint = urlBuilderFor("/v2/charts/:repo/:name/values"); + +export type RequestHelmChartValues = (repo: string, name: string, version: string) => Promise; + +const requestHelmChartValuesInjectable = getInjectable({ + id: "request-helm-chart-values", + instantiate: (di): RequestHelmChartValues => { + const apiBase = di.inject(apiBaseInjectionToken); + + return (repo, name, version) => ( + apiBase.get(requestValuesEndpoint.compile({ repo, name }, { version })) + ); + }, +}); + +export default requestHelmChartValuesInjectable; diff --git a/src/common/k8s-api/endpoints/helm-charts.api/request-versions.injectable.ts b/src/common/k8s-api/endpoints/helm-charts.api/request-versions.injectable.ts new file mode 100644 index 0000000000..410d8ea596 --- /dev/null +++ b/src/common/k8s-api/endpoints/helm-charts.api/request-versions.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 { getInjectable } from "@ogre-tools/injectable"; +import { urlBuilderFor } from "../../../utils/buildUrl"; +import { apiBaseInjectionToken } from "../../api-base"; +import { HelmChart } from "../helm-charts.api"; +import type { RawHelmChart } from "../helm-charts.api"; +import { isDefined } from "../../../utils"; + +const requestVersionsEndpoint = urlBuilderFor("/v2/charts/:repo/:name/versions"); + +export type RequestHelmChartVersions = (repo: string, chartName: string) => Promise; + +const requestHelmChartVersionsInjectable = getInjectable({ + id: "request-helm-chart-versions", + instantiate: (di): RequestHelmChartVersions => { + const apiBase = di.inject(apiBaseInjectionToken); + + return async (repo, name) => { + const rawVersions = await apiBase.get(requestVersionsEndpoint.compile({ name, repo })) as RawHelmChart[]; + + return rawVersions + .map(version => HelmChart.create(version, { onError: "log" })) + .filter(isDefined); + }; + }, +}); + +export default requestHelmChartVersionsInjectable; diff --git a/src/common/k8s-api/endpoints/helm-releases.api.ts b/src/common/k8s-api/endpoints/helm-releases.api.ts index f9e11073c0..3460378e59 100644 --- a/src/common/k8s-api/endpoints/helm-releases.api.ts +++ b/src/common/k8s-api/endpoints/helm-releases.api.ts @@ -3,65 +3,14 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { apiBase } from "../index"; import type { ItemObject } from "../../item.store"; -import type { JsonApiData } from "../json-api"; -import { buildURLPositional } from "../../utils/buildUrl"; -import type { HelmReleaseDetails } from "../../../renderer/components/+helm-releases/release-details/release-details-model/call-for-helm-release/call-for-helm-release-details/call-for-helm-release-details.injectable"; +import type { HelmReleaseDetails } from "./helm-releases.api/request-details.injectable"; export interface HelmReleaseUpdateDetails { log: string; release: HelmReleaseDetails; } -export interface HelmReleaseRevision { - revision: number; - updated: string; - status: string; - chart: string; - app_version: string; - description: string; -} - -type EndpointParams = {} - | { namespace: string } - | { namespace: string; name: string } - | { namespace: string; name: string; route: string }; - -interface EndpointQuery { - all?: boolean; -} - -export const endpoint = buildURLPositional("/v2/releases/:namespace?/:name?/:route?"); - -export async function deleteRelease(name: string, namespace: string): Promise { - const path = endpoint({ name, namespace }); - - return apiBase.del(path); -} - -export async function getReleaseValues(name: string, namespace: string, all?: boolean): Promise { - const route = "values"; - const path = endpoint({ name, namespace, route }, { all }); - - return apiBase.get(path); -} - -export async function getReleaseHistory(name: string, namespace: string): Promise { - const route = "history"; - const path = endpoint({ name, namespace, route }); - - return apiBase.get(path); -} - -export async function rollbackRelease(name: string, namespace: string, revision: number): Promise { - const route = "rollback"; - const path = endpoint({ name, namespace, route }); - const data = { revision }; - - return apiBase.put(path, { data }); -} - export interface HelmReleaseDto { appVersion: string; name: string; diff --git a/src/common/k8s-api/endpoints/helm-releases.api/request-configuration.global-override-for-injectable.ts b/src/common/k8s-api/endpoints/helm-releases.api/request-configuration.global-override-for-injectable.ts new file mode 100644 index 0000000000..18b3bd0ec0 --- /dev/null +++ b/src/common/k8s-api/endpoints/helm-releases.api/request-configuration.global-override-for-injectable.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getGlobalOverride } from "../../../test-utils/get-global-override"; +import requestHelmReleaseConfigurationInjectable from "./request-configuration.injectable"; + +export default getGlobalOverride(requestHelmReleaseConfigurationInjectable, () => () => { + throw new Error("Tried to call requestHelmReleaseConfiguration with no override"); +}); diff --git a/src/common/k8s-api/endpoints/helm-releases.api/request-configuration.injectable.ts b/src/common/k8s-api/endpoints/helm-releases.api/request-configuration.injectable.ts new file mode 100644 index 0000000000..1bfe168b11 --- /dev/null +++ b/src/common/k8s-api/endpoints/helm-releases.api/request-configuration.injectable.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { urlBuilderFor } from "../../../utils/buildUrl"; +import { apiBaseInjectionToken } from "../../api-base"; + +export type RequestHelmReleaseConfiguration = ( + name: string, + namespace: string, + all: boolean +) => Promise; + +const requestConfigurationEnpoint = urlBuilderFor("/v2/releases/:namespace/:name/values"); + +const requestHelmReleaseConfigurationInjectable = getInjectable({ + id: "request-helm-release-configuration", + + instantiate: (di): RequestHelmReleaseConfiguration => { + const apiBase = di.inject(apiBaseInjectionToken); + + return (name, namespace, all: boolean) => ( + apiBase.get(requestConfigurationEnpoint.compile({ name, namespace }, { all })) + ); + }, +}); + +export default requestHelmReleaseConfigurationInjectable; diff --git a/src/common/k8s-api/endpoints/helm-releases.api/request-create.injectable.ts b/src/common/k8s-api/endpoints/helm-releases.api/request-create.injectable.ts new file mode 100644 index 0000000000..bad802d3cc --- /dev/null +++ b/src/common/k8s-api/endpoints/helm-releases.api/request-create.injectable.ts @@ -0,0 +1,42 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import yaml from "js-yaml"; +import { getInjectable } from "@ogre-tools/injectable"; +import type { HelmReleaseUpdateDetails } from "../helm-releases.api"; +import { apiBaseInjectionToken } from "../../api-base"; +import { urlBuilderFor } from "../../../utils/buildUrl"; + +interface HelmReleaseCreatePayload { + name?: string; + repo: string; + chart: string; + namespace: string; + version: string; + values: string; +} + +export type RequestCreateHelmRelease = (payload: HelmReleaseCreatePayload) => Promise; + +const requestCreateEndpoint = urlBuilderFor("/v2/releases"); + +const requestCreateHelmReleaseInjectable = getInjectable({ + id: "request-create-helm-release", + + instantiate: (di): RequestCreateHelmRelease => { + const apiBase = di.inject(apiBaseInjectionToken); + + return ({ repo, chart, values, ...data }) => { + return apiBase.post(requestCreateEndpoint.compile({}), { + data: { + chart: `${repo}/${chart}`, + values: yaml.load(values), + ...data, + }, + }); + }; + }, +}); + +export default requestCreateHelmReleaseInjectable; diff --git a/src/common/k8s-api/endpoints/helm-releases.api/request-delete.injectable.ts b/src/common/k8s-api/endpoints/helm-releases.api/request-delete.injectable.ts new file mode 100644 index 0000000000..66b2013770 --- /dev/null +++ b/src/common/k8s-api/endpoints/helm-releases.api/request-delete.injectable.ts @@ -0,0 +1,22 @@ +/** + * 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 { urlBuilderFor } from "../../../utils/buildUrl"; +import { apiBaseInjectionToken } from "../../api-base"; + +export type RequestDeleteHelmRelease = (name: string, namespace: string) => Promise; + +const requestDeleteEndpoint = urlBuilderFor("/v2/releases/:namespace/:name"); + +const requestDeleteHelmReleaseInjectable = getInjectable({ + id: "request-delete-helm-release", + instantiate: (di): RequestDeleteHelmRelease => { + const apiBase = di.inject(apiBaseInjectionToken); + + return (name, namespace) => apiBase.del(requestDeleteEndpoint.compile({ name, namespace })); + }, +}); + +export default requestDeleteHelmReleaseInjectable; diff --git a/src/common/k8s-api/endpoints/helm-releases.api/request-details.injectable.ts b/src/common/k8s-api/endpoints/helm-releases.api/request-details.injectable.ts new file mode 100644 index 0000000000..0b712cd78e --- /dev/null +++ b/src/common/k8s-api/endpoints/helm-releases.api/request-details.injectable.ts @@ -0,0 +1,41 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import type { KubeJsonApiData } from "../../kube-json-api"; +import { apiBaseInjectionToken } from "../../api-base"; +import { urlBuilderFor } from "../../../utils/buildUrl"; + +export interface HelmReleaseDetails { + resources: KubeJsonApiData[]; + name: string; + namespace: string; + version: string; + config: string; // release values + manifest: string; + info: { + deleted: string; + description: string; + first_deployed: string; + last_deployed: string; + notes: string; + status: string; + }; +} + +export type CallForHelmReleaseDetails = (name: string, namespace: string) => Promise; + +const requestDetailsEnpoint = urlBuilderFor("/v2/releases/:namespace/:name"); + +const requestHelmReleaseDetailsInjectable = getInjectable({ + id: "call-for-helm-release-details", + + instantiate: (di): CallForHelmReleaseDetails => { + const apiBase = di.inject(apiBaseInjectionToken); + + return (name, namespace) => apiBase.get(requestDetailsEnpoint.compile({ name, namespace })); + }, +}); + +export default requestHelmReleaseDetailsInjectable; diff --git a/src/common/k8s-api/endpoints/helm-releases.api/request-history.injectable.ts b/src/common/k8s-api/endpoints/helm-releases.api/request-history.injectable.ts new file mode 100644 index 0000000000..b6e9794fe7 --- /dev/null +++ b/src/common/k8s-api/endpoints/helm-releases.api/request-history.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 { getInjectable } from "@ogre-tools/injectable"; +import { urlBuilderFor } from "../../../utils/buildUrl"; +import { apiBaseInjectionToken } from "../../api-base"; + +export interface HelmReleaseRevision { + revision: number; + updated: string; + status: string; + chart: string; + app_version: string; + description: string; +} + +export type RequestHelmReleaseHistory = (name: string, namespace: string) => Promise; + +const requestHistoryEnpoint = urlBuilderFor("/v2/releases/:namespace/:name/history"); + +const requestHelmReleaseHistoryInjectable = getInjectable({ + id: "request-helm-release-history", + instantiate: (di): RequestHelmReleaseHistory => { + const apiBase = di.inject(apiBaseInjectionToken); + + return (name, namespace) => apiBase.get(requestHistoryEnpoint.compile({ name, namespace })); + }, +}); + +export default requestHelmReleaseHistoryInjectable; diff --git a/src/common/k8s-api/endpoints/helm-releases.api/request-releases.injectable.ts b/src/common/k8s-api/endpoints/helm-releases.api/request-releases.injectable.ts new file mode 100644 index 0000000000..2619e289c6 --- /dev/null +++ b/src/common/k8s-api/endpoints/helm-releases.api/request-releases.injectable.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { urlBuilderFor } from "../../../utils/buildUrl"; +import { apiBaseInjectionToken } from "../../api-base"; +import type { HelmReleaseDto } from "../helm-releases.api"; + +export type RequestHelmReleases = (namespace?: string) => Promise; + +const requestHelmReleasesEndpoint = urlBuilderFor("/v2/releases/:namespace?"); + +const requestHelmReleasesInjectable = getInjectable({ + id: "request-helm-releases", + + instantiate: (di): RequestHelmReleases => { + const apiBase = di.inject(apiBaseInjectionToken); + + return (namespace) => apiBase.get(requestHelmReleasesEndpoint.compile({ namespace })); + }, +}); + +export default requestHelmReleasesInjectable; diff --git a/src/common/k8s-api/endpoints/helm-releases.api/request-rollback.injectable.ts b/src/common/k8s-api/endpoints/helm-releases.api/request-rollback.injectable.ts new file mode 100644 index 0000000000..1a9229b73a --- /dev/null +++ b/src/common/k8s-api/endpoints/helm-releases.api/request-rollback.injectable.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { urlBuilderFor } from "../../../utils/buildUrl"; +import { apiBaseInjectionToken } from "../../api-base"; + +export type RequestHelmReleaseRollback = (name: string, namespace: string, revision: number) => Promise; + +const requestRollbackEndpoint = urlBuilderFor("/v2/releases/:namespace/:name"); + +const requestHelmReleaseRollbackInjectable = getInjectable({ + id: "request-helm-release-rollback", + instantiate: (di): RequestHelmReleaseRollback => { + const apiBase = di.inject(apiBaseInjectionToken); + + return async (name, namespace, revision) => { + await apiBase.put( + requestRollbackEndpoint.compile({ name, namespace }), + { data: { revision }}, + ); + }; + }, +}); + +export default requestHelmReleaseRollbackInjectable; diff --git a/src/common/k8s-api/endpoints/helm-releases.api/request-update.injectable.ts b/src/common/k8s-api/endpoints/helm-releases.api/request-update.injectable.ts new file mode 100644 index 0000000000..2703ea2c56 --- /dev/null +++ b/src/common/k8s-api/endpoints/helm-releases.api/request-update.injectable.ts @@ -0,0 +1,49 @@ +/** + * 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 yaml from "js-yaml"; +import { apiBaseInjectionToken } from "../../api-base"; +import { urlBuilderFor } from "../../../utils/buildUrl"; + +interface HelmReleaseUpdatePayload { + repo: string; + chart: string; + version: string; + values: string; +} + +export type RequestHelmReleaseUpdate = ( + name: string, + namespace: string, + payload: HelmReleaseUpdatePayload +) => Promise<{ updateWasSuccessful: true } | { updateWasSuccessful: false; error: unknown }>; + +const requestUpdateEndpoint = urlBuilderFor("/v2/releases/:namespace/:name"); + +const requestHelmReleaseUpdateInjectable = getInjectable({ + id: "request-helm-release-update", + + instantiate: (di): RequestHelmReleaseUpdate => { + const apiBase = di.inject(apiBaseInjectionToken); + + return async (name, namespace, { repo, chart, values, ...data }) => { + try { + await apiBase.put(requestUpdateEndpoint.compile({ name, namespace }), { + data: { + chart: `${repo}/${chart}`, + values: yaml.load(values), + ...data, + }, + }); + } catch (e) { + return { updateWasSuccessful: false, error: e }; + } + + return { updateWasSuccessful: true }; + }; + }, +}); + +export default requestHelmReleaseUpdateInjectable; diff --git a/src/common/k8s-api/endpoints/helm-releases.api/request-values.injectable.ts b/src/common/k8s-api/endpoints/helm-releases.api/request-values.injectable.ts new file mode 100644 index 0000000000..d30f9973c4 --- /dev/null +++ b/src/common/k8s-api/endpoints/helm-releases.api/request-values.injectable.ts @@ -0,0 +1,22 @@ +/** + * 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 { urlBuilderFor } from "../../../utils/buildUrl"; +import { apiBaseInjectionToken } from "../../api-base"; + +export type RequestHelmReleaseValues = (name: string, namespace: string, all?: boolean) => Promise; + +const requestValuesEndpoint = urlBuilderFor("/v2/release/:namespace/:name/values"); + +const requestHelmReleaseValuesInjectable = getInjectable({ + id: "request-helm-release-values", + instantiate: (di): RequestHelmReleaseValues => { + const apiBase = di.inject(apiBaseInjectionToken); + + return (name, namespace, all) => apiBase.get(requestValuesEndpoint.compile({ name, namespace }, { all })); + }, +}); + +export default requestHelmReleaseValuesInjectable; diff --git a/src/common/k8s-api/endpoints/ingress.api.ts b/src/common/k8s-api/endpoints/ingress.api.ts index 84910d2783..d223530013 100644 --- a/src/common/k8s-api/endpoints/ingress.api.ts +++ b/src/common/k8s-api/endpoints/ingress.api.ts @@ -6,8 +6,6 @@ import type { NamespaceScopedMetadata, TypedLocalObjectReference } from "../kube-object"; import { KubeObject } from "../kube-object"; import { hasTypedProperty, isString, iter } from "../../utils"; -import type { MetricData } from "./metrics.api"; -import { metricsApi } from "./metrics.api"; import type { DerivedKubeApiOptions, IgnoredKubeApiOptions } from "../kube-api"; import { KubeApi } from "../kube-api"; import type { RequireExactlyOne } from "type-fest"; @@ -24,26 +22,6 @@ export class IngressApi extends KubeApi { } } -export function getMetricsForIngress(ingress: string, namespace: string): Promise { - const opts = { category: "ingress", ingress, namespace }; - - return metricsApi.getMetrics({ - bytesSentSuccess: opts, - bytesSentFailure: opts, - requestDurationSeconds: opts, - responseDurationSeconds: opts, - }, { - namespace, - }); -} - -export interface IngressMetricData extends Partial> { - bytesSentSuccess: MetricData; - bytesSentFailure: MetricData; - requestDurationSeconds: MetricData; - responseDurationSeconds: MetricData; -} - export interface ILoadBalancerIngress { hostname?: string; ip?: string; diff --git a/src/common/k8s-api/endpoints/job.api.ts b/src/common/k8s-api/endpoints/job.api.ts index 84e166ecc4..66f4ef3768 100644 --- a/src/common/k8s-api/endpoints/job.api.ts +++ b/src/common/k8s-api/endpoints/job.api.ts @@ -5,8 +5,7 @@ import type { DerivedKubeApiOptions, IgnoredKubeApiOptions } from "../kube-api"; import { KubeApi } from "../kube-api"; -import { metricsApi } from "./metrics.api"; -import type { PodMetricData, PodSpec } from "./pod.api"; +import type { PodSpec } from "./pod.api"; import type { Container } from "./types/container"; import type { KubeObjectStatus, LabelSelector, NamespaceScopedMetadata } from "../kube-object"; import { KubeObject } from "../kube-object"; @@ -103,20 +102,3 @@ export class JobApi extends KubeApi { }); } } - -export function getMetricsForJobs(jobs: Job[], namespace: string, selector = ""): Promise { - const podSelector = jobs.map(job => `${job.getName()}-[[:alnum:]]{5}`).join("|"); - const opts = { category: "pods", pods: podSelector, namespace, selector }; - - return metricsApi.getMetrics({ - cpuUsage: opts, - memoryUsage: opts, - fsUsage: opts, - fsWrites: opts, - fsReads: opts, - networkReceive: opts, - networkTransmit: opts, - }, { - namespace, - }); -} diff --git a/src/common/k8s-api/endpoints/metrics.api.ts b/src/common/k8s-api/endpoints/metrics.api.ts index b5469e8a24..406ab1d0b2 100644 --- a/src/common/k8s-api/endpoints/metrics.api.ts +++ b/src/common/k8s-api/endpoints/metrics.api.ts @@ -6,7 +6,7 @@ // Metrics api import moment from "moment"; -import { apiBase } from "../index"; +import { isDefined, object } from "../../utils"; export interface MetricData { status: string; @@ -29,63 +29,6 @@ export interface MetricResult { values: [number, string][]; } -export interface MetricProviderInfo { - name: string; - id: string; - isConfigurable: boolean; -} - -export interface IMetricsReqParams { - start?: number | string; // timestamp in seconds or valid date-string - end?: number | string; - step?: number; // step in seconds (default: 60s = each point 1m) - range?: number; // time-range in seconds for data aggregation (default: 3600s = last 1h) - namespace?: string; // rbac-proxy validation param -} - -export interface IResourceMetrics { - [metric: string]: T; - cpuUsage: T; - memoryUsage: T; - fsUsage: T; - fsWrites: T; - fsReads: T; - networkReceive: T; - networkTransmit: T; -} - -async function getMetrics(query: string, reqParams?: IMetricsReqParams): Promise; -async function getMetrics(query: string[], reqParams?: IMetricsReqParams): Promise; -async function getMetrics(query: Record>>, reqParams?: IMetricsReqParams): Promise>; - -async function getMetrics(query: string | string[] | Partial>>>, reqParams: IMetricsReqParams = {}): Promise>> { - const { range = 3600, step = 60, namespace } = reqParams; - let { start, end } = reqParams; - - if (!start && !end) { - const timeNow = Date.now() / 1000; - const now = moment.unix(timeNow).startOf("minute").unix(); // round date to minutes - - start = now - range; - end = now; - } - - return apiBase.post("/metrics", { - data: query, - query: { - start, end, step, - "kubernetes_namespace": namespace, - }, - }); -} - -export const metricsApi = { - getMetrics, - async getMetricProviders(): Promise { - return apiBase.get("/metrics/providers"); - }, -}; - export function normalizeMetrics(metrics: MetricData | undefined | null, frames = 60): MetricData { if (!metrics?.data?.result) { return { @@ -145,7 +88,7 @@ export function isMetricsEmpty(metrics: Partial>) { return Object.values(metrics).every(metric => !metric?.data?.result?.length); } -export function getItemMetrics(metrics: Partial> | null | undefined, itemName: string): Partial> | undefined { +export function getItemMetrics(metrics: Partial> | null | undefined, itemName: string): Partial> | undefined { if (!metrics) { return undefined; } @@ -166,22 +109,16 @@ export function getItemMetrics(metrics: Partial> | nu return itemMetrics; } -export function getMetricLastPoints>>(metrics: T): Record { - const result: Partial<{ [metric: string]: number }> = {}; - - Object.keys(metrics).forEach(metricName => { - try { - const metric = metrics[metricName]; - - if (metric?.data.result.length) { - result[metricName] = +metric.data.result[0].values.slice(-1)[0][1]; - } - } catch { - // ignore error - } - - return result; - }, {}); - - return result as Record; +export function getMetricLastPoints(metrics: Partial>): Partial> { + return object.fromEntries( + object.entries(metrics) + .map(([metricName, metric]) => { + try { + return [metricName, +metric.data.result[0].values.slice(-1)[0][1]] as const; + } catch { + return undefined; + } + }) + .filter(isDefined), + ); } diff --git a/src/common/k8s-api/endpoints/metrics.api/request-cluster-metrics-by-node-names.injectable.ts b/src/common/k8s-api/endpoints/metrics.api/request-cluster-metrics-by-node-names.injectable.ts new file mode 100644 index 0000000000..84268e3f6a --- /dev/null +++ b/src/common/k8s-api/endpoints/metrics.api/request-cluster-metrics-by-node-names.injectable.ts @@ -0,0 +1,63 @@ +/** + * 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 { MetricData } from "../metrics.api"; +import type { RequestMetricsParams } from "./request-metrics.injectable"; +import requestMetricsInjectable from "./request-metrics.injectable"; + +export interface ClusterMetricData { + memoryUsage: MetricData; + memoryRequests: MetricData; + memoryLimits: MetricData; + memoryCapacity: MetricData; + memoryAllocatableCapacity: MetricData; + cpuUsage: MetricData; + cpuRequests: MetricData; + cpuLimits: MetricData; + cpuCapacity: MetricData; + cpuAllocatableCapacity: MetricData; + podUsage: MetricData; + podCapacity: MetricData; + podAllocatableCapacity: MetricData; + fsSize: MetricData; + fsUsage: MetricData; +} + +export type RequestClusterMetricsByNodeNames = (nodeNames: string[], params?: RequestMetricsParams) => Promise; + +const requestClusterMetricsByNodeNamesInjectable = getInjectable({ + id: "get-cluster-metrics-by-node-names", + instantiate: (di): RequestClusterMetricsByNodeNames => { + const requestMetrics = di.inject(requestMetricsInjectable); + + return (nodeNames, params) => { + const opts = { + category: "cluster", + nodes: nodeNames.join("|"), + }; + + return requestMetrics({ + memoryUsage: opts, + workloadMemoryUsage: opts, + memoryRequests: opts, + memoryLimits: opts, + memoryCapacity: opts, + memoryAllocatableCapacity: opts, + cpuUsage: opts, + cpuRequests: opts, + cpuLimits: opts, + cpuCapacity: opts, + cpuAllocatableCapacity: opts, + podUsage: opts, + podCapacity: opts, + podAllocatableCapacity: opts, + fsSize: opts, + fsUsage: opts, + }, params); + }; + }, +}); + +export default requestClusterMetricsByNodeNamesInjectable; diff --git a/src/common/k8s-api/endpoints/metrics.api/request-ingress-metrics.injectable.ts b/src/common/k8s-api/endpoints/metrics.api/request-ingress-metrics.injectable.ts new file mode 100644 index 0000000000..8167a6bef4 --- /dev/null +++ b/src/common/k8s-api/endpoints/metrics.api/request-ingress-metrics.injectable.ts @@ -0,0 +1,38 @@ +/** + * 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 { MetricData } from "../metrics.api"; +import requestMetricsInjectable from "./request-metrics.injectable"; + +export interface IngressMetricData { + bytesSentSuccess: MetricData; + bytesSentFailure: MetricData; + requestDurationSeconds: MetricData; + responseDurationSeconds: MetricData; +} + +export type RequestIngressMetrics = (ingress: string, namespace: string) => Promise; + +const requestIngressMetricsInjectable = getInjectable({ + id: "request-ingress-metrics", + instantiate: (di): RequestIngressMetrics => { + const requestMetrics = di.inject(requestMetricsInjectable); + + return (ingress, namespace) => { + const opts = { category: "ingress", ingress, namespace }; + + return requestMetrics({ + bytesSentSuccess: opts, + bytesSentFailure: opts, + requestDurationSeconds: opts, + responseDurationSeconds: opts, + }, { + namespace, + }); + }; + }, +}); + +export default requestIngressMetricsInjectable; diff --git a/src/common/k8s-api/endpoints/metrics.api/request-metrics-for-all-nodes.injectable.ts b/src/common/k8s-api/endpoints/metrics.api/request-metrics-for-all-nodes.injectable.ts new file mode 100644 index 0000000000..6789d6debc --- /dev/null +++ b/src/common/k8s-api/endpoints/metrics.api/request-metrics-for-all-nodes.injectable.ts @@ -0,0 +1,44 @@ +/** + * 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 { MetricData } from "../metrics.api"; +import requestMetricsInjectable from "./request-metrics.injectable"; + +export interface NodeMetricData { + memoryUsage: MetricData; + workloadMemoryUsage: MetricData; + memoryCapacity: MetricData; + memoryAllocatableCapacity: MetricData; + cpuUsage: MetricData; + cpuCapacity: MetricData; + fsUsage: MetricData; + fsSize: MetricData; +} + +export type RequestAllNodeMetrics = () => Promise; + +const requestAllNodeMetricsInjectable = getInjectable({ + id: "request-all-node-metrics", + instantiate: (di): RequestAllNodeMetrics => { + const requestMetrics = di.inject(requestMetricsInjectable); + + return () => { + const opts = { category: "nodes" }; + + return requestMetrics({ + memoryUsage: opts, + workloadMemoryUsage: opts, + memoryCapacity: opts, + memoryAllocatableCapacity: opts, + cpuUsage: opts, + cpuCapacity: opts, + fsSize: opts, + fsUsage: opts, + }); + }; + }, +}); + +export default requestAllNodeMetricsInjectable; diff --git a/src/common/k8s-api/endpoints/metrics.api/request-metrics.injectable.ts b/src/common/k8s-api/endpoints/metrics.api/request-metrics.injectable.ts new file mode 100644 index 0000000000..0da0bc95ec --- /dev/null +++ b/src/common/k8s-api/endpoints/metrics.api/request-metrics.injectable.ts @@ -0,0 +1,73 @@ +/** + * 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 { getSecondsFromUnixEpoch } from "../../../utils/date/get-current-date-time"; +import { apiBaseInjectionToken } from "../../api-base"; +import type { MetricData } from "../metrics.api"; + + +export interface RequestMetricsParams { + /** + * timestamp in seconds or valid date-string + */ + start?: number | string; + + /** + * timestamp in seconds or valid date-string + */ + end?: number | string; + + /** + * step in seconds + * @default 60 (1 minute) + */ + step?: number; + + /** + * time-range in seconds for data aggregation + * @default 3600 (1 hour) + */ + range?: number; + + /** + * rbac-proxy validation param + */ + namespace?: string; +} + +export interface RequestMetrics { + (query: string, params?: RequestMetricsParams): Promise; + (query: string[], params?: RequestMetricsParams): Promise; + (query: Record>>, params?: RequestMetricsParams): Promise>; +} + +const requestMetricsInjectable = getInjectable({ + id: "request-metrics", + instantiate: (di) => { + const apiBase = di.inject(apiBaseInjectionToken); + + return (async (query: object, params: RequestMetricsParams = {}) => { + const { range = 3600, step = 60, namespace } = params; + let { start, end } = params; + + if (!start && !end) { + const now = getSecondsFromUnixEpoch(); + + start = now - range; + end = now; + } + + return apiBase.post("/metrics", { + data: query, + query: { + start, end, step, + "kubernetes_namespace": namespace, + }, + }); + }) as RequestMetrics; + }, +}); + +export default requestMetricsInjectable; diff --git a/src/common/k8s-api/endpoints/metrics.api/request-persistent-volume-claim-metrics.injectable.ts b/src/common/k8s-api/endpoints/metrics.api/request-persistent-volume-claim-metrics.injectable.ts new file mode 100644 index 0000000000..5cafb5ade0 --- /dev/null +++ b/src/common/k8s-api/endpoints/metrics.api/request-persistent-volume-claim-metrics.injectable.ts @@ -0,0 +1,35 @@ +/** + * 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 { MetricData } from "../metrics.api"; +import type { PersistentVolumeClaim } from "../persistent-volume-claim.api"; +import requestMetricsInjectable from "./request-metrics.injectable"; + +export interface PersistentVolumeClaimMetricData { + diskUsage: MetricData; + diskCapacity: MetricData; +} + +export type RequestPersistentVolumeClaimMetrics = (claim: PersistentVolumeClaim) => Promise; + +const requestPersistentVolumeClaimMetricsInjectable = getInjectable({ + id: "request-persistent-volume-claim-metrics", + instantiate: (di): RequestPersistentVolumeClaimMetrics => { + const requestMetrics = di.inject(requestMetricsInjectable); + + return (claim) => { + const opts = { category: "pvc", pvc: claim.getName(), namespace: claim.getNs() }; + + return requestMetrics({ + diskUsage: opts, + diskCapacity: opts, + }, { + namespace: opts.namespace, + }); + }; + }, +}); + +export default requestPersistentVolumeClaimMetricsInjectable; diff --git a/src/common/k8s-api/endpoints/metrics.api/request-pod-metrics-for-daemon-sets.injectable.ts b/src/common/k8s-api/endpoints/metrics.api/request-pod-metrics-for-daemon-sets.injectable.ts new file mode 100644 index 0000000000..d5653574ff --- /dev/null +++ b/src/common/k8s-api/endpoints/metrics.api/request-pod-metrics-for-daemon-sets.injectable.ts @@ -0,0 +1,46 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import type { DaemonSet } from "../daemon-set.api"; +import type { MetricData } from "../metrics.api"; +import requestMetricsInjectable from "./request-metrics.injectable"; + +export interface DaemonSetPodMetricData { + cpuUsage: MetricData; + memoryUsage: MetricData; + fsUsage: MetricData; + fsWrites: MetricData; + fsReads: MetricData; + networkReceive: MetricData; + networkTransmit: MetricData; +} + +export type RequestPodMetricsForDaemonSets = (daemonsets: DaemonSet[], namespace: string, selector?: string) => Promise; + +const requestPodMetricsForDaemonSetsInjectable = getInjectable({ + id: "request-pod-metrics-for-daemon-sets", + instantiate: (di): RequestPodMetricsForDaemonSets => { + const requestMetrics = di.inject(requestMetricsInjectable); + + return (daemonSets, namespace, selector = "") => { + const podSelector = daemonSets.map(daemonSet => `${daemonSet.getName()}-[[:alnum:]]{5}`).join("|"); + const opts = { category: "pods", pods: podSelector, namespace, selector }; + + return requestMetrics({ + cpuUsage: opts, + memoryUsage: opts, + fsUsage: opts, + fsWrites: opts, + fsReads: opts, + networkReceive: opts, + networkTransmit: opts, + }, { + namespace, + }); + }; + }, +}); + +export default requestPodMetricsForDaemonSetsInjectable; diff --git a/src/common/k8s-api/endpoints/metrics.api/request-pod-metrics-for-deployments.injectable.ts b/src/common/k8s-api/endpoints/metrics.api/request-pod-metrics-for-deployments.injectable.ts new file mode 100644 index 0000000000..ced8ccd7fe --- /dev/null +++ b/src/common/k8s-api/endpoints/metrics.api/request-pod-metrics-for-deployments.injectable.ts @@ -0,0 +1,46 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import type { Deployment } from "../deployment.api"; +import type { MetricData } from "../metrics.api"; +import requestMetricsInjectable from "./request-metrics.injectable"; + +export interface DeploymentPodMetricData { + cpuUsage: MetricData; + memoryUsage: MetricData; + fsUsage: MetricData; + fsWrites: MetricData; + fsReads: MetricData; + networkReceive: MetricData; + networkTransmit: MetricData; +} + +export type RequestPodMetricsForDeployments = (deployments: Deployment[], namespace: string, selector?: string) => Promise; + +const requestPodMetricsForDeploymentsInjectable = getInjectable({ + id: "request-pod-metrics-for-deployments", + instantiate: (di): RequestPodMetricsForDeployments => { + const requestMetrics = di.inject(requestMetricsInjectable); + + return (deployments, namespace, selector = "") => { + const podSelector = deployments.map(deployment => `${deployment.getName()}-[[:alnum:]]{9,}-[[:alnum:]]{5}`).join("|"); + const opts = { category: "pods", pods: podSelector, namespace, selector }; + + return requestMetrics({ + cpuUsage: opts, + memoryUsage: opts, + fsUsage: opts, + fsWrites: opts, + fsReads: opts, + networkReceive: opts, + networkTransmit: opts, + }, { + namespace, + }); + }; + }, +}); + +export default requestPodMetricsForDeploymentsInjectable; diff --git a/src/common/k8s-api/endpoints/metrics.api/request-pod-metrics-for-jobs.injectable.ts b/src/common/k8s-api/endpoints/metrics.api/request-pod-metrics-for-jobs.injectable.ts new file mode 100644 index 0000000000..a812fae59c --- /dev/null +++ b/src/common/k8s-api/endpoints/metrics.api/request-pod-metrics-for-jobs.injectable.ts @@ -0,0 +1,46 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import type { Job } from "../job.api"; +import type { MetricData } from "../metrics.api"; +import requestMetricsInjectable from "./request-metrics.injectable"; + +export interface JobPodMetricData { + cpuUsage: MetricData; + memoryUsage: MetricData; + fsUsage: MetricData; + fsWrites: MetricData; + fsReads: MetricData; + networkReceive: MetricData; + networkTransmit: MetricData; +} + +export type RequestPodMetricsForJobs = (jobs: Job[], namespace: string, selector?: string) => Promise; + +const requestPodMetricsForJobsInjectable = getInjectable({ + id: "request-pod-metrics-for-jobs", + instantiate: (di): RequestPodMetricsForJobs => { + const requestMetrics = di.inject(requestMetricsInjectable); + + return (jobs, namespace, selector) => { + const podSelector = jobs.map(job => `${job.getName()}-[[:alnum:]]{5}`).join("|"); + const opts = { category: "pods", pods: podSelector, namespace, selector }; + + return requestMetrics({ + cpuUsage: opts, + memoryUsage: opts, + fsUsage: opts, + fsWrites: opts, + fsReads: opts, + networkReceive: opts, + networkTransmit: opts, + }, { + namespace, + }); + }; + }, +}); + +export default requestPodMetricsForJobsInjectable; diff --git a/src/common/k8s-api/endpoints/metrics.api/request-pod-metrics-for-replica-sets.injectable.ts b/src/common/k8s-api/endpoints/metrics.api/request-pod-metrics-for-replica-sets.injectable.ts new file mode 100644 index 0000000000..4186cba7b4 --- /dev/null +++ b/src/common/k8s-api/endpoints/metrics.api/request-pod-metrics-for-replica-sets.injectable.ts @@ -0,0 +1,46 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import type { MetricData } from "../metrics.api"; +import type { ReplicaSet } from "../replica-set.api"; +import requestMetricsInjectable from "./request-metrics.injectable"; + +export interface ReplicaSetPodMetricData { + cpuUsage: MetricData; + memoryUsage: MetricData; + fsUsage: MetricData; + fsWrites: MetricData; + fsReads: MetricData; + networkReceive: MetricData; + networkTransmit: MetricData; +} + +export type RequestPodMetricsForReplicaSets = (replicaSets: ReplicaSet[], namespace: string, selector?: string) => Promise; + +const requestPodMetricsForReplicaSetsInjectable = getInjectable({ + id: "request-pod-metrics-for-replica-sets", + instantiate: (di): RequestPodMetricsForReplicaSets => { + const requestMetrics = di.inject(requestMetricsInjectable); + + return (replicaSets, namespace, selector = "") => { + const podSelector = replicaSets.map(replicaSet => `${replicaSet.getName()}-[[:alnum:]]{5}`).join("|"); + const opts = { category: "pods", pods: podSelector, namespace, selector }; + + return requestMetrics({ + cpuUsage: opts, + memoryUsage: opts, + fsUsage: opts, + fsWrites: opts, + fsReads: opts, + networkReceive: opts, + networkTransmit: opts, + }, { + namespace, + }); + }; + }, +}); + +export default requestPodMetricsForReplicaSetsInjectable; diff --git a/src/common/k8s-api/endpoints/metrics.api/request-pod-metrics-for-stateful-sets.injectable.ts b/src/common/k8s-api/endpoints/metrics.api/request-pod-metrics-for-stateful-sets.injectable.ts new file mode 100644 index 0000000000..227961bc02 --- /dev/null +++ b/src/common/k8s-api/endpoints/metrics.api/request-pod-metrics-for-stateful-sets.injectable.ts @@ -0,0 +1,47 @@ +/** + * 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 { MetricData } from "../metrics.api"; +import type { StatefulSet } from "../stateful-set.api"; +import requestMetricsInjectable from "./request-metrics.injectable"; + +export interface StatefulSetPodMetricData { + cpuUsage: MetricData; + memoryUsage: MetricData; + fsUsage: MetricData; + fsWrites: MetricData; + fsReads: MetricData; + networkReceive: MetricData; + networkTransmit: MetricData; +} + +export type RequestPodMetricsForStatefulSets = (statefulSets: StatefulSet[], namespace: string, selector?: string) => Promise; + +const requestPodMetricsForStatefulSetsInjectable = getInjectable({ + id: "request-pod-metrics-for-stateful-sets", + instantiate: (di): RequestPodMetricsForStatefulSets => { + const requestMetrics = di.inject(requestMetricsInjectable); + + return (statefulSets, namespace, selector = "") => { + const podSelector = statefulSets.map(statefulset => `${statefulset.getName()}-[[:digit:]]+`).join("|"); + const opts = { category: "pods", pods: podSelector, namespace, selector }; + + return requestMetrics({ + cpuUsage: opts, + memoryUsage: opts, + fsUsage: opts, + fsWrites: opts, + fsReads: opts, + networkReceive: opts, + networkTransmit: opts, + }, { + namespace, + }); + }; + }, +}); + +export default requestPodMetricsForStatefulSetsInjectable; + diff --git a/src/common/k8s-api/endpoints/metrics.api/request-pod-metrics-in-namespace.injectable.ts b/src/common/k8s-api/endpoints/metrics.api/request-pod-metrics-in-namespace.injectable.ts new file mode 100644 index 0000000000..872ffd740e --- /dev/null +++ b/src/common/k8s-api/endpoints/metrics.api/request-pod-metrics-in-namespace.injectable.ts @@ -0,0 +1,44 @@ +/** + * 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 { MetricData } from "../metrics.api"; +import requestMetricsInjectable from "./request-metrics.injectable"; + +export interface PodMetricInNamespaceData { + cpuUsage: MetricData; + memoryUsage: MetricData; + fsUsage: MetricData; + fsWrites: MetricData; + fsReads: MetricData; + networkReceive: MetricData; + networkTransmit: MetricData; +} + +export type RequestPodMetricsInNamespace = (namespace: string, selector?: string) => Promise; + +const requestPodMetricsInNamespaceInjectable = getInjectable({ + id: "request-pod-metrics-in-namespace", + instantiate: (di): RequestPodMetricsInNamespace => { + const requestMetrics = di.inject(requestMetricsInjectable); + + return (namespace, selector) => { + const opts = { category: "pods", pods: ".*", namespace, selector }; + + return requestMetrics({ + cpuUsage: opts, + memoryUsage: opts, + fsUsage: opts, + fsWrites: opts, + fsReads: opts, + networkReceive: opts, + networkTransmit: opts, + }, { + namespace, + }); + }; + }, +}); + +export default requestPodMetricsInNamespaceInjectable; diff --git a/src/common/k8s-api/endpoints/metrics.api/request-pod-metrics.injectable.ts b/src/common/k8s-api/endpoints/metrics.api/request-pod-metrics.injectable.ts new file mode 100644 index 0000000000..e14e5dc293 --- /dev/null +++ b/src/common/k8s-api/endpoints/metrics.api/request-pod-metrics.injectable.ts @@ -0,0 +1,54 @@ +/** + * 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 { MetricData } from "../metrics.api"; +import type { Pod } from "../pod.api"; +import requestMetricsInjectable from "./request-metrics.injectable"; + +export interface PodMetricData { + cpuUsage: MetricData; + memoryUsage: MetricData; + fsUsage: MetricData; + fsWrites: MetricData; + fsReads: MetricData; + networkReceive: MetricData; + networkTransmit: MetricData; + cpuRequests: MetricData; + cpuLimits: MetricData; + memoryRequests: MetricData; + memoryLimits: MetricData; +} + +export type RequestPodMetrics = (pods: Pod[], namespace: string, selector?: string) => Promise; + +const requestPodMetricsInjectable = getInjectable({ + id: "request-pod-metrics", + instantiate: (di): RequestPodMetrics => { + const requestMetrics = di.inject(requestMetricsInjectable); + + return (pods, namespace, selector = "pod, namespace") => { + const podSelector = pods.map(pod => pod.getName()).join("|"); + const opts = { category: "pods", pods: podSelector, namespace, selector }; + + return requestMetrics({ + cpuUsage: opts, + cpuRequests: opts, + cpuLimits: opts, + memoryUsage: opts, + memoryRequests: opts, + memoryLimits: opts, + fsUsage: opts, + fsWrites: opts, + fsReads: opts, + networkReceive: opts, + networkTransmit: opts, + }, { + namespace, + }); + }; + }, +}); + +export default requestPodMetricsInjectable; diff --git a/src/common/k8s-api/endpoints/metrics.api/request-providers.injectable.ts b/src/common/k8s-api/endpoints/metrics.api/request-providers.injectable.ts new file mode 100644 index 0000000000..4711333ca6 --- /dev/null +++ b/src/common/k8s-api/endpoints/metrics.api/request-providers.injectable.ts @@ -0,0 +1,26 @@ +/** + * 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 { apiBaseInjectionToken } from "../../api-base"; + +export interface MetricProviderInfo { + name: string; + id: string; + isConfigurable: boolean; +} + +export type RequestMetricsProviders = () => Promise; + +const requestMetricsProvidersInjectable = getInjectable({ + id: "request-metrics-providers", + instantiate: (di): RequestMetricsProviders => { + const apiBase = di.inject(apiBaseInjectionToken); + + return () => apiBase.get("/metrics/providers"); + }, +}); + +export default requestMetricsProvidersInjectable; diff --git a/src/common/k8s-api/endpoints/namespace.api.ts b/src/common/k8s-api/endpoints/namespace.api.ts index 6a2c7e2a95..774d91cf61 100644 --- a/src/common/k8s-api/endpoints/namespace.api.ts +++ b/src/common/k8s-api/endpoints/namespace.api.ts @@ -7,8 +7,6 @@ import type { DerivedKubeApiOptions, IgnoredKubeApiOptions } from "../kube-api"; import { KubeApi } from "../kube-api"; import type { ClusterScopedMetadata, KubeObjectStatus } from "../kube-object"; import { KubeObject } from "../kube-object"; -import { metricsApi } from "./metrics.api"; -import type { PodMetricData } from "./pod.api"; export enum NamespaceStatusKind { ACTIVE = "Active", @@ -45,19 +43,3 @@ export class NamespaceApi extends KubeApi { }); } } - -export function getMetricsForNamespace(namespace: string, selector = ""): Promise { - const opts = { category: "pods", pods: ".*", namespace, selector }; - - return metricsApi.getMetrics({ - cpuUsage: opts, - memoryUsage: opts, - fsUsage: opts, - fsWrites: opts, - fsReads: opts, - networkReceive: opts, - networkTransmit: opts, - }, { - namespace, - }); -} diff --git a/src/common/k8s-api/endpoints/node.api.ts b/src/common/k8s-api/endpoints/node.api.ts index 15da73311b..e1d726bed1 100644 --- a/src/common/k8s-api/endpoints/node.api.ts +++ b/src/common/k8s-api/endpoints/node.api.ts @@ -6,8 +6,6 @@ import type { BaseKubeObjectCondition, ClusterScopedMetadata } from "../kube-object"; import { KubeObject } from "../kube-object"; import { cpuUnitsToNumber, unitsToBytes, isObject } from "../../../renderer/utils"; -import type { MetricData } from "./metrics.api"; -import { metricsApi } from "./metrics.api"; import type { DerivedKubeApiOptions, IgnoredKubeApiOptions } from "../kube-api"; import { KubeApi } from "../kube-api"; import { TypedRegEx } from "typed-regex"; @@ -21,32 +19,6 @@ export class NodeApi extends KubeApi { } } -export function getMetricsForAllNodes(): Promise { - const opts = { category: "nodes" }; - - return metricsApi.getMetrics({ - memoryUsage: opts, - workloadMemoryUsage: opts, - memoryCapacity: opts, - memoryAllocatableCapacity: opts, - cpuUsage: opts, - cpuCapacity: opts, - fsSize: opts, - fsUsage: opts, - }); -} - -export interface NodeMetricData extends Partial> { - memoryUsage: MetricData; - workloadMemoryUsage: MetricData; - memoryCapacity: MetricData; - memoryAllocatableCapacity: MetricData; - cpuUsage: MetricData; - cpuCapacity: MetricData; - fsUsage: MetricData; - fsSize: MetricData; -} - export interface NodeTaint { key: string; value?: string; diff --git a/src/common/k8s-api/endpoints/persistent-volume-claim.api.ts b/src/common/k8s-api/endpoints/persistent-volume-claim.api.ts index 733a9768c5..4c3c575699 100644 --- a/src/common/k8s-api/endpoints/persistent-volume-claim.api.ts +++ b/src/common/k8s-api/endpoints/persistent-volume-claim.api.ts @@ -5,8 +5,6 @@ import type { LabelSelector, NamespaceScopedMetadata, TypedLocalObjectReference } from "../kube-object"; import { KubeObject } from "../kube-object"; -import type { MetricData } from "./metrics.api"; -import { metricsApi } from "./metrics.api"; import type { Pod } from "./pod.api"; import type { DerivedKubeApiOptions, IgnoredKubeApiOptions } from "../kube-api"; import { KubeApi } from "../kube-api"; @@ -22,22 +20,6 @@ export class PersistentVolumeClaimApi extends KubeApi { } } -export function getMetricsForPvc(pvc: PersistentVolumeClaim): Promise { - const opts = { category: "pvc", pvc: pvc.getName(), namespace: pvc.getNs() }; - - return metricsApi.getMetrics({ - diskUsage: opts, - diskCapacity: opts, - }, { - namespace: opts.namespace, - }); -} - -export interface PersistentVolumeClaimMetricData extends Partial> { - diskUsage: MetricData; - diskCapacity: MetricData; -} - export interface PersistentVolumeClaimSpec { accessModes?: string[]; dataSource?: TypedLocalObjectReference; diff --git a/src/common/k8s-api/endpoints/pod.api.ts b/src/common/k8s-api/endpoints/pod.api.ts index 5822c0c5ef..95dd694c1e 100644 --- a/src/common/k8s-api/endpoints/pod.api.ts +++ b/src/common/k8s-api/endpoints/pod.api.ts @@ -3,8 +3,6 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type { MetricData } from "./metrics.api"; -import { metricsApi } from "./metrics.api"; import type { DerivedKubeApiOptions, IgnoredKubeApiOptions, ResourceDescriptor } from "../kube-api"; import { KubeApi } from "../kube-api"; import type { RequireExactlyOne } from "type-fest"; @@ -33,41 +31,6 @@ export class PodApi extends KubeApi { } } -export function getMetricsForPods(pods: Pod[], namespace: string, selector = "pod, namespace"): Promise { - const podSelector = pods.map(pod => pod.getName()).join("|"); - const opts = { category: "pods", pods: podSelector, namespace, selector }; - - return metricsApi.getMetrics({ - cpuUsage: opts, - cpuRequests: opts, - cpuLimits: opts, - memoryUsage: opts, - memoryRequests: opts, - memoryLimits: opts, - fsUsage: opts, - fsWrites: opts, - fsReads: opts, - networkReceive: opts, - networkTransmit: opts, - }, { - namespace, - }); -} - -export interface PodMetricData extends Partial> { - cpuUsage: MetricData; - memoryUsage: MetricData; - fsUsage: MetricData; - fsWrites: MetricData; - fsReads: MetricData; - networkReceive: MetricData; - networkTransmit: MetricData; - cpuRequests?: MetricData; - cpuLimits?: MetricData; - memoryRequests?: MetricData; - memoryLimits?: MetricData; -} - // Reference: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.19/#read-log-pod-v1-core export interface PodLogsQuery { container?: string; diff --git a/src/common/k8s-api/endpoints/replica-set.api.ts b/src/common/k8s-api/endpoints/replica-set.api.ts index ea3cdc25b9..401ba6cf05 100644 --- a/src/common/k8s-api/endpoints/replica-set.api.ts +++ b/src/common/k8s-api/endpoints/replica-set.api.ts @@ -5,8 +5,6 @@ import type { DerivedKubeApiOptions, IgnoredKubeApiOptions } from "../kube-api"; import { KubeApi } from "../kube-api"; -import { metricsApi } from "./metrics.api"; -import type { PodMetricData } from "./pod.api"; import type { KubeObjectStatus, LabelSelector, NamespaceScopedMetadata } from "../kube-object"; import { KubeObject } from "../kube-object"; import type { PodTemplateSpec } from "./types/pod-template-spec"; @@ -41,23 +39,6 @@ export class ReplicaSetApi extends KubeApi { } } -export function getMetricsForReplicaSets(replicasets: ReplicaSet[], namespace: string, selector = ""): Promise { - const podSelector = replicasets.map(replicaset => `${replicaset.getName()}-[[:alnum:]]{5}`).join("|"); - const opts = { category: "pods", pods: podSelector, namespace, selector }; - - return metricsApi.getMetrics({ - cpuUsage: opts, - memoryUsage: opts, - fsUsage: opts, - fsWrites: opts, - fsReads: opts, - networkReceive: opts, - networkTransmit: opts, - }, { - namespace, - }); -} - export interface ReplicaSetSpec { replicas?: number; selector: LabelSelector; diff --git a/src/common/k8s-api/endpoints/resource-applier.api.ts b/src/common/k8s-api/endpoints/resource-applier.api.ts deleted file mode 100644 index 9f0c3bb6cd..0000000000 --- a/src/common/k8s-api/endpoints/resource-applier.api.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 yaml from "js-yaml"; -import type { KubeJsonApiData } from "../kube-json-api"; -import { apiBase } from "../index"; -import type { Patch } from "rfc6902"; - -export const annotations = [ - "kubectl.kubernetes.io/last-applied-configuration", -]; - -export async function update(resource: object | string): Promise { - if (typeof resource === "string") { - const parsed = yaml.load(resource); - - if (!parsed || typeof parsed !== "object") { - throw new Error("Cannot update resource to string or number"); - } - - resource = parsed; - } - - return apiBase.post("/stack", { data: resource }); -} - -export async function patch(name: string, kind: string, ns: string | undefined, patch: Patch): Promise { - return apiBase.patch("/stack", { - data: { - name, - kind, - ns, - patch, - }, - }); -} diff --git a/src/common/k8s-api/endpoints/resource-applier.api/request-patch.injectable.ts b/src/common/k8s-api/endpoints/resource-applier.api/request-patch.injectable.ts new file mode 100644 index 0000000000..c8ad3435fd --- /dev/null +++ b/src/common/k8s-api/endpoints/resource-applier.api/request-patch.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 } from "@ogre-tools/injectable"; +import type { Patch } from "rfc6902"; +import { apiBaseInjectionToken } from "../../api-base"; +import type { KubeJsonApiData } from "../../kube-json-api"; + +export type RequestKubeObjectPatch = (name: string, kind: string, ns: string | undefined, patch: Patch) => Promise; + +const requestKubeObjectPatchInjectable = getInjectable({ + id: "request-kube-object-patch", + instantiate: (di): RequestKubeObjectPatch => { + const apiBase = di.inject(apiBaseInjectionToken); + + return (name, kind, ns, patch) => ( + apiBase.patch("/stack", { + data: { + name, + kind, + ns, + patch, + }, + }) + ); + }, +}); + +export default requestKubeObjectPatchInjectable; diff --git a/src/common/k8s-api/endpoints/resource-applier.api/request-update.injectable.ts b/src/common/k8s-api/endpoints/resource-applier.api/request-update.injectable.ts new file mode 100644 index 0000000000..52824cd86c --- /dev/null +++ b/src/common/k8s-api/endpoints/resource-applier.api/request-update.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 { apiBaseInjectionToken } from "../../api-base"; +import type { KubeJsonApiData } from "../../kube-json-api"; + +export type RequestKubeObjectCreation = (resourceDescriptor: string) => Promise; + +const requestKubeObjectCreationInjectable = getInjectable({ + id: "request-kube-object-creation", + instantiate: (di): RequestKubeObjectCreation => { + const apiBase = di.inject(apiBaseInjectionToken); + + return (data) => apiBase.post("/stack", { data }); + }, +}); + +export default requestKubeObjectCreationInjectable; diff --git a/src/common/k8s-api/endpoints/stateful-set.api.ts b/src/common/k8s-api/endpoints/stateful-set.api.ts index f6e36c36fb..1d6895f934 100644 --- a/src/common/k8s-api/endpoints/stateful-set.api.ts +++ b/src/common/k8s-api/endpoints/stateful-set.api.ts @@ -5,8 +5,6 @@ import type { DerivedKubeApiOptions, IgnoredKubeApiOptions } from "../kube-api"; import { KubeApi } from "../kube-api"; -import { metricsApi } from "./metrics.api"; -import type { PodMetricData } from "./pod.api"; import type { LabelSelector, NamespaceScopedMetadata } from "../kube-object"; import { KubeObject } from "../kube-object"; import type { PodTemplateSpec } from "./types/pod-template-spec"; @@ -46,23 +44,6 @@ export class StatefulSetApi extends KubeApi { } } -export function getMetricsForStatefulSets(statefulSets: StatefulSet[], namespace: string, selector = ""): Promise { - const podSelector = statefulSets.map(statefulset => `${statefulset.getName()}-[[:digit:]]+`).join("|"); - const opts = { category: "pods", pods: podSelector, namespace, selector }; - - return metricsApi.getMetrics({ - cpuUsage: opts, - memoryUsage: opts, - fsUsage: opts, - fsWrites: opts, - fsReads: opts, - networkReceive: opts, - networkTransmit: opts, - }, { - namespace, - }); -} - export interface StatefulSetSpec { serviceName: string; replicas: number; diff --git a/src/common/k8s-api/index.ts b/src/common/k8s-api/index.ts deleted file mode 100644 index 0d47a643f3..0000000000 --- a/src/common/k8s-api/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -export { apiBase } from "./api-base"; -export { apiKube } from "./api-kube"; diff --git a/src/common/k8s-api/json-api.ts b/src/common/k8s-api/json-api.ts index 36b4fdd9f5..c3e07bfa94 100644 --- a/src/common/k8s-api/json-api.ts +++ b/src/common/k8s-api/json-api.ts @@ -9,12 +9,12 @@ import { Agent as HttpAgent } from "http"; import { Agent as HttpsAgent } from "https"; import { merge } from "lodash"; import type { Response, RequestInit } from "node-fetch"; -import fetch from "node-fetch"; import { stringify } from "querystring"; import type { Patch } from "rfc6902"; import type { PartialDeep, ValueOf } from "type-fest"; import { EventEmitter } from "../../common/event-emitter"; -import logger from "../../common/logger"; +import type { Logger } from "../../common/logger"; +import type { Fetch } from "../fetch/fetch.injectable"; import type { Defaulted } from "../utils"; import { json } from "../utils"; @@ -59,6 +59,11 @@ export type ParamsAndQuery = ( : Params & { query?: undefined } ); +export interface JsonApiDependencies { + fetch: Fetch; + readonly logger: Logger; +} + export class JsonApi = JsonApiParams> { static readonly reqInitDefault = { headers: { @@ -71,7 +76,7 @@ export class JsonApi = Js debug: false, }; - constructor(public readonly config: JsonApiConfig, reqInit?: RequestInit) { + constructor(protected readonly dependencies: JsonApiDependencies, public readonly config: JsonApiConfig, reqInit?: RequestInit) { this.config = Object.assign({}, JsonApi.configDefault, config); this.reqInit = merge({}, JsonApi.reqInitDefault, reqInit); this.parseResponse = this.parseResponse.bind(this); @@ -105,7 +110,7 @@ export class JsonApi = Js reqUrl += (reqUrl.includes("?") ? "&" : "?") + queryString; } - return fetch(reqUrl, reqInit); + return this.dependencies.fetch(reqUrl, reqInit); } get( @@ -177,7 +182,7 @@ export class JsonApi = Js reqInit, }; - const res = await fetch(reqUrl, reqInit); + const res = await this.dependencies.fetch(reqUrl, reqInit); return this.parseResponse(res, infoLog); } @@ -233,7 +238,7 @@ export class JsonApi = Js protected writeLog(log: JsonApiLog) { const { method, reqUrl, ...params } = log; - logger.debug(`[JSON-API] request ${method} ${reqUrl}`, params); + this.dependencies.logger.debug(`[JSON-API] request ${method} ${reqUrl}`, params); } } diff --git a/src/common/k8s-api/kube-api.ts b/src/common/k8s-api/kube-api.ts index ddc2456668..73d6ab1a82 100644 --- a/src/common/k8s-api/kube-api.ts +++ b/src/common/k8s-api/kube-api.ts @@ -5,28 +5,25 @@ // Base class for building all kubernetes apis -import { isFunction, merge } from "lodash"; +import { merge } from "lodash"; import { stringify } from "querystring"; -import { apiKubePrefix, isDevelopment } from "../../common/vars"; -import { apiBase, apiKube } from "./index"; import { createKubeApiURL, parseKubeApi } from "./kube-api-parse"; import type { KubeObjectConstructor, KubeJsonApiDataFor, KubeObjectMetadata } from "./kube-object"; import { KubeObject, KubeStatus, isKubeStatusData } from "./kube-object"; import byline from "byline"; import type { IKubeWatchEvent } from "./kube-watch-event"; -import type { KubeJsonApiData } from "./kube-json-api"; -import { KubeJsonApi } from "./kube-json-api"; +import type { KubeJsonApiData, KubeJsonApi } from "./kube-json-api"; import type { Disposer } from "../utils"; import { isDefined, noop, WrappedAbortController } from "../utils"; -import type { RequestInit } from "node-fetch"; -import type { AgentOptions } from "https"; -import { Agent } from "https"; +import type { RequestInit, Response } from "node-fetch"; import type { Patch } from "rfc6902"; import assert from "assert"; import type { PartialDeep } from "type-fest"; import logger from "../logger"; import { Environments, getEnvironmentSpecificLegacyGlobalDiForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; import autoRegistrationEmitterInjectable from "./api-manager/auto-registration-emitter.injectable"; +import { asLegacyGlobalForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api"; +import { apiKubeInjectionToken } from "./api-kube"; import type AbortController from "abort-controller"; /** @@ -145,146 +142,39 @@ export interface KubeApiResourceList { resources: KubeApiResource[]; } -export interface ILocalKubeApiConfig { - metadata: { - uid: string; - }; -} - export type PropagationPolicy = undefined | "Orphan" | "Foreground" | "Background"; -/** - * @deprecated - */ -export interface IKubeApiCluster extends ILocalKubeApiConfig { } - -export interface IRemoteKubeApiConfig { - cluster: { - server: string; - caData?: string; - skipTLSVerify?: boolean; - }; - user: { - token?: string | (() => Promise); - clientCertificateData?: string; - clientKeyData?: string; - }; - /** - * Custom instance of https.agent to use for the requests - * - * @remarks the custom agent replaced default agent, options skipTLSVerify, - * clientCertificateData, clientKeyData and caData are ignored. - */ - agent?: Agent; -} - -export function forCluster< - Object extends KubeObject, - Api extends KubeApi, - Data extends KubeJsonApiDataFor, ->(cluster: ILocalKubeApiConfig, kubeClass: KubeObjectConstructor, apiClass: new (apiOpts: KubeApiOptions) => Api): Api; -export function forCluster< - Object extends KubeObject, - Data extends KubeJsonApiDataFor, ->(cluster: ILocalKubeApiConfig, kubeClass: KubeObjectConstructor, apiClass?: new (apiOpts: KubeApiOptions) => KubeApi): KubeApi; - -export function forCluster< - Object extends KubeObject, - Data extends KubeJsonApiDataFor, ->(cluster: ILocalKubeApiConfig, kubeClass: KubeObjectConstructor, apiClass: (new (apiOpts: KubeApiOptions) => KubeApi) = KubeApi): KubeApi { - const url = new URL(apiBase.config.serverAddress); - const request = new KubeJsonApi({ - serverAddress: apiBase.config.serverAddress, - apiBase: apiKubePrefix, - debug: isDevelopment, - }, { - headers: { - "Host": `${cluster.metadata.uid}.localhost:${url.port}`, - }, - }); - - return new apiClass({ - objectConstructor: kubeClass as KubeObjectConstructor>, - request, - }); -} - -export function forRemoteCluster< - Object extends KubeObject, - Api extends KubeApi, - Data extends KubeJsonApiDataFor, ->(config: IRemoteKubeApiConfig, kubeClass: KubeObjectConstructor, apiClass: new (apiOpts: KubeApiOptions) => Api): Api; -export function forRemoteCluster< - Object extends KubeObject, - Data extends KubeJsonApiDataFor, ->(config: IRemoteKubeApiConfig, kubeClass: KubeObjectConstructor, apiClass?: new (apiOpts: KubeApiOptions) => KubeApi): KubeApi; - -export function forRemoteCluster< - Object extends KubeObject, - Api extends KubeApi, - Data extends KubeJsonApiDataFor, ->(config: IRemoteKubeApiConfig, kubeClass: KubeObjectConstructor, apiClass: new (apiOpts: KubeApiOptions) => KubeApi = KubeApi): KubeApi { - const reqInit: RequestInit = {}; - const agentOptions: AgentOptions = {}; - - if (config.cluster.skipTLSVerify === true) { - agentOptions.rejectUnauthorized = false; - } - - if (config.user.clientCertificateData) { - agentOptions.cert = config.user.clientCertificateData; - } - - if (config.user.clientKeyData) { - agentOptions.key = config.user.clientKeyData; - } - - if (config.cluster.caData) { - agentOptions.ca = config.cluster.caData; - } - - if (Object.keys(agentOptions).length > 0) { - reqInit.agent = new Agent(agentOptions); - } - - if (config.agent) { - reqInit.agent = config.agent; - } - - const token = config.user.token; - const request = new KubeJsonApi({ - serverAddress: config.cluster.server, - apiBase: "", - debug: isDevelopment, - ...(token ? { - getRequestOptions: async () => ({ - headers: { - "Authorization": `Bearer ${isFunction(token) ? await token() : token}`, - }, - }), - } : {}), - }, reqInit); - - if (!apiClass) { - apiClass = KubeApi as new (apiOpts: KubeApiOptions) => Api; - } - - return new apiClass({ - objectConstructor: kubeClass as KubeObjectConstructor>, - request, - }); -} - -export type KubeApiWatchCallback = (data: IKubeWatchEvent, error: any) => void; +export type KubeApiWatchCallback = (data: IKubeWatchEvent | null, error: KubeStatus | Response | null | any) => void; export interface KubeApiWatchOptions> { - namespace: string; + /** + * If the resource is namespaced then the default is `"default"` + */ + namespace?: string; + + /** + * This will be called when either an error occurs or some data is received + */ callback?: KubeApiWatchCallback; + + /** + * This is a way of aborting the request + */ abortController?: AbortController; + + /** + * The ID used for tracking within logs + */ watchId?: string; + + /** + * @default false + */ retry?: boolean; - // timeout in seconds + /** + * timeout in seconds + */ timeout?: number; } @@ -368,7 +258,7 @@ export class KubeApi< constructor(opts: KubeApiOptions) { const { objectConstructor, - request = apiKube, + request = asLegacyGlobalForExtensionApi(apiKubeInjectionToken), kind = objectConstructor.kind, isNamespaced, apiBase: fullApiPathname = objectConstructor.apiBase, @@ -411,13 +301,14 @@ export class KubeApi< */ private async getLatestApiPrefixGroup() { // Note that this.fullApiPathname is the "full" url, whereas this.apiBase is parsed - const apiBases = [this.fullApiPathname, this.objectConstructor.apiBase, ...this.fallbackApiBases ?? []]; + const rawApiBases = [ + this.fullApiPathname, + this.objectConstructor.apiBase, + ...this.fallbackApiBases ?? [], + ].filter(isDefined); + const apiBases = new Set(rawApiBases); for (const apiUrl of apiBases) { - if (!apiUrl) { - continue; - } - try { // Split e.g. "/apis/extensions/v1beta1/ingresses" to parts const { apiPrefix, apiGroup, apiVersionWithGroup, resource } = parseKubeApi(apiUrl); @@ -502,18 +393,47 @@ export class KubeApi< }); } - getUrl({ name, namespace }: Partial = {}, query?: Partial) { + /** + * This method differs from {@link formatUrlForNotListing} because this treats `""` as "all namespaces" + * @param namespace The namespace to list in or `""` for all namespaces + */ + formatUrlForListing(namespace: string) { + return createKubeApiURL({ + apiPrefix: this.apiPrefix, + apiVersion: this.apiVersionWithGroup, + resource: this.apiResource, + namespace: this.isNamespaced + ? namespace ?? "default" + : undefined, + }); + } + + /** + * Format a URL pathname and query for acting upon a specific resource. + */ + formatUrlForNotListing(resource?: Partial, query?: Partial): string; + + formatUrlForNotListing({ name, namespace }: Partial = {}, query?: Partial) { const resourcePath = createKubeApiURL({ apiPrefix: this.apiPrefix, apiVersion: this.apiVersionWithGroup, resource: this.apiResource, - namespace: this.isNamespaced ? namespace ?? "default" : undefined, + namespace: this.isNamespaced + ? namespace || "default" + : undefined, name, }); return resourcePath + (query ? `?${stringify(this.normalizeQuery(query))}` : ""); } + /** + * @deprecated use {@link formatUrlForNotListing} instead + */ + getUrl(resource?: Partial, query?: Partial) { + return this.formatUrlForNotListing(resource, query); + } + protected normalizeQuery(query: Partial = {}) { if (query.labelSelector) { query.labelSelector = [query.labelSelector].flat().join(","); @@ -591,7 +511,7 @@ export class KubeApi< async list({ namespace = "", reqInit }: KubeApiListOptions = {}, query?: KubeApiQueryParams): Promise { await this.checkPreferredVersion(); - const url = this.getUrl({ namespace }); + const url = this.formatUrlForListing(namespace); const res = await this.request.get(url, { query }, reqInit); const parsed = this.parseResponse(res, namespace); @@ -609,7 +529,7 @@ export class KubeApi< async get(desc: ResourceDescriptor, query?: KubeApiQueryParams): Promise { await this.checkPreferredVersion(); - const url = this.getUrl(desc); + const url = this.formatUrlForNotListing(desc); const res = await this.request.get(url, { query }); const parsed = this.parseResponse(res); @@ -623,7 +543,7 @@ export class KubeApi< async create({ name, namespace }: Partial, partialData?: PartialDeep): Promise { await this.checkPreferredVersion(); - const apiUrl = this.getUrl({ namespace }); + const apiUrl = this.formatUrlForNotListing({ namespace }); const data = merge(partialData, { kind: this.kind, apiVersion: this.apiVersionWithGroup, @@ -644,7 +564,7 @@ export class KubeApi< async update({ name, namespace }: ResourceDescriptor, data: PartialDeep): Promise { await this.checkPreferredVersion(); - const apiUrl = this.getUrl({ namespace, name }); + const apiUrl = this.formatUrlForNotListing({ namespace, name }); const res = await this.request.put(apiUrl, { data: merge(data, { @@ -663,9 +583,13 @@ export class KubeApi< return parsed; } + async patch(desc: ResourceDescriptor, data: PartialDeep): Promise; + async patch(desc: ResourceDescriptor, data: PartialDeep, strategy: "strategic" | "merge"): Promise; + async patch(desc: ResourceDescriptor, data: Patch, strategy: "json"): Promise; + async patch(desc: ResourceDescriptor, data: PartialDeep | Patch, strategy: KubeApiPatchType): Promise; async patch(desc: ResourceDescriptor, data: PartialDeep | Patch, strategy: KubeApiPatchType = "strategic"): Promise { await this.checkPreferredVersion(); - const apiUrl = this.getUrl(desc); + const apiUrl = this.formatUrlForNotListing(desc); const res = await this.request.patch(apiUrl, { data }, { headers: { @@ -683,7 +607,7 @@ export class KubeApi< async delete({ propagationPolicy = "Background", ...desc }: DeleteResourceDescriptor) { await this.checkPreferredVersion(); - const apiUrl = this.getUrl(desc); + const apiUrl = this.formatUrlForNotListing(desc); return this.request.del(apiUrl, { query: { @@ -692,22 +616,27 @@ export class KubeApi< }); } - getWatchUrl(namespace = "", query: KubeApiQueryParams = {}) { - return this.getUrl({ namespace }, { + getWatchUrl(namespace?: string, query: KubeApiQueryParams = {}) { + return this.formatUrlForNotListing({ namespace }, { watch: 1, resourceVersion: this.getResourceVersion(namespace), ...query, }); } - watch(opts: KubeApiWatchOptions = { namespace: "", retry: false }): () => void { + watch(opts?: KubeApiWatchOptions): () => void { let errorReceived = false; let timedRetry: NodeJS.Timeout; - const { namespace, callback = noop, retry, timeout } = opts; - const { watchId = `${this.kind.toLowerCase()}-${this.watchId++}` } = opts; + const { + namespace, + callback = noop as KubeApiWatchCallback, + retry = false, + timeout, + watchId = `${this.kind.toLowerCase()}-${this.watchId++}`, + } = opts ?? {}; // Create AbortController for this request - const abortController = new WrappedAbortController(opts.abortController); + const abortController = new WrappedAbortController(opts?.abortController); abortController.signal.addEventListener("abort", () => { logger.info(`[KUBE-API] watch (${watchId}) aborted ${watchUrl}`); @@ -778,7 +707,7 @@ export class KubeApi< byline(response.body).on("data", (line) => { try { - const event = JSON.parse(line) as IKubeWatchEvent>; + const event = JSON.parse(line) as IKubeWatchEvent; if (event.type === "ERROR" && isKubeStatusData(event.object)) { errorReceived = true; diff --git a/src/common/k8s-api/kube-json-api.ts b/src/common/k8s-api/kube-json-api.ts index 2624f030c8..16ca5cda70 100644 --- a/src/common/k8s-api/kube-json-api.ts +++ b/src/common/k8s-api/kube-json-api.ts @@ -6,8 +6,6 @@ import type { JsonApiData, JsonApiError } from "./json-api"; import { JsonApi } from "./json-api"; import type { Response } from "node-fetch"; -import { apiKubePrefix, isDebugging } from "../vars"; -import { apiBase } from "./api-base"; import type { KubeJsonApiObjectMetadata } from "./kube-object"; export interface KubeJsonApiListMetadata { @@ -47,20 +45,6 @@ export interface KubeJsonApiError extends JsonApiError { } export class KubeJsonApi extends JsonApi { - static forCluster(clusterId: string): KubeJsonApi { - const url = new URL(apiBase.config.serverAddress); - - return new this({ - serverAddress: apiBase.config.serverAddress, - apiBase: apiKubePrefix, - debug: isDebugging, - }, { - headers: { - "Host": `${clusterId}.localhost:${url.port}`, - }, - }); - } - protected parseError(error: KubeJsonApiError | string, res: Response): string[] { if (typeof error === "string") { return [error]; diff --git a/src/common/k8s-api/kube-object.ts b/src/common/k8s-api/kube-object.ts index 0851729bc7..24e34eff4b 100644 --- a/src/common/k8s-api/kube-object.ts +++ b/src/common/k8s-api/kube-object.ts @@ -9,11 +9,15 @@ import moment from "moment"; import type { KubeJsonApiData, KubeJsonApiDataList, KubeJsonApiListMetadata } from "./kube-json-api"; import { autoBind, formatDuration, hasOptionalTypedProperty, hasTypedProperty, isObject, isString, isNumber, bindPredicate, isTypedArray, isRecord, json } from "../utils"; import type { ItemObject } from "../item.store"; -import { apiKube } from "./index"; -import * as resourceApplierApi from "./endpoints/resource-applier.api"; import type { Patch } from "rfc6902"; import assert from "assert"; import type { JsonObject } from "type-fest"; +import { asLegacyGlobalFunctionForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/as-legacy-global-function-for-extension-api"; +import requestKubeObjectPatchInjectable from "./endpoints/resource-applier.api/request-patch.injectable"; +import { asLegacyGlobalForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api"; +import { apiKubeInjectionToken } from "./api-kube"; +import requestKubeObjectCreationInjectable from "./endpoints/resource-applier.api/request-update.injectable"; +import { dump } from "js-yaml"; export type KubeJsonApiDataFor = K extends KubeObject ? KubeJsonApiData @@ -375,6 +379,12 @@ export type ScopedNamespace = ( : string | undefined ); +const resourceApplierAnnotationsForFiltering = [ + "kubectl.kubernetes.io/last-applied-configuration", +]; + +const filterOutResourceApplierAnnotations = (label: string) => !resourceApplierAnnotationsForFiltering.some(key => label.startsWith(key)); + export class KubeObject< Metadata extends KubeObjectMetadata = KubeObjectMetadata, Status = unknown, @@ -588,11 +598,11 @@ export class KubeObject< getAnnotations(filter = false): string[] { const labels = KubeObject.stringifyLabels(this.metadata.annotations); - return filter ? labels.filter(label => { - const skip = resourceApplierApi.annotations.some(key => label.startsWith(key)); + if (!filter) { + return labels; + } - return !skip; - }) : labels; + return labels.filter(filterOutResourceApplierAnnotations); } getOwnerRefs() { @@ -634,7 +644,9 @@ export class KubeObject< } } - return resourceApplierApi.patch(this.getName(), this.kind, this.getNs(), patch); + const requestKubeObjectPatch = asLegacyGlobalFunctionForExtensionApi(requestKubeObjectPatchInjectable); + + return requestKubeObjectPatch(this.getName(), this.kind, this.getNs(), patch); } /** @@ -647,11 +659,13 @@ export class KubeObject< * @deprecated use KubeApi.update instead */ async update(data: Partial): Promise { - // use unified resource-applier api for updating all k8s objects - return resourceApplierApi.update({ + const requestKubeObjectCreation = asLegacyGlobalFunctionForExtensionApi(requestKubeObjectCreationInjectable); + const descriptor = dump({ ...this.toPlainObject(), ...data, }); + + return requestKubeObjectCreation(descriptor); } /** @@ -660,6 +674,8 @@ export class KubeObject< delete(params?: object) { assert(this.selfLink, "selfLink must be present to delete self"); + const apiKube = asLegacyGlobalForExtensionApi(apiKubeInjectionToken); + return apiKube.del(this.selfLink, params); } } diff --git a/src/common/k8s-api/window-location.global-override-for-injectable.ts b/src/common/k8s-api/window-location.global-override-for-injectable.ts new file mode 100644 index 0000000000..616e110c88 --- /dev/null +++ b/src/common/k8s-api/window-location.global-override-for-injectable.ts @@ -0,0 +1,12 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getGlobalOverride } from "../test-utils/get-global-override"; +import windowLocationInjectable from "./window-location.injectable"; + +export default getGlobalOverride(windowLocationInjectable, () => ({ + host: "localhost", + port: "12345", +})); diff --git a/src/common/k8s-api/window-location.injectable.ts b/src/common/k8s-api/window-location.injectable.ts new file mode 100644 index 0000000000..80bcd44be6 --- /dev/null +++ b/src/common/k8s-api/window-location.injectable.ts @@ -0,0 +1,17 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; + +const windowLocationInjectable = getInjectable({ + id: "window-location", + instantiate: () => { + const { host, port } = window.location; + + return { host, port }; + }, + causesSideEffects: true, +}); + +export default windowLocationInjectable; diff --git a/src/common/kube-helpers.ts b/src/common/kube-helpers.ts index f24d4dbe2d..dc388efbc8 100644 --- a/src/common/kube-helpers.ts +++ b/src/common/kube-helpers.ts @@ -4,31 +4,14 @@ */ import { KubeConfig } from "@kubernetes/client-node"; -import fse from "fs-extra"; -import path from "path"; -import os from "os"; import yaml from "js-yaml"; import logger from "../main/logger"; import type { Cluster, Context, User } from "@kubernetes/client-node/dist/config_types"; import { newClusters, newContexts, newUsers } from "@kubernetes/client-node/dist/config_types"; -import { isDefined, resolvePath } from "./utils"; +import { isDefined } from "./utils"; import Joi from "joi"; import type { PartialDeep } from "type-fest"; -export const kubeConfigDefaultPath = path.join(os.homedir(), ".kube", "config"); - -export function loadConfigFromFileSync(filePath: string): ConfigResult { - const content = fse.readFileSync(resolvePath(filePath), "utf-8"); - - return loadConfigFromString(content); -} - -export async function loadConfigFromFile(filePath: string): Promise { - const content = await fse.readFile(resolvePath(filePath), "utf-8"); - - return loadConfigFromString(content); -} - const clusterSchema = Joi.object({ name: Joi .string() diff --git a/src/common/kube-helpers/load-config-from-file.injectable.ts b/src/common/kube-helpers/load-config-from-file.injectable.ts new file mode 100644 index 0000000000..afa9d3c070 --- /dev/null +++ b/src/common/kube-helpers/load-config-from-file.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 } from "@ogre-tools/injectable"; +import readFileInjectable from "../fs/read-file.injectable"; +import type { ConfigResult } from "../kube-helpers"; +import { loadConfigFromString } from "../kube-helpers"; +import resolveTildeInjectable from "../path/resolve-tilde.injectable"; + +export type LoadConfigfromFile = (filePath: string) => Promise; + +const loadConfigfromFileInjectable = getInjectable({ + id: "load-configfrom-file", + instantiate: (di): LoadConfigfromFile => { + const readFile = di.inject(readFileInjectable); + const resolveTilde = di.inject(resolveTildeInjectable); + + return async (filePath) => loadConfigFromString(await readFile(resolveTilde(filePath))); + }, +}); + +export default loadConfigfromFileInjectable; diff --git a/src/common/os/home-directory-path.global-override-for-injectable.ts b/src/common/os/home-directory-path.global-override-for-injectable.ts new file mode 100644 index 0000000000..f5869831a6 --- /dev/null +++ b/src/common/os/home-directory-path.global-override-for-injectable.ts @@ -0,0 +1,9 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getGlobalOverride } from "../test-utils/get-global-override"; +import homeDirectoryPathInjectable from "./home-directory-path.injectable"; + +export default getGlobalOverride(homeDirectoryPathInjectable, () => "/some-home-directory"); diff --git a/src/common/os/home-directory-path.injectable.ts b/src/common/os/home-directory-path.injectable.ts new file mode 100644 index 0000000000..b6ba1dfee0 --- /dev/null +++ b/src/common/os/home-directory-path.injectable.ts @@ -0,0 +1,14 @@ +/** + * 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 { homedir } from "os"; + +const homeDirectoryPathInjectable = getInjectable({ + id: "home-directory-path", + instantiate: () => homedir(), + causesSideEffects: true, +}); + +export default homeDirectoryPathInjectable; diff --git a/src/common/os/temp-directory-path.global-override-for-injectable.ts b/src/common/os/temp-directory-path.global-override-for-injectable.ts new file mode 100644 index 0000000000..05615644f9 --- /dev/null +++ b/src/common/os/temp-directory-path.global-override-for-injectable.ts @@ -0,0 +1,9 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getGlobalOverride } from "../test-utils/get-global-override"; +import tempDirectoryPathInjectable from "./temp-directory-path.injectable"; + +export default getGlobalOverride(tempDirectoryPathInjectable, () => "/some-temp-directory"); diff --git a/src/common/os/temp-directory-path.injectable.ts b/src/common/os/temp-directory-path.injectable.ts new file mode 100644 index 0000000000..46fc5db67d --- /dev/null +++ b/src/common/os/temp-directory-path.injectable.ts @@ -0,0 +1,14 @@ +/** + * 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 { tmpdir } from "os"; + +const tempDirectoryPathInjectable = getInjectable({ + id: "temp-directory-path", + instantiate: () => tmpdir(), + causesSideEffects: true, +}); + +export default tempDirectoryPathInjectable; diff --git a/src/common/path/get-absolute-path.global-override-for-injectable.ts b/src/common/path/get-absolute-path.global-override-for-injectable.ts new file mode 100644 index 0000000000..15f377cb2c --- /dev/null +++ b/src/common/path/get-absolute-path.global-override-for-injectable.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import path from "path"; +import { getGlobalOverride } from "../test-utils/get-global-override"; +import getAbsolutePathInjectable from "./get-absolute-path.injectable"; + +export default getGlobalOverride(getAbsolutePathInjectable, () => path.posix.resolve); diff --git a/src/common/path/get-basename.global-override-for-injectable.ts b/src/common/path/get-basename.global-override-for-injectable.ts new file mode 100644 index 0000000000..913ec9c5c2 --- /dev/null +++ b/src/common/path/get-basename.global-override-for-injectable.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import path from "path"; +import { getGlobalOverride } from "../test-utils/get-global-override"; +import getBasenameOfPathInjectable from "./get-basename.injectable"; + +export default getGlobalOverride(getBasenameOfPathInjectable, () => path.posix.basename); diff --git a/src/common/path/get-basename.injectable.ts b/src/common/path/get-basename.injectable.ts new file mode 100644 index 0000000000..be92bde7f5 --- /dev/null +++ b/src/common/path/get-basename.injectable.ts @@ -0,0 +1,16 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import path from "path"; + +export type GetBasenameOfPath = (path: string) => string; + +const getBasenameOfPathInjectable = getInjectable({ + id: "get-basename-of-path", + instantiate: (): GetBasenameOfPath => path.basename, + causesSideEffects: true, +}); + +export default getBasenameOfPathInjectable; diff --git a/src/common/path/get-dirname.global-override-for-injectable.ts b/src/common/path/get-dirname.global-override-for-injectable.ts new file mode 100644 index 0000000000..ed694de182 --- /dev/null +++ b/src/common/path/get-dirname.global-override-for-injectable.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import path from "path"; +import { getGlobalOverride } from "../test-utils/get-global-override"; +import getDirnameOfPathInjectable from "./get-dirname.injectable"; + +export default getGlobalOverride(getDirnameOfPathInjectable, () => path.posix.dirname); diff --git a/src/common/path/get-dirname.injectable.ts b/src/common/path/get-dirname.injectable.ts new file mode 100644 index 0000000000..93b4496767 --- /dev/null +++ b/src/common/path/get-dirname.injectable.ts @@ -0,0 +1,16 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import path from "path"; + +export type GetDirnameOfPath = (path: string) => string; + +const getDirnameOfPathInjectable = getInjectable({ + id: "get-dirname-of-path", + instantiate: (): GetDirnameOfPath => path.dirname, + causesSideEffects: true, +}); + +export default getDirnameOfPathInjectable; diff --git a/src/common/path/get-relative-path.global-override-for-injectable.ts b/src/common/path/get-relative-path.global-override-for-injectable.ts new file mode 100644 index 0000000000..9e96b70301 --- /dev/null +++ b/src/common/path/get-relative-path.global-override-for-injectable.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import path from "path"; +import { getGlobalOverride } from "../test-utils/get-global-override"; +import getRelativePathInjectable from "./get-relative-path.injectable"; + +export default getGlobalOverride(getRelativePathInjectable, () => path.posix.relative); diff --git a/src/common/path/get-relative-path.injectable.ts b/src/common/path/get-relative-path.injectable.ts new file mode 100644 index 0000000000..18b5d832de --- /dev/null +++ b/src/common/path/get-relative-path.injectable.ts @@ -0,0 +1,16 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import path from "path"; + +export type GetRelativePath = (from: string, to: string) => string; + +const getRelativePathInjectable = getInjectable({ + id: "get-relative-path", + instantiate: (): GetRelativePath => path.relative, + causesSideEffects: true, +}); + +export default getRelativePathInjectable; diff --git a/src/common/path/is-logical-child-path.injectable.ts b/src/common/path/is-logical-child-path.injectable.ts new file mode 100644 index 0000000000..1a52b66c0f --- /dev/null +++ b/src/common/path/is-logical-child-path.injectable.ts @@ -0,0 +1,51 @@ +/** + * 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 getAbsolutePathInjectable from "./get-absolute-path.injectable"; +import getDirnameOfPathInjectable from "./get-dirname.injectable"; + +/** + * Checks if `testPath` represents a potential filesystem entry that would be + * logically "within" the `parentPath` directory. + * + * This function will return `true` in the above case, and `false` otherwise. + * It will return `false` if the two paths are the same (after resolving them). + * + * The function makes no FS calls and is platform dependant. Meaning that the + * results are only guaranteed to be correct for the platform you are running + * on. + * @param parentPath The known path of a directory + * @param testPath The path that is to be tested + */ +export type IsLogicalChildPath = (parentPath: string, testPath: string) => boolean; + +const isLogicalChildPathInjectable = getInjectable({ + id: "is-logical-child-path", + instantiate: (di): IsLogicalChildPath => { + const getAbsolutePath = di.inject(getAbsolutePathInjectable); + const getDirnameOfPath = di.inject(getDirnameOfPathInjectable); + + return (parentPath, testPath) => { + const resolvedParentPath = getAbsolutePath(parentPath); + let resolvedTestPath = getAbsolutePath(testPath); + + if (resolvedParentPath === resolvedTestPath) { + return false; + } + + while (resolvedTestPath.length >= resolvedParentPath.length) { + if (resolvedTestPath === resolvedParentPath) { + return true; + } + + resolvedTestPath = getDirnameOfPath(resolvedTestPath); + } + + return false; + }; + }, +}); + +export default isLogicalChildPathInjectable; diff --git a/src/common/path/join-paths.global-override-for-injectable.ts b/src/common/path/join-paths.global-override-for-injectable.ts new file mode 100644 index 0000000000..d3e9d5e4c2 --- /dev/null +++ b/src/common/path/join-paths.global-override-for-injectable.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import path from "path"; +import { getGlobalOverride } from "../test-utils/get-global-override"; +import joinPathsInjectable from "./join-paths.injectable"; + +export default getGlobalOverride(joinPathsInjectable, () => path.posix.join); diff --git a/src/common/path/parse.global-override-for-injectable.ts b/src/common/path/parse.global-override-for-injectable.ts new file mode 100644 index 0000000000..fad97db696 --- /dev/null +++ b/src/common/path/parse.global-override-for-injectable.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import path from "path"; +import { getGlobalOverride } from "../test-utils/get-global-override"; +import parsePathInjectable from "./parse.injectable"; + +export default getGlobalOverride(parsePathInjectable, () => path.posix.parse); diff --git a/src/common/path/parse.injectable.ts b/src/common/path/parse.injectable.ts new file mode 100644 index 0000000000..a32dfb3fa5 --- /dev/null +++ b/src/common/path/parse.injectable.ts @@ -0,0 +1,14 @@ +/** + * 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 path from "path"; + +const parsePathInjectable = getInjectable({ + id: "parse-path", + instantiate: () => path.parse, + causesSideEffects: true, +}); + +export default parsePathInjectable; diff --git a/src/common/path/resolve-path.injectable.ts b/src/common/path/resolve-path.injectable.ts new file mode 100644 index 0000000000..75a1e98c59 --- /dev/null +++ b/src/common/path/resolve-path.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 } from "@ogre-tools/injectable"; +import getAbsolutePathInjectable from "./get-absolute-path.injectable"; +import resolveTildeInjectable from "./resolve-tilde.injectable"; + +export type ResolvePath = (path: string) => string; + +const resolvePathInjectable = getInjectable({ + id: "resolve-path", + instantiate: (di): ResolvePath => { + const getAbsolutePath = di.inject(getAbsolutePathInjectable); + const resolveTilde = di.inject(resolveTildeInjectable); + + return (filePath) => getAbsolutePath(resolveTilde(filePath)); + }, +}); + +export default resolvePathInjectable; diff --git a/src/common/path/resolve-tilde.injectable.ts b/src/common/path/resolve-tilde.injectable.ts new file mode 100644 index 0000000000..86d267aa4f --- /dev/null +++ b/src/common/path/resolve-tilde.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 { getInjectable } from "@ogre-tools/injectable"; +import homeDirectoryPathInjectable from "../os/home-directory-path.injectable"; +import fileSystemSeparatorInjectable from "./separator.injectable"; + +export type ResolveTilde = (path: string) => string; + +const resolveTildeInjectable = getInjectable({ + id: "resolve-tilde", + instantiate: (di): ResolveTilde => { + const homeDirectoryPath = di.inject(homeDirectoryPathInjectable); + const fileSystemSeparator = di.inject(fileSystemSeparatorInjectable); + + return (filePath) => { + if (filePath === "~") { + return homeDirectoryPath; + } + + if (filePath === `~${fileSystemSeparator}`) { + return `${homeDirectoryPath}${filePath.slice(1)}`; + } + + return filePath; + }; + }, +}); + +export default resolveTildeInjectable; diff --git a/src/common/path/separator.global-override-for-injectable.ts b/src/common/path/separator.global-override-for-injectable.ts new file mode 100644 index 0000000000..655f8908b0 --- /dev/null +++ b/src/common/path/separator.global-override-for-injectable.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import path from "path"; +import { getGlobalOverride } from "../test-utils/get-global-override"; +import fileSystemSeparatorInjectable from "./separator.injectable"; + +export default getGlobalOverride(fileSystemSeparatorInjectable, () => path.posix.sep); diff --git a/src/common/path/separator.injectable.ts b/src/common/path/separator.injectable.ts new file mode 100644 index 0000000000..5b0413b56f --- /dev/null +++ b/src/common/path/separator.injectable.ts @@ -0,0 +1,14 @@ +/** + * 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 path from "path"; + +const fileSystemSeparatorInjectable = getInjectable({ + id: "file-system-separator", + instantiate: () => path.sep, + causesSideEffects: true, +}); + +export default fileSystemSeparatorInjectable; diff --git a/src/common/test-utils/get-absolute-path-fake.ts b/src/common/test-utils/get-absolute-path-fake.ts deleted file mode 100644 index 89b5faa446..0000000000 --- a/src/common/test-utils/get-absolute-path-fake.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import type { GetAbsolutePath } from "../path/get-absolute-path.injectable"; - -export const getAbsolutePathFake: GetAbsolutePath = (...args) => { - const maybeAbsolutePath = args.join("/"); - - if (isAbsolutePath(maybeAbsolutePath)) { - return maybeAbsolutePath; - } - - return `/some-absolute-root-directory/${maybeAbsolutePath}`; -}; - -const isAbsolutePath = (path: string) => path.startsWith("/"); diff --git a/src/common/test-utils/join-paths-fake.ts b/src/common/test-utils/join-paths-fake.ts deleted file mode 100644 index 2796423d3c..0000000000 --- a/src/common/test-utils/join-paths-fake.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import type { JoinPaths } from "../path/join-paths.injectable"; - -export const joinPathsFake: JoinPaths = (...args) => args.join("/"); diff --git a/src/common/user-store/file-name-migration.injectable.ts b/src/common/user-store/file-name-migration.injectable.ts index caf94dc491..106f559ef0 100644 --- a/src/common/user-store/file-name-migration.injectable.ts +++ b/src/common/user-store/file-name-migration.injectable.ts @@ -4,10 +4,10 @@ */ import fse from "fs-extra"; -import path from "path"; import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable"; import { isErrnoException } from "../utils"; import { getInjectable } from "@ogre-tools/injectable"; +import joinPathsInjectable from "../path/join-paths.injectable"; export type UserStoreFileNameMigration = () => Promise; @@ -15,8 +15,9 @@ const userStoreFileNameMigrationInjectable = getInjectable({ id: "user-store-file-name-migration", instantiate: (di): UserStoreFileNameMigration => { const userDataPath = di.inject(directoryForUserDataInjectable); - const configJsonPath = path.join(userDataPath, "config.json"); - const lensUserStoreJsonPath = path.join(userDataPath, "lens-user-store.json"); + const joinPaths = di.inject(joinPathsInjectable); + const configJsonPath = joinPaths(userDataPath, "config.json"); + const lensUserStoreJsonPath = joinPaths(userDataPath, "lens-user-store.json"); return async () => { try { diff --git a/src/common/user-store/user-store.ts b/src/common/user-store/user-store.ts index 6c995996bf..8a8ff6735c 100644 --- a/src/common/user-store/user-store.ts +++ b/src/common/user-store/user-store.ts @@ -7,7 +7,6 @@ import { app } from "electron"; import { action, computed, observable, reaction, makeObservable, isObservableArray, isObservableSet, isObservableMap } from "mobx"; import { BaseStore } from "../base-store"; import migrations from "../../migrations/user-store"; -import { kubeConfigDefaultPath } from "../kube-helpers"; import { getOrInsertSet, toggle, toJS, object } from "../../renderer/utils"; import { DESCRIPTORS } from "./preferences-helpers"; import type { UserPreferencesModel, StoreType } from "./preferences-helpers"; @@ -38,12 +37,6 @@ export class UserStore extends BaseStore /* implements UserStore @observable lastSeenAppVersion = "0.0.0"; - /** - * used in add-cluster page for providing context - * @deprecated No longer used - */ - @observable kubeConfigPath = kubeConfigDefaultPath; - /** * @deprecated No longer used */ diff --git a/src/common/utils/__tests__/paths.test.ts b/src/common/utils/__tests__/paths.test.ts index f5e8a7ccf5..5a52843432 100644 --- a/src/common/utils/__tests__/paths.test.ts +++ b/src/common/utils/__tests__/paths.test.ts @@ -3,12 +3,29 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { describeIf } from "../../../../integration/helpers/utils"; -import { isWindows } from "../../vars"; -import { isLogicalChildPath } from "../paths"; +import type { DiContainer } from "@ogre-tools/injectable"; +import path from "path"; +import { getDiForUnitTesting } from "../../../main/getDiForUnitTesting"; +import getAbsolutePathInjectable from "../../path/get-absolute-path.injectable"; +import getDirnameOfPathInjectable from "../../path/get-dirname.injectable"; +import type { IsLogicalChildPath } from "../../path/is-logical-child-path.injectable"; +import isLogicalChildPathInjectable from "../../path/is-logical-child-path.injectable"; describe("isLogicalChildPath", () => { - describeIf(isWindows)("windows tests", () => { + let di: DiContainer; + let isLogicalChildPath: IsLogicalChildPath; + + beforeEach(() => { + di = getDiForUnitTesting(); + }); + + describe("when using win32 paths", () => { + beforeEach(() => { + di.override(getAbsolutePathInjectable, () => path.win32.resolve); + di.override(getDirnameOfPathInjectable, () => path.win32.dirname); + isLogicalChildPath = di.inject(isLogicalChildPathInjectable); + }); + it.each([ { parentPath: "C:\\Foo", @@ -40,7 +57,13 @@ describe("isLogicalChildPath", () => { }); }); - describeIf(!isWindows)("posix tests", () => { + describe("when using posix paths", () => { + beforeEach(() => { + di.override(getAbsolutePathInjectable, () => path.posix.resolve); + di.override(getDirnameOfPathInjectable, () => path.posix.dirname); + isLogicalChildPath = di.inject(isLogicalChildPathInjectable); + }); + it.each([ { parentPath: "/foo", diff --git a/src/common/utils/buildUrl.ts b/src/common/utils/buildUrl.ts index 61639cea4d..82d114cd08 100644 --- a/src/common/utils/buildUrl.ts +++ b/src/common/utils/buildUrl.ts @@ -35,3 +35,24 @@ export function buildURLPositional

return buildURL(path, { params, query, fragment }); }; } + +export type UrlParamsFor = + Pathname extends `${string}/:${infer A}?/${infer Tail}` + ? Partial> & UrlParamsFor<`/${Tail}`> + : Pathname extends `${string}/:${infer A}/${infer Tail}` + ? Record & UrlParamsFor<`/${Tail}`> + : Pathname extends `${string}/:${infer A}?` + ? Partial> + : Pathname extends `${string}/:${infer A}` + ? Record + : {}; + +export interface UrlBuilder { + compile(params: UrlParamsFor, query?: object, fragment?: string): string; +} + +export function urlBuilderFor(pathname: Pathname): UrlBuilder { + return { + compile: buildURLPositional(pathname), + }; +} diff --git a/src/common/utils/channel/request-from-channel-injection-token.ts b/src/common/utils/channel/request-from-channel-injection-token.ts index 5f4492543f..f4d4fdbe0c 100644 --- a/src/common/utils/channel/request-from-channel-injection-token.ts +++ b/src/common/utils/channel/request-from-channel-injection-token.ts @@ -12,7 +12,7 @@ export type RequestFromChannel = < channel: TChannel, ...request: TChannel["_requestSignature"] extends void ? [] - : [TChannel["_requestSignature"]] + : [SetRequired["_requestSignature"]] ) => Promise["_responseSignature"]>; export const requestFromChannelInjectionToken = diff --git a/src/common/utils/date/get-current-date-time.ts b/src/common/utils/date/get-current-date-time.ts index bf3df2bd78..aa4d5e7fa3 100644 --- a/src/common/utils/date/get-current-date-time.ts +++ b/src/common/utils/date/get-current-date-time.ts @@ -5,3 +5,7 @@ import moment from "moment"; export const getCurrentDateTime = () => moment().utc().format(); + +export const getMillisecondsFromUnixEpoch = () => Date.now(); + +export const getSecondsFromUnixEpoch = () => Math.floor(getMillisecondsFromUnixEpoch() / 1000); diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts index e16f803c56..a9acaede86 100644 --- a/src/common/utils/index.ts +++ b/src/common/utils/index.ts @@ -22,7 +22,6 @@ export * from "./hash-set"; export * from "./n-fircate"; export * from "./noop"; export * from "./observable-crate/impl"; -export * from "./paths"; export * from "./promise-exec"; export * from "./readonly"; export * from "./reject-promise"; diff --git a/src/common/utils/paths.ts b/src/common/utils/paths.ts deleted file mode 100644 index ed31e612fb..0000000000 --- a/src/common/utils/paths.ts +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import path from "path"; -import os from "os"; - -export function resolveTilde(filePath: string) { - if (filePath === "~") { - return os.homedir(); - } - - if (filePath.startsWith("~/")) { - return `${os.homedir()}${filePath.slice(1)}`; - } - - return filePath; -} - -export function resolvePath(filePath: string): string { - return path.resolve(resolveTilde(filePath)); -} - -/** - * Checks if `testPath` represents a potential filesystem entry that would be - * logically "within" the `parentPath` directory. - * - * This function will return `true` in the above case, and `false` otherwise. - * It will return `false` if the two paths are the same (after resolving them). - * - * The function makes no FS calls and is platform dependant. Meaning that the - * results are only guaranteed to be correct for the platform you are running - * on. - * @param parentPath The known path of a directory - * @param testPath The path that is to be tested - */ -export function isLogicalChildPath(parentPath: string, testPath: string): boolean { - parentPath = path.resolve(parentPath); - testPath = path.resolve(testPath); - - if (parentPath === testPath) { - return false; - } - - while (testPath.length >= parentPath.length) { - if (testPath === parentPath) { - return true; - } - - testPath = path.dirname(testPath); - } - - return false; -} diff --git a/src/common/utils/sort-compare.ts b/src/common/utils/sort-compare.ts index ee32ab45c4..35d88c01b9 100644 --- a/src/common/utils/sort-compare.ts +++ b/src/common/utils/sort-compare.ts @@ -3,11 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type { SemVer } from "semver"; import semver, { coerce } from "semver"; -import * as iter from "./iter"; -import type { RawHelmChart } from "../k8s-api/endpoints/helm-charts.api"; -import logger from "../logger"; export enum Ordering { LESS = -1, @@ -49,52 +45,31 @@ export function sortCompare(left: T, right: T): Ordering { return Ordering.GREATER; } -interface ChartVersion { - version: string; - __version?: SemVer | null; -} - -export function sortCompareChartVersions(left: ChartVersion, right: ChartVersion): Ordering { - if (left.__version && right.__version) { - return semver.compare(right.__version, left.__version); - } - - if (!left.__version && right.__version) { - return Ordering.GREATER; - } - - if (left.__version && !right.__version) { - return Ordering.LESS; - } - - return sortCompare(left.version, right.version); -} - - - -export function sortCharts(charts: RawHelmChart[]) { - interface ExtendedHelmChart extends RawHelmChart { - __version?: SemVer | null; - } - - const chartsWithVersion = Array.from( - iter.map( - charts, - chart => { - const __version = coerce(chart.version, { loose: true }); - - if (!__version) { - logger.warn(`[HELM-SERVICE]: Version from helm chart is not loosely coercable to semver.`, { name: chart.name, version: chart.version, repo: chart.repo }); - } - - (chart as ExtendedHelmChart).__version = __version; - - return chart as ExtendedHelmChart; - }, - ), - ); - - return chartsWithVersion - .sort(sortCompareChartVersions) - .map(chart => (delete chart.__version, chart as RawHelmChart)); +/** + * This function sorts of list of items that have what should be a semver version formated string + * as the field `version` but if it is not loosely coercable to semver falls back to sorting them + * alphanumerically + */ +export function sortBySemverVersion(versioneds: T[]): T[] { + return versioneds + .map(versioned => ({ + __version: coerce(versioned.version, { loose: true }), + raw: versioned, + })) + .sort((left, right) => { + if (left.__version && right.__version) { + return semver.compare(right.__version, left.__version); + } + + if (!left.__version && right.__version) { + return Ordering.GREATER; + } + + if (left.__version && !right.__version) { + return Ordering.LESS; + } + + return sortCompare(left.raw.version, right.raw.version); + }) + .map(({ raw }) => raw); } diff --git a/src/common/utils/tar.ts b/src/common/utils/tar.ts index d351ec2507..58008dfe9d 100644 --- a/src/common/utils/tar.ts +++ b/src/common/utils/tar.ts @@ -5,7 +5,7 @@ // Helper for working with tarball files (.tar, .tgz) // Docs: https://github.com/npm/node-tar -import type { ExtractOptions, FileStat } from "tar"; +import type { FileStat } from "tar"; import tar from "tar"; import path from "path"; import { parse } from "./json"; @@ -62,18 +62,10 @@ export async function listTarEntries(filePath: string): Promise { await tar.list({ file: filePath, - onentry: (entry: FileStat) => { + onentry: (entry) => { entries.push(path.normalize(entry.path as unknown as string)); }, }); return entries; } - -export function extractTar(filePath: string, opts: ExtractOptions & { sync?: boolean } = {}) { - return tar.extract({ - file: filePath, - cwd: path.dirname(filePath), - ...opts, - }); -} diff --git a/src/common/vars.ts b/src/common/vars.ts index a5507e1ee6..94ac631896 100644 --- a/src/common/vars.ts +++ b/src/common/vars.ts @@ -23,6 +23,9 @@ export const isWindows = process.platform === "win32"; */ export const isLinux = process.platform === "linux"; +/** + * @deprecated switch to using `isDebuggingInjectable` + */ export const isDebugging = ["true", "1", "yes", "y", "on"].includes((process.env.DEBUG ?? "").toLowerCase()); /** diff --git a/src/common/vars/base-bundled-binaries-dir.injectable.ts b/src/common/vars/base-bundled-binaries-dir.injectable.ts index f0d78482c8..968ccc5c5b 100644 --- a/src/common/vars/base-bundled-binaries-dir.injectable.ts +++ b/src/common/vars/base-bundled-binaries-dir.injectable.ts @@ -4,17 +4,17 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import bundledResourcesDirectoryInjectable from "./bundled-resources-dir.injectable"; -import getAbsolutePathInjectable from "../path/get-absolute-path.injectable"; import normalizedPlatformArchitectureInjectable from "./normalized-platform-architecture.injectable"; +import joinPathsInjectable from "../path/join-paths.injectable"; const baseBundledBinariesDirectoryInjectable = getInjectable({ id: "base-bundled-binaries-directory", instantiate: (di) => { const bundledResourcesDirectory = di.inject(bundledResourcesDirectoryInjectable); const normalizedPlatformArchitecture = di.inject(normalizedPlatformArchitectureInjectable); - const getAbsolutePath = di.inject(getAbsolutePathInjectable); + const joinPaths = di.inject(joinPathsInjectable); - return getAbsolutePath( + return joinPaths( bundledResourcesDirectory, normalizedPlatformArchitecture, ); diff --git a/src/common/vars/bundled-resources-dir.injectable.ts b/src/common/vars/bundled-resources-dir.injectable.ts index e20a638384..2058b1d5d3 100644 --- a/src/common/vars/bundled-resources-dir.injectable.ts +++ b/src/common/vars/bundled-resources-dir.injectable.ts @@ -5,20 +5,20 @@ import { getInjectable } from "@ogre-tools/injectable"; import isProductionInjectable from "./is-production.injectable"; import normalizedPlatformInjectable from "./normalized-platform.injectable"; -import getAbsolutePathInjectable from "../path/get-absolute-path.injectable"; import lensResourcesDirInjectable from "./lens-resources-dir.injectable"; +import joinPathsInjectable from "../path/join-paths.injectable"; const bundledResourcesDirectoryInjectable = getInjectable({ id: "bundled-resources-directory", instantiate: (di) => { const isProduction = di.inject(isProductionInjectable); const normalizedPlatform = di.inject(normalizedPlatformInjectable); - const getAbsolutePath = di.inject(getAbsolutePathInjectable); + const joinPaths = di.inject(joinPathsInjectable); const lensResourcesDir = di.inject(lensResourcesDirInjectable); return isProduction ? lensResourcesDir - : getAbsolutePath(lensResourcesDir, "binaries", "client", normalizedPlatform); + : joinPaths(lensResourcesDir, "binaries", "client", normalizedPlatform); }, }); diff --git a/src/common/vars/is-debugging.global-override-for-injectable.ts b/src/common/vars/is-debugging.global-override-for-injectable.ts new file mode 100644 index 0000000000..7aa500ff2e --- /dev/null +++ b/src/common/vars/is-debugging.global-override-for-injectable.ts @@ -0,0 +1,9 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getGlobalOverride } from "../test-utils/get-global-override"; +import isDebuggingInjectable from "./is-debugging.injectable"; + +export default getGlobalOverride(isDebuggingInjectable, () => false); diff --git a/src/common/vars/is-debugging.injectable.ts b/src/common/vars/is-debugging.injectable.ts new file mode 100644 index 0000000000..079086e628 --- /dev/null +++ b/src/common/vars/is-debugging.injectable.ts @@ -0,0 +1,13 @@ +/** + * 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"; + +const isDebuggingInjectable = getInjectable({ + id: "is-debugging", + instantiate: () => ["true", "1", "yes", "y", "on"].includes((process.env.DEBUG ?? "").toLowerCase()), + causesSideEffects: true, +}); + +export default isDebuggingInjectable; diff --git a/src/common/vars/static-files-directory.injectable.ts b/src/common/vars/static-files-directory.injectable.ts index c881f12b97..e65e854e9c 100644 --- a/src/common/vars/static-files-directory.injectable.ts +++ b/src/common/vars/static-files-directory.injectable.ts @@ -3,17 +3,17 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import getAbsolutePathInjectable from "../path/get-absolute-path.injectable"; +import joinPathsInjectable from "../path/join-paths.injectable"; import lensResourcesDirInjectable from "./lens-resources-dir.injectable"; const staticFilesDirectoryInjectable = getInjectable({ id: "static-files-directory", instantiate: (di) => { - const getAbsolutePath = di.inject(getAbsolutePathInjectable); + const joinPaths = di.inject(joinPathsInjectable); const lensResourcesDir = di.inject(lensResourcesDirInjectable); - return getAbsolutePath(lensResourcesDir, "static"); + return joinPaths(lensResourcesDir, "static"); }, }); diff --git a/src/extensions/common-api/k8s-api.ts b/src/extensions/common-api/k8s-api.ts index 7fd25b08a1..f5bf1e4953 100644 --- a/src/extensions/common-api/k8s-api.ts +++ b/src/extensions/common-api/k8s-api.ts @@ -9,18 +9,28 @@ export { ResourceStack } from "../../common/k8s/resource-stack"; import apiManagerInjectable from "../../common/k8s-api/api-manager/manager.injectable"; +import createKubeApiForClusterInjectable from "../../common/k8s-api/create-kube-api-for-cluster.injectable"; +import createKubeApiForRemoteClusterInjectable from "../../common/k8s-api/create-kube-api-for-remote-cluster.injectable"; +import { asLegacyGlobalFunctionForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-function-for-extension-api"; import { asLegacyGlobalForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api"; export const apiManager = asLegacyGlobalForExtensionApi(apiManagerInjectable); +export const forCluster = asLegacyGlobalFunctionForExtensionApi(createKubeApiForClusterInjectable); +export const forRemoteCluster = asLegacyGlobalFunctionForExtensionApi(createKubeApiForRemoteClusterInjectable); -export { - KubeApi, - forCluster, - forRemoteCluster, - type ILocalKubeApiConfig, - type IRemoteKubeApiConfig, - type IKubeApiCluster, -} from "../../common/k8s-api/kube-api"; +export { KubeApi } from "../../common/k8s-api/kube-api"; + +/** + * @deprecated This type is unused + */ +export interface IKubeApiCluster { + metadata: { + uid: string; + }; +} + +export type { CreateKubeApiForRemoteClusterConfig as IRemoteKubeApiConfig } from "../../common/k8s-api/create-kube-api-for-remote-cluster.injectable"; +export type { CreateKubeApiForLocalClusterConfig as ILocalKubeApiConfig } from "../../common/k8s-api/create-kube-api-for-cluster.injectable"; export { KubeObject, diff --git a/src/extensions/extension-discovery/extension-discovery.injectable.ts b/src/extensions/extension-discovery/extension-discovery.injectable.ts index df5efadaf6..09d0f3b0ae 100644 --- a/src/extensions/extension-discovery/extension-discovery.injectable.ts +++ b/src/extensions/extension-discovery/extension-discovery.injectable.ts @@ -16,6 +16,19 @@ import readJsonFileInjectable from "../../common/fs/read-json-file.injectable"; import loggerInjectable from "../../common/logger.injectable"; import pathExistsInjectable from "../../common/fs/path-exists.injectable"; import watchInjectable from "../../common/fs/watch/watch.injectable"; +import accessPathInjectable from "../../common/fs/access-path.injectable"; +import copyInjectable from "../../common/fs/copy.injectable"; +import ensureDirInjectable from "../../common/fs/ensure-dir.injectable"; +import isProductionInjectable from "../../common/vars/is-production.injectable"; +import lstatInjectable from "../../common/fs/lstat.injectable"; +import readDirectoryInjectable from "../../common/fs/read-directory.injectable"; +import fileSystemSeparatorInjectable from "../../common/path/separator.injectable"; +import getBasenameOfPathInjectable from "../../common/path/get-basename.injectable"; +import getDirnameOfPathInjectable from "../../common/path/get-dirname.injectable"; +import getRelativePathInjectable from "../../common/path/get-relative-path.injectable"; +import joinPathsInjectable from "../../common/path/join-paths.injectable"; +import removePathInjectable from "../../common/fs/remove-path.injectable"; +import homeDirectoryPathInjectable from "../../common/os/home-directory-path.injectable"; const extensionDiscoveryInjectable = getInjectable({ id: "extension-discovery", @@ -33,6 +46,19 @@ const extensionDiscoveryInjectable = getInjectable({ pathExists: di.inject(pathExistsInjectable), watch: di.inject(watchInjectable), logger: di.inject(loggerInjectable), + accessPath: di.inject(accessPathInjectable), + copy: di.inject(copyInjectable), + removePath: di.inject(removePathInjectable), + ensureDirectory: di.inject(ensureDirInjectable), + isProduction: di.inject(isProductionInjectable), + lstat: di.inject(lstatInjectable), + readDirectory: di.inject(readDirectoryInjectable), + fileSystemSeparator: di.inject(fileSystemSeparatorInjectable), + getBasenameOfPath: di.inject(getBasenameOfPathInjectable), + getDirnameOfPath: di.inject(getDirnameOfPathInjectable), + getRelativePath: di.inject(getRelativePathInjectable), + joinPaths: di.inject(joinPathsInjectable), + homeDirectoryPath: di.inject(homeDirectoryPathInjectable), }), }); diff --git a/src/extensions/extension-discovery/extension-discovery.test.ts b/src/extensions/extension-discovery/extension-discovery.test.ts index e5a04bd5eb..f3630addaf 100644 --- a/src/extensions/extension-discovery/extension-discovery.test.ts +++ b/src/extensions/extension-discovery/extension-discovery.test.ts @@ -4,37 +4,40 @@ */ import type { FSWatcher } from "chokidar"; -import path from "path"; -import os from "os"; -import { Console } from "console"; import { getDiForUnitTesting } from "../../main/getDiForUnitTesting"; import extensionDiscoveryInjectable from "../extension-discovery/extension-discovery.injectable"; import type { ExtensionDiscovery } from "../extension-discovery/extension-discovery"; import installExtensionInjectable from "../extension-installer/install-extension/install-extension.injectable"; import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; -import mockFs from "mock-fs"; import { delay } from "../../renderer/utils"; -import { observable, when } from "mobx"; +import { observable, runInAction, when } from "mobx"; import readJsonFileInjectable from "../../common/fs/read-json-file.injectable"; import pathExistsInjectable from "../../common/fs/path-exists.injectable"; import watchInjectable from "../../common/fs/watch/watch.injectable"; import extensionApiVersionInjectable from "../../common/vars/extension-api-version.injectable"; - -console = new Console(process.stdout, process.stderr); // fix mockFS +import removePathInjectable from "../../common/fs/remove-path.injectable"; +import type { JoinPaths } from "../../common/path/join-paths.injectable"; +import joinPathsInjectable from "../../common/path/join-paths.injectable"; +import homeDirectoryPathInjectable from "../../common/os/home-directory-path.injectable"; describe("ExtensionDiscovery", () => { let extensionDiscovery: ExtensionDiscovery; let readJsonFileMock: jest.Mock; let pathExistsMock: jest.Mock; let watchMock: jest.Mock; + let joinPaths: JoinPaths; + let homeDirectoryPath: string; beforeEach(() => { const di = getDiForUnitTesting({ doGeneralOverrides: true }); - di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); + di.override(directoryForUserDataInjectable, () => "/some-directory-for-user-data"); di.override(installExtensionInjectable, () => () => Promise.resolve()); di.override(extensionApiVersionInjectable, () => "5.0.0"); + joinPaths = di.inject(joinPathsInjectable); + homeDirectoryPath = di.inject(homeDirectoryPathInjectable); + readJsonFileMock = jest.fn(); di.override(readJsonFileInjectable, () => readJsonFileMock); @@ -44,21 +47,17 @@ describe("ExtensionDiscovery", () => { watchMock = jest.fn(); di.override(watchInjectable, () => watchMock); - mockFs(); + di.override(removePathInjectable, () => async () => {}); // allow deleting files for now extensionDiscovery = di.inject(extensionDiscoveryInjectable); }); - afterEach(() => { - mockFs.restore(); - }); - it("emits add for added extension", async () => { const letTestFinish = observable.box(false); let addHandler!: (filePath: string) => void; readJsonFileMock.mockImplementation((p) => { - expect(p).toBe(path.join(os.homedir(), ".k8slens/extensions/my-extension/package.json")); + expect(p).toBe(joinPaths(homeDirectoryPath, ".k8slens/extensions/my-extension/package.json")); return { name: "my-extension", @@ -89,7 +88,7 @@ describe("ExtensionDiscovery", () => { extensionDiscovery.events.on("add", extension => { expect(extension).toEqual({ absolutePath: expect.any(String), - id: path.normalize("some-directory-for-user-data/node_modules/my-extension/package.json"), + id: "/some-directory-for-user-data/node_modules/my-extension/package.json", isBundled: false, isEnabled: false, isCompatible: true, @@ -100,12 +99,12 @@ describe("ExtensionDiscovery", () => { lens: "5.0.0", }, }, - manifestPath: path.normalize("some-directory-for-user-data/node_modules/my-extension/package.json"), + manifestPath: "/some-directory-for-user-data/node_modules/my-extension/package.json", }); - letTestFinish.set(true); + runInAction(() => letTestFinish.set(true)); }); - addHandler(path.join(extensionDiscovery.localFolderPath, "/my-extension/package.json")); + addHandler(joinPaths(extensionDiscovery.localFolderPath, "/my-extension/package.json")); await when(() => letTestFinish.get()); }); @@ -133,7 +132,7 @@ describe("ExtensionDiscovery", () => { extensionDiscovery.events.on("add", onAdd); - addHandler(path.join(extensionDiscovery.localFolderPath, "/my-extension/node_modules/dep/package.json")); + addHandler(joinPaths(extensionDiscovery.localFolderPath, "/my-extension/node_modules/dep/package.json")); await delay(10); diff --git a/src/extensions/extension-discovery/extension-discovery.ts b/src/extensions/extension-discovery/extension-discovery.ts index e6f500805a..c7781d78a6 100644 --- a/src/extensions/extension-discovery/extension-discovery.ts +++ b/src/extensions/extension-discovery/extension-discovery.ts @@ -5,16 +5,12 @@ import { ipcRenderer } from "electron"; import { EventEmitter } from "events"; -import fse from "fs-extra"; import { makeObservable, observable, reaction, when } from "mobx"; -import os from "os"; -import path from "path"; import { broadcastMessage, ipcMainHandle, ipcRendererOn } from "../../common/ipc"; import { isErrnoException, toJS } from "../../common/utils"; import type { ExtensionsStore } from "../extensions-store/extensions-store"; import type { ExtensionLoader } from "../extension-loader"; import type { LensExtensionId, LensExtensionManifest } from "../lens-extension"; -import { isProduction } from "../../common/vars"; import type { ExtensionInstallationStateStore } from "../extension-installation-state-store/extension-installation-state-store"; import type { PackageJson } from "type-fest"; import { extensionDiscoveryStateChannel } from "../../common/ipc/extension-handling"; @@ -23,20 +19,46 @@ import type { ReadJson } from "../../common/fs/read-json-file.injectable"; import type { Logger } from "../../common/logger"; import type { PathExists } from "../../common/fs/path-exists.injectable"; import type { Watch } from "../../common/fs/watch/watch.injectable"; +import type { Stats } from "fs"; +import { constants } from "fs"; +import type { LStat } from "../../common/fs/lstat.injectable"; +import type { ReadDirectory } from "../../common/fs/read-directory.injectable"; +import type { EnsureDirectory } from "../../common/fs/ensure-dir.injectable"; +import type { AccessPath } from "../../common/fs/access-path.injectable"; +import type { Copy } from "../../common/fs/copy.injectable"; +import type { JoinPaths } from "../../common/path/join-paths.injectable"; +import type { GetBasenameOfPath } from "../../common/path/get-basename.injectable"; +import type { GetDirnameOfPath } from "../../common/path/get-dirname.injectable"; +import type { GetRelativePath } from "../../common/path/get-relative-path.injectable"; +import type { RemovePath } from "../../common/fs/remove-path.injectable"; +import type TypedEventEmitter from "typed-emitter"; interface Dependencies { - extensionLoader: ExtensionLoader; - extensionsStore: ExtensionsStore; - extensionInstallationStateStore: ExtensionInstallationStateStore; + readonly extensionLoader: ExtensionLoader; + readonly extensionsStore: ExtensionsStore; + readonly extensionInstallationStateStore: ExtensionInstallationStateStore; + readonly extensionPackageRootDirectory: string; + readonly staticFilesDirectory: string; + readonly logger: Logger; + readonly isProduction: boolean; + readonly fileSystemSeparator: string; + readonly homeDirectoryPath: string; isCompatibleExtension: (manifest: LensExtensionManifest) => boolean; installExtension: (name: string) => Promise; installExtensions: (packageJsonPath: string, packagesJson: PackageJson) => Promise; - extensionPackageRootDirectory: string; - staticFilesDirectory: string; readJsonFile: ReadJson; pathExists: PathExists; + removePath: RemovePath; + lstat: LStat; watch: Watch; - logger: Logger; + readDirectory: ReadDirectory; + ensureDirectory: EnsureDirectory; + accessPath: AccessPath; + copy: Copy; + joinPaths: JoinPaths; + getBasenameOfPath: GetBasenameOfPath; + getDirnameOfPath: GetDirnameOfPath; + getRelativePath: GetRelativePath; } export interface InstalledExtension { @@ -67,12 +89,17 @@ interface ExtensionDiscoveryChannelMessage { * Returns true if the lstat is for a directory-like file (e.g. isDirectory or symbolic link) * @param lstat the stats to compare */ -const isDirectoryLike = (lstat: fse.Stats) => lstat.isDirectory() || lstat.isSymbolicLink(); +const isDirectoryLike = (lstat: Stats) => lstat.isDirectory() || lstat.isSymbolicLink(); interface LoadFromFolderOptions { isBundled?: boolean; } +interface ExtensionDiscoveryEvents { + add: (ext: InstalledExtension) => void; + remove: (extId: LensExtensionId) => void; +} + /** * Discovers installed bundled and local extensions from the filesystem. * Also watches for added and removed local extensions by watching the directory. @@ -96,30 +123,30 @@ export class ExtensionDiscovery { return when(() => this.isLoaded); } - public events = new EventEmitter(); + public readonly events: TypedEventEmitter = new EventEmitter(); constructor(protected readonly dependencies: Dependencies) { makeObservable(this); } get localFolderPath(): string { - return path.join(os.homedir(), ".k8slens", "extensions"); + return this.dependencies.joinPaths(this.dependencies.homeDirectoryPath, ".k8slens", "extensions"); } get packageJsonPath(): string { - return path.join(this.dependencies.extensionPackageRootDirectory, manifestFilename); + return this.dependencies.joinPaths(this.dependencies.extensionPackageRootDirectory, manifestFilename); } get inTreeTargetPath(): string { - return path.join(this.dependencies.extensionPackageRootDirectory, "extensions"); + return this.dependencies.joinPaths(this.dependencies.extensionPackageRootDirectory, "extensions"); } get inTreeFolderPath(): string { - return path.resolve(this.dependencies.staticFilesDirectory, "../extensions"); + return this.dependencies.joinPaths(this.dependencies.staticFilesDirectory, "../extensions"); } get nodeModulesPath(): string { - return path.join(this.dependencies.extensionPackageRootDirectory, "node_modules"); + return this.dependencies.joinPaths(this.dependencies.extensionPackageRootDirectory, "node_modules"); } /** @@ -184,24 +211,24 @@ export class ExtensionDiscovery { handleWatchFileAdd = async (manifestPath: string): Promise => { // e.g. "foo/package.json" - const relativePath = path.relative(this.localFolderPath, manifestPath); + const relativePath = this.dependencies.getRelativePath(this.localFolderPath, manifestPath); // Converts "foo/package.json" to ["foo", "package.json"], where length of 2 implies // that the added file is in a folder under local folder path. // This safeguards against a file watch being triggered under a sub-directory which is not an extension. - const isUnderLocalFolderPath = relativePath.split(path.sep).length === 2; + const isUnderLocalFolderPath = relativePath.split(this.dependencies.fileSystemSeparator).length === 2; - if (path.basename(manifestPath) === manifestFilename && isUnderLocalFolderPath) { + if (this.dependencies.getBasenameOfPath(manifestPath) === manifestFilename && isUnderLocalFolderPath) { try { this.dependencies.extensionInstallationStateStore.setInstallingFromMain(manifestPath); - const absPath = path.dirname(manifestPath); + const absPath = this.dependencies.getDirnameOfPath(manifestPath); // this.loadExtensionFromPath updates this.packagesJson const extension = await this.loadExtensionFromFolder(absPath); if (extension) { // Remove a broken symlink left by a previous installation if it exists. - await fse.remove(extension.manifestPath); + await this.dependencies.removePath(extension.manifestPath); // Install dependencies for the new extension await this.dependencies.installExtension(extension.absolutePath); @@ -226,8 +253,8 @@ export class ExtensionDiscovery { handleWatchUnlinkEvent = async (filePath: string): Promise => { // Check that the removed path is directly under this.localFolderPath // Note that the watcher can create unlink events for subdirectories of the extension - const extensionFolderName = path.basename(filePath); - const expectedPath = path.relative(this.localFolderPath, filePath); + const extensionFolderName = this.dependencies.getBasenameOfPath(filePath); + const expectedPath = this.dependencies.getRelativePath(this.localFolderPath, filePath); if (expectedPath !== extensionFolderName) { return; @@ -264,7 +291,7 @@ export class ExtensionDiscovery { * @param name e.g. "@mirantis/lens-extension-cc" */ removeSymlinkByPackageName(name: string): Promise { - return fse.remove(this.getInstalledPath(name)); + return this.dependencies.removePath(this.getInstalledPath(name)); } /** @@ -286,7 +313,7 @@ export class ExtensionDiscovery { await this.removeSymlinkByPackageName(manifest.name); // fs.remove does nothing if the path doesn't exist anymore - await fse.remove(absolutePath); + await this.dependencies.removePath(absolutePath); } async load(): Promise> { @@ -301,34 +328,29 @@ export class ExtensionDiscovery { `${logModule} loading extensions from ${this.dependencies.extensionPackageRootDirectory}`, ); - // fs.remove won't throw if path is missing - await fse.remove(path.join(this.dependencies.extensionPackageRootDirectory, "package-lock.json")); + await this.dependencies.removePath(this.dependencies.joinPaths(this.dependencies.extensionPackageRootDirectory, "package-lock.json")); - try { - // Verify write access to static/extensions, which is needed for symlinking - await fse.access(this.inTreeFolderPath, fse.constants.W_OK); + const canWriteToInTreeFolder = await this.dependencies.accessPath(this.inTreeFolderPath, constants.W_OK); + if (canWriteToInTreeFolder) { // Set bundled folder path to static/extensions this.bundledFolderPath = this.inTreeFolderPath; - } catch { - // If there is error accessing static/extensions, we need to copy in-tree extensions so that we can symlink them properly on "npm install". - // The error can happen if there is read-only rights to static/extensions, which would fail symlinking. - + } else { // Remove e.g. /Users//Library/Application Support/LensDev/extensions - await fse.remove(this.inTreeTargetPath); + await this.dependencies.removePath(this.inTreeTargetPath); // Create folder e.g. /Users//Library/Application Support/LensDev/extensions - await fse.ensureDir(this.inTreeTargetPath); + await this.dependencies.ensureDirectory(this.inTreeTargetPath); // Copy static/extensions to e.g. /Users//Library/Application Support/LensDev/extensions - await fse.copy(this.inTreeFolderPath, this.inTreeTargetPath); + await this.dependencies.copy(this.inTreeFolderPath, this.inTreeTargetPath); // Set bundled folder path to e.g. /Users//Library/Application Support/LensDev/extensions this.bundledFolderPath = this.inTreeTargetPath; } - await fse.ensureDir(this.nodeModulesPath); - await fse.ensureDir(this.localFolderPath); + await this.dependencies.ensureDirectory(this.nodeModulesPath); + await this.dependencies.ensureDirectory(this.localFolderPath); const extensions = await this.ensureExtensions(); @@ -342,7 +364,7 @@ export class ExtensionDiscovery { * e.g. "/Users//Library/Application Support/Lens/node_modules/@publisher/extension" */ protected getInstalledPath(name: string): string { - return path.join(this.nodeModulesPath, name); + return this.dependencies.joinPaths(this.nodeModulesPath, name); } /** @@ -350,7 +372,7 @@ export class ExtensionDiscovery { * e.g. "/Users//Library/Application Support/Lens/node_modules/@publisher/extension/package.json" */ protected getInstalledManifestPath(name: string): string { - return path.join(this.getInstalledPath(name), manifestFilename); + return this.dependencies.joinPaths(this.getInstalledPath(name), manifestFilename); } /** @@ -362,10 +384,11 @@ export class ExtensionDiscovery { const manifest = await this.dependencies.readJsonFile(manifestPath) as unknown as LensExtensionManifest; const id = this.getInstalledManifestPath(manifest.name); const isEnabled = this.dependencies.extensionsStore.isEnabled({ id, isBundled }); - const extensionDir = path.dirname(manifestPath); - const packedName = manifest.name.replaceAll("@", "").replaceAll("/", "-"); - const npmPackage = path.join(extensionDir, `${packedName}-${manifest.version}.tgz`); - const absolutePath = (isProduction && await this.dependencies.pathExists(npmPackage)) ? npmPackage : extensionDir; + const extensionDir = this.dependencies.getDirnameOfPath(manifestPath); + const npmPackage = this.dependencies.joinPaths(extensionDir, `${manifest.name}-${manifest.version}.tgz`); + const absolutePath = this.dependencies.isProduction && await this.dependencies.pathExists(npmPackage) + ? npmPackage + : extensionDir; const isCompatible = isBundled || this.dependencies.isCompatibleExtension(manifest); return { @@ -416,10 +439,10 @@ export class ExtensionDiscovery { async loadBundledExtensions(): Promise { const extensions: InstalledExtension[] = []; const folderPath = this.bundledFolderPath; - const paths = await fse.readdir(folderPath); + const paths = await this.dependencies.readDirectory(folderPath); for (const fileName of paths) { - const absPath = path.resolve(folderPath, fileName); + const absPath = this.dependencies.joinPaths(folderPath, fileName); const extension = await this.loadExtensionFromFolder(absPath, { isBundled: true }); if (extension) { @@ -433,7 +456,7 @@ export class ExtensionDiscovery { async loadFromFolder(folderPath: string, bundledExtensions: string[]): Promise { const extensions: InstalledExtension[] = []; - const paths = await fse.readdir(folderPath); + const paths = await this.dependencies.readDirectory(folderPath); for (const fileName of paths) { // do not allow to override bundled extensions @@ -441,17 +464,21 @@ export class ExtensionDiscovery { continue; } - const absPath = path.resolve(folderPath, fileName); + const absPath = this.dependencies.joinPaths(folderPath, fileName); - if (!fse.existsSync(absPath)) { - continue; - } + try { + const lstat = await this.dependencies.lstat(absPath); - const lstat = await fse.lstat(absPath); + // skip non-directories + if (!isDirectoryLike(lstat)) { + continue; + } + } catch (error) { + if (isErrnoException(error) && error.code === "ENOENT") { + continue; + } - // skip non-directories - if (!isDirectoryLike(lstat)) { - continue; + throw error; } const extension = await this.loadExtensionFromFolder(absPath); @@ -471,7 +498,7 @@ export class ExtensionDiscovery { * @param folderPath Folder path to extension */ async loadExtensionFromFolder(folderPath: string, { isBundled = false }: LoadFromFolderOptions = {}): Promise { - const manifestPath = path.resolve(folderPath, manifestFilename); + const manifestPath = this.dependencies.joinPaths(folderPath, manifestFilename); return this.getByManifest(manifestPath, { isBundled }); } diff --git a/src/extensions/extension-loader/extension-loader.injectable.ts b/src/extensions/extension-loader/extension-loader.injectable.ts index 7fe1cd5421..48edce4446 100644 --- a/src/extensions/extension-loader/extension-loader.injectable.ts +++ b/src/extensions/extension-loader/extension-loader.injectable.ts @@ -9,6 +9,9 @@ import { createExtensionInstanceInjectionToken } from "./create-extension-instan import extensionInstancesInjectable from "./extension-instances.injectable"; import type { LensExtension } from "../lens-extension"; import extensionInjectable from "./extension/extension.injectable"; +import loggerInjectable from "../../common/logger.injectable"; +import joinPathsInjectable from "../../common/path/join-paths.injectable"; +import getDirnameOfPathInjectable from "../../common/path/get-dirname.injectable"; const extensionLoaderInjectable = getInjectable({ id: "extension-loader", @@ -18,6 +21,9 @@ const extensionLoaderInjectable = getInjectable({ createExtensionInstance: di.inject(createExtensionInstanceInjectionToken), extensionInstances: di.inject(extensionInstancesInjectable), getExtension: (instance: LensExtension) => di.inject(extensionInjectable, instance), + logger: di.inject(loggerInjectable), + joinPaths: di.inject(joinPathsInjectable), + getDirnameOfPath: di.inject(getDirnameOfPathInjectable), }), }); diff --git a/src/extensions/extension-loader/extension-loader.ts b/src/extensions/extension-loader/extension-loader.ts index 16b28de8e1..5c63451276 100644 --- a/src/extensions/extension-loader/extension-loader.ts +++ b/src/extensions/extension-loader/extension-loader.ts @@ -7,11 +7,9 @@ import { ipcMain, ipcRenderer } from "electron"; import { isEqual } from "lodash"; import type { ObservableMap } from "mobx"; import { action, computed, makeObservable, observable, observe, reaction, when } from "mobx"; -import path from "path"; import { broadcastMessage, ipcMainOn, ipcRendererOn, ipcMainHandle } from "../../common/ipc"; import type { Disposer } from "../../common/utils"; import { isDefined, toJS } from "../../common/utils"; -import logger from "../../main/logger"; import type { InstalledExtension } from "../extension-discovery/extension-discovery"; import type { LensExtension, LensExtensionConstructor, LensExtensionId } from "../lens-extension"; import type { LensRendererExtension } from "../lens-renderer-extension"; @@ -23,14 +21,20 @@ import assert from "assert"; import { EventEmitter } from "../../common/event-emitter"; import type { CreateExtensionInstance } from "./create-extension-instance.token"; import type { Extension } from "./extension/extension.injectable"; +import type { Logger } from "../../common/logger"; +import type { JoinPaths } from "../../common/path/join-paths.injectable"; +import type { GetDirnameOfPath } from "../../common/path/get-dirname.injectable"; const logModule = "[EXTENSIONS-LOADER]"; interface Dependencies { + readonly extensionInstances: ObservableMap; + readonly logger: Logger; updateExtensionsState: (extensionsState: Record) => void; createExtensionInstance: CreateExtensionInstance; - readonly extensionInstances: ObservableMap; getExtension: (instance: LensExtension) => Extension; + joinPaths: JoinPaths; + getDirnameOfPath: GetDirnameOfPath; } export interface ExtensionLoading { @@ -159,7 +163,7 @@ export class ExtensionLoader { @action removeInstance(lensExtensionId: LensExtensionId) { - logger.info(`${logModule} deleting extension instance ${lensExtensionId}`); + this.dependencies.logger.info(`${logModule} deleting extension instance ${lensExtensionId}`); const instance = this.dependencies.extensionInstances.get(lensExtensionId); if (!instance) { @@ -177,7 +181,7 @@ export class ExtensionLoader { this.dependencies.extensionInstances.delete(lensExtensionId); this.nonInstancesByName.delete(instance.name); } catch (error) { - logger.error(`${logModule}: deactivation extension error`, { lensExtensionId, error }); + this.dependencies.logger.error(`${logModule}: deactivation extension error`, { lensExtensionId, error }); } } @@ -252,7 +256,7 @@ export class ExtensionLoader { } loadOnClusterManagerRenderer = () => { - logger.debug(`${logModule}: load on main renderer (cluster manager)`); + this.dependencies.logger.debug(`${logModule}: load on main renderer (cluster manager)`); return this.autoInitExtensions(async (ext) => { const extension = ext as LensRendererExtension; @@ -274,7 +278,7 @@ export class ExtensionLoader { }; loadOnClusterRenderer = () => { - logger.debug(`${logModule}: load on cluster renderer (dashboard)`); + this.dependencies.logger.debug(`${logModule}: load on cluster renderer (dashboard)`); this.autoInitExtensions(async () => []); }; @@ -313,7 +317,7 @@ export class ExtensionLoader { activated: instance.activate(), }; } catch (err) { - logger.error(`${logModule}: error loading extension`, { ext: extension, err }); + this.dependencies.logger.error(`${logModule}: error loading extension`, { ext: extension, err }); } } else if (!extension.isEnabled && alreadyInit) { this.removeInstance(extId); @@ -330,7 +334,7 @@ export class ExtensionLoader { extensions.map(extension => // If extension activation fails, log error extension.activated.catch((error) => { - logger.error(`${logModule}: activation extension error`, { ext: extension.installedExtension, error }); + this.dependencies.logger.error(`${logModule}: activation extension error`, { ext: extension.installedExtension, error }); }), ), ); @@ -344,7 +348,7 @@ export class ExtensionLoader { // Return ExtensionLoading[] return extensions.map(extension => { const loaded = extension.instance.enable(register).catch((err) => { - logger.error(`${logModule}: failed to enable`, { ext: extension, err }); + this.dependencies.logger.error(`${logModule}: failed to enable`, { ext: extension, err }); }); return { @@ -370,14 +374,14 @@ export class ExtensionLoader { return null; } - const extAbsolutePath = path.resolve(path.join(path.dirname(extension.manifestPath), extRelativePath)); + const extAbsolutePath = this.dependencies.joinPaths(this.dependencies.getDirnameOfPath(extension.manifestPath), extRelativePath); try { return __non_webpack_require__(extAbsolutePath).default; } catch (error) { const message = (error instanceof Error ? error.stack : undefined) || error; - logger.error(`${logModule}: can't load ${entryPointName} for "${extension.manifest.name}": ${message}`, { extension }); + this.dependencies.logger.error(`${logModule}: can't load ${entryPointName} for "${extension.manifest.name}": ${message}`, { extension }); } return null; diff --git a/src/extensions/extension-loader/file-system-provisioner-store/directory-for-extension-data.injectable.ts b/src/extensions/extension-loader/file-system-provisioner-store/directory-for-extension-data.injectable.ts index 896c74da6d..469ed85949 100644 --- a/src/extensions/extension-loader/file-system-provisioner-store/directory-for-extension-data.injectable.ts +++ b/src/extensions/extension-loader/file-system-provisioner-store/directory-for-extension-data.injectable.ts @@ -4,16 +4,16 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; -import getAbsolutePathInjectable from "../../../common/path/get-absolute-path.injectable"; +import joinPathsInjectable from "../../../common/path/join-paths.injectable"; const directoryForExtensionDataInjectable = getInjectable({ id: "directory-for-extension-data", instantiate: (di) => { - const getAbsolutePath = di.inject(getAbsolutePathInjectable); + const joinPaths = di.inject(joinPathsInjectable); const directoryForUserData = di.inject(directoryForUserDataInjectable); - return getAbsolutePath(directoryForUserData, "extension_data"); + return joinPaths(directoryForUserData, "extension_data"); }, }); diff --git a/src/features/cluster/__snapshots__/order-of-sidebar-items.test.tsx.snap b/src/features/cluster/__snapshots__/order-of-sidebar-items.test.tsx.snap index 7f76811494..8a293a41f9 100644 --- a/src/features/cluster/__snapshots__/order-of-sidebar-items.test.tsx.snap +++ b/src/features/cluster/__snapshots__/order-of-sidebar-items.test.tsx.snap @@ -484,6 +484,7 @@ exports[`cluster - order of sidebar items when rendered renders 1`] = ` > -

+
Close ⌘+W
@@ -1096,6 +1099,7 @@ exports[`cluster - order of sidebar items when rendered when parent is expanded > -
+
Close ⌘+W
diff --git a/src/features/cluster/__snapshots__/sidebar-and-tab-navigation-for-core.test.tsx.snap b/src/features/cluster/__snapshots__/sidebar-and-tab-navigation-for-core.test.tsx.snap index 49fcc917a4..6d64889cff 100644 --- a/src/features/cluster/__snapshots__/sidebar-and-tab-navigation-for-core.test.tsx.snap +++ b/src/features/cluster/__snapshots__/sidebar-and-tab-navigation-for-core.test.tsx.snap @@ -457,6 +457,7 @@ exports[`cluster - sidebar and tab navigation for core given core registrations > -
+
Close ⌘+W
@@ -990,6 +993,7 @@ exports[`cluster - sidebar and tab navigation for core given core registrations > -
+
Close ⌘+W
@@ -1543,6 +1549,7 @@ exports[`cluster - sidebar and tab navigation for core given core registrations > -
+
Close ⌘+W
@@ -1976,6 +1985,7 @@ exports[`cluster - sidebar and tab navigation for core given core registrations > -
+
Close ⌘+W
@@ -2388,6 +2400,7 @@ exports[`cluster - sidebar and tab navigation for core given core registrations > -
+
Close ⌘+W
@@ -2941,6 +2956,7 @@ exports[`cluster - sidebar and tab navigation for core given core registrations > -
+
Close ⌘+W
@@ -3474,6 +3492,7 @@ exports[`cluster - sidebar and tab navigation for core given core registrations > -
+
Close ⌘+W
diff --git a/src/features/cluster/__snapshots__/sidebar-and-tab-navigation-for-extensions.test.tsx.snap b/src/features/cluster/__snapshots__/sidebar-and-tab-navigation-for-extensions.test.tsx.snap index fff3da0126..ac415ae3e2 100644 --- a/src/features/cluster/__snapshots__/sidebar-and-tab-navigation-for-extensions.test.tsx.snap +++ b/src/features/cluster/__snapshots__/sidebar-and-tab-navigation-for-extensions.test.tsx.snap @@ -457,6 +457,7 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit > -
+
Close ⌘+W
@@ -990,6 +993,7 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit > -
+
Close ⌘+W
@@ -1559,6 +1565,7 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit > -
+
Close ⌘+W
@@ -2047,6 +2056,7 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit > -
+
Close ⌘+W
@@ -2535,6 +2547,7 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit > -
+
Close ⌘+W
@@ -2986,6 +3001,7 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit > -
+
Close ⌘+W
@@ -3555,6 +3573,7 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit > -
+
Close ⌘+W
@@ -4088,6 +4109,7 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit > -
+
Close ⌘+W
diff --git a/src/features/cluster/__snapshots__/visibility-of-sidebar-items.test.tsx.snap b/src/features/cluster/__snapshots__/visibility-of-sidebar-items.test.tsx.snap index ed36d5134f..7fe8edf8cf 100644 --- a/src/features/cluster/__snapshots__/visibility-of-sidebar-items.test.tsx.snap +++ b/src/features/cluster/__snapshots__/visibility-of-sidebar-items.test.tsx.snap @@ -429,6 +429,7 @@ exports[`cluster - visibility of sidebar items given kube resource for route is > -
+
Close ⌘+W
@@ -974,6 +977,7 @@ exports[`cluster - visibility of sidebar items given kube resource for route is > -
+
Close ⌘+W
diff --git a/src/features/cluster/__snapshots__/workload-overview.test.tsx.snap b/src/features/cluster/__snapshots__/workload-overview.test.tsx.snap index 6dbe47992f..e53ace250f 100644 --- a/src/features/cluster/__snapshots__/workload-overview.test.tsx.snap +++ b/src/features/cluster/__snapshots__/workload-overview.test.tsx.snap @@ -484,6 +484,7 @@ exports[`workload overview when navigating to workload overview renders 1`] = ` > -
+
Close ⌘+W
diff --git a/src/features/cluster/delete-dialog/__snapshots__/delete-cluster-dialog.test.tsx.snap b/src/features/cluster/delete-dialog/__snapshots__/delete-cluster-dialog.test.tsx.snap new file mode 100644 index 0000000000..907f60bb8f --- /dev/null +++ b/src/features/cluster/delete-dialog/__snapshots__/delete-cluster-dialog.test.tsx.snap @@ -0,0 +1,2192 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Deleting a cluster when an internal kubeconfig cluster is used when the dialog is opened renders 1`] = ` + +
+
+
+
+ + + home + + + + + arrow_back + + + + + arrow_forward + + +
+
+
+
+
+
+
+
+
+
+ + + arrow_left + + +
+
+ 0 +
+
+ + + arrow_right + + +
+
+
+
+
+
+
+
+
+ + +`; + +exports[`Deleting a cluster when the kubeconfig has multiple clusters when the dialog is opened for not the current cluster renders 1`] = ` + +
+
+
+
+ + + home + + + + + arrow_back + + + + + arrow_forward + + +
+
+
+
+
+
+
+
+
+
+ + + arrow_left + + +
+
+ 0 +
+
+ + + arrow_right + + +
+
+
+
+
+
+
+
+
+ + +`; + +exports[`Deleting a cluster when the kubeconfig has multiple clusters when the dialog is opened for not the current cluster when context switching checkbox is clicked renders 1`] = ` + +
+
+
+
+ + + home + + + + + arrow_back + + + + + arrow_forward + + +
+
+
+
+
+
+
+
+
+
+ + + arrow_left + + +
+
+ 0 +
+
+ + + arrow_right + + +
+
+
+
+
+
+
+
+
+ + +`; + +exports[`Deleting a cluster when the kubeconfig has multiple clusters when the dialog is opened for the current cluster renders 1`] = ` + +
+
+
+
+ + + home + + + + + arrow_back + + + + + arrow_forward + + +
+
+
+
+
+
+
+
+
+
+ + + arrow_left + + +
+
+ 0 +
+
+ + + arrow_right + + +
+
+
+
+
+
+
+
+
+ + +`; + +exports[`Deleting a cluster when the kubeconfig has only one cluster when the dialog is opened renders 1`] = ` + +
+
+
+
+ + + home + + + + + arrow_back + + + + + arrow_forward + + +
+
+
+
+
+
+
+
+
+
+ + + arrow_left + + +
+
+ 0 +
+
+ + + arrow_right + + +
+
+
+
+
+
+
+
+
+ + +`; diff --git a/src/features/cluster/delete-dialog/common/clear-as-deleting-channel.injectable.ts b/src/features/cluster/delete-dialog/common/clear-as-deleting-channel.injectable.ts new file mode 100644 index 0000000000..d45fecb3c2 --- /dev/null +++ b/src/features/cluster/delete-dialog/common/clear-as-deleting-channel.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 type { ClusterId } from "../../../../common/cluster-types"; +import type { RequestChannel } from "../../../../common/utils/channel/request-channel-injection-token"; +import { requestChannelInjectionToken } from "../../../../common/utils/channel/request-channel-injection-token"; + +export type ClearClusterAsDeletingChannel = RequestChannel; + +const clearClusterAsDeletingChannelInjectable = getInjectable({ + id: "clear-cluster-as-deleting-channel", + instantiate: (): ClearClusterAsDeletingChannel => ({ + id: "clear-cluster-as-deleting", + }), + injectionToken: requestChannelInjectionToken, +}); + +export default clearClusterAsDeletingChannelInjectable; diff --git a/src/features/cluster/delete-dialog/common/delete-channel.injectable.ts b/src/features/cluster/delete-dialog/common/delete-channel.injectable.ts new file mode 100644 index 0000000000..9242b062c5 --- /dev/null +++ b/src/features/cluster/delete-dialog/common/delete-channel.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 type { ClusterId } from "../../../../common/cluster-types"; +import type { RequestChannel } from "../../../../common/utils/channel/request-channel-injection-token"; +import { requestChannelInjectionToken } from "../../../../common/utils/channel/request-channel-injection-token"; + +export type DeleteClusterChannel = RequestChannel; + +const deleteClusterChannelInjectable = getInjectable({ + id: "delete-cluster-channel", + instantiate: (): DeleteClusterChannel => ({ + id: "delete-cluster", + }), + injectionToken: requestChannelInjectionToken, +}); + +export default deleteClusterChannelInjectable; diff --git a/src/features/cluster/delete-dialog/common/set-as-deleting-channel.injectable.ts b/src/features/cluster/delete-dialog/common/set-as-deleting-channel.injectable.ts new file mode 100644 index 0000000000..b625dfa14e --- /dev/null +++ b/src/features/cluster/delete-dialog/common/set-as-deleting-channel.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 type { ClusterId } from "../../../../common/cluster-types"; +import type { RequestChannel } from "../../../../common/utils/channel/request-channel-injection-token"; +import { requestChannelInjectionToken } from "../../../../common/utils/channel/request-channel-injection-token"; + +export type SetClusterAsDeletingChannel = RequestChannel; + +const setClusterAsDeletingChannelInjectable = getInjectable({ + id: "set-cluster-as-deleting-channel", + instantiate: (): SetClusterAsDeletingChannel => ({ + id: "set-cluster-as-deleting", + }), + injectionToken: requestChannelInjectionToken, +}); + +export default setClusterAsDeletingChannelInjectable; diff --git a/src/features/cluster/delete-dialog/delete-cluster-dialog.test.tsx b/src/features/cluster/delete-dialog/delete-cluster-dialog.test.tsx new file mode 100644 index 0000000000..a65d19b052 --- /dev/null +++ b/src/features/cluster/delete-dialog/delete-cluster-dialog.test.tsx @@ -0,0 +1,284 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +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 storesAndApisCanBeCreatedInjectable from "../../../renderer/stores-apis-can-be-created.injectable"; +import type { Cluster } from "../../../common/cluster/cluster"; +import navigateToCatalogInjectable from "../../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable"; +import appEventBusInjectable from "../../../common/app-event-bus/app-event-bus.injectable"; +import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable"; +import joinPathsInjectable from "../../../common/path/join-paths.injectable"; + +const currentClusterServerUrl = "https://localhost"; +const nonCurrentClusterServerUrl = "http://localhost"; +const multiClusterConfig = ` +apiVersion: v1 +clusters: +- cluster: + server: ${currentClusterServerUrl} + name: some-current-context-cluster +- cluster: + server: ${nonCurrentClusterServerUrl} + name: some-non-current-context-cluster +contexts: +- context: + cluster: some-current-context-cluster + user: some-user + name: some-current-context +- context: + cluster: some-non-current-context-cluster + user: some-user + name: some-non-current-context +current-context: some-current-context +kind: Config +preferences: {} +users: +- name: some-user + user: + token: kubeconfig-user-q4lm4:xxxyyyy +`; + +const singleClusterServerUrl = "http://localhost"; +const singleClusterConfig = ` +apiVersion: v1 +clusters: +- cluster: + server: ${singleClusterServerUrl} + name: some-cluster +contexts: +- context: + cluster: some-cluster + user: some-user + name: some-context +current-context: some-context +kind: Config +preferences: {} +users: +- name: some-user + user: + token: kubeconfig-user-q4lm4:xxxyyyy +`; + +describe("Deleting a cluster", () => { + let builder: ApplicationBuilder; + let openDeleteClusterDialog: OpenDeleteClusterDialog; + let createCluster: CreateCluster; + let rendered: RenderResult; + let config: KubeConfig; + + beforeEach(async () => { + config = new KubeConfig(); + 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"); + }); + + builder.beforeWindowStart((windowDi) => { + windowDi.override(storesAndApisCanBeCreatedInjectable, () => true); + openDeleteClusterDialog = windowDi.inject(openDeleteClusterDialogInjectable); + + // TODO: remove this line when all global uses of appEventBus are removed + windowDi.permitSideEffects(appEventBusInjectable); + }); + + builder.afterWindowStart(windowDi => { + createCluster = windowDi.inject(createClusterInjectionToken); + + const navigateToCatalog = windowDi.inject(navigateToCatalogInjectable); + + navigateToCatalog(); + }); + + rendered = await builder.render(); + }); + + describe("when the kubeconfig has multiple clusters", () => { + let currentCluster: Cluster; + let nonCurrentCluster: Cluster; + + beforeEach(() => { + config.loadFromString(multiClusterConfig); + + currentCluster = createCluster({ + id: "some-current-context-cluster", + contextName: "some-current-context", + preferences: { + clusterName: "some-current-context-cluster", + }, + kubeConfigPath: "./temp-kube-config", + }, { + clusterServerUrl: currentClusterServerUrl, + }); + nonCurrentCluster = createCluster({ + id: "some-non-current-context-cluster", + contextName: "some-non-current-context", + preferences: { + clusterName: "some-non-current-context-cluster", + }, + kubeConfigPath: "./temp-kube-config", + }, { + clusterServerUrl: currentClusterServerUrl, + }); + }); + + describe("when the dialog is opened for the current cluster", () => { + // TODO: replace with actual behaviour instead of technical use + beforeEach(async () => { + openDeleteClusterDialog({ + cluster: currentCluster, + config, + }); + + await rendered.findByTestId("delete-cluster-dialog"); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + it("shows context switcher", () => { + expect(rendered.queryByText("Select new context...")).toBeInTheDocument(); + }); + + it("shows warning", () => { + expect(rendered.queryByTestId("current-context-warning")).toBeInTheDocument(); + }); + }); + + describe("when the dialog is opened for not the current cluster", () => { + // TODO: replace with actual behaviour instead of technical use + beforeEach(async () => { + openDeleteClusterDialog({ + cluster: nonCurrentCluster, + config, + }); + + await rendered.findByTestId("delete-cluster-dialog"); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + it("shows warning", () => { + expect(rendered.queryByTestId("kubeconfig-change-warning")).toBeInTheDocument(); + }); + + it("does not show context switcher", () => { + expect(rendered.queryByText("Select new context...")).not.toBeInTheDocument(); + }); + + describe("when context switching checkbox is clicked", () => { + beforeEach(() => { + rendered.getByTestId("delete-cluster-dialog-context-switch").click(); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + it("shows context switcher", () => { + expect(rendered.queryByText("Select new context...")).toBeInTheDocument(); + }); + }); + }); + }); + + describe("when an internal kubeconfig cluster is used", () => { + let currentCluster: Cluster; + + beforeEach(() => { + config.loadFromString(singleClusterConfig); + + const directoryForKubeConfigs = builder.applicationWindow.only.di.inject(directoryForKubeConfigsInjectable); + const joinPaths = builder.applicationWindow.only.di.inject(joinPathsInjectable); + + currentCluster = createCluster({ + id: "some-cluster", + contextName: "some-context", + preferences: { + clusterName: "some-cluster", + }, + kubeConfigPath: joinPaths(directoryForKubeConfigs, "some-cluster.json"), + }, { + clusterServerUrl: singleClusterServerUrl, + }); + }); + + describe("when the dialog is opened", () => { + // TODO: replace with actual behaviour instead of technical use + beforeEach(async () => { + openDeleteClusterDialog({ + cluster: currentCluster, + config, + }); + + await rendered.findByTestId("delete-cluster-dialog"); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + it("shows warning", () => { + expect(rendered.queryByTestId("internal-kubeconfig-warning")).toBeInTheDocument(); + }); + }); + }); + + describe("when the kubeconfig has only one cluster", () => { + let currentCluster: Cluster; + + beforeEach(() => { + config.loadFromString(singleClusterConfig); + + currentCluster = createCluster({ + id: "some-cluster", + contextName: "some-context", + preferences: { + clusterName: "some-cluster", + }, + kubeConfigPath: "./temp-kube-config", + }, { + clusterServerUrl: singleClusterServerUrl, + }); + }); + + describe("when the dialog is opened", () => { + // TODO: replace with actual behaviour instead of technical use + beforeEach(async () => { + openDeleteClusterDialog({ + cluster: currentCluster, + config, + }); + + await rendered.findByTestId("delete-cluster-dialog"); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); + }); + + it("shows warning", () => { + expect(rendered.queryByTestId("no-more-contexts-warning")).toBeInTheDocument(); + }); + }); + }); +}); diff --git a/src/features/cluster/delete-dialog/main/clear-as-deleteing-channel-handler.injectable.ts b/src/features/cluster/delete-dialog/main/clear-as-deleteing-channel-handler.injectable.ts new file mode 100644 index 0000000000..f1b89573d8 --- /dev/null +++ b/src/features/cluster/delete-dialog/main/clear-as-deleteing-channel-handler.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 } from "@ogre-tools/injectable"; +import { requestChannelListenerInjectionToken } from "../../../../common/utils/channel/request-channel-listener-injection-token"; +import clustersThatAreBeingDeletedInjectable from "../../../../main/cluster/are-being-deleted.injectable"; +import clearClusterAsDeletingChannelInjectable from "../common/clear-as-deleting-channel.injectable"; + +const clearClusterAsDeletingChannelHandlerInjectable = getInjectable({ + id: "clear-cluster-as-deleting-channel-handler", + instantiate: (di) => { + const clustersThatAreBeingDeleted = di.inject(clustersThatAreBeingDeletedInjectable); + + return { + channel: di.inject(clearClusterAsDeletingChannelInjectable), + handler: (clusterId) => clustersThatAreBeingDeleted.delete(clusterId), + }; + }, + injectionToken: requestChannelListenerInjectionToken, +}); + +export default clearClusterAsDeletingChannelHandlerInjectable; diff --git a/src/features/cluster/delete-dialog/main/delete-channel-handler.injectable.ts b/src/features/cluster/delete-dialog/main/delete-channel-handler.injectable.ts new file mode 100644 index 0000000000..8945ffe625 --- /dev/null +++ b/src/features/cluster/delete-dialog/main/delete-channel-handler.injectable.ts @@ -0,0 +1,56 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import appEventBusInjectable from "../../../../common/app-event-bus/app-event-bus.injectable"; +import clusterFramesInjectable from "../../../../common/cluster-frames.injectable"; +import clusterStoreInjectable from "../../../../common/cluster-store/cluster-store.injectable"; +import directoryForLensLocalStorageInjectable from "../../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable"; +import deleteFileInjectable from "../../../../common/fs/delete-file.injectable"; +import joinPathsInjectable from "../../../../common/path/join-paths.injectable"; +import { requestChannelListenerInjectionToken } from "../../../../common/utils/channel/request-channel-listener-injection-token"; +import deleteClusterChannelInjectable from "../common/delete-channel.injectable"; + +const deleteClusterChannelHandlerInjectable = getInjectable({ + id: "delete-cluster-channel-handler", + instantiate: (di) => { + const appEventBus = di.inject(appEventBusInjectable); + const clusterStore = di.inject(clusterStoreInjectable); + const clusterFrames = di.inject(clusterFramesInjectable); + const joinPaths = di.inject(joinPathsInjectable); + const directoryForLensLocalStorage = di.inject(directoryForLensLocalStorageInjectable); + const deleteFile = di.inject(deleteFileInjectable); + + return { + channel: di.inject(deleteClusterChannelInjectable), + handler: async (clusterId) =>{ + appEventBus.emit({ name: "cluster", action: "remove" }); + + const cluster = clusterStore.getById(clusterId); + + if (!cluster) { + return; + } + + cluster.disconnect(); + clusterFrames.delete(cluster.id); + + // Remove from the cluster store as well, this should clear any old settings + clusterStore.clusters.delete(cluster.id); + + try { + // remove the local storage file + const localStorageFilePath = joinPaths(directoryForLensLocalStorage, `${cluster.id}.json`); + + await deleteFile(localStorageFilePath); + } catch { + // ignore error + } + }, + }; + }, + injectionToken: requestChannelListenerInjectionToken, +}); + +export default deleteClusterChannelHandlerInjectable; diff --git a/src/features/cluster/delete-dialog/main/set-as-deleteing-channel-handler.injectable.ts b/src/features/cluster/delete-dialog/main/set-as-deleteing-channel-handler.injectable.ts new file mode 100644 index 0000000000..0b8862e384 --- /dev/null +++ b/src/features/cluster/delete-dialog/main/set-as-deleteing-channel-handler.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 } from "@ogre-tools/injectable"; +import { requestChannelListenerInjectionToken } from "../../../../common/utils/channel/request-channel-listener-injection-token"; +import clustersThatAreBeingDeletedInjectable from "../../../../main/cluster/are-being-deleted.injectable"; +import setClusterAsDeletingChannelInjectable from "../common/set-as-deleting-channel.injectable"; + +const setClusterAsDeletingChannelHandlerInjectable = getInjectable({ + id: "set-cluster-as-deleting-channel-handler", + instantiate: (di) => { + const clustersThatAreBeingDeleted = di.inject(clustersThatAreBeingDeletedInjectable); + + return { + channel: di.inject(setClusterAsDeletingChannelInjectable), + handler: (clusterId) => clustersThatAreBeingDeleted.add(clusterId), + }; + }, + injectionToken: requestChannelListenerInjectionToken, +}); + +export default setClusterAsDeletingChannelHandlerInjectable; diff --git a/src/features/cluster/delete-dialog/renderer/request-clear-as-deleting.injectable.ts b/src/features/cluster/delete-dialog/renderer/request-clear-as-deleting.injectable.ts new file mode 100644 index 0000000000..b480630f6c --- /dev/null +++ b/src/features/cluster/delete-dialog/renderer/request-clear-as-deleting.injectable.ts @@ -0,0 +1,22 @@ +/** + * 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 { ClusterId } from "../../../../common/cluster-types"; +import requestFromChannelInjectable from "../../../../renderer/utils/channel/request-from-channel.injectable"; +import clearClusterAsDeletingChannelInjectable from "../common/clear-as-deleting-channel.injectable"; + +export type RequestClearClusterAsDeleting = (clusterId: ClusterId) => Promise; + +const requestClearClusterAsDeletingInjectable = getInjectable({ + id: "request-clear-cluster-as-deleting", + instantiate: (di): RequestClearClusterAsDeleting => { + const requestChannel = di.inject(requestFromChannelInjectable); + const clearClusterAsDeletingChannel = di.inject(clearClusterAsDeletingChannelInjectable); + + return (clusterId) => requestChannel(clearClusterAsDeletingChannel, clusterId); + }, +}); + +export default requestClearClusterAsDeletingInjectable; diff --git a/src/features/cluster/delete-dialog/renderer/request-delete.injectable.ts b/src/features/cluster/delete-dialog/renderer/request-delete.injectable.ts new file mode 100644 index 0000000000..602923e60e --- /dev/null +++ b/src/features/cluster/delete-dialog/renderer/request-delete.injectable.ts @@ -0,0 +1,22 @@ +/** + * 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 { ClusterId } from "../../../../common/cluster-types"; +import requestFromChannelInjectable from "../../../../renderer/utils/channel/request-from-channel.injectable"; +import deleteClusterChannelInjectable from "../common/delete-channel.injectable"; + +export type RequestDeleteCluster = (clusterId: ClusterId) => Promise; + +const requestDeleteClusterInjectable = getInjectable({ + id: "request-delete-cluster", + instantiate: (di): RequestDeleteCluster => { + const requestChannel = di.inject(requestFromChannelInjectable); + const deleteClusterChannel = di.inject(deleteClusterChannelInjectable); + + return (clusterId) => requestChannel(deleteClusterChannel, clusterId); + }, +}); + +export default requestDeleteClusterInjectable; diff --git a/src/features/cluster/delete-dialog/renderer/request-set-as-deleting.injectable.ts b/src/features/cluster/delete-dialog/renderer/request-set-as-deleting.injectable.ts new file mode 100644 index 0000000000..997348d44d --- /dev/null +++ b/src/features/cluster/delete-dialog/renderer/request-set-as-deleting.injectable.ts @@ -0,0 +1,22 @@ +/** + * 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 { ClusterId } from "../../../../common/cluster-types"; +import requestFromChannelInjectable from "../../../../renderer/utils/channel/request-from-channel.injectable"; +import setClusterAsDeletingChannelInjectable from "../common/set-as-deleting-channel.injectable"; + +export type RequestSetClusterAsDeleting = (clusterId: ClusterId) => Promise; + +const requestSetClusterAsDeletingInjectable = getInjectable({ + id: "request-set-cluster-as-deleting", + instantiate: (di): RequestSetClusterAsDeleting => { + const requestChannel = di.inject(requestFromChannelInjectable); + const setClusterAsDeletingChannel = di.inject(setClusterAsDeletingChannelInjectable); + + return (clusterId) => requestChannel(setClusterAsDeletingChannel, clusterId); + }, +}); + +export default requestSetClusterAsDeletingInjectable; diff --git a/src/features/cluster/extension-api/__snapshots__/disable-cluster-pages-when-cluster-is-not-relevant.test.tsx.snap b/src/features/cluster/extension-api/__snapshots__/disable-cluster-pages-when-cluster-is-not-relevant.test.tsx.snap index d7daa6bf14..7c81e1dfd7 100644 --- a/src/features/cluster/extension-api/__snapshots__/disable-cluster-pages-when-cluster-is-not-relevant.test.tsx.snap +++ b/src/features/cluster/extension-api/__snapshots__/disable-cluster-pages-when-cluster-is-not-relevant.test.tsx.snap @@ -310,6 +310,7 @@ exports[`disable-cluster-pages-when-cluster-is-not-relevant given extension shou > -
+
Close ⌘+W
@@ -817,6 +820,7 @@ exports[`disable-cluster-pages-when-cluster-is-not-relevant given extension shou > -
+
Close ⌘+W
@@ -1324,6 +1330,7 @@ exports[`disable-cluster-pages-when-cluster-is-not-relevant given not yet known > -
+
Close ⌘+W
diff --git a/src/features/cluster/extension-api/__snapshots__/disable-sidebar-items-when-cluster-is-not-relevant.test.tsx.snap b/src/features/cluster/extension-api/__snapshots__/disable-sidebar-items-when-cluster-is-not-relevant.test.tsx.snap index 3dde5af5d5..5642dbcf22 100644 --- a/src/features/cluster/extension-api/__snapshots__/disable-sidebar-items-when-cluster-is-not-relevant.test.tsx.snap +++ b/src/features/cluster/extension-api/__snapshots__/disable-sidebar-items-when-cluster-is-not-relevant.test.tsx.snap @@ -448,6 +448,7 @@ exports[`disable sidebar items when cluster is not relevant given extension shou > -
+
Close ⌘+W
@@ -955,6 +958,7 @@ exports[`disable sidebar items when cluster is not relevant given extension shou > -
+
Close ⌘+W
@@ -1462,6 +1468,7 @@ exports[`disable sidebar items when cluster is not relevant given not yet known > -
+
Close ⌘+W
diff --git a/src/features/cluster/kube-object-details/extension-api/__snapshots__/disable-kube-object-detail-items-when-cluster-is-not-relevant.test.tsx.snap b/src/features/cluster/kube-object-details/extension-api/__snapshots__/disable-kube-object-detail-items-when-cluster-is-not-relevant.test.tsx.snap index 60f0d0095a..0221082e02 100644 --- a/src/features/cluster/kube-object-details/extension-api/__snapshots__/disable-kube-object-detail-items-when-cluster-is-not-relevant.test.tsx.snap +++ b/src/features/cluster/kube-object-details/extension-api/__snapshots__/disable-kube-object-detail-items-when-cluster-is-not-relevant.test.tsx.snap @@ -367,6 +367,7 @@ exports[`disable kube object detail items when cluster is not relevant given ext > -
+
Close ⌘+W
@@ -849,6 +852,7 @@ exports[`disable kube object detail items when cluster is not relevant given ext > -
+
Close ⌘+W
@@ -1331,6 +1337,7 @@ exports[`disable kube object detail items when cluster is not relevant given not > -
+
Close ⌘+W
diff --git a/src/features/cluster/kube-object-menu/extension-api/__snapshots__/disable-kube-object-menu-items-when-cluster-is-not-relevant.test.tsx.snap b/src/features/cluster/kube-object-menu/extension-api/__snapshots__/disable-kube-object-menu-items-when-cluster-is-not-relevant.test.tsx.snap index 9459045e02..292a820f4b 100644 --- a/src/features/cluster/kube-object-menu/extension-api/__snapshots__/disable-kube-object-menu-items-when-cluster-is-not-relevant.test.tsx.snap +++ b/src/features/cluster/kube-object-menu/extension-api/__snapshots__/disable-kube-object-menu-items-when-cluster-is-not-relevant.test.tsx.snap @@ -315,6 +315,7 @@ exports[`disable kube object menu items when cluster is not relevant given exten > -
+
Close ⌘+W
@@ -701,6 +704,7 @@ exports[`disable kube object menu items when cluster is not relevant given exten > -
+
Close ⌘+W
@@ -1087,6 +1093,7 @@ exports[`disable kube object menu items when cluster is not relevant given not y > -
+
Close ⌘+W
diff --git a/src/features/cluster/kube-object-status-icon/__snapshots__/show-status-for-a-kube-object.test.tsx.snap b/src/features/cluster/kube-object-status-icon/__snapshots__/show-status-for-a-kube-object.test.tsx.snap index 042e9f02ab..dec48cdc12 100644 --- a/src/features/cluster/kube-object-status-icon/__snapshots__/show-status-for-a-kube-object.test.tsx.snap +++ b/src/features/cluster/kube-object-status-icon/__snapshots__/show-status-for-a-kube-object.test.tsx.snap @@ -304,6 +304,7 @@ exports[`show status for a kube object given application starts and in test page > -
+
Close ⌘+W
@@ -725,6 +728,7 @@ exports[`show status for a kube object given application starts and in test page > -
+
Close ⌘+W
@@ -1146,6 +1152,7 @@ exports[`show status for a kube object given application starts and in test page > -
+
Close ⌘+W
@@ -1527,6 +1536,7 @@ exports[`show status for a kube object given application starts and in test page > -
+
Close ⌘+W
@@ -1908,6 +1920,7 @@ exports[`show status for a kube object given application starts and in test page > -
+
Close ⌘+W
@@ -2329,6 +2344,7 @@ exports[`show status for a kube object given application starts and in test page > -
+
Close ⌘+W
diff --git a/src/features/cluster/kube-object-status-icon/extension-api/__snapshots__/disable-kube-object-statuses-when-cluster-is-not-relevant.test.tsx.snap b/src/features/cluster/kube-object-status-icon/extension-api/__snapshots__/disable-kube-object-statuses-when-cluster-is-not-relevant.test.tsx.snap index 001f97e00b..9fc7e6bbf1 100644 --- a/src/features/cluster/kube-object-status-icon/extension-api/__snapshots__/disable-kube-object-statuses-when-cluster-is-not-relevant.test.tsx.snap +++ b/src/features/cluster/kube-object-status-icon/extension-api/__snapshots__/disable-kube-object-statuses-when-cluster-is-not-relevant.test.tsx.snap @@ -344,6 +344,7 @@ exports[`disable kube object statuses when cluster is not relevant given extensi > -
+
Close ⌘+W
@@ -725,6 +728,7 @@ exports[`disable kube object statuses when cluster is not relevant given extensi > -
+
Close ⌘+W
@@ -1106,6 +1112,7 @@ exports[`disable kube object statuses when cluster is not relevant given not yet > -
+
Close ⌘+W
diff --git a/src/features/cluster/namespaces/__snapshots__/edit-namespace-from-new-tab.test.tsx.snap b/src/features/cluster/namespaces/__snapshots__/edit-namespace-from-new-tab.test.tsx.snap index f46517dc0b..b087beee3d 100644 --- a/src/features/cluster/namespaces/__snapshots__/edit-namespace-from-new-tab.test.tsx.snap +++ b/src/features/cluster/namespaces/__snapshots__/edit-namespace-from-new-tab.test.tsx.snap @@ -548,6 +548,7 @@ exports[`cluster/namespaces - edit namespace from new tab when navigating to nam > -
+
Close ⌘+W
@@ -1183,6 +1186,7 @@ exports[`cluster/namespaces - edit namespace from new tab when navigating to nam > -
+
Close ⌘+W
@@ -1823,6 +1829,7 @@ exports[`cluster/namespaces - edit namespace from new tab when navigating to nam > -
+
Close ⌘+W
@@ -2532,6 +2541,7 @@ exports[`cluster/namespaces - edit namespace from new tab when navigating to nam > -
+
Close ⌘+W
@@ -2581,6 +2593,7 @@ exports[`cluster/namespaces - edit namespace from new tab when navigating to nam > -
+
Close ⌘+W
@@ -3216,6 +3231,7 @@ exports[`cluster/namespaces - edit namespace from new tab when navigating to nam > -
+
Close ⌘+W
@@ -3265,6 +3283,7 @@ exports[`cluster/namespaces - edit namespace from new tab when navigating to nam > -
+
Close ⌘+W
@@ -3972,6 +3993,7 @@ exports[`cluster/namespaces - edit namespace from new tab when navigating to nam > -
+
Close ⌘+W
@@ -4021,6 +4045,7 @@ exports[`cluster/namespaces - edit namespace from new tab when navigating to nam > -
+
Close ⌘+W
@@ -4730,6 +4757,7 @@ exports[`cluster/namespaces - edit namespace from new tab when navigating to nam > -
+
Close ⌘+W
@@ -5453,6 +5483,7 @@ exports[`cluster/namespaces - edit namespace from new tab when navigating to nam > -
+
Close ⌘+W
@@ -6171,6 +6204,7 @@ exports[`cluster/namespaces - edit namespace from new tab when navigating to nam > -
+
Close ⌘+W
@@ -6880,6 +6916,7 @@ exports[`cluster/namespaces - edit namespace from new tab when navigating to nam > -
+
Close ⌘+W
@@ -7589,6 +7628,7 @@ exports[`cluster/namespaces - edit namespace from new tab when navigating to nam > -
+
Close ⌘+W
@@ -8845,6 +8887,7 @@ exports[`cluster/namespaces - edit namespace from new tab when navigating to nam > -
+
Close ⌘+W
@@ -10657,6 +10702,7 @@ exports[`cluster/namespaces - edit namespace from new tab when navigating to nam > -
+
Close ⌘+W
diff --git a/src/features/cluster/namespaces/__snapshots__/edit-namespace-from-previously-opened-tab.test.tsx.snap b/src/features/cluster/namespaces/__snapshots__/edit-namespace-from-previously-opened-tab.test.tsx.snap index e23184d8fc..f82cbc2e0e 100644 --- a/src/features/cluster/namespaces/__snapshots__/edit-namespace-from-previously-opened-tab.test.tsx.snap +++ b/src/features/cluster/namespaces/__snapshots__/edit-namespace-from-previously-opened-tab.test.tsx.snap @@ -455,6 +455,7 @@ exports[`cluster/namespaces - edit namespaces from previously opened tab given t > -
+
Close ⌘+W
@@ -997,6 +1000,7 @@ exports[`cluster/namespaces - edit namespaces from previously opened tab given t > -
+
Close ⌘+W
diff --git a/src/features/cluster/sidebar-and-tab-navigation-for-core.test.tsx b/src/features/cluster/sidebar-and-tab-navigation-for-core.test.tsx index 2272fc0db6..519e6bb3c8 100644 --- a/src/features/cluster/sidebar-and-tab-navigation-for-core.test.tsx +++ b/src/features/cluster/sidebar-and-tab-navigation-for-core.test.tsx @@ -22,7 +22,6 @@ import pathExistsInjectable from "../../common/fs/path-exists.injectable"; import readJsonFileInjectable from "../../common/fs/read-json-file.injectable"; import { navigateToRouteInjectionToken } from "../../common/front-end-routing/navigate-to-route-injection-token"; import sidebarStorageInjectable from "../../renderer/components/layout/sidebar-storage/sidebar-storage.injectable"; -import hostedClusterIdInjectable from "../../renderer/cluster-frame-context/hosted-cluster-id.injectable"; import { advanceFakeTime, useFakeTime } from "../../common/test-utils/use-fake-time"; import storageSaveDelayInjectable from "../../renderer/utils/create-storage/storage-save-delay.injectable"; @@ -38,7 +37,6 @@ describe("cluster - sidebar and tab navigation for core", () => { builder.setEnvironmentToClusterFrame(); builder.beforeWindowStart((windowDi) => { - windowDi.override(hostedClusterIdInjectable, () => "some-hosted-cluster-id"); windowDi.override(storageSaveDelayInjectable, () => 250); windowDi.override( @@ -98,7 +96,7 @@ describe("cluster - sidebar and tab navigation for core", () => { const writeJsonFileFake = windowDi.inject(writeJsonFileInjectable); await writeJsonFileFake( - "/some-directory-for-lens-local-storage/some-hosted-cluster-id.json", + "/some-directory-for-lens-local-storage/some-cluster-id.json", { sidebar: { expanded: { "some-parent-id": true }, @@ -286,7 +284,7 @@ describe("cluster - sidebar and tab navigation for core", () => { const readJsonFileFake = windowDi.inject(readJsonFileInjectable); const actual = await readJsonFileFake( - "/some-directory-for-lens-local-storage/some-hosted-cluster-id.json", + "/some-directory-for-lens-local-storage/some-cluster-id.json", ); expect(actual).toEqual({ diff --git a/src/features/cluster/sidebar-and-tab-navigation-for-extensions.test.tsx b/src/features/cluster/sidebar-and-tab-navigation-for-extensions.test.tsx index 8d3de582ad..a228cc880e 100644 --- a/src/features/cluster/sidebar-and-tab-navigation-for-extensions.test.tsx +++ b/src/features/cluster/sidebar-and-tab-navigation-for-extensions.test.tsx @@ -15,7 +15,6 @@ import pathExistsInjectable from "../../common/fs/path-exists.injectable"; import readJsonFileInjectable from "../../common/fs/read-json-file.injectable"; import { navigateToRouteInjectionToken } from "../../common/front-end-routing/navigate-to-route-injection-token"; import assert from "assert"; -import hostedClusterIdInjectable from "../../renderer/cluster-frame-context/hosted-cluster-id.injectable"; import { advanceFakeTime, useFakeTime } from "../../common/test-utils/use-fake-time"; import type { IObservableValue } from "mobx"; import { runInAction, computed, observable } from "mobx"; @@ -34,7 +33,6 @@ describe("cluster - sidebar and tab navigation for extensions", () => { applicationBuilder.setEnvironmentToClusterFrame(); applicationBuilder.beforeWindowStart((windowDi) => { - windowDi.override(hostedClusterIdInjectable, () => "some-hosted-cluster-id"); windowDi.override(storageSaveDelayInjectable, () => 250); windowDi.override( @@ -178,7 +176,7 @@ describe("cluster - sidebar and tab navigation for extensions", () => { const writeJsonFileFake = windowDi.inject(writeJsonFileInjectable); await writeJsonFileFake( - "/some-directory-for-lens-local-storage/some-hosted-cluster-id.json", + "/some-directory-for-lens-local-storage/some-cluster-id.json", { sidebar: { expanded: { "some-extension-name-some-parent-id": true }, @@ -214,7 +212,7 @@ describe("cluster - sidebar and tab navigation for extensions", () => { const writeJsonFileFake = windowDi.inject(writeJsonFileInjectable); await writeJsonFileFake( - "/some-directory-for-lens-local-storage/some-hosted-cluster-id.json", + "/some-directory-for-lens-local-storage/some-cluster-id.json", { sidebar: { expanded: { "some-extension-name-some-unknown-parent-id": true }, @@ -244,7 +242,7 @@ describe("cluster - sidebar and tab navigation for extensions", () => { const writeJsonFileFake = windowDi.inject(writeJsonFileInjectable); await writeJsonFileFake( - "/some-directory-for-lens-local-storage/some-hosted-cluster-id.json", + "/some-directory-for-lens-local-storage/some-cluster-id.json", { someThingButSidebar: {}, }, @@ -390,7 +388,7 @@ describe("cluster - sidebar and tab navigation for extensions", () => { const pathExistsFake = windowDi.inject(pathExistsInjectable); const actual = await pathExistsFake( - "/some-directory-for-lens-local-storage/some-hosted-cluster-id.json", + "/some-directory-for-lens-local-storage/some-cluster-id.json", ); expect(actual).toBe(false); @@ -402,7 +400,7 @@ describe("cluster - sidebar and tab navigation for extensions", () => { const readJsonFileFake = windowDi.inject(readJsonFileInjectable); const actual = await readJsonFileFake( - "/some-directory-for-lens-local-storage/some-hosted-cluster-id.json", + "/some-directory-for-lens-local-storage/some-cluster-id.json", ); expect(actual).toEqual({ diff --git a/src/features/cluster/workloads/overview/extension-api/__snapshots__/disable-workloads-overview-details-when-cluster-is-not-relevant.test.tsx.snap b/src/features/cluster/workloads/overview/extension-api/__snapshots__/disable-workloads-overview-details-when-cluster-is-not-relevant.test.tsx.snap index 2fd23c30cc..4dc26ef987 100644 --- a/src/features/cluster/workloads/overview/extension-api/__snapshots__/disable-workloads-overview-details-when-cluster-is-not-relevant.test.tsx.snap +++ b/src/features/cluster/workloads/overview/extension-api/__snapshots__/disable-workloads-overview-details-when-cluster-is-not-relevant.test.tsx.snap @@ -435,6 +435,7 @@ exports[`disable workloads overview details when cluster is not relevant given e > -
+
Close ⌘+W
@@ -942,6 +945,7 @@ exports[`disable workloads overview details when cluster is not relevant given e > -
+
Close ⌘+W
@@ -1449,6 +1455,7 @@ exports[`disable workloads overview details when cluster is not relevant given n > -
+
Close ⌘+W
diff --git a/src/features/helm-charts/__snapshots__/add-custom-helm-repository-in-preferences.test.ts.snap b/src/features/helm-charts/__snapshots__/add-custom-helm-repository-in-preferences.test.ts.snap index c02c727480..433cf01bac 100644 --- a/src/features/helm-charts/__snapshots__/add-custom-helm-repository-in-preferences.test.ts.snap +++ b/src/features/helm-charts/__snapshots__/add-custom-helm-repository-in-preferences.test.ts.snap @@ -272,7 +272,7 @@ exports[`add custom helm repository in preferences when navigating to preference @@ -303,7 +303,7 @@ exports[`add custom helm repository in preferences when navigating to preference > @@ -850,7 +850,7 @@ exports[`add custom helm repository in preferences when navigating to preference @@ -881,7 +881,7 @@ exports[`add custom helm repository in preferences when navigating to preference > @@ -1439,7 +1439,7 @@ exports[`add custom helm repository in preferences when navigating to preference @@ -1470,7 +1470,7 @@ exports[`add custom helm repository in preferences when navigating to preference > @@ -2139,7 +2139,7 @@ exports[`add custom helm repository in preferences when navigating to preference @@ -2170,7 +2170,7 @@ exports[`add custom helm repository in preferences when navigating to preference > @@ -2736,7 +2736,7 @@ exports[`add custom helm repository in preferences when navigating to preference @@ -2767,7 +2767,7 @@ exports[`add custom helm repository in preferences when navigating to preference > @@ -3436,7 +3436,7 @@ exports[`add custom helm repository in preferences when navigating to preference @@ -3467,7 +3467,7 @@ exports[`add custom helm repository in preferences when navigating to preference > @@ -4318,7 +4318,7 @@ exports[`add custom helm repository in preferences when navigating to preference @@ -4349,7 +4349,7 @@ exports[`add custom helm repository in preferences when navigating to preference > @@ -5018,7 +5018,7 @@ exports[`add custom helm repository in preferences when navigating to preference @@ -5049,7 +5049,7 @@ exports[`add custom helm repository in preferences when navigating to preference > @@ -5900,7 +5900,7 @@ exports[`add custom helm repository in preferences when navigating to preference @@ -5931,7 +5931,7 @@ exports[`add custom helm repository in preferences when navigating to preference > @@ -6601,7 +6601,7 @@ exports[`add custom helm repository in preferences when navigating to preference @@ -6632,7 +6632,7 @@ exports[`add custom helm repository in preferences when navigating to preference > @@ -6916,7 +6916,7 @@ exports[`add custom helm repository in preferences when navigating to preference />
-
+
Close ⌘+W
diff --git a/src/features/helm-charts/installing-chart/installing-helm-chart-from-new-tab.test.ts b/src/features/helm-charts/installing-chart/installing-helm-chart-from-new-tab.test.ts index f86471e897..9d6c43790d 100644 --- a/src/features/helm-charts/installing-chart/installing-helm-chart-from-new-tab.test.ts +++ b/src/features/helm-charts/installing-chart/installing-helm-chart-from-new-tab.test.ts @@ -8,98 +8,56 @@ import type { RenderResult } from "@testing-library/react"; import { fireEvent } from "@testing-library/react"; import type { ApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder"; import { getApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder"; -import type { CallForHelmCharts } from "../../../renderer/components/+helm-charts/helm-charts/call-for-helm-charts.injectable"; -import callForHelmChartsInjectable from "../../../renderer/components/+helm-charts/helm-charts/call-for-helm-charts.injectable"; import { HelmChart } from "../../../common/k8s-api/endpoints/helm-charts.api"; import getRandomInstallChartTabIdInjectable from "../../../renderer/components/dock/install-chart/get-random-install-chart-tab-id.injectable"; -import type { CallForHelmChartValues } from "../../../renderer/components/dock/install-chart/chart-data/call-for-helm-chart-values.injectable"; -import callForHelmChartValuesInjectable from "../../../renderer/components/dock/install-chart/chart-data/call-for-helm-chart-values.injectable"; -import type { CallForCreateHelmRelease } from "../../../renderer/components/+helm-releases/create-release/call-for-create-helm-release.injectable"; -import callForCreateHelmReleaseInjectable from "../../../renderer/components/+helm-releases/create-release/call-for-create-helm-release.injectable"; +import type { RequestCreateHelmRelease } from "../../../common/k8s-api/endpoints/helm-releases.api/request-create.injectable"; +import requestCreateHelmReleaseInjectable from "../../../common/k8s-api/endpoints/helm-releases.api/request-create.injectable"; import currentPathInjectable from "../../../renderer/routes/current-path.injectable"; -import namespaceStoreInjectable from "../../../renderer/components/+namespaces/store.injectable"; -import type { NamespaceStore } from "../../../renderer/components/+namespaces/store"; -import type { CallForHelmChartReadme } from "../../../renderer/components/+helm-charts/details/readme/call-for-helm-chart-readme.injectable"; -import callForHelmChartReadmeInjectable from "../../../renderer/components/+helm-charts/details/readme/call-for-helm-chart-readme.injectable"; -import type { CallForHelmChartVersions } from "../../../renderer/components/+helm-charts/details/versions/call-for-helm-chart-versions.injectable"; -import callForHelmChartVersionsInjectable from "../../../renderer/components/+helm-charts/details/versions/call-for-helm-chart-versions.injectable"; import writeJsonFileInjectable from "../../../common/fs/write-json-file.injectable"; import directoryForLensLocalStorageInjectable from "../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable"; -import hostedClusterIdInjectable from "../../../renderer/cluster-frame-context/hosted-cluster-id.injectable"; import dockStoreInjectable from "../../../renderer/components/dock/dock/store.injectable"; import readJsonFileInjectable from "../../../common/fs/read-json-file.injectable"; import type { DiContainer } from "@ogre-tools/injectable"; -import callForHelmReleasesInjectable from "../../../renderer/components/+helm-releases/call-for-helm-releases/call-for-helm-releases.injectable"; -import callForHelmReleaseDetailsInjectable from "../../../renderer/components/+helm-releases/release-details/release-details-model/call-for-helm-release/call-for-helm-release-details/call-for-helm-release-details.injectable"; +import type { RequestHelmCharts } from "../../../common/k8s-api/endpoints/helm-charts.api/request-charts.injectable"; +import type { RequestHelmChartVersions } from "../../../common/k8s-api/endpoints/helm-charts.api/request-versions.injectable"; +import type { RequestHelmChartReadme } from "../../../common/k8s-api/endpoints/helm-charts.api/request-readme.injectable"; +import type { RequestHelmChartValues } from "../../../common/k8s-api/endpoints/helm-charts.api/request-values.injectable"; +import requestHelmChartsInjectable from "../../../common/k8s-api/endpoints/helm-charts.api/request-charts.injectable"; +import requestHelmChartVersionsInjectable from "../../../common/k8s-api/endpoints/helm-charts.api/request-versions.injectable"; +import requestHelmChartReadmeInjectable from "../../../common/k8s-api/endpoints/helm-charts.api/request-readme.injectable"; +import requestHelmChartValuesInjectable from "../../../common/k8s-api/endpoints/helm-charts.api/request-values.injectable"; +import type { RequestDetailedHelmRelease } from "../../../renderer/components/+helm-releases/release-details/release-details-model/request-detailed-helm-release.injectable"; +import requestDetailedHelmReleaseInjectable from "../../../renderer/components/+helm-releases/release-details/release-details-model/request-detailed-helm-release.injectable"; describe("installing helm chart from new tab", () => { let builder: ApplicationBuilder; - let callForHelmChartsMock: AsyncFnMock; - let callForHelmChartVersionsMock: AsyncFnMock; - let callForHelmChartReadmeMock: AsyncFnMock; - let callForHelmChartValuesMock: AsyncFnMock; - let callForCreateHelmReleaseMock: AsyncFnMock; + let requestDetailedHelmReleaseMock: AsyncFnMock; + let requestHelmChartsMock: AsyncFnMock; + let requestHelmChartVersionsMock: AsyncFnMock; + let requestHelmChartReadmeMock: AsyncFnMock; + let requestHelmChartValuesMock: AsyncFnMock; + let requestCreateHelmReleaseMock: AsyncFnMock; beforeEach(() => { builder = getApplicationBuilder(); builder.setEnvironmentToClusterFrame(); - callForHelmChartsMock = asyncFn(); - callForHelmChartVersionsMock = asyncFn(); - callForHelmChartReadmeMock = asyncFn(); - callForHelmChartValuesMock = asyncFn(); - callForCreateHelmReleaseMock = asyncFn(); + requestDetailedHelmReleaseMock = asyncFn(); + requestHelmChartsMock = asyncFn(); + requestHelmChartVersionsMock = asyncFn(); + requestHelmChartReadmeMock = asyncFn(); + requestHelmChartValuesMock = asyncFn(); + requestCreateHelmReleaseMock = asyncFn(); builder.beforeWindowStart((windowDi) => { - windowDi.override(callForHelmReleasesInjectable, () => async () => []); - windowDi.override(callForHelmReleaseDetailsInjectable, () => () => new Promise(() => {})); - - windowDi.override( - directoryForLensLocalStorageInjectable, - () => "/some-directory-for-lens-local-storage", - ); - - windowDi.override(hostedClusterIdInjectable, () => "some-cluster-id"); - - windowDi.override( - callForHelmChartsInjectable, - () => callForHelmChartsMock, - ); - - windowDi.override( - callForHelmChartVersionsInjectable, - () => callForHelmChartVersionsMock, - ); - - windowDi.override( - callForHelmChartReadmeInjectable, - () => callForHelmChartReadmeMock, - ); - - windowDi.override( - callForHelmChartValuesInjectable, - () => callForHelmChartValuesMock, - ); - - windowDi.override( - callForCreateHelmReleaseInjectable, - () => callForCreateHelmReleaseMock, - ); - - // TODO: Replace store mocking with mock for the actual side-effect (where the namespaces are coming from) - windowDi.override( - namespaceStoreInjectable, - () => - ({ - contextNamespaces: [], - items: [ - { getName: () => "default" }, - { getName: () => "some-other-namespace" }, - ], - selectNamespaces: () => {}, - } as unknown as NamespaceStore), - ); + windowDi.override(directoryForLensLocalStorageInjectable, () => "/some-directory-for-lens-local-storage"); + windowDi.override(requestDetailedHelmReleaseInjectable, () => requestDetailedHelmReleaseMock); + windowDi.override(requestHelmChartsInjectable, () => requestHelmChartsMock); + windowDi.override(requestHelmChartVersionsInjectable, () => requestHelmChartVersionsMock); + windowDi.override(requestHelmChartReadmeInjectable, () => requestHelmChartReadmeMock); + windowDi.override(requestHelmChartValuesInjectable, () => requestHelmChartValuesMock); + windowDi.override(requestCreateHelmReleaseInjectable, () => requestCreateHelmReleaseMock); windowDi.override(getRandomInstallChartTabIdInjectable, () => jest @@ -108,6 +66,9 @@ describe("installing helm chart from new tab", () => { .mockReturnValueOnce("some-second-tab-id"), ); }); + + builder.namespaces.add("default"); + builder.namespaces.add("some-other-namespace"); }); describe("given tab for installing chart was not previously opened and application is started", () => { @@ -136,7 +97,6 @@ describe("installing helm chart from new tab", () => { // TODO: Make TerminalWindow unit testable to allow realistic behaviour dockStore.closeTab("terminal"); - }); it("renders", () => { @@ -144,14 +104,13 @@ describe("installing helm chart from new tab", () => { }); describe("when navigating to helm charts", () => { - beforeEach(async () => { builder.helmCharts.navigate({ chartName: "some-name", repo: "some-repository", }); - await callForHelmChartsMock.resolve([ + await requestHelmChartsMock.resolve([ HelmChart.create({ apiVersion: "some-api-version", name: "some-name", @@ -185,7 +144,7 @@ describe("installing helm chart from new tab", () => { }), ]); - await callForHelmChartVersionsMock.resolve([ + await requestHelmChartVersionsMock.resolve([ HelmChart.create({ apiVersion: "some-api-version", name: "some-name", @@ -219,7 +178,7 @@ describe("installing helm chart from new tab", () => { }), ]); - await callForHelmChartReadmeMock.resolve("some-readme"); + await requestHelmChartReadmeMock.resolve("some-readme"); }); it("renders", () => { @@ -228,7 +187,7 @@ describe("installing helm chart from new tab", () => { describe("when selecting to install the chart", () => { beforeEach(() => { - callForHelmChartVersionsMock.mockClear(); + requestHelmChartVersionsMock.mockClear(); const installButton = rendered.getByTestId( "install-chart-for-some-repository-some-name", @@ -248,7 +207,7 @@ describe("installing helm chart from new tab", () => { }); it("calls for default configuration of the chart", () => { - expect(callForHelmChartValuesMock).toHaveBeenCalledWith( + expect(requestHelmChartValuesMock).toHaveBeenCalledWith( "some-repository", "some-name", "some-version", @@ -256,7 +215,7 @@ describe("installing helm chart from new tab", () => { }); it("calls for available versions", () => { - expect(callForHelmChartVersionsMock).toHaveBeenCalledWith( + expect(requestHelmChartVersionsMock).toHaveBeenCalledWith( "some-repository", "some-name", ); @@ -268,18 +227,8 @@ describe("installing helm chart from new tab", () => { ).toBeInTheDocument(); }); - it("given default configuration resolves but versions have not resolved yet, still shows the spinner", async () => { - await callForHelmChartValuesMock.resolve( - "some-default-configuration", - ); - - expect( - rendered.getByTestId("install-chart-tab-spinner"), - ).toBeInTheDocument(); - }); - it("given versions resolve but default configuration has not resolved yet, still shows the spinner", async () => { - await callForHelmChartVersionsMock.resolve([]); + await requestHelmChartVersionsMock.resolve([]); expect( rendered.getByTestId("install-chart-tab-spinner"), @@ -288,11 +237,11 @@ describe("installing helm chart from new tab", () => { describe("when default configuration and versions resolve", () => { beforeEach(async () => { - await callForHelmChartValuesMock.resolve( + await requestHelmChartValuesMock.resolve( "some-default-configuration", ); - await callForHelmChartVersionsMock.resolve([ + await requestHelmChartVersionsMock.resolve([ HelmChart.create({ apiVersion: "some-api-version", name: "some-name", @@ -385,7 +334,7 @@ describe("installing helm chart from new tab", () => { }); it("calls for installation with default configuration", () => { - expect(callForCreateHelmReleaseMock).toHaveBeenCalledWith({ + expect(requestCreateHelmReleaseMock).toHaveBeenCalledWith({ chart: "some-name", name: undefined, namespace: "default", @@ -397,7 +346,7 @@ describe("installing helm chart from new tab", () => { describe("when installation resolves", () => { beforeEach(async () => { - await callForCreateHelmReleaseMock.resolve({ + await requestCreateHelmReleaseMock.resolve({ log: "some-execution-output", release: { @@ -496,8 +445,8 @@ describe("installing helm chart from new tab", () => { describe("given opening details for second chart, when details resolve", () => { beforeEach(async () => { - callForHelmChartReadmeMock.mockClear(); - callForHelmChartVersionsMock.mockClear(); + requestHelmChartReadmeMock.mockClear(); + requestHelmChartVersionsMock.mockClear(); const row = rendered.getByTestId( "helm-chart-row-for-some-repository-some-other-name", @@ -505,7 +454,7 @@ describe("installing helm chart from new tab", () => { fireEvent.click(row); - await callForHelmChartVersionsMock.resolve([ + await requestHelmChartVersionsMock.resolve([ HelmChart.create({ apiVersion: "some-api-version", name: "some-other-name", @@ -523,7 +472,7 @@ describe("installing helm chart from new tab", () => { }), ]); - await callForHelmChartReadmeMock.resolve("some-readme"); + await requestHelmChartReadmeMock.resolve("some-readme"); }); it("renders", () => { @@ -532,7 +481,7 @@ describe("installing helm chart from new tab", () => { describe("when selecting to install second chart", () => { beforeEach(() => { - callForHelmChartVersionsMock.mockClear(); + requestHelmChartVersionsMock.mockClear(); const installButton = rendered.getByTestId( "install-chart-for-some-repository-some-other-name", @@ -560,7 +509,7 @@ describe("installing helm chart from new tab", () => { }); it("calls for default configuration of the second chart", () => { - expect(callForHelmChartValuesMock).toHaveBeenCalledWith( + expect(requestHelmChartValuesMock).toHaveBeenCalledWith( "some-repository", "some-other-name", "some-version", @@ -568,7 +517,7 @@ describe("installing helm chart from new tab", () => { }); it("calls for available versions for the second chart", () => { - expect(callForHelmChartVersionsMock).toHaveBeenCalledWith( + expect(requestHelmChartVersionsMock).toHaveBeenCalledWith( "some-repository", "some-other-name", ); @@ -582,11 +531,11 @@ describe("installing helm chart from new tab", () => { describe("when configuration and versions resolve", () => { beforeEach(async () => { - await callForHelmChartValuesMock.resolve( + await requestHelmChartValuesMock.resolve( "some-other-default-configuration", ); - await callForHelmChartVersionsMock.resolve([]); + await requestHelmChartVersionsMock.resolve([]); }); it("renders", () => { @@ -607,7 +556,7 @@ describe("installing helm chart from new tab", () => { fireEvent.click(installButton); expect( - callForCreateHelmReleaseMock, + requestCreateHelmReleaseMock, ).toHaveBeenCalledWith({ chart: "some-other-name", name: undefined, @@ -620,8 +569,8 @@ describe("installing helm chart from new tab", () => { describe("when selecting the dock tab for installing first chart", () => { beforeEach(() => { - callForHelmChartValuesMock.mockClear(); - callForHelmChartVersionsMock.mockClear(); + requestHelmChartValuesMock.mockClear(); + requestHelmChartVersionsMock.mockClear(); const tab = rendered.getByTestId( "dock-tab-for-some-first-tab-id", @@ -636,13 +585,13 @@ describe("installing helm chart from new tab", () => { it("does not call for default configuration", () => { expect( - callForHelmChartValuesMock, + requestHelmChartValuesMock, ).not.toHaveBeenCalled(); }); it("does not call for available versions", () => { expect( - callForHelmChartVersionsMock, + requestHelmChartVersionsMock, ).not.toHaveBeenCalled(); }); @@ -660,7 +609,7 @@ describe("installing helm chart from new tab", () => { fireEvent.click(installButton); expect( - callForCreateHelmReleaseMock, + requestCreateHelmReleaseMock, ).toHaveBeenCalledWith({ chart: "some-name", name: undefined, @@ -679,8 +628,8 @@ describe("installing helm chart from new tab", () => { let menu: { selectOption: (labelText: string) => void }; beforeEach(() => { - callForHelmChartVersionsMock.mockClear(); - callForHelmChartValuesMock.mockClear(); + requestHelmChartVersionsMock.mockClear(); + requestHelmChartValuesMock.mockClear(); const menuId = "install-chart-version-select-for-some-first-tab-id"; @@ -708,7 +657,7 @@ describe("installing helm chart from new tab", () => { }); it("calls for default configuration for the version of chart", () => { - expect(callForHelmChartValuesMock).toHaveBeenCalledWith( + expect(requestHelmChartValuesMock).toHaveBeenCalledWith( "some-repository", "some-name", "some-other-version", @@ -725,7 +674,7 @@ describe("installing helm chart from new tab", () => { it("does not call for versions again", () => { expect( - callForHelmChartVersionsMock, + requestHelmChartVersionsMock, ).not.toHaveBeenCalled(); }); @@ -747,7 +696,7 @@ describe("installing helm chart from new tab", () => { describe("when default configuration resolves", () => { beforeEach(async () => { - await callForHelmChartValuesMock.resolve( + await requestHelmChartValuesMock.resolve( "some-default-configuration-for-other-version", ); }); @@ -772,7 +721,7 @@ describe("installing helm chart from new tab", () => { fireEvent.click(installButton); expect( - callForCreateHelmReleaseMock, + requestCreateHelmReleaseMock, ).toHaveBeenCalledWith({ chart: "some-name", name: undefined, @@ -829,7 +778,7 @@ describe("installing helm chart from new tab", () => { fireEvent.click(installButton); - expect(callForCreateHelmReleaseMock).toHaveBeenCalledWith( + expect(requestCreateHelmReleaseMock).toHaveBeenCalledWith( { chart: "some-name", name: undefined, @@ -892,7 +841,7 @@ describe("installing helm chart from new tab", () => { ) .selectOption("some-other-version"); - await callForHelmChartValuesMock.resolve( + await requestHelmChartValuesMock.resolve( "some-default-configuration-for-other-version", ); @@ -948,7 +897,7 @@ describe("installing helm chart from new tab", () => { fireEvent.click(installButton); - expect(callForCreateHelmReleaseMock).toHaveBeenCalledWith({ + expect(requestCreateHelmReleaseMock).toHaveBeenCalledWith({ chart: "some-name", name: undefined, namespace: "default", @@ -965,7 +914,7 @@ describe("installing helm chart from new tab", () => { ) .selectOption("some-other-version"); - await callForHelmChartValuesMock.resolve( + await requestHelmChartValuesMock.resolve( "some-default-configuration-for-other-version", ); @@ -1014,7 +963,7 @@ describe("installing helm chart from new tab", () => { fireEvent.click(installButton); - expect(callForCreateHelmReleaseMock).toHaveBeenCalledWith({ + expect(requestCreateHelmReleaseMock).toHaveBeenCalledWith({ chart: "some-name", name: "some-custom-name", namespace: "default", diff --git a/src/features/helm-charts/installing-chart/installing-helm-chart-from-previously-opened-tab.test.ts b/src/features/helm-charts/installing-chart/installing-helm-chart-from-previously-opened-tab.test.ts index c49f3a7942..978e1635a7 100644 --- a/src/features/helm-charts/installing-chart/installing-helm-chart-from-previously-opened-tab.test.ts +++ b/src/features/helm-charts/installing-chart/installing-helm-chart-from-previously-opened-tab.test.ts @@ -9,23 +9,23 @@ import type { ApplicationBuilder } from "../../../renderer/components/test-utils import { getApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder"; import { HelmChart } from "../../../common/k8s-api/endpoints/helm-charts.api"; import getRandomInstallChartTabIdInjectable from "../../../renderer/components/dock/install-chart/get-random-install-chart-tab-id.injectable"; -import type { CallForHelmChartValues } from "../../../renderer/components/dock/install-chart/chart-data/call-for-helm-chart-values.injectable"; -import callForHelmChartValuesInjectable from "../../../renderer/components/dock/install-chart/chart-data/call-for-helm-chart-values.injectable"; import namespaceStoreInjectable from "../../../renderer/components/+namespaces/store.injectable"; import type { NamespaceStore } from "../../../renderer/components/+namespaces/store"; -import type { CallForHelmChartVersions } from "../../../renderer/components/+helm-charts/details/versions/call-for-helm-chart-versions.injectable"; -import callForHelmChartVersionsInjectable from "../../../renderer/components/+helm-charts/details/versions/call-for-helm-chart-versions.injectable"; import writeJsonFileInjectable from "../../../common/fs/write-json-file.injectable"; import directoryForLensLocalStorageInjectable from "../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable"; import hostedClusterIdInjectable from "../../../renderer/cluster-frame-context/hosted-cluster-id.injectable"; import { TabKind } from "../../../renderer/components/dock/dock/store"; import { controlWhenStoragesAreReady } from "../../../renderer/utils/create-storage/storages-are-ready"; -import callForCreateHelmReleaseInjectable from "../../../renderer/components/+helm-releases/create-release/call-for-create-helm-release.injectable"; +import requestCreateHelmReleaseInjectable from "../../../common/k8s-api/endpoints/helm-releases.api/request-create.injectable"; +import type { RequestHelmChartVersions } from "../../../common/k8s-api/endpoints/helm-charts.api/request-versions.injectable"; +import requestHelmChartVersionsInjectable from "../../../common/k8s-api/endpoints/helm-charts.api/request-versions.injectable"; +import type { RequestHelmChartValues } from "../../../common/k8s-api/endpoints/helm-charts.api/request-values.injectable"; +import requestHelmChartValuesInjectable from "../../../common/k8s-api/endpoints/helm-charts.api/request-values.injectable"; describe("installing helm chart from previously opened tab", () => { let builder: ApplicationBuilder; - let callForHelmChartVersionsMock: AsyncFnMock; - let callForHelmChartValuesMock: AsyncFnMock; + let requestHelmChartVersionsMock: AsyncFnMock; + let requestHelmChartValuesMock: AsyncFnMock; let storagesAreReady: () => Promise; beforeEach(() => { @@ -33,38 +33,17 @@ describe("installing helm chart from previously opened tab", () => { builder.setEnvironmentToClusterFrame(); - callForHelmChartVersionsMock = asyncFn(); - callForHelmChartValuesMock = asyncFn(); + requestHelmChartVersionsMock = asyncFn(); + requestHelmChartValuesMock = asyncFn(); builder.beforeWindowStart((windowDi) => { - windowDi.override( - directoryForLensLocalStorageInjectable, - () => "/some-directory-for-lens-local-storage", - ); - - windowDi.override(hostedClusterIdInjectable, () => "some-cluster-id"); - storagesAreReady = controlWhenStoragesAreReady(windowDi); - windowDi.override( - callForHelmChartVersionsInjectable, - () => callForHelmChartVersionsMock, - ); - - windowDi.override( - callForHelmChartValuesInjectable, - () => callForHelmChartValuesMock, - ); - - windowDi.override( - callForHelmChartValuesInjectable, - () => callForHelmChartValuesMock, - ); - - windowDi.override( - callForCreateHelmReleaseInjectable, - () => jest.fn(), - ); + windowDi.override(directoryForLensLocalStorageInjectable, () => "/some-directory-for-lens-local-storage"); + windowDi.override(hostedClusterIdInjectable, () => "some-cluster-id"); + windowDi.override(requestHelmChartVersionsInjectable, () => requestHelmChartVersionsMock); + windowDi.override(requestHelmChartValuesInjectable, () => requestHelmChartValuesMock); + windowDi.override(requestCreateHelmReleaseInjectable, () => jest.fn()); // TODO: Replace store mocking with mock for the actual side-effect (where the namespaces are coming from) windowDi.override( @@ -154,7 +133,7 @@ describe("installing helm chart from previously opened tab", () => { }); it("calls for default configuration of the chart", () => { - expect(callForHelmChartValuesMock).toHaveBeenCalledWith( + expect(requestHelmChartValuesMock).toHaveBeenCalledWith( "some-repository", "some-name", "some-other-version", @@ -162,7 +141,7 @@ describe("installing helm chart from previously opened tab", () => { }); it("calls for available versions", () => { - expect(callForHelmChartVersionsMock).toHaveBeenCalledWith( + expect(requestHelmChartVersionsMock).toHaveBeenCalledWith( "some-repository", "some-name", ); @@ -170,11 +149,11 @@ describe("installing helm chart from previously opened tab", () => { describe("when configuration and version resolves", () => { beforeEach(async () => { - await callForHelmChartValuesMock.resolve( + await requestHelmChartValuesMock.resolve( "some-default-configuration", ); - await callForHelmChartVersionsMock.resolve([ + await requestHelmChartVersionsMock.resolve([ HelmChart.create({ apiVersion: "some-api-version", name: "some-name", diff --git a/src/features/helm-charts/installing-chart/opening-dock-tab-for-installing-helm-chart.test.ts b/src/features/helm-charts/installing-chart/opening-dock-tab-for-installing-helm-chart.test.ts index ddc682afd2..fa37f5d97f 100644 --- a/src/features/helm-charts/installing-chart/opening-dock-tab-for-installing-helm-chart.test.ts +++ b/src/features/helm-charts/installing-chart/opening-dock-tab-for-installing-helm-chart.test.ts @@ -8,69 +8,44 @@ import type { RenderResult } from "@testing-library/react"; import { fireEvent } from "@testing-library/react"; import type { ApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder"; import { getApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder"; -import type { CallForHelmCharts } from "../../../renderer/components/+helm-charts/helm-charts/call-for-helm-charts.injectable"; -import callForHelmChartsInjectable from "../../../renderer/components/+helm-charts/helm-charts/call-for-helm-charts.injectable"; import { HelmChart } from "../../../common/k8s-api/endpoints/helm-charts.api"; import getRandomInstallChartTabIdInjectable from "../../../renderer/components/dock/install-chart/get-random-install-chart-tab-id.injectable"; -import callForHelmChartValuesInjectable from "../../../renderer/components/dock/install-chart/chart-data/call-for-helm-chart-values.injectable"; -import callForCreateHelmReleaseInjectable from "../../../renderer/components/+helm-releases/create-release/call-for-create-helm-release.injectable"; -import type { CallForHelmChartReadme } from "../../../renderer/components/+helm-charts/details/readme/call-for-helm-chart-readme.injectable"; -import callForHelmChartReadmeInjectable from "../../../renderer/components/+helm-charts/details/readme/call-for-helm-chart-readme.injectable"; -import type { CallForHelmChartVersions } from "../../../renderer/components/+helm-charts/details/versions/call-for-helm-chart-versions.injectable"; -import callForHelmChartVersionsInjectable from "../../../renderer/components/+helm-charts/details/versions/call-for-helm-chart-versions.injectable"; +import requestCreateHelmReleaseInjectable from "../../../common/k8s-api/endpoints/helm-releases.api/request-create.injectable"; import { flushPromises } from "../../../common/test-utils/flush-promises"; import directoryForLensLocalStorageInjectable from "../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable"; import hostedClusterIdInjectable from "../../../renderer/cluster-frame-context/hosted-cluster-id.injectable"; import dockStoreInjectable from "../../../renderer/components/dock/dock/store.injectable"; +import type { RequestHelmCharts } from "../../../common/k8s-api/endpoints/helm-charts.api/request-charts.injectable"; +import type { RequestHelmChartVersions } from "../../../common/k8s-api/endpoints/helm-charts.api/request-versions.injectable"; +import type { RequestHelmChartReadme } from "../../../common/k8s-api/endpoints/helm-charts.api/request-readme.injectable"; +import requestHelmChartsInjectable from "../../../common/k8s-api/endpoints/helm-charts.api/request-charts.injectable"; +import requestHelmChartVersionsInjectable from "../../../common/k8s-api/endpoints/helm-charts.api/request-versions.injectable"; +import requestHelmChartReadmeInjectable from "../../../common/k8s-api/endpoints/helm-charts.api/request-readme.injectable"; +import requestHelmChartValuesInjectable from "../../../common/k8s-api/endpoints/helm-charts.api/request-values.injectable"; describe("opening dock tab for installing helm chart", () => { let builder: ApplicationBuilder; - let callForHelmChartsMock: AsyncFnMock; - let callForHelmChartVersionsMock: AsyncFnMock; - let callForHelmChartReadmeMock: AsyncFnMock; - let callForHelmChartValuesMock: jest.Mock; + let requestHelmChartsMock: AsyncFnMock; + let requestHelmChartVersionsMock: AsyncFnMock; + let requestHelmChartReadmeMock: AsyncFnMock; + let requestHelmChartValuesMock: jest.Mock; beforeEach(() => { builder = getApplicationBuilder(); - callForHelmChartsMock = asyncFn(); - callForHelmChartVersionsMock = asyncFn(); - callForHelmChartReadmeMock = asyncFn(); - callForHelmChartValuesMock = jest.fn(); + requestHelmChartsMock = asyncFn(); + requestHelmChartVersionsMock = asyncFn(); + requestHelmChartReadmeMock = asyncFn(); + requestHelmChartValuesMock = jest.fn(); builder.beforeWindowStart((windowDi) => { - windowDi.override( - directoryForLensLocalStorageInjectable, - () => "/some-directory-for-lens-local-storage", - ); - + windowDi.override(directoryForLensLocalStorageInjectable, () => "/some-directory-for-lens-local-storage"); windowDi.override(hostedClusterIdInjectable, () => "some-cluster-id"); - - windowDi.override( - callForHelmChartsInjectable, - () => callForHelmChartsMock, - ); - - windowDi.override( - callForHelmChartVersionsInjectable, - () => callForHelmChartVersionsMock, - ); - - windowDi.override( - callForHelmChartReadmeInjectable, - () => callForHelmChartReadmeMock, - ); - - windowDi.override( - callForHelmChartValuesInjectable, - () => callForHelmChartValuesMock, - ); - - windowDi.override( - callForCreateHelmReleaseInjectable, - () => jest.fn(), - ); - + windowDi.override(requestHelmChartsInjectable, () => requestHelmChartsMock); + windowDi.override(requestHelmChartVersionsInjectable, () => requestHelmChartVersionsMock); + windowDi.override(requestHelmChartReadmeInjectable, () => requestHelmChartReadmeMock); + windowDi.override(requestHelmChartValuesInjectable, () => requestHelmChartValuesMock); + windowDi.override(requestCreateHelmReleaseInjectable, () => jest.fn()); windowDi.override(getRandomInstallChartTabIdInjectable, () => jest .fn(() => "some-irrelevant-tab-id") @@ -102,12 +77,12 @@ describe("opening dock tab for installing helm chart", () => { }); it("calls for charts", () => { - expect(callForHelmChartsMock).toHaveBeenCalled(); + expect(requestHelmChartsMock).toHaveBeenCalled(); }); describe("when charts resolve", () => { beforeEach(async () => { - await callForHelmChartsMock.resolve([ + await requestHelmChartsMock.resolve([ HelmChart.create({ apiVersion: "some-api-version", name: "some-name", @@ -160,7 +135,7 @@ describe("opening dock tab for installing helm chart", () => { }); it("calls for chart versions", () => { - expect(callForHelmChartVersionsMock).toHaveBeenCalledWith( + expect(requestHelmChartVersionsMock).toHaveBeenCalledWith( "some-repository", "some-name", ); @@ -174,7 +149,7 @@ describe("opening dock tab for installing helm chart", () => { describe("when chart versions resolve", () => { beforeEach(async () => { - await callForHelmChartVersionsMock.resolve([ + await requestHelmChartVersionsMock.resolve([ HelmChart.create({ apiVersion: "some-api-version", name: "some-name", @@ -210,7 +185,7 @@ describe("opening dock tab for installing helm chart", () => { }); it("calls for chart readme for the version", () => { - expect(callForHelmChartReadmeMock).toHaveBeenCalledWith( + expect(requestHelmChartReadmeMock).toHaveBeenCalledWith( "some-repository", "some-name", "some-version", @@ -243,7 +218,7 @@ describe("opening dock tab for installing helm chart", () => { describe("when readme resolves", () => { beforeEach(async () => { - await callForHelmChartReadmeMock.resolve("some-readme"); + await requestHelmChartReadmeMock.resolve("some-readme"); }); it("renders", () => { @@ -258,7 +233,7 @@ describe("opening dock tab for installing helm chart", () => { describe("when selecting different version", () => { beforeEach(() => { - callForHelmChartReadmeMock.mockClear(); + requestHelmChartReadmeMock.mockClear(); builder.select .openMenu( @@ -280,7 +255,7 @@ describe("opening dock tab for installing helm chart", () => { }); it("calls for chart readme for the version", () => { - expect(callForHelmChartReadmeMock).toHaveBeenCalledWith( + expect(requestHelmChartReadmeMock).toHaveBeenCalledWith( "some-repository", "some-name", "some-other-version", @@ -289,7 +264,7 @@ describe("opening dock tab for installing helm chart", () => { describe("when readme resolves", () => { beforeEach(async () => { - await callForHelmChartReadmeMock.resolve("some-readme"); + await requestHelmChartReadmeMock.resolve("some-readme"); }); it("renders", () => { @@ -305,7 +280,7 @@ describe("opening dock tab for installing helm chart", () => { await flushPromises(); - expect(callForHelmChartValuesMock).toHaveBeenCalledWith( + expect(requestHelmChartValuesMock).toHaveBeenCalledWith( "some-repository", "some-name", "some-other-version", @@ -316,7 +291,7 @@ describe("opening dock tab for installing helm chart", () => { describe("when selecting to install the chart", () => { beforeEach(() => { - callForHelmChartVersionsMock.mockClear(); + requestHelmChartVersionsMock.mockClear(); const installButton = rendered.getByTestId( "install-chart-for-some-repository-some-name", diff --git a/src/features/helm-charts/listing-active-helm-repositories-in-preferences.test.ts b/src/features/helm-charts/listing-active-helm-repositories-in-preferences.test.ts index c8407767ef..19aaf28938 100644 --- a/src/features/helm-charts/listing-active-helm-repositories-in-preferences.test.ts +++ b/src/features/helm-charts/listing-active-helm-repositories-in-preferences.test.ts @@ -10,7 +10,7 @@ import readYamlFileInjectable from "../../common/fs/read-yaml-file.injectable"; import type { AsyncFnMock } from "@async-fn/jest"; import asyncFn from "@async-fn/jest"; import type { HelmRepositoriesFromYaml } from "../../main/helm/repositories/get-active-helm-repositories/get-active-helm-repositories.injectable"; -import execFileInjectable from "../../common/fs/exec-file.injectable"; +import execFileInjectable, { type ExecFile } from "../../common/fs/exec-file.injectable"; import helmBinaryPathInjectable from "../../main/helm/helm-binary-path.injectable"; import loggerInjectable from "../../common/logger.injectable"; import type { Logger } from "../../common/logger"; @@ -21,7 +21,7 @@ describe("listing active helm repositories in preferences", () => { let builder: ApplicationBuilder; let rendered: RenderResult; let readYamlFileMock: AsyncFnMock; - let execFileMock: AsyncFnMock>; + let execFileMock: AsyncFnMock; let loggerStub: Logger; let showErrorNotificationMock: jest.Mock; diff --git a/src/features/helm-charts/upgrade-chart/__snapshots__/upgrade-chart-new-tab.test.ts.snap b/src/features/helm-charts/upgrade-chart/__snapshots__/upgrade-chart-new-tab.test.ts.snap new file mode 100644 index 0000000000..52d08559d6 --- /dev/null +++ b/src/features/helm-charts/upgrade-chart/__snapshots__/upgrade-chart-new-tab.test.ts.snap @@ -0,0 +1,5785 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`New Upgrade Helm Chart Dock Tab given a namespace is selected when navigating to the helm releases view renders 1`] = ` + +
+
+
+