From 289cd1e62ec80024a44c0bb4afb3881a6107e83a Mon Sep 17 00:00:00 2001 From: Kingdon Barrett Date: Fri, 8 Sep 2023 10:31:17 -0400 Subject: [PATCH] Edge rebase @ 0.25.1-edge.7 (#463) * Allow users to disable running `flux check` When I tested it locally it looked like we're calling `flux check` a bit more often than we should. This may have been fixed by now, but the flag remains so users who are experiencing issues from `flux check` can disable it entirely. * allow users to additionally suppress debugging messages Emits some debugging messages that are meant for the other authors of the extension to notice (and users won't be as interested in how many times we were apparently going to call flux check) Signed-off-by: Kingdon Barrett * Add a hard timeout for all processes we spawn (#460) timeout kill all exec processes. default time is 10s * aync cluster treeview flux check with async cluster treeview proper async sources treeview correct expand all * responsive ui with oidc-login WIP * preserve collapsible states safer execs realtime namespace icons * 60 seconds exec kill * namespace UI better * namespace ui improvements * module tree-kill * node 20 missing npm packages npm audit fix fully qualify types my cluster has HelmReleases.cluster.loft.sh and it is wreaking all kinds of havoc here, let's fix this in the prerelease/edge channel ASAP Signed-off-by: Kingdon Barrett simplify treeview states WIP api-resources loader WIP revert Kind.APIGroup gitops.editor.openKubeconfig api-resource loader and views Signed-off-by: Kingdon Barrett * docs and templates treeview loads * update github actions workflows Signed-off-by: Kingdon Barrett * First try: fully qualified Flux API names Signed-off-by: Kingdon Barrett * More fully qualified kinds * fix type error Signed-off-by: Kingdon Barrett * fix linting error Signed-off-by: Kingdon Barrett * remove debugger stuff Signed-off-by: Kingdon Barrett * fix expand all for workloads * fqkinds record lookup * add timeout configuration to after async Signed-off-by: Kingdon Barrett * remove console.log and lint imports * treeview semantics cleanup * restore k8s proxy * refactor data providers to not break on context switches --------- Signed-off-by: Kingdon Barrett Co-authored-by: Juozas Gaigalas --- .github/workflows/build-vsix.yml | 14 +-- .github/workflows/ci.yml | 14 +-- .vscode/settings.json | 6 +- README.md | 6 +- package-lock.json | 95 +++++++++++++++ package.json | 94 ++++++--------- src/cli/checkVersions.ts | 19 +-- src/cli/flux/fluxTools.ts | 17 ++- src/cli/kubernetes/apiResources.ts | 58 ++++++--- src/cli/kubernetes/clusterProvider.ts | 4 +- src/cli/kubernetes/kubectlGet.ts | 23 ++-- src/cli/kubernetes/kubectlProxy.ts | 24 ++-- src/cli/kubernetes/kubernetesConfig.ts | 86 ++++++++------ src/cli/kubernetes/kubernetesConfigWatcher.ts | 2 +- src/cli/kubernetes/kubernetesToolsKubectl.ts | 4 - src/cli/shell/exec.ts | 27 ++++- src/commands/commands.ts | 3 +- src/commands/createFromTemplate.ts | 1 - src/commands/deleteSource.ts | 8 +- src/commands/deleteWorkload.ts | 6 +- src/commands/enableDisableGitOps.ts | 6 +- src/commands/expandAll.ts | 10 +- src/commands/openResource.ts | 7 ++ src/commands/refreshTreeViews.ts | 17 ++- src/commands/resume.ts | 8 +- src/commands/setClusterProvider.ts | 4 +- src/commands/setCurrentKubernetesContext.ts | 2 +- src/commands/suspend.ts | 18 +-- src/data/contextData.ts | 64 ++++++++++ src/extension.ts | 43 +++---- src/index.d.ts | 1 + src/k8s/client.ts | 12 +- src/k8s/createKubeProxyConfig.ts | 7 +- src/k8s/informers.ts | 3 +- src/k8s/list.ts | 5 - src/test/suite/extension.test.ts | 1 + src/types/extensionIds.ts | 12 +- src/types/flux/helmRelease.ts | 2 +- src/types/kubernetes/kubernetesTypes.ts | 16 ++- src/ui/promptToInstallFlux.ts | 2 +- .../dataProviders/asyncDataProvider.ts | 83 +++++++++++++ .../dataProviders/clusterDataProvider.ts | 112 +++--------------- .../treeviews/dataProviders/dataProvider.ts | 78 ------------ .../documentationDataProvider.ts | 10 +- .../kubernetesObjectDataProvider.ts | 54 ++++++--- .../dataProviders/simpleDataProvider.ts | 93 +++++++++++++++ .../dataProviders/sourceDataProvider.ts | 32 +++-- .../dataProviders/templateDataProvider.ts | 10 +- .../dataProviders/workloadDataProvider.ts | 74 +++++------- src/ui/treeviews/nodes/cluster/clusterNode.ts | 107 +++++++++++++---- src/ui/treeviews/nodes/gitOpsTemplateNode.ts | 3 +- src/ui/treeviews/nodes/namespaceNode.ts | 28 ++++- src/ui/treeviews/nodes/treeNode.ts | 12 +- src/ui/treeviews/treeViews.ts | 31 ++--- .../configureGitOps/lib/createAzure.ts | 18 +-- .../configureGitOps/lib/createGeneric.ts | 8 +- .../createFromTemplate/receiveMessage.ts | 2 - src/utils/kubeConfigCompare.ts | 2 +- src/utils/makeTreeviewInfoNode.ts | 38 ++++++ src/utils/treeNodeUtils.ts | 4 +- tslint-imports.json | 11 ++ webview-ui/configureGitOps/package-lock.json | 28 ++--- .../createFromTemplate/package-lock.json | 12 +- 63 files changed, 979 insertions(+), 622 deletions(-) create mode 100644 src/data/contextData.ts create mode 100644 src/ui/treeviews/dataProviders/asyncDataProvider.ts delete mode 100644 src/ui/treeviews/dataProviders/dataProvider.ts create mode 100644 src/ui/treeviews/dataProviders/simpleDataProvider.ts create mode 100644 src/utils/makeTreeviewInfoNode.ts create mode 100644 tslint-imports.json diff --git a/.github/workflows/build-vsix.yml b/.github/workflows/build-vsix.yml index 82f3f5a2..fdff08f0 100644 --- a/.github/workflows/build-vsix.yml +++ b/.github/workflows/build-vsix.yml @@ -64,17 +64,17 @@ jobs: fi; - name: Clone Repository - uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # pin@v2 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # pin@v3 with: fetch-depth: 0 - name: Setup Flux CLI - uses: fluxcd/flux2/action@44d69d6fc0c353e79c1bad021a4aca135033bce8 # pin@main + uses: fluxcd/flux2/action@1730f3c46bddf0a29787d8d4fa5ace283f298e49 # pin@main with: token: ${{ secrets.GITHUB_TOKEN }} - name: Setup Node version - uses: actions/setup-node@f1f314fca9dfce2769ece7d933488f076716723e # pin@v1 + uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # pin@v3 with: node-version: 19 @@ -96,11 +96,11 @@ jobs: - name: Setup Kubernetes uses: engineerd/setup-kind@aa272fe2a7309878ffc2a81c56cfe3ef108ae7d0 # pin@v0.5.0 with: - version: v0.19.0 - image: kindest/node:v1.27.3 + version: v0.20.0 + image: kindest/node:v1.28.0 - name: Run Tests - uses: GabrielBB/xvfb-action@86d97bde4a65fe9b290c0b3fb92c2c4ed0e5302d # pin@v1 + uses: coactions/setup-xvfb@b6b4fcfb9f5a895edadc3bc76318fae0ac17c8b3 # pin@v1 with: run: npm test options: "-screen 0 1600x1200x24" @@ -199,7 +199,7 @@ jobs: echo "GIT_TAG=$GIT_TAG" >> $GITHUB_ENV - name: GitHub Release - uses: ncipollo/release-action@a2e71bdd4e7dab70ca26a852f29600c98b33153e # pin@v1 + uses: ncipollo/release-action@6c75be85e571768fa31b40abf38de58ba0397db5 # pin@v1 with: artifacts: "./gitops-tools-*" bodyFile: ${{ github.workspace }}-CHANGELOG.txt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1eaa6a4f..0d2f6b1e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,27 +11,27 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # pin@v3 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # pin@v3 - - uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # pin@v3 + - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # pin@v3 with: - node-version: '19' + node-version: '20' - run: npm install - name: Setup Kubernetes uses: engineerd/setup-kind@aa272fe2a7309878ffc2a81c56cfe3ef108ae7d0 # pin@v0.5.0 with: - version: v0.19.0 - image: kindest/node:v1.27.3 + version: v0.20.0 + image: kindest/node:v1.28.0 - name: Setup Flux CLI - uses: fluxcd/flux2/action@44d69d6fc0c353e79c1bad021a4aca135033bce8 # pin@main + uses: fluxcd/flux2/action@1730f3c46bddf0a29787d8d4fa5ace283f298e49 # pin@main with: token: ${{ secrets.GITHUB_TOKEN }} - name: extension test - uses: GabrielBB/xvfb-action@86d97bde4a65fe9b290c0b3fb92c2c4ed0e5302d # pin@v1 + uses: coactions/setup-xvfb@b6b4fcfb9f5a895edadc3bc76318fae0ac17c8b3 # pin@v1 with: run: 'npm test' options: "-screen 0 1600x1200x24" diff --git a/.vscode/settings.json b/.vscode/settings.json index 5adc2423..129a253b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,5 +11,9 @@ }, // Turn off tsc task auto detection since we have the necessary tasks as npm scripts "typescript.tsc.autoDetect": "off", - "debug.node.autoAttach": "on" + "debug.node.autoAttach": "on", + "editor.codeActionsOnSave": { + "source.fixAll": true, + "source.organizeImports": true + }, } \ No newline at end of file diff --git a/README.md b/README.md index d5ec76a2..7bc2230a 100644 --- a/README.md +++ b/README.md @@ -149,11 +149,9 @@ We rely on the Kubernetes extension to discover and connect to clusters. If you Confirm that your configuration context shows in a terminal running `kubectl config get-contexts` +### _Switching from an unreachable cluster context to a working cluster_ - - - - +Unreachable or laggy clusters can create long running that cluster resource queries that finish after switching to a working cluster context. This can lead to the slow cluster data overwriting the current cluster treeview. **Clusters** -> **Refresh** button will reinitialize the views with current data. Timeout settings can be adjusted under **GitOps** section in VSCode Settings. ### _Timeouts and flux check warnings_ diff --git a/package-lock.json b/package-lock.json index 8b9ba805..fe7637c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,9 +12,11 @@ "@kubernetes/client-node": "^0.18.1", "@types/ws": "^8.5.4", "@vscode/extension-telemetry": "^0.4.7", + "bufferutil": "^4.0.7", "change-case": "^4.1.2", "extract-zip": "^2.0.1", "git-url-parse": "^13.0.0", + "is-running": "^2.1.0", "jose": ">=2.0.6", "lite-deep-equal": "^1.0.6", "parse-path": ">=5.0.0", @@ -24,6 +26,8 @@ "shelljs": "^0.8.5", "tinytim": "^0.1.1", "tough-cookie": ">=4.1.3", + "tree-kill": "^1.2.2", + "utf-8-validate": "^6.0.3", "uuid": "^9.0.0", "vite": ">=2.9.16", "vscode-kubernetes-tools-api": "^1.3.0", @@ -32,6 +36,7 @@ }, "devDependencies": { "@types/git-url-parse": "^9.0.1", + "@types/is-running": "^2.1.0", "@types/mocha": "^9.1.0", "@types/node": "14.x", "@types/semver": "^7.3.9", @@ -646,6 +651,12 @@ "@types/node": "*" } }, + "node_modules/@types/is-running": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/is-running/-/is-running-2.1.0.tgz", + "integrity": "sha512-y1JGY9nExw7elDF/PUGqV4wQi7PVqa4hsxHf2fXLpc0jpD5kzx5BRldqeuqJQAuctnbsIqWxzYoGxSayvgynBQ==", + "dev": true + }, "node_modules/@types/js-yaml": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.3.tgz", @@ -1442,6 +1453,18 @@ "node": ">=0.2.0" } }, + "node_modules/bufferutil": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.7.tgz", + "integrity": "sha512-kukuqc39WOHtdxtw4UScxF/WVnMFVSQVKhtx3AjZJzhd0RGZZldcrfSEbVsWWe6KNH253574cq5F+wpv0G9pJw==", + "hasInstallScript": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, "node_modules/byline": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", @@ -2850,6 +2873,11 @@ "node": ">=0.10.0" } }, + "node_modules/is-running": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-running/-/is-running-2.1.0.tgz", + "integrity": "sha512-mjJd3PujZMl7j+D395WTIO5tU5RIDBfVSRtRR4VOJou3H66E38UjbjvDGh3slJzPuolsb+yQFqwHNNdyp5jg3w==" + }, "node_modules/is-ssh": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/is-ssh/-/is-ssh-1.4.0.tgz", @@ -3375,6 +3403,16 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" }, + "node_modules/node-gyp-build": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.0.tgz", + "integrity": "sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/node-releases": { "version": "1.1.75", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.75.tgz", @@ -4594,6 +4632,14 @@ "node": "*" } }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "bin": { + "tree-kill": "cli.js" + } + }, "node_modules/ts-loader": { "version": "9.2.8", "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.2.8.tgz", @@ -4791,6 +4837,18 @@ "requires-port": "^1.0.0" } }, + "node_modules/utf-8-validate": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-6.0.3.tgz", + "integrity": "sha512-uIuGf9TWQ/y+0Lp+KGZCMuJWc3N9BHA+l/UmHd/oUHwJJDeysyTRxNQVkbzsIWfGFbRe3OcgML/i0mvVRPOyDA==", + "hasInstallScript": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -5556,6 +5614,12 @@ "@types/node": "*" } }, + "@types/is-running": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/is-running/-/is-running-2.1.0.tgz", + "integrity": "sha512-y1JGY9nExw7elDF/PUGqV4wQi7PVqa4hsxHf2fXLpc0jpD5kzx5BRldqeuqJQAuctnbsIqWxzYoGxSayvgynBQ==", + "dev": true + }, "@types/js-yaml": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.3.tgz", @@ -6172,6 +6236,14 @@ "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s=", "dev": true }, + "bufferutil": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.7.tgz", + "integrity": "sha512-kukuqc39WOHtdxtw4UScxF/WVnMFVSQVKhtx3AjZJzhd0RGZZldcrfSEbVsWWe6KNH253574cq5F+wpv0G9pJw==", + "requires": { + "node-gyp-build": "^4.3.0" + } + }, "byline": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", @@ -7263,6 +7335,11 @@ "isobject": "^3.0.1" } }, + "is-running": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-running/-/is-running-2.1.0.tgz", + "integrity": "sha512-mjJd3PujZMl7j+D395WTIO5tU5RIDBfVSRtRR4VOJou3H66E38UjbjvDGh3slJzPuolsb+yQFqwHNNdyp5jg3w==" + }, "is-ssh": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/is-ssh/-/is-ssh-1.4.0.tgz", @@ -7675,6 +7752,11 @@ } } }, + "node-gyp-build": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.0.tgz", + "integrity": "sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==" + }, "node-releases": { "version": "1.1.75", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.75.tgz", @@ -8585,6 +8667,11 @@ "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=", "dev": true }, + "tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==" + }, "ts-loader": { "version": "9.2.8", "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.2.8.tgz", @@ -8745,6 +8832,14 @@ "requires-port": "^1.0.0" } }, + "utf-8-validate": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-6.0.3.tgz", + "integrity": "sha512-uIuGf9TWQ/y+0Lp+KGZCMuJWc3N9BHA+l/UmHd/oUHwJJDeysyTRxNQVkbzsIWfGFbRe3OcgML/i0mvVRPOyDA==", + "requires": { + "node-gyp-build": "^4.3.0" + } + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index 479b76b6..b01b11e3 100644 --- a/package.json +++ b/package.json @@ -163,7 +163,7 @@ "command": "gitops.createKustomization", "title": "Create Kustomization from Path", "icon": "$(add)", - "enablement": "!gitops:noClusterSelected && !gitops:currentClusterGitOpsNotEnabled", + "enablement": "!gitops:clusterUnreachable && !gitops:currentClusterGitOpsNotEnabled", "category": "GitOps" }, { @@ -171,28 +171,28 @@ "title": "Add Source", "category": "GitOps", "icon": "$(add)", - "enablement": "!gitops:noClusterSelected && !gitops:currentClusterGitOpsNotEnabled" + "enablement": "!gitops:clusterUnreachable && !gitops:currentClusterGitOpsNotEnabled" }, { "command": "gitops.addKustomization", "title": "Add Kustomization", "category": "GitOps", "icon": "$(add)", - "enablement": "!gitops:noClusterSelected && !gitops:currentClusterGitOpsNotEnabled" + "enablement": "!gitops:clusterUnreachable && !gitops:currentClusterGitOpsNotEnabled" }, { "command": "gitops.views.expandAllSources", "title": "Expand All", "category": "GitOps", "icon": "$(expand-all)", - "enablement": "!gitops:noClusterSelected && !gitops:currentClusterGitOpsNotEnabled && !gitops:noClusterSelected && !gitops:noSources" + "enablement": "!gitops:clusterUnreachable && !gitops:currentClusterGitOpsNotEnabled" }, { "command": "gitops.views.expandAllWorkloads", "title": "Expand All", "category": "GitOps", "icon": "$(expand-all)", - "enablement": "!gitops:noClusterSelected && !gitops:currentClusterGitOpsNotEnabled && !gitops:noClusterSelected && !gitops:noWorkloads" + "enablement": "!gitops:clusterUnreachable && !gitops:currentClusterGitOpsNotEnabled" }, { "command": "gitops.views.deleteWorkload", @@ -217,7 +217,12 @@ }, { "command": "gitops.editor.openResource", - "title": "View Config", + "title": "Open Resource", + "category": "GitOps" + }, + { + "command": "gitops.editor.openKubeconfig", + "title": "Open Kubeconfig", "category": "GitOps" }, { @@ -278,6 +283,16 @@ "configuration": { "title": "GitOps Tools", "properties": { + "gitops.doFluxCheck": { + "type": "boolean", + "default": true, + "description": "Enable Flux Check (uncheck to skip flux check)" + }, + "gitops.suppressDebugMessages": { + "type": "boolean", + "default": false, + "description": "Do not emit debug-level messages (only error, info, warn)" + }, "gitops.weaveGitopsEnterprise": { "type": "boolean", "default": false, @@ -290,70 +305,21 @@ }, "gitops.execTimeout": { "type": "string", - "default": "0", - "description": "Seconds until SIGKILL for every shell exec (except `kubectl proxy`). Set to 0 for no timeout." + "default": "60", + "description": "Seconds until SIGTERM for every shell exec (except `kubectl proxy`). Set to 0 for no timeout." } } }, "viewsWelcome": [ - { - "view": "gitops.views.clusters", - "contents": "Loading Clusters ...", - "when": "gitops:loadingClusters" - }, - { - "view": "gitops.views.clusters", - "contents": "No clusters.", - "when": "!gitops:loadingClusters && gitops:noClusters" - }, - { - "view": "gitops.views.clusters", - "contents": "Failed to load cluster contexts.", - "when": "!gitops:loadingClusters && gitops:failedToLoadClusterContexts" - }, { "view": "gitops.views.sources", "contents": "[Enable GitOps](command:gitops.flux.install) for the selected Cluster to view Sources.", - "when": "gitops:currentClusterGitOpsNotEnabled && !gitops:noClusterSelected" - }, - { - "view": "gitops.views.sources", - "contents": "Loading Sources ...", - "when": "gitops:loadingSources && !gitops:noClusterSelected && !gitops:currentClusterGitOpsNotEnabled" - }, - { - "view": "gitops.views.sources", - "contents": "No sources.", - "when": "!gitops:loadingSources && !gitops:currentClusterGitOpsNotEnabled && !gitops:noClusterSelected && gitops:noSources" - }, - { - "view": "gitops.views.sources", - "contents": "Select GitOps Cluster to view Sources.", - "when": "gitops:noClusterSelected" + "when": "gitops:currentClusterGitOpsNotEnabled && !gitops:clusterUnreachable" }, { "view": "gitops.views.workloads", "contents": "[Enable GitOps](command:gitops.flux.install) for the selected Cluster to view Workloads.", - "when": "gitops:currentClusterGitOpsNotEnabled" - }, - { - "view": "gitops.views.workloads", - "contents": "Loading Workloads ...", - "when": "gitops:loadingWorkloads && !gitops:noClusterSelected && !gitops:currentClusterGitOpsNotEnabled" - }, - { - "view": "gitops.views.workloads", - "contents": "No workloads.", - "when": "!gitops:loadingWorkloads && !gitops:currentClusterGitOpsNotEnabled && !gitops:noClusterSelected && gitops:noWorkloads" - }, - { - "view": "gitops.views.workloads", - "contents": "Select GitOps Cluster to view Workloads.", - "when": "gitops:noClusterSelected" - }, - { - "view": "gitops.views.documentation", - "contents": "Loading Topics ..." + "when": "gitops:currentClusterGitOpsNotEnabled && !gitops:clusterUnreachable" } ], "menus": { @@ -368,6 +334,11 @@ "group": "1", "when": "view == gitops.views.clusters" }, + { + "command": "gitops.editor.openKubeconfig", + "group": "1", + "when": "view == gitops.views.clusters" + }, { "command": "gitops.addSource", "group": "navigation@0", @@ -604,6 +575,7 @@ ], "devDependencies": { "@types/git-url-parse": "^9.0.1", + "@types/is-running": "^2.1.0", "@types/mocha": "^9.1.0", "@types/node": "14.x", "@types/semver": "^7.3.9", @@ -631,9 +603,11 @@ "@kubernetes/client-node": "^0.18.1", "@types/ws": "^8.5.4", "@vscode/extension-telemetry": "^0.4.7", + "bufferutil": "^4.0.7", "change-case": "^4.1.2", "extract-zip": "^2.0.1", "git-url-parse": "^13.0.0", + "is-running": "^2.1.0", "jose": ">=2.0.6", "lite-deep-equal": "^1.0.6", "parse-path": ">=5.0.0", @@ -643,6 +617,8 @@ "shelljs": "^0.8.5", "tinytim": "^0.1.1", "tough-cookie": ">=4.1.3", + "tree-kill": "^1.2.2", + "utf-8-validate": "^6.0.3", "uuid": "^9.0.0", "vite": ">=2.9.16", "vscode-kubernetes-tools-api": "^1.3.0", diff --git a/src/cli/checkVersions.ts b/src/cli/checkVersions.ts index 5192be74..93cdd891 100644 --- a/src/cli/checkVersions.ts +++ b/src/cli/checkVersions.ts @@ -3,7 +3,6 @@ import { commands, Uri, window } from 'vscode'; import * as shell from 'cli/shell/exec'; import { enabledWGE, telemetry } from 'extension'; import { Errorable, failed } from 'types/errorable'; -import { CommandId } from 'types/extensionIds'; import { TelemetryError } from 'types/telemetryEventNames'; import { clusterDataProvider } from 'ui/treeviews/treeViews'; import { parseJson } from 'utils/jsonUtils'; @@ -83,7 +82,7 @@ export async function getFluxVersion(): Promise> { if (fluxVersionShellResult.code === 0) { fluxVersion = parseJson(fluxVersionShellResult.stdout.trim()).flux; - clusterDataProvider.refreshCurrentNode(); + clusterDataProvider.redrawCurrentNode(); return { succeeded: true, @@ -99,22 +98,6 @@ export async function getFluxVersion(): Promise> { } } -/** - * Show warning notification only in case the - * flux prerequisite check has failed. - * @see https://fluxcd.io/docs/cmd/flux_check/ - */ -export async function checkFluxPrerequisites() { - const prerequisiteShellResult = await shell.execWithOutput('flux check --pre', { revealOutputView: false }); - - if (prerequisiteShellResult.code !== 0) { - const showOutput = 'Show Output'; - const showOutputConfirm = await window.showWarningMessage('Flux prerequisites check failed.', showOutput); - if (showOutput === showOutputConfirm) { - commands.executeCommand(CommandId.ShowOutputChannel); - } - } -} /** * Return git version or undefined depending diff --git a/src/cli/flux/fluxTools.ts b/src/cli/flux/fluxTools.ts index fcea8d3f..94860a1b 100644 --- a/src/cli/flux/fluxTools.ts +++ b/src/cli/flux/fluxTools.ts @@ -2,7 +2,7 @@ import safesh from 'shell-escape-tag'; import { window } from 'vscode'; import * as shell from 'cli/shell/exec'; -import { telemetry } from 'extension'; +import { enabledFluxChecks, telemetry } from 'extension'; import { FluxSource, FluxTreeResources, FluxWorkload } from 'types/fluxCliTypes'; import { TelemetryError } from 'types/telemetryEventNames'; import { parseJson } from 'utils/jsonUtils'; @@ -64,13 +64,16 @@ class FluxTools { * https://github.com/fluxcd/flux2/blob/main/cmd/flux/check.go */ async check(context: string): Promise<{ prerequisites: FluxPrerequisite[]; controllers: FluxController[]; } | undefined> { + if (!enabledFluxChecks()) { + return undefined; + } const result = await shell.execWithOutput(safesh`flux check --context ${context}`, { revealOutputView: false }); if (result.code !== 0) { telemetry.sendError(TelemetryError.FAILED_TO_RUN_FLUX_CHECK); const stderr = result?.stderr; if (stderr) { - window.showErrorMessage(String(result?.stderr || '')); + window.showWarningMessage(String(result?.stderr || '')); } return undefined; } @@ -130,11 +133,17 @@ class FluxTools { */ async tree(name: string, namespace: string): Promise { - const treeShellResult = await shell.exec(`flux tree kustomization ${name} -n ${namespace} -o json`); + const cmd = `flux tree kustomization ${name} -n ${namespace} -o json`; + const treeShellResult = await shell.exec(cmd); if (treeShellResult.code !== 0) { telemetry.sendError(TelemetryError.FAILED_TO_RUN_FLUX_TREE); - window.showErrorMessage(`Failed to get resources created by the kustomization ${name}. ERROR: ${treeShellResult?.stderr}`); + let errorData = treeShellResult.stderr; + if (treeShellResult.code === null) { + errorData += `Command '${cmd}' timed out`; + } + // + (treeShellResult.code === null ? 'Command timed out' : ''; + window.showWarningMessage(`Failed to get resources created by the kustomization ${name}. ERROR: ${errorData}`); return; } diff --git a/src/cli/kubernetes/apiResources.ts b/src/cli/kubernetes/apiResources.ts index 26702541..5f844fd6 100644 --- a/src/cli/kubernetes/apiResources.ts +++ b/src/cli/kubernetes/apiResources.ts @@ -1,27 +1,35 @@ -import { telemetry } from 'extension'; +import { redrawhResourcesTreeViews, refreshResourcesTreeViews } from 'commands/refreshTreeViews'; +import { currentContextData } from 'data/contextData'; +import { setVSCodeContext, telemetry } from 'extension'; +import { ContextId } from 'types/extensionIds'; +import { Kind } from 'types/kubernetes/kubernetesTypes'; import { TelemetryError } from 'types/telemetryEventNames'; +import { clusterDataProvider } from 'ui/treeviews/treeViews'; +import { restartKubeProxy } from './kubectlProxy'; import { invokeKubectlCommand } from './kubernetesToolsKubectl'; -import { Kind } from 'types/kubernetes/kubernetesTypes'; +export enum ApiState { + Loading, + Loaded, + ClusterUnreachable, +} -type KindApiParams = { +export type KindApiParams = { plural: string; // configmaps, deployments, gitrepositories, ... group: string; // '', apps, source.toolkit.fluxcd.io, ... version: string; // v1, v1beta2, ... }; -/* - * Current cluster supported kubernetes resource kinds. - */ -let apiResources: Map | undefined; + /** * Return all available kubernetes resource kinds */ export function getAvailableResourcePlurals(): string[] | undefined { + const context = currentContextData(); const plurals: string[] = []; - if(apiResources) { - apiResources.forEach((value, key) => { + if(context.apiResources) { + context.apiResources.forEach((value, key) => { plurals.push(value.plural); }); @@ -31,19 +39,30 @@ export function getAvailableResourcePlurals(): string[] | undefined { export function getAPIParams(kind: Kind): KindApiParams | undefined { - if(apiResources) { - return apiResources.get(kind); + const context = currentContextData(); + + if(context.apiResources) { + return context.apiResources.get(kind); } } export async function loadAvailableResourceKinds() { - apiResources = undefined; + const context = currentContextData(); + context.apiResources = undefined; + context.apiState = ApiState.Loading; + // will set their content to Loading API... + redrawhResourcesTreeViews(); const kindsShellResult = await invokeKubectlCommand('api-resources --verbs=list -o wide'); if (kindsShellResult?.code !== 0) { telemetry.sendError(TelemetryError.FAILED_TO_GET_AVAILABLE_RESOURCE_KINDS); console.warn(`Failed to get resource kinds: ${kindsShellResult?.stderr}`); + context.apiState = ApiState.ClusterUnreachable; + setVSCodeContext(ContextId.ClusterUnreachable, true); + clusterDataProvider.updateCurrentContextChildNodes(); + refreshResourcesTreeViews(); + redrawhResourcesTreeViews(); return; } @@ -51,7 +70,7 @@ export async function loadAvailableResourceKinds() { .split('\n') .filter(line => line.length).slice(1); - apiResources = new Map(); + context.apiResources = new Map(); lines.map(line => { let cols = line.split(/\s+/); @@ -69,8 +88,19 @@ export async function loadAvailableResourceKinds() { version = group; group = ''; } - apiResources?.set(kind, { plural, group, version }); + context.apiResources?.set(kind, { plural, group, version }); }); console.log('apiResources loaded'); + + context.apiState = ApiState.Loaded; + setVSCodeContext(ContextId.ClusterUnreachable, false); + clusterDataProvider.updateCurrentContextChildNodes(); + + await restartKubeProxy(); + + // give proxy init callbacks time to fire + setTimeout(() => { + refreshResourcesTreeViews(); + }, 100); } diff --git a/src/cli/kubernetes/clusterProvider.ts b/src/cli/kubernetes/clusterProvider.ts index 06a8390c..1f151e4a 100644 --- a/src/cli/kubernetes/clusterProvider.ts +++ b/src/cli/kubernetes/clusterProvider.ts @@ -6,9 +6,9 @@ import { ClusterProvider } from 'types/kubernetes/clusterProvider'; import { ConfigMap, Node } from 'types/kubernetes/kubernetesTypes'; import { TelemetryError } from 'types/telemetryEventNames'; import { parseJson, parseJsonItems } from 'utils/jsonUtils'; -import { getFluxControllers, notAnErrorServerNotRunning } from './kubectlGet'; -import { invokeKubectlCommand } from './kubernetesToolsKubectl'; +import { notAnErrorServerNotRunning } from './kubectlGet'; import { kubeConfig } from './kubernetesConfig'; +import { invokeKubectlCommand } from './kubernetesToolsKubectl'; /** * Try to detect known cluster providers. Returns user selected cluster type if that is set. diff --git a/src/cli/kubernetes/kubectlGet.ts b/src/cli/kubernetes/kubectlGet.ts index 59698ee0..15758741 100644 --- a/src/cli/kubernetes/kubectlGet.ts +++ b/src/cli/kubernetes/kubectlGet.ts @@ -9,12 +9,12 @@ import { HelmRelease } from 'types/flux/helmRelease'; import { HelmRepository } from 'types/flux/helmRepository'; import { Kustomization } from 'types/flux/kustomization'; import { OCIRepository } from 'types/flux/ociRepository'; -import { Deployment, Kind, KubernetesObject, Pod } from 'types/kubernetes/kubernetesTypes'; +import { Deployment, Kind, KubernetesObject, Pod, qualifyToolkitKind } from 'types/kubernetes/kubernetesTypes'; import { TelemetryError } from 'types/telemetryEventNames'; import { parseJson, parseJsonItems } from 'utils/jsonUtils'; -import { invokeKubectlCommand } from './kubernetesToolsKubectl'; -import { getAvailableResourcePlurals } from './apiResources'; import { window } from 'vscode'; +import { getAvailableResourcePlurals } from './apiResources'; +import { invokeKubectlCommand } from './kubernetesToolsKubectl'; /** * RegExp for the Error that should not be sent in telemetry. * Server doesn't have a resource type = when GitOps not enabled @@ -40,17 +40,14 @@ export async function getResource(name: string, namespace: string, kind: string) } export async function getResourcesAllNamespaces(kind: Kind, telemetryError: TelemetryError): Promise { - const t1 = Date.now(); - const list = await k8sList(kind); if(list !== undefined) { - console.log(`k8sList ${kind}`, list.length, ' ∆', Date.now() - t1); return list as T[]; } + let fqKind = qualifyToolkitKind(kind); - const shellResult = await invokeKubectlCommand(`get ${kind} -A -o json`); - + const shellResult = await invokeKubectlCommand(`get ${fqKind} -A -o json`); if (shellResult?.code !== 0) { console.warn(`Failed to \`kubectl get ${kind} -A\`: ${shellResult?.stderr}`); if (shellResult?.stderr && !notAnErrorServerDoesntHaveResourceTypeRegExp.test(shellResult.stderr)) { @@ -60,7 +57,6 @@ export async function getResourcesAllNamespaces(kind } const items = parseJsonItems(shellResult.stdout); - console.log(`kubectl get ${kind}`, items.length, ' ∆', Date.now() - t1); return items as T[]; } @@ -104,12 +100,14 @@ export async function getFluxControllers(context?: string): Promise { +): Promise { // return []; const resourceKinds = getAvailableResourcePlurals(); if (!resourceKinds) { - return []; + return; } const labelNameSelector = `-l ${workload}.toolkit.fluxcd.io/name=${name}`; @@ -140,7 +138,7 @@ export async function getChildrenOfWorkload( if (!shellResult || shellResult.code !== 0) { telemetry.sendError(TelemetryError.FAILED_TO_GET_CHILDREN_OF_A_WORKLOAD); window.showErrorMessage(`Failed to get ${workload} created resources: ${shellResult?.stderr}`); - return []; + return; } return parseJsonItems(shellResult.stdout); @@ -178,4 +176,3 @@ export async function getPodsOfADeployment(name = '', namespace = ''): Promise

{ - console.log('~proxy exit', p.pid, code); if(proxyProc?.pid === p.pid) { stopKubeProxy(); } }); p.on('error', err => { - console.log('~proxy error', p.pid, err); p.kill(); }); p.stdout?.on('data', (data: string) => { - console.log(`~proxy ${p.pid} STDOUT: ${data}`); if(data.includes('Starting to serve on')) { const port = parseInt(data.split(':')[1].trim()); - const proxyKc = createProxyConfig(port); - console.log('kubeproxy config ready'); - - createK8sClients(proxyKc); + kubeProxyConfig = createProxyConfig(port); + createK8sClients(); } }); p.stderr?.on('data', (data: string) => { - console.log(`~proxy ${p.pid} STDERR: ${data}`); + console.warn(`kubectl proxy ${p.pid} STDERR: ${data}`); p.kill(); }); } @@ -67,16 +60,13 @@ function procListen(p: ChildProcess) { export async function stopKubeProxy() { if(proxyProc) { if(!proxyProc.killed) { - console.log(`~proxy.kill() ${proxyProc.pid}`); proxyProc.kill(); } proxyProc = undefined; + kubeProxyConfig = undefined; destroyK8sClients(); - // isConnecting = false; - console.log('stopped kube proxy'); } - } export async function restartKubeProxy() { diff --git a/src/cli/kubernetes/kubernetesConfig.ts b/src/cli/kubernetes/kubernetesConfig.ts index ef7112dc..38f9db5d 100644 --- a/src/cli/kubernetes/kubernetesConfig.ts +++ b/src/cli/kubernetes/kubernetesConfig.ts @@ -4,72 +4,80 @@ import { window } from 'vscode'; import * as k8s from '@kubernetes/client-node'; import { ActionOnInvalid } from '@kubernetes/client-node/dist/config_types'; import { shellCodeError } from 'cli/shell/exec'; -import { refreshAllTreeViews } from 'commands/refreshTreeViews'; import { setVSCodeContext, telemetry } from 'extension'; import { ContextId } from 'types/extensionIds'; import { TelemetryError } from 'types/telemetryEventNames'; -import { refreshClustersTreeView } from 'ui/treeviews/treeViews'; +import { reloadClustersTreeView } from 'ui/treeviews/treeViews'; import { kcContextsListChanged, kcCurrentContextChanged, kcTextChanged } from 'utils/kubeConfigCompare'; -import { loadAvailableResourceKinds } from './apiResources'; -import { restartKubeProxy } from './kubectlProxy'; +import { loadAvailableResourceKinds as loadApiResources } from './apiResources'; import { loadKubeConfigPath } from './kubernetesConfigWatcher'; import { invokeKubectlCommand } from './kubernetesToolsKubectl'; +export enum KubeConfigState { + /* effectively KubeConfigState.Loading has meaning obnly at the extension init + * because subsequent kubeconfig updates are swapped-in atomically. but we keep track of it anyway + */ + Loading, + Loaded, + Failed, + NoContextSelected, +} + +export let kubeConfigState: KubeConfigState = KubeConfigState.Loading; -export const kubeConfig: k8s.KubeConfig = new k8s.KubeConfig(); +export const kubeConfig: k8s.KubeConfig = new k8s.KubeConfig(); // reload the kubeconfig via kubernetes-tools. fire events if things have changed export async function syncKubeConfig(forceReloadResourceKinds = false) { - console.log('syncKubeConfig'); + kubeConfigState = KubeConfigState.Loading; const configShellResult = await invokeKubectlCommand('config view'); if (configShellResult?.code !== 0) { telemetry.sendError(TelemetryError.FAILED_TO_GET_KUBECTL_CONFIG); const path = await loadKubeConfigPath(); window.showErrorMessage(`Failed to load kubeconfig: ${path} ${shellCodeError(configShellResult)}`); + kubeConfigState = KubeConfigState.Failed; return; } const newKubeConfig = new k8s.KubeConfig(); newKubeConfig.loadFromString(configShellResult.stdout, {onInvalidEntry: ActionOnInvalid.FILTER}); + kubeConfigState = KubeConfigState.Loaded; if (kcTextChanged(kubeConfig, newKubeConfig)) { - const contextsListChanged = kcContextsListChanged(kubeConfig, newKubeConfig); - const contextChanged = kcCurrentContextChanged(kubeConfig, newKubeConfig); - - // load the changed kubeconfig globally so that the following code use the new config - kubeConfig.loadFromString(configShellResult.stdout); - - if(contextsListChanged) { - refreshClustersTreeView(); - } - - if(contextChanged || forceReloadResourceKinds) { - loadAvailableResourceKinds(); - } - - if(contextChanged) { - console.log('currentContext changed', kubeConfig.getCurrentContext()); - vscodeOnCurrentContextChanged(); - await restartKubeProxy(); - // give proxy a chance to start - setTimeout(() => { - refreshAllTreeViews(); - }, 100); - } + await kubeconfigChanged(newKubeConfig, forceReloadResourceKinds); } else if(forceReloadResourceKinds) { - await loadAvailableResourceKinds(); + loadApiResources(); } } -async function vscodeOnCurrentContextChanged() { - setVSCodeContext(ContextId.NoClusterSelected, false); - setVSCodeContext(ContextId.CurrentClusterGitOpsNotEnabled, false); - setVSCodeContext(ContextId.NoSources, false); - setVSCodeContext(ContextId.NoWorkloads, false); - setVSCodeContext(ContextId.FailedToLoadClusterContexts, false); + + +async function kubeconfigChanged(newKubeConfig: k8s.KubeConfig, forceReloadResourceKinds: boolean) { + const contextsListChanged = kcContextsListChanged(kubeConfig, newKubeConfig); + const contextChanged = kcCurrentContextChanged(kubeConfig, newKubeConfig); + + // load the changed kubeconfig globally so that the following code use the new config + kubeConfig.loadFromString(newKubeConfig.exportConfig(), {onInvalidEntry: ActionOnInvalid.FILTER}); + + if(!currentContextExists()) { + kubeConfigState = KubeConfigState.NoContextSelected; + } + + + if (contextChanged) { + setVSCodeContext(ContextId.CurrentClusterGitOpsNotEnabled, false); + setVSCodeContext(ContextId.ClusterUnreachable, false); + } + + if (contextChanged || forceReloadResourceKinds) { + reloadClustersTreeView(); + loadApiResources(); + } else if (contextsListChanged) { + reloadClustersTreeView(); + } } /** @@ -97,3 +105,9 @@ export async function setCurrentContext(contextName: string): Promise context.name === name); +} + diff --git a/src/cli/kubernetes/kubernetesConfigWatcher.ts b/src/cli/kubernetes/kubernetesConfigWatcher.ts index 808ce676..423bfdb1 100644 --- a/src/cli/kubernetes/kubernetesConfigWatcher.ts +++ b/src/cli/kubernetes/kubernetesConfigWatcher.ts @@ -7,7 +7,7 @@ import { syncKubeConfig } from './kubernetesConfig'; let fsWacher: vscode.FileSystemWatcher | undefined; -let kubeConfigPath: string | undefined; +export let kubeConfigPath: string | undefined; export async function loadKubeConfigPath(): Promise { const configuration = await kubernetes.extension.configuration.v1_1; diff --git a/src/cli/kubernetes/kubernetesToolsKubectl.ts b/src/cli/kubernetes/kubernetesToolsKubectl.ts index 11fcd1d4..9dd41fd5 100644 --- a/src/cli/kubernetes/kubernetesToolsKubectl.ts +++ b/src/cli/kubernetes/kubernetesToolsKubectl.ts @@ -47,11 +47,7 @@ export async function invokeKubectlCommand(command: string, printOutput = true): let kubectlShellResult; const commandWithArgs = `kubectl ${command} --request-timeout ${getRequestTimeout()}`; - const t1 = Date.now(); kubectlShellResult = await shell.exec(commandWithArgs); - const t2 = Date.now(); - console.log(`${command} ∆`, t2 - t1); - if(printOutput) { output.send(`> kubectl ${command}`, { diff --git a/src/cli/shell/exec.ts b/src/cli/shell/exec.ts index 42b0315f..0c16fcdb 100644 --- a/src/cli/shell/exec.ts +++ b/src/cli/shell/exec.ts @@ -1,13 +1,16 @@ +// kills them dead even if child processes are not joined correctly +import tkill from 'tree-kill'; +import isRunning from 'is-running'; + import { ChildProcess } from 'child_process'; import * as shelljs from 'shelljs'; import { Progress, ProgressLocation, window, workspace } from 'vscode'; + import { GlobalStateKey } from 'data/globalState'; import { globalState } from 'extension'; import { output } from './output'; -// 🚧 WORK IN PROGRESS. - /** * Ignore `"vs-kubernetes.use-wsl" setting. * Always return false. @@ -193,6 +196,10 @@ export async function execWithOutput( childProcess.on('exit', (code: number) => { output.send('\n', { newline: 'none', revealOutputView: false }); + if(code === null) { + stderr = `exec '${cmd}' timed out.\nSTDERR: ${stderr}`; + } + resolve({ code, stdout, @@ -208,7 +215,10 @@ function execCore(cmd: string, opts: any, callback?: ProcCallback, stdin?: strin if (getUseWsl()) { cmd = `wsl ${cmd}`; } - const proc = shelljs.exec(cmd, opts, (code, stdout, stderr) => resolve({code : code, stdout : stdout, stderr : stderr})); + const proc = shelljs.exec(cmd, opts, (code, stdout, stderr) => { + // console.warn('RESOLVE', cmd, code, stdout, stderr); + resolve({code : code, stdout : stdout, stderr : stderr}); + }); setExecTimeoutKill(proc); if (stdin) { proc.stdin?.end(stdin); @@ -238,7 +248,16 @@ function setExecTimeoutKill(proc: ChildProcess) { setTimeout(() => { if (proc.exitCode === null) { - proc.kill(9); + console.warn('timeout SIGTERM', proc.pid, proc.spawnargs.join(' ')); + // make sure all child processes are killed + tkill(proc.pid, 15); + + setTimeout(() => { + if (isRunning(proc.pid)) { + console.warn('timeout SIGKILL', proc.pid, proc.spawnargs.join(' ')); + tkill(proc.pid, 9); + } + }, 2000); } }, timeoutSeconds * 1000); } diff --git a/src/commands/commands.ts b/src/commands/commands.ts index 4aff56e0..1840d9aa 100644 --- a/src/commands/commands.ts +++ b/src/commands/commands.ts @@ -20,7 +20,7 @@ import { fluxReconcileRepositoryForPath } from './fluxReconcileGitRepositoryForP import { fluxReconcileSourceCommand } from './fluxReconcileSource'; import { fluxReconcileWorkload, fluxReconcileWorkloadWithSource } from './fluxReconcileWorkload'; import { installFluxCli } from './installFluxCli'; -import { openResource } from './openResource'; +import { openResource, openKubeconfig } from './openResource'; import { pullGitRepository } from './pullGitRepository'; import { resume } from './resume'; import { setClusterProvider } from './setClusterProvider'; @@ -82,6 +82,7 @@ export function registerCommands(context: ExtensionContext) { // editor registerCommand(CommandId.EditorOpenResource, openResource); + registerCommand(CommandId.EditorOpenKubeconfig, openKubeconfig); // webview registerCommand(CommandId.ShowLogs, showLogs); diff --git a/src/commands/createFromTemplate.ts b/src/commands/createFromTemplate.ts index 03c2d049..91d9ae63 100644 --- a/src/commands/createFromTemplate.ts +++ b/src/commands/createFromTemplate.ts @@ -1,4 +1,3 @@ -import * as vscode from 'vscode'; import { GitOpsTemplateNode } from 'ui/treeviews/nodes/gitOpsTemplateNode'; import { openCreateFromTemplatePanel } from 'ui/webviews/createFromTemplate/openWebview'; diff --git a/src/commands/deleteSource.ts b/src/commands/deleteSource.ts index 934da810..dd35af83 100644 --- a/src/commands/deleteSource.ts +++ b/src/commands/deleteSource.ts @@ -2,6 +2,7 @@ import { window } from 'vscode'; import { AzureClusterProvider, azureTools } from 'cli/azure/azureTools'; import { fluxTools } from 'cli/flux/fluxTools'; +import { kubeConfig } from 'cli/kubernetes/kubernetesConfig'; import { telemetry } from 'extension'; import { failed } from 'types/errorable'; import { FluxSource } from 'types/fluxCliTypes'; @@ -11,8 +12,7 @@ import { BucketNode } from 'ui/treeviews/nodes/source/bucketNode'; import { GitRepositoryNode } from 'ui/treeviews/nodes/source/gitRepositoryNode'; import { HelmRepositoryNode } from 'ui/treeviews/nodes/source/helmRepositoryNode'; import { OCIRepositoryNode } from 'ui/treeviews/nodes/source/ociRepositoryNode'; -import { getCurrentClusterInfo, refreshSourcesTreeView, refreshWorkloadsTreeView } from 'ui/treeviews/treeViews'; -import { kubeConfig } from 'cli/kubernetes/kubernetesConfig'; +import { getCurrentClusterInfo, reloadSourcesTreeView, reloadWorkloadsTreeView } from 'ui/treeviews/treeViews'; /** * Delete a source @@ -54,10 +54,10 @@ export async function deleteSource(sourceNode: GitRepositoryNode | OCIRepository if (currentClusterInfo.result.isAzure) { await azureTools.deleteSource(sourceName, contextName, currentClusterInfo.result.clusterProvider as AzureClusterProvider); - refreshWorkloadsTreeView(); + reloadWorkloadsTreeView(); } else { await fluxTools.delete(sourceType, sourceName, sourceNamespace); } - refreshSourcesTreeView(); + reloadSourcesTreeView(); } diff --git a/src/commands/deleteWorkload.ts b/src/commands/deleteWorkload.ts index 9b9f0133..c908404d 100644 --- a/src/commands/deleteWorkload.ts +++ b/src/commands/deleteWorkload.ts @@ -2,6 +2,7 @@ import { window } from 'vscode'; import { AzureClusterProvider, azureTools } from 'cli/azure/azureTools'; import { fluxTools } from 'cli/flux/fluxTools'; +import { kubeConfig } from 'cli/kubernetes/kubernetesConfig'; import { telemetry } from 'extension'; import { failed } from 'types/errorable'; import { FluxWorkload } from 'types/fluxCliTypes'; @@ -9,8 +10,7 @@ import { Kind } from 'types/kubernetes/kubernetesTypes'; import { TelemetryEvent } from 'types/telemetryEventNames'; import { HelmReleaseNode } from 'ui/treeviews/nodes/workload/helmReleaseNode'; import { KustomizationNode } from 'ui/treeviews/nodes/workload/kustomizationNode'; -import { getCurrentClusterInfo, refreshWorkloadsTreeView } from 'ui/treeviews/treeViews'; -import { kubeConfig } from 'cli/kubernetes/kubernetesConfig'; +import { getCurrentClusterInfo, reloadWorkloadsTreeView } from 'ui/treeviews/treeViews'; /** @@ -73,5 +73,5 @@ export async function deleteWorkload(workloadNode: KustomizationNode | HelmRelea await fluxTools.delete(workloadType, workloadName, workloadNamespace); } - refreshWorkloadsTreeView(); + reloadWorkloadsTreeView(); } diff --git a/src/commands/enableDisableGitOps.ts b/src/commands/enableDisableGitOps.ts index be847ad5..e3746b51 100644 --- a/src/commands/enableDisableGitOps.ts +++ b/src/commands/enableDisableGitOps.ts @@ -3,14 +3,12 @@ import { window } from 'vscode'; import { azureTools, isAzureProvider } from 'cli/azure/azureTools'; import { fluxTools } from 'cli/flux/fluxTools'; import { detectClusterProvider } from 'cli/kubernetes/clusterProvider'; +import { kubeConfig } from 'cli/kubernetes/kubernetesConfig'; +import { refreshAllTreeViewsCommand } from 'commands/refreshTreeViews'; import { disableConfirmations, telemetry } from 'extension'; -import { failed } from 'types/errorable'; import { ClusterProvider } from 'types/kubernetes/clusterProvider'; import { TelemetryEvent } from 'types/telemetryEventNames'; import { ClusterNode } from 'ui/treeviews/nodes/cluster/clusterNode'; -import { getCurrentClusterInfo } from 'ui/treeviews/treeViews'; -import { refreshAllTreeViewsCommand } from 'commands/refreshTreeViews'; -import { kubeConfig } from 'cli/kubernetes/kubernetesConfig'; diff --git a/src/commands/expandAll.ts b/src/commands/expandAll.ts index 6f9b660b..87a5e096 100644 --- a/src/commands/expandAll.ts +++ b/src/commands/expandAll.ts @@ -1,13 +1,9 @@ -import { TreeNode } from 'ui/treeviews/nodes/treeNode'; -import { sourceDataProvider, sourceTreeView, workloadDataProvider } from 'ui/treeviews/treeViews'; -import { TreeItemCollapsibleState } from 'vscode'; +import { sourceDataProvider, workloadDataProvider } from 'ui/treeviews/treeViews'; export async function expandAllSources() { - sourceDataProvider.expandNewTree = true; - sourceDataProvider.refresh(); + sourceDataProvider.expandAll(); } export async function expandAllWorkloads() { - workloadDataProvider.expandNewTree = true; - workloadDataProvider.refresh(); + workloadDataProvider.expandAll(); } diff --git a/src/commands/openResource.ts b/src/commands/openResource.ts index 58512926..7a3aa461 100644 --- a/src/commands/openResource.ts +++ b/src/commands/openResource.ts @@ -1,5 +1,6 @@ import { Uri, window, workspace } from 'vscode'; +import { kubeConfigPath } from 'cli/kubernetes/kubernetesConfigWatcher'; import { telemetry } from 'extension'; import { TelemetryError } from 'types/telemetryEventNames'; @@ -18,3 +19,9 @@ export async function openResource(uri: Uri): Promise { telemetry.sendError(TelemetryError.FAILED_TO_OPEN_RESOURCE); }); } + +export async function openKubeconfig() { + if (kubeConfigPath) { + openResource(Uri.file(kubeConfigPath)); + } +} diff --git a/src/commands/refreshTreeViews.ts b/src/commands/refreshTreeViews.ts index 53f6e25b..6fd2b562 100644 --- a/src/commands/refreshTreeViews.ts +++ b/src/commands/refreshTreeViews.ts @@ -1,5 +1,5 @@ import { syncKubeConfig } from 'cli/kubernetes/kubernetesConfig'; -import { refreshClustersTreeView, refreshSourcesTreeView, refreshTemplatesTreeView, refreshWorkloadsTreeView } from '../ui/treeviews/treeViews'; +import { reloadClustersTreeView, reloadSourcesTreeView, reloadTemplatesTreeView, reloadWorkloadsTreeView, sourceDataProvider, templateDateProvider, workloadDataProvider } from '../ui/treeviews/treeViews'; /** * Clicked button on the cluster tree view @@ -11,9 +11,8 @@ export async function refreshAllTreeViewsCommand() { } export async function refreshAllTreeViews() { - console.log('refreshAllTreeViews'); - refreshClustersTreeView(); + reloadClustersTreeView(); refreshResourcesTreeViews(); } @@ -26,7 +25,13 @@ export function refreshResourcesTreeViewsCommand() { } export function refreshResourcesTreeViews() { - refreshSourcesTreeView(); - refreshWorkloadsTreeView(); - refreshTemplatesTreeView(); + reloadSourcesTreeView(); + reloadWorkloadsTreeView(); + reloadTemplatesTreeView(); +} + +export function redrawhResourcesTreeViews() { + sourceDataProvider.redraw(); + workloadDataProvider.redraw(); + templateDateProvider.redraw(); } diff --git a/src/commands/resume.ts b/src/commands/resume.ts index 371e91f5..023c82d4 100644 --- a/src/commands/resume.ts +++ b/src/commands/resume.ts @@ -10,7 +10,7 @@ import { HelmRepositoryNode } from 'ui/treeviews/nodes/source/helmRepositoryNode import { OCIRepositoryNode } from 'ui/treeviews/nodes/source/ociRepositoryNode'; import { HelmReleaseNode } from 'ui/treeviews/nodes/workload/helmReleaseNode'; import { KustomizationNode } from 'ui/treeviews/nodes/workload/kustomizationNode'; -import { getCurrentClusterInfo, refreshSourcesTreeView, refreshWorkloadsTreeView } from 'ui/treeviews/treeViews'; +import { getCurrentClusterInfo, reloadSourcesTreeView, reloadWorkloadsTreeView } from 'ui/treeviews/treeViews'; /** * Resume source or workload reconciliation and refresh its Tree View. @@ -47,11 +47,11 @@ export async function resume(node: GitRepositoryNode | HelmReleaseNode | HelmRep } if (node instanceof GitRepositoryNode || node instanceof OCIRepositoryNode || node instanceof HelmRepositoryNode) { - refreshSourcesTreeView(); + reloadSourcesTreeView(); if (currentClusterInfo.result.isAzure) { - refreshWorkloadsTreeView(); + reloadWorkloadsTreeView(); } } else { - refreshWorkloadsTreeView(); + reloadWorkloadsTreeView(); } } diff --git a/src/commands/setClusterProvider.ts b/src/commands/setClusterProvider.ts index 448c47da..75a12ecb 100644 --- a/src/commands/setClusterProvider.ts +++ b/src/commands/setClusterProvider.ts @@ -5,7 +5,7 @@ import { ClusterMetadata } from 'data/globalState'; import { globalState } from 'extension'; import { KnownClusterProviders, knownClusterProviders } from 'types/kubernetes/clusterProvider'; import { ClusterNode } from 'ui/treeviews/nodes/cluster/clusterNode'; -import { refreshClustersTreeView } from 'ui/treeviews/treeViews'; +import { reloadClustersTreeView } from 'ui/treeviews/treeViews'; import { refreshAllTreeViews } from './refreshTreeViews'; export async function setClusterProvider(clusterNode: ClusterNode) { @@ -37,7 +37,7 @@ export async function setClusterProvider(clusterNode: ClusterNode) { if(clusterNode.context.name === kubeConfig.getCurrentContext()) { refreshAllTreeViews(); } else { - refreshClustersTreeView(); + reloadClustersTreeView(); } } } diff --git a/src/commands/setCurrentKubernetesContext.ts b/src/commands/setCurrentKubernetesContext.ts index a3ec48d9..311bad0b 100644 --- a/src/commands/setCurrentKubernetesContext.ts +++ b/src/commands/setCurrentKubernetesContext.ts @@ -7,6 +7,6 @@ import { ClusterNode } from 'ui/treeviews/nodes/cluster/clusterNode'; export async function setCurrentKubernetesContext(clusterContext: ClusterNode): Promise { const setContextResult = await setCurrentContext(clusterContext.context.name); if (setContextResult?.isChanged) { - syncKubeConfig(); + await syncKubeConfig(); } } diff --git a/src/commands/suspend.ts b/src/commands/suspend.ts index 13bf797c..26d65cf1 100644 --- a/src/commands/suspend.ts +++ b/src/commands/suspend.ts @@ -1,16 +1,16 @@ import { window } from 'vscode'; -import { AzureClusterProvider, azureTools, isAzureProvider } from 'cli/azure/azureTools'; -import { failed } from 'types/errorable'; +import { AzureClusterProvider, azureTools } from 'cli/azure/azureTools'; import { fluxTools } from 'cli/flux/fluxTools'; +import { kubeConfig } from 'cli/kubernetes/kubernetesConfig'; +import { failed } from 'types/errorable'; import { FluxSource, FluxWorkload } from 'types/fluxCliTypes'; import { GitRepositoryNode } from 'ui/treeviews/nodes/source/gitRepositoryNode'; -import { HelmReleaseNode } from 'ui/treeviews/nodes/workload/helmReleaseNode'; import { HelmRepositoryNode } from 'ui/treeviews/nodes/source/helmRepositoryNode'; -import { KustomizationNode } from 'ui/treeviews/nodes/workload/kustomizationNode'; import { OCIRepositoryNode } from 'ui/treeviews/nodes/source/ociRepositoryNode'; -import { getCurrentClusterInfo, refreshSourcesTreeView, refreshWorkloadsTreeView } from 'ui/treeviews/treeViews'; -import { kubeConfig } from 'cli/kubernetes/kubernetesConfig'; +import { HelmReleaseNode } from 'ui/treeviews/nodes/workload/helmReleaseNode'; +import { KustomizationNode } from 'ui/treeviews/nodes/workload/kustomizationNode'; +import { getCurrentClusterInfo, reloadSourcesTreeView, reloadWorkloadsTreeView } from 'ui/treeviews/treeViews'; /** * Suspend source or workload reconciliation and refresh its Tree View. @@ -49,11 +49,11 @@ export async function suspend(node: GitRepositoryNode | HelmReleaseNode | Kustom } if (node instanceof GitRepositoryNode || node instanceof OCIRepositoryNode || node instanceof HelmRepositoryNode) { - refreshSourcesTreeView(); + reloadSourcesTreeView(); if (currentClusterInfo.result.isAzure) { - refreshWorkloadsTreeView(); + reloadWorkloadsTreeView(); } } else { - refreshWorkloadsTreeView(); + reloadWorkloadsTreeView(); } } diff --git a/src/data/contextData.ts b/src/data/contextData.ts new file mode 100644 index 00000000..a987a52b --- /dev/null +++ b/src/data/contextData.ts @@ -0,0 +1,64 @@ +import { kubeConfig } from 'cli/kubernetes/kubernetesConfig'; +import { Kind } from 'types/kubernetes/kubernetesTypes'; +import { TreeNode } from 'ui/treeviews/nodes/treeNode'; +import { TreeItemCollapsibleState } from 'vscode'; +import { ApiState, KindApiParams } from '../cli/kubernetes/apiResources'; + + +export class ContextData { + public viewData: { [key: string]: ViewData; }; + public contextName = ''; + public apiState = ApiState.Loading; + // Current cluster supported kubernetes resource kinds. + public apiResources: Map | undefined; + + constructor(contextName: string) { + this.contextName = contextName; + this.viewData = { + 'source': new ViewData(), + 'workload': new ViewData(), + 'template': new ViewData(), + }; + } + +} + +const contextDatas = new Map(); + +export function currentContextData() { + let currentData = contextDatas.get(kubeConfig.currentContext); + if (!currentData) { + currentData = new ContextData(kubeConfig.currentContext); + contextDatas.set(kubeConfig.currentContext, currentData); + } + return currentData; +} + +export class ViewData { + public nodes: TreeNode[] = []; + public collapsibleStates = new Map(); + public loading = false; + + saveCollapsibleStates() { + this.collapsibleStates.clear(); + + for (const node of this.nodes) { + const name = node.resource?.metadata?.name; + if (name) { + this.collapsibleStates.set(name, node.collapsibleState || TreeItemCollapsibleState.Collapsed); + } + } + } + + loadCollapsibleStates() { + for (const node of this.nodes) { + const name = node.resource?.metadata?.name; + if (name) { + const state = this.collapsibleStates.get(name); + if (state) { + node.collapsibleState = state; + } + } + } + } +} diff --git a/src/extension.ts b/src/extension.ts index 5d454d4b..223de489 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -3,17 +3,16 @@ import { commands, ExtensionContext, ExtensionMode, window, workspace } from 'vs import { kubeProxyKeepAlive, stopKubeProxy } from 'cli/kubernetes/kubectlProxy'; import { syncKubeConfig } from 'cli/kubernetes/kubernetesConfig'; import { initKubeConfigWatcher } from 'cli/kubernetes/kubernetesConfigWatcher'; -import { checkFluxPrerequisites, checkWGEVersion } from './cli/checkVersions'; +import { checkWGEVersion } from './cli/checkVersions'; import * as shell from './cli/shell/exec'; import { registerCommands } from './commands/commands'; import { getExtensionVersion } from './commands/showInstalledVersions'; import { showNewUserGuide } from './commands/showNewUserGuide'; import { GlobalState, GlobalStateKey } from './data/globalState'; import { Telemetry } from './data/telemetry'; -import { succeeded } from './types/errorable'; import { CommandId, ContextId, GitOpsExtensionConstants } from './types/extensionIds'; import { TelemetryEvent } from './types/telemetryEventNames'; -import { promptToInstallFlux } from './ui/promptToInstallFlux'; +import { checkInstalledFluxVersion } from './ui/promptToInstallFlux'; import { statusBar } from './ui/statusBar'; import { clusterDataProvider, createTreeViews, sourceDataProvider, workloadDataProvider } from './ui/treeviews/treeViews'; @@ -71,8 +70,8 @@ export async function activate(context: ExtensionContext) { } - // show error notification if flux is not installed - checkFluxVersions(); + // check version and show 'Install Flux?' dialog if flux is not installed + checkInstalledFluxVersion(); checkWGEVersion(); @@ -87,31 +86,15 @@ export async function activate(context: ExtensionContext) { return api; } -async function checkFluxVersions() { - const fluxFoundResult = await promptToInstallFlux(); - if (succeeded(fluxFoundResult)) { - // check flux prerequisites - checkFluxPrerequisites(); - } -} - async function initData() { - // load kubeconfig (could hang w) - // syncKubeConfig(true).then(() => { - // initKubeConfigWatcher(); - // }); - syncKubeConfig(true); initKubeConfigWatcher(); kubeProxyKeepAlive(); - // schedule load start for tree view data for the event loop - // then k8s proxy client is more likely to be ready - // to avoid using the slower kubectl client - setTimeout(() => { - createTreeViews(); - }, 100); - + // wait for kubectl proxy to start for faster initial tree view loading + // setTimeout(() => { + createTreeViews(); + // }, 200); } function listenExtensionConfigChanged() { @@ -121,7 +104,6 @@ function listenExtensionConfigChanged() { } const selected = await window.showInformationMessage('Configuration changed. Reload VS Code to apply?', 'Reload'); - console.log(e); if(selected === 'Reload') { await commands.executeCommand(CommandId.VSCodeReload); } @@ -132,6 +114,15 @@ export function enabledWGE(): boolean { return workspace.getConfiguration('gitops').get('weaveGitopsEnterprise') || false; } +export function enabledFluxChecks(): boolean { + return workspace.getConfiguration('gitops').get('doFluxCheck') || false; + +} + +export function suppressDebugMessages(): boolean { + return workspace.getConfiguration('gitops').get('suppressDebugMessages') || false; +} + /** * Called when extension is deactivated. diff --git a/src/index.d.ts b/src/index.d.ts index ab4ef6fb..97683641 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -1,3 +1,4 @@ declare module 'tinytim'; declare module 'shell-escape-tag'; declare module 'lite-deep-equal'; +declare module 'tree-kill'; diff --git a/src/k8s/client.ts b/src/k8s/client.ts index a27f9cc2..3e004c9e 100644 --- a/src/k8s/client.ts +++ b/src/k8s/client.ts @@ -1,15 +1,19 @@ import * as k8s from '@kubernetes/client-node'; import { createInformers, destroyInformers } from './informers'; +import { kubeProxyConfig } from 'cli/kubernetes/kubectlProxy'; export let k8sCoreApi: k8s.CoreV1Api | undefined; export let k8sCustomApi: k8s.CustomObjectsApi | undefined; -export function createK8sClients(kc: k8s.KubeConfig) { +export function createK8sClients() { destroyK8sClients(); - k8sCoreApi = kc.makeApiClient(k8s.CoreV1Api); - k8sCustomApi = kc.makeApiClient(k8s.CustomObjectsApi); - createInformers(kc); + if(kubeProxyConfig) { + k8sCoreApi = kubeProxyConfig.makeApiClient(k8s.CoreV1Api); + k8sCustomApi = kubeProxyConfig.makeApiClient(k8s.CustomObjectsApi); + + createInformers(kubeProxyConfig); + } } export function destroyK8sClients() { diff --git a/src/k8s/createKubeProxyConfig.ts b/src/k8s/createKubeProxyConfig.ts index f923f5f8..01e44213 100644 --- a/src/k8s/createKubeProxyConfig.ts +++ b/src/k8s/createKubeProxyConfig.ts @@ -1,5 +1,4 @@ import * as k8s from '@kubernetes/client-node'; -import { ActionOnInvalid } from '@kubernetes/client-node/dist/config_types'; import { kubeConfig } from 'cli/kubernetes/kubernetesConfig'; @@ -9,7 +8,11 @@ export function createProxyConfig(port: number) { server: `http://127.0.0.1:${port}`, }; - const user = kubeConfig.getCurrentUser(); + const user = {...kubeConfig.getCurrentUser()}; + if(user) { + user['exec'] = undefined; + } + const context = { name: kubeConfig.getCurrentContext(), diff --git a/src/k8s/informers.ts b/src/k8s/informers.ts index bb1909cd..51dd31ab 100644 --- a/src/k8s/informers.ts +++ b/src/k8s/informers.ts @@ -18,6 +18,7 @@ export function createInformers(kc: k8s.KubeConfig) { FluxWorkloadKinds.forEach(kind => { createInformer(kc, workloadDataProvider, kind); }); + } export function destroyInformers() { @@ -58,6 +59,7 @@ async function createInformer(kc: k8s.KubeConfig, receiver: KubernetesObjectData function registerInformerEvents(informer: k8s.Informer, receiver: KubernetesObjectDataProvider) { informer?.on('add', (obj: KubernetesObject) => { + receiver.add(obj); }); @@ -70,7 +72,6 @@ function registerInformerEvents(informer: k8s.Informer, receiv }); informer?.on('error', (err: Error) => { - console.log('*- informer Error', err); destroyInformers(); }); } diff --git a/src/k8s/list.ts b/src/k8s/list.ts index 5ec4579a..7464e5ac 100644 --- a/src/k8s/list.ts +++ b/src/k8s/list.ts @@ -6,12 +6,10 @@ import { k8sCoreApi, k8sCustomApi } from './client'; export async function k8sList(kind: Kind): Promise { const api = getAPIParams(kind); if(!api) { - console.log('k8sList no apiParams'); return; } if(!k8sCustomApi) { - console.log('k8sList no k8sCustomApi'); return; } @@ -26,14 +24,11 @@ export async function k8sList(kind: Kind): Promise { if(!k8sCoreApi) { - console.log('k8sList no k8sCustomApi'); return; } try { - const t1 = Date.now(); const result = await k8sCoreApi.listNamespace(); - console.log('k8sList Namespace ∆', Date.now() - t1); const kbody = result.body as KubernetesListObject; return kbody.items.map(ns => { diff --git a/src/test/suite/extension.test.ts b/src/test/suite/extension.test.ts index 054d2530..f454de9c 100644 --- a/src/test/suite/extension.test.ts +++ b/src/test/suite/extension.test.ts @@ -69,6 +69,7 @@ suite('Extension Test Suite', () => { // Flux should be uninstalled after tests are done after(async function() { + this.timeout(4000); // default 2000ms is too brief, sometimes takes longer let cluster = await getTreeItem(api.data.clusterTreeViewProvider, currentContext); await vscode.commands.executeCommand('gitops.flux.uninstall', cluster); }); diff --git a/src/types/extensionIds.ts b/src/types/extensionIds.ts index f461f9ca..26aef5f0 100644 --- a/src/types/extensionIds.ts +++ b/src/types/extensionIds.ts @@ -61,6 +61,7 @@ export const enum CommandId { // editor EditorOpenResource = 'gitops.editor.openResource', + EditorOpenKubeconfig = 'gitops.editor.openKubeconfig', // webview ShowLogs = 'gitops.editor.showLogs', @@ -81,17 +82,8 @@ export const enum CommandId { * GitOps context types. */ export const enum ContextId { - NoClusterSelected = 'gitops:noClusterSelected', CurrentClusterGitOpsNotEnabled = 'gitops:currentClusterGitOpsNotEnabled', - - LoadingClusters = 'gitops:loadingClusters', - LoadingSources = 'gitops:loadingSources', - LoadingWorkloads = 'gitops:loadingWorkloads', - - FailedToLoadClusterContexts = 'gitops:failedToLoadClusterContexts', - NoClusters = 'gitops:noClusters', - NoSources = 'gitops:noSources', - NoWorkloads = 'gitops:noWorkloads', + ClusterUnreachable = 'gitops:clusterUnreachable', IsDev = 'gitops:isDev', IsWGE = 'gitops:isWGE', diff --git a/src/types/flux/helmRelease.ts b/src/types/flux/helmRelease.ts index 2a9b11f6..e4837fe5 100644 --- a/src/types/flux/helmRelease.ts +++ b/src/types/flux/helmRelease.ts @@ -1,5 +1,5 @@ import { Condition, Kind, KubernetesJSON, KubernetesObject } from 'types/kubernetes/kubernetesTypes'; -import { DependsOn, KustomizationKubeConfig, Kustomization, NamespacedObjectKindReference } from './kustomization'; +import { DependsOn, Kustomization, KustomizationKubeConfig, NamespacedObjectKindReference } from './kustomization'; /** * Helm release info object. diff --git a/src/types/kubernetes/kubernetesTypes.ts b/src/types/kubernetes/kubernetesTypes.ts index a8906298..df9dcee1 100644 --- a/src/types/kubernetes/kubernetesTypes.ts +++ b/src/types/kubernetes/kubernetesTypes.ts @@ -50,6 +50,20 @@ export const enum Kind { GitOpsTemplate = 'GitOpsTemplate', } +const fullKinds: Record = { + Bucket: 'Buckets.source.toolkit.fluxcd.io', + GitRepository: 'GitRepositories.source.toolkit.fluxcd.io', + OCIRepository: 'OCIRepositories.source.toolkit.fluxcd.io', + HelmRepository: 'HelmRepositories.source.toolkit.fluxcd.io', + HelmRelease: 'HelmReleases.helm.toolkit.fluxcd.io', + Kustomization: 'Kustomizations.kustomize.toolkit.fluxcd.io', + GitOpsTemplate: 'GitOpsTemplates.templates.weave.works', +}; + +export function qualifyToolkitKind(kind: string): string { + return fullKinds[kind] || kind; +} + export const enum SourceKind { Bucket = 'Bucket', @@ -78,5 +92,3 @@ export interface LocalObjectReference { export interface KubernetesJSON { [key: string]: unknown; } - - diff --git a/src/ui/promptToInstallFlux.ts b/src/ui/promptToInstallFlux.ts index 54cdd4cc..fe8ca867 100644 --- a/src/ui/promptToInstallFlux.ts +++ b/src/ui/promptToInstallFlux.ts @@ -9,7 +9,7 @@ import { Errorable, failed } from 'types/errorable'; * (only when flux was not found). */ -export async function promptToInstallFlux(): Promise> { +export async function checkInstalledFluxVersion(): Promise> { const version = await getFluxVersion(); if (failed(version)) { showInstallFluxNotification(); diff --git a/src/ui/treeviews/dataProviders/asyncDataProvider.ts b/src/ui/treeviews/dataProviders/asyncDataProvider.ts new file mode 100644 index 00000000..aae63317 --- /dev/null +++ b/src/ui/treeviews/dataProviders/asyncDataProvider.ts @@ -0,0 +1,83 @@ +import { ApiState } from 'cli/kubernetes/apiResources'; +import { KubeConfigState, kubeConfigState } from 'cli/kubernetes/kubernetesConfig'; +import { ContextData, ViewData, currentContextData } from 'data/contextData'; +import { InfoNode, infoNodes } from 'utils/makeTreeviewInfoNode'; +import { TreeNode } from '../nodes/treeNode'; +import { clusterDataProvider } from '../treeViews'; +import { SimpleDataProvider } from './simpleDataProvider'; + +/**` + * Defines tree view data provider base class for all GitOps tree views. + */ +export class AsyncDataProvider extends SimpleDataProvider{ + get nodes() { + return this.viewData(currentContextData()).nodes; + } + + // child views override this to provide their own view data + protected viewData(contextData: ContextData) { + return new ViewData(); + } + + public currentViewData() { + return this.viewData(currentContextData()); + } + + + // give nodes for vscode to render based on async data loading state + protected async getRootNodes(): Promise { + const context = currentContextData(); + + if(context.apiState === ApiState.Loading) { + return infoNodes(InfoNode.LoadingApi); + } + + if(context.apiState === ApiState.ClusterUnreachable) { + return infoNodes(InfoNode.ClusterUnreachable); + } + + // return empty array so that vscode welcome view with embedded link "Enable Gitops ..." is shown + if(clusterDataProvider.currentContextIsGitOpsNotEnabled()) { + return []; + } + + if (this.currentViewData().loading || kubeConfigState === KubeConfigState.Loading) { + return infoNodes(InfoNode.Loading); + } + + if(this.currentViewData().nodes.length === 0) { + return infoNodes(InfoNode.NoResources); + } + + return this.currentViewData().nodes; + } + + + public async reload() { + const context = currentContextData(); + const viewData = this.viewData(context); + + + if(viewData.loading) { + return; + } + + viewData.loading = true; + viewData.saveCollapsibleStates(); + if(viewData.nodes.length === 0) { + // show Loading... if no nodes yet + this.redraw(); + } + viewData.nodes = []; // clear them first for good luck + viewData.nodes = await this.loadRootNodes(); + viewData.loadCollapsibleStates(); + viewData.loading = false; + console.log(`finish loading ${context.contextName} ${this.constructor.name}`); + + this.redraw(); + } + + +} + + diff --git a/src/ui/treeviews/dataProviders/clusterDataProvider.ts b/src/ui/treeviews/dataProviders/clusterDataProvider.ts index 08cf9221..96cf6e00 100644 --- a/src/ui/treeviews/dataProviders/clusterDataProvider.ts +++ b/src/ui/treeviews/dataProviders/clusterDataProvider.ts @@ -1,79 +1,35 @@ -import { fluxTools } from 'cli/flux/fluxTools'; -import { getFluxControllers } from 'cli/kubernetes/kubectlGet'; import { kubeConfig } from 'cli/kubernetes/kubernetesConfig'; -import { setVSCodeContext } from 'extension'; -import { ContextId } from 'types/extensionIds'; import { statusBar } from 'ui/statusBar'; -import { TreeItem } from 'vscode'; -import { ClusterDeploymentNode } from '../nodes/cluster/clusterDeploymentNode'; import { ClusterNode } from '../nodes/cluster/clusterNode'; -import { TreeNode } from '../nodes/treeNode'; -import { refreshClustersTreeView, revealClusterNode } from '../treeViews'; -import { DataProvider } from './dataProvider'; +import { SimpleDataProvider } from './simpleDataProvider'; /** * Defines Clusters data provider for loading configured kubernetes clusters * and contexts in GitOps Clusters tree view. */ -export class ClusterDataProvider extends DataProvider { - - /** - * Keep a reference to all the nodes in the Clusters Tree View. - */ - private clusterNodes: ClusterNode[] = []; +export class ClusterDataProvider extends SimpleDataProvider { public getCurrentClusterNode(): ClusterNode | undefined { - return this.clusterNodes.find(c => c.context.name === kubeConfig?.getCurrentContext()); + const nodes = this.nodes as ClusterNode[]; + return nodes.find(c => c.context.name === kubeConfig?.getCurrentContext()); } - public refreshCurrentNode() { - this.refresh(this.getCurrentClusterNode()); + public redrawCurrentNode() { + this.redraw(this.getCurrentClusterNode()); } - /** - * Check if the cluster node exists or not. - */ - public includesTreeNode(treeItem: TreeItem, clusterNodes: TreeNode[] = this.clusterNodes) { - for (const clusterNode of clusterNodes) { - if (treeItem === clusterNode) { - return true; - } - const includesInNested = this.includesTreeNode(treeItem, clusterNode.children); - if (includesInNested) { - return true; - } - } - return false; - } /** * Creates Clusters tree view items from local kubernetes config. */ - async buildTree(): Promise { - console.log('started cluster buildTree'); - - const t1 = Date.now(); - - setVSCodeContext(ContextId.FailedToLoadClusterContexts, false); - setVSCodeContext(ContextId.NoClusters, false); - setVSCodeContext(ContextId.LoadingClusters, true); + async loadRootNodes() { statusBar.startLoadingTree(); - this.clusterNodes = []; - - if (!kubeConfig) { - setVSCodeContext(ContextId.NoClusters, false); - setVSCodeContext(ContextId.FailedToLoadClusterContexts, true); - setVSCodeContext(ContextId.LoadingClusters, false); - statusBar.stopLoadingTree(); - return []; - } - - const clusterNodes: ClusterNode[] = []; + let currentContextTreeItem: ClusterNode | undefined; if (kubeConfig.getContexts().length === 0) { - setVSCodeContext(ContextId.NoClusters, true); + statusBar.stopLoadingTree(); return []; } @@ -86,52 +42,22 @@ export class ClusterDataProvider extends DataProvider { clusterNodes.push(clusterNode); } - // Update async status of the deployments (flux commands take a while to run) - currentContextTreeItem?.updateNodeContext(); - this.updateDeploymentStatus(currentContextTreeItem); - - statusBar.stopLoadingTree(); - setVSCodeContext(ContextId.LoadingClusters, false); - this.clusterNodes = clusterNodes; - const t2 = Date.now(); - console.log('cluster buildTree ∆', t2 - t1); return clusterNodes; } - /** - * Update deployment status for flux controllers. - * Get status from running flux commands instead of kubectl. - */ - async updateDeploymentStatus(clusterNode?: ClusterNode) { - if (!clusterNode || clusterNode.children.length === 0) { - return; - } - const fluxCheckResult = await fluxTools.check(clusterNode.context.name); - if (!fluxCheckResult) { - return; - } - - // Match controllers fetched with flux with controllers - // fetched with kubectl and update tree nodes. - for (const clusterController of (clusterNode.children as ClusterDeploymentNode[])) { - for (const controller of fluxCheckResult.controllers) { - const clusterControllerName = clusterController.resource.metadata.name?.trim(); - const deploymentName = controller.name.trim(); + public updateCurrentContextChildNodes() { + const currentContextTreeItem = this.getCurrentClusterNode(); + currentContextTreeItem?.updateNodeChildren(); + } - if (clusterControllerName === deploymentName) { - clusterController.description = controller.status; - if (controller.success) { - clusterController.setStatus('success'); - } else { - clusterController.setStatus('failure'); - } - } - } - refreshClustersTreeView(clusterController); + public currentContextIsGitOpsNotEnabled() { + const node = this.getCurrentClusterNode(); + // undefined is not false + if(node && typeof node.isGitOpsEnabled === 'boolean') { + return !node.isGitOpsEnabled; } + return false; } - - } diff --git a/src/ui/treeviews/dataProviders/dataProvider.ts b/src/ui/treeviews/dataProviders/dataProvider.ts deleted file mode 100644 index 4de95e8d..00000000 --- a/src/ui/treeviews/dataProviders/dataProvider.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { Event, EventEmitter, TreeDataProvider, TreeItem } from 'vscode'; -import { TreeNode } from '../nodes/treeNode'; - -/** - * Defines tree view data provider base class for all GitOps tree views. - */ -export class DataProvider implements TreeDataProvider { - protected treeItems: TreeItem[] | null = null; - protected _onDidChangeTreeData: EventEmitter = new EventEmitter(); - readonly onDidChangeTreeData: Event = this._onDidChangeTreeData.event; - - - public expandNewTree = false; - - /** - * Reloads tree view item and its children. - * @param treeItem Tree item to refresh. - */ - public async refresh(treeItem?: TreeItem) { - if (!treeItem) { - // Only clear all root nodes when no node was passed - this.treeItems = null; - } - this._onDidChangeTreeData.fire(treeItem); - } - - /** - * Gets tree view item for the specified tree element. - * @param element Tree element. - * @returns Tree view item. - */ - public getTreeItem(element: TreeItem): TreeItem { - return element; - } - - /** - * Gets tree element parent. - * @param element Tree item to get parent for. - * @returns Parent tree item or null for the top level nodes. - */ - public getParent(element: TreeItem): TreeItem | null { - if (element instanceof TreeNode && element.parent) { - return element.parent; - } - return null; - } - - /** - * Gets children for the specified tree element. - * Creates new tree view items for the root node. - * @param element The tree element to get children for. - * @returns Tree element children or empty array. - */ - public async getChildren(element?: TreeItem): Promise { - if (!this.treeItems) { - this.treeItems = await this.buildTree(); - } - - if (element instanceof TreeNode) { - return element.children; - } - - if (!element && this.treeItems) { - return this.treeItems; - } - - return []; - } - - /** - * Creates initial tree view items collection. - * @returns - */ - buildTree(): Promise { - return Promise.resolve([]); - } - -} diff --git a/src/ui/treeviews/dataProviders/documentationDataProvider.ts b/src/ui/treeviews/dataProviders/documentationDataProvider.ts index 2870220b..7eefb437 100644 --- a/src/ui/treeviews/dataProviders/documentationDataProvider.ts +++ b/src/ui/treeviews/dataProviders/documentationDataProvider.ts @@ -1,16 +1,20 @@ import { documentationLinks } from '../documentationConfig'; import { DocumentationNode } from '../nodes/documentationNode'; -import { DataProvider } from './dataProvider'; +import { SimpleDataProvider } from './simpleDataProvider'; /** * Defines data provider for Documentation tree view. */ -export class DocumentationDataProvider extends DataProvider { +export class DocumentationDataProvider extends SimpleDataProvider { + + protected async getRootNodes() { + return this.nodes; + } /** * Creates documentation tree view from documenation links config. */ - async buildTree(): Promise { + async loadRootNodes() { const treeNodes: DocumentationNode[] = []; for (const link of documentationLinks) { diff --git a/src/ui/treeviews/dataProviders/kubernetesObjectDataProvider.ts b/src/ui/treeviews/dataProviders/kubernetesObjectDataProvider.ts index a48cb031..36a3ddd9 100644 --- a/src/ui/treeviews/dataProviders/kubernetesObjectDataProvider.ts +++ b/src/ui/treeviews/dataProviders/kubernetesObjectDataProvider.ts @@ -1,18 +1,19 @@ import { getNamespace } from 'cli/kubernetes/kubectlGetNamespace'; +import { currentContextData } from 'data/contextData'; import { GitRepository } from 'types/flux/gitRepository'; -import { KubernetesObject, Namespace } from 'types/kubernetes/kubernetesTypes'; +import { KubernetesObject } from 'types/kubernetes/kubernetesTypes'; +import { groupNodesByNamespace, sortNodes } from 'utils/treeNodeUtils'; import { NamespaceNode } from '../nodes/namespaceNode'; import { GitRepositoryNode } from '../nodes/source/gitRepositoryNode'; -import { DataProvider } from './dataProvider'; -import { sortNodes } from 'utils/treeNodeUtils'; +import { TreeNode } from '../nodes/treeNode'; +import { AsyncDataProvider } from './asyncDataProvider'; /** * Superclass for data providers that group objects by namespace: Source and Workload data providers */ -export abstract class KubernetesObjectDataProvider extends DataProvider { - +export abstract class KubernetesObjectDataProvider extends AsyncDataProvider { public namespaceNodeTreeItems(): NamespaceNode[] { - return (this.treeItems?.filter(node => node instanceof NamespaceNode) as NamespaceNode[] || []); + return (this.nodes?.filter(node => node instanceof NamespaceNode) as NamespaceNode[] || []); } private findNamespaceNode(nsName?: string): NamespaceNode | undefined { @@ -28,8 +29,6 @@ export abstract class KubernetesObjectDataProvider extends DataProvider { } public async add(object: KubernetesObject) { - console.log('add', object); - if(!object.metadata?.namespace) { return; } @@ -41,24 +40,26 @@ export abstract class KubernetesObjectDataProvider extends DataProvider { return; } namespaceNode = new NamespaceNode(ns); - this.treeItems?.push(namespaceNode); - sortNodes(this.treeItems); + this.nodes?.push(namespaceNode); + sortNodes(this.nodes); namespaceNode.expand(); - this._onDidChangeTreeData.fire(undefined); + this.redraw(); } if(namespaceNode.findChildByResource(object)) { this.update(object); + namespaceNode.updateLabel(); + this.redraw(namespaceNode); return; } const resourceNode = new GitRepositoryNode(object as GitRepository); namespaceNode.addChild(resourceNode); sortNodes(namespaceNode.children); - namespaceNode.updateLabel(); - this._onDidChangeTreeData.fire(namespaceNode); + namespaceNode.updateLabel(); + this.redraw(namespaceNode); } public update(object: KubernetesObject) { @@ -71,13 +72,12 @@ export abstract class KubernetesObjectDataProvider extends DataProvider { if(node && node.resource) { node.resource = object; node.updateStatus(); - this._onDidChangeTreeData.fire(node); + namespaceNode.updateLabel(); + this.redraw(namespaceNode); } } public delete(object: KubernetesObject) { - console.log('delete', object); - const namespaceNode = this.findParentNamespaceNode(object); if(!namespaceNode) { return; @@ -89,13 +89,29 @@ export abstract class KubernetesObjectDataProvider extends DataProvider { if(namespaceNode.children.length > 0) { // namespace has other children namespaceNode.updateLabel(); - this._onDidChangeTreeData.fire(namespaceNode); + this.redraw(namespaceNode); } else { // namespace has no more children. should be removed - this.treeItems?.splice(this.treeItems?.indexOf(namespaceNode), 1); - this._onDidChangeTreeData.fire(undefined); + this.nodes?.splice(this.nodes?.indexOf(namespaceNode), 1); + this.redraw(undefined); } } } + async expandAll() { + const resourceNodes: TreeNode[] = []; + + this.nodes.forEach(node => { + if (node instanceof NamespaceNode) { + const children = node.children as TreeNode[]; + resourceNodes.push(...children); + } + }); + + // rebuild top level nodes or the tree will not redraw + const viewData = this.viewData(currentContextData()); + [viewData.nodes] = await groupNodesByNamespace(resourceNodes, true, true); + this.redraw(); + } + } diff --git a/src/ui/treeviews/dataProviders/simpleDataProvider.ts b/src/ui/treeviews/dataProviders/simpleDataProvider.ts new file mode 100644 index 00000000..8a567086 --- /dev/null +++ b/src/ui/treeviews/dataProviders/simpleDataProvider.ts @@ -0,0 +1,93 @@ +import { KubeConfigState, kubeConfigState } from 'cli/kubernetes/kubernetesConfig'; +import { InfoNode, infoNodes } from 'utils/makeTreeviewInfoNode'; +import { Event, EventEmitter, TreeDataProvider, TreeItem } from 'vscode'; +import { TreeNode } from '../nodes/treeNode'; + + +/**` + * Defines tree view data provider base class for all GitOps tree views. + */ +export class SimpleDataProvider implements TreeDataProvider { + private _nodes: TreeNode[] = []; + + get nodes() { + return this._nodes; + } + + + protected loading = false; + + protected _onDidChangeTreeData: EventEmitter = new EventEmitter(); + readonly onDidChangeTreeData: Event = this._onDidChangeTreeData.event; + + + /* if treeItem is undefined, redraw all tree items */ + public redraw(treeItem?: TreeItem) { + this._onDidChangeTreeData.fire(treeItem); + } + + /** + * Gets tree view item for the specified tree element. + * @param element Tree element. + * @returns Tree view item. + */ + public getTreeItem(element: TreeItem): TreeItem { + return element; + } + + /** + * Gets tree element parent. + * @param element Tree item to get parent for. + * @returns Parent tree item or null for the top level nodes. + */ + public getParent(element: TreeItem): TreeItem | null { + if (element instanceof TreeNode && element.parent) { + return element.parent; + } + return null; + } + + // this is called by vscode treeview redraw to get the nodes to display + public async getChildren(element?: TreeItem): Promise { + if(!element) { + return this.getRootNodes(); + } else if (element instanceof TreeNode) { + return element.children; + } + + return []; + } + + // give nodes for vscode to render based on async data loading state + protected async getRootNodes(): Promise { + if (this.loading || kubeConfigState === KubeConfigState.Loading) { + return infoNodes(InfoNode.Loading); + } + if(this.nodes.length === 0) { + return infoNodes(InfoNode.NoResources); + } + + return this.nodes; + } + + + public async reload() { + if(this.loading) { + return; + } + + this.loading = true; + this._nodes = []; + this._nodes = await this.loadRootNodes(); + this.loading = false; + + this.redraw(); + } + + async loadRootNodes(): Promise { + return []; + } + +} + + diff --git a/src/ui/treeviews/dataProviders/sourceDataProvider.ts b/src/ui/treeviews/dataProviders/sourceDataProvider.ts index a52cb184..54e8600c 100644 --- a/src/ui/treeviews/dataProviders/sourceDataProvider.ts +++ b/src/ui/treeviews/dataProviders/sourceDataProvider.ts @@ -1,17 +1,15 @@ import { getBuckets, getGitRepositories, getHelmRepositories, getOciRepositories } from 'cli/kubernetes/kubectlGet'; -import { setVSCodeContext } from 'extension'; -import { ContextId } from 'types/extensionIds'; +import { getNamespaces } from 'cli/kubernetes/kubectlGetNamespace'; +import { ContextData } from 'data/contextData'; import { statusBar } from 'ui/statusBar'; import { sortByMetadataName } from 'utils/sortByMetadataName'; -import { groupNodesByNamespace } from '../../../utils/treeNodeUtils'; -import { NamespaceNode } from '../nodes/namespaceNode'; +import { groupNodesByNamespace } from 'utils/treeNodeUtils'; import { BucketNode } from '../nodes/source/bucketNode'; import { GitRepositoryNode } from '../nodes/source/gitRepositoryNode'; import { HelmRepositoryNode } from '../nodes/source/helmRepositoryNode'; import { OCIRepositoryNode } from '../nodes/source/ociRepositoryNode'; import { SourceNode } from '../nodes/source/sourceNode'; import { KubernetesObjectDataProvider } from './kubernetesObjectDataProvider'; -import { getNamespaces } from 'cli/kubernetes/kubectlGetNamespace'; /** * Defines Sources data provider for loading Git/Helm repositories @@ -19,16 +17,17 @@ import { getNamespaces } from 'cli/kubernetes/kubectlGetNamespace'; */ export class SourceDataProvider extends KubernetesObjectDataProvider { + protected viewData(contextData: ContextData) { + return contextData.viewData.source; + } + /** * Creates Source tree view items for the currently selected kubernetes cluster. - * @returns Source tree view items to display. */ - async buildTree(): Promise { + async loadRootNodes() { statusBar.startLoadingTree(); - const treeNodes: SourceNode[] = []; - - setVSCodeContext(ContextId.LoadingSources, true); + const sourceNodes: SourceNode[] = []; // Fetch all sources asynchronously and at once const [gitRepositories, ociRepositories, helmRepositories, buckets, _] = await Promise.all([ @@ -41,29 +40,26 @@ export class SourceDataProvider extends KubernetesObjectDataProvider { // add git repositories to the tree for (const gitRepository of sortByMetadataName(gitRepositories)) { - treeNodes.push(new GitRepositoryNode(gitRepository)); + sourceNodes.push(new GitRepositoryNode(gitRepository)); } // add oci repositories to the tree for (const ociRepository of sortByMetadataName(ociRepositories)) { - treeNodes.push(new OCIRepositoryNode(ociRepository)); + sourceNodes.push(new OCIRepositoryNode(ociRepository)); } for (const helmRepository of sortByMetadataName(helmRepositories)) { - treeNodes.push(new HelmRepositoryNode(helmRepository)); + sourceNodes.push(new HelmRepositoryNode(helmRepository)); } // add buckets to the tree for (const bucket of sortByMetadataName(buckets)) { - treeNodes.push(new BucketNode(bucket)); + sourceNodes.push(new BucketNode(bucket)); } - setVSCodeContext(ContextId.LoadingSources, false); - setVSCodeContext(ContextId.NoSources, treeNodes.length === 0); statusBar.stopLoadingTree(); - const [groupedNodes] = await groupNodesByNamespace(treeNodes, this.expandNewTree); - this.expandNewTree = false; + const [groupedNodes] = await groupNodesByNamespace(sourceNodes, false, true); return groupedNodes; } } diff --git a/src/ui/treeviews/dataProviders/templateDataProvider.ts b/src/ui/treeviews/dataProviders/templateDataProvider.ts index 2a051e4d..37579b1f 100644 --- a/src/ui/treeviews/dataProviders/templateDataProvider.ts +++ b/src/ui/treeviews/dataProviders/templateDataProvider.ts @@ -1,11 +1,15 @@ import { getGitOpsTemplates } from 'cli/kubernetes/kubectlGet'; +import { ContextData } from 'data/contextData'; import { sortByMetadataName } from 'utils/sortByMetadataName'; import { GitOpsTemplateNode } from '../nodes/gitOpsTemplateNode'; -import { DataProvider } from './dataProvider'; +import { AsyncDataProvider } from './asyncDataProvider'; -export class TemplateDataProvider extends DataProvider { +export class TemplateDataProvider extends AsyncDataProvider { + protected viewData(contextData: ContextData) { + return contextData.viewData.template; + } - async buildTree(): Promise { + async loadRootNodes() { const nodes = []; const templates = await getGitOpsTemplates(); diff --git a/src/ui/treeviews/dataProviders/workloadDataProvider.ts b/src/ui/treeviews/dataProviders/workloadDataProvider.ts index f6069cd5..b6c6bbba 100644 --- a/src/ui/treeviews/dataProviders/workloadDataProvider.ts +++ b/src/ui/treeviews/dataProviders/workloadDataProvider.ts @@ -1,38 +1,35 @@ import { fluxTools } from 'cli/flux/fluxTools'; import { getChildrenOfWorkload, getHelmReleases, getKustomizations } from 'cli/kubernetes/kubectlGet'; import { getNamespaces } from 'cli/kubernetes/kubectlGetNamespace'; -import { setVSCodeContext } from 'extension'; -import { ContextId } from 'types/extensionIds'; -import { Namespace } from 'types/kubernetes/kubernetesTypes'; +import { ContextData } from 'data/contextData'; import { statusBar } from 'ui/statusBar'; -import { addFluxTreeToNode, groupNodesByNamespace } from 'utils/treeNodeUtils'; +import { InfoNode, infoNodes } from 'utils/makeTreeviewInfoNode'; import { sortByMetadataName } from 'utils/sortByMetadataName'; +import { addFluxTreeToNode, groupNodesByNamespace } from 'utils/treeNodeUtils'; import { AnyResourceNode } from '../nodes/anyResourceNode'; -import { NamespaceNode } from '../nodes/namespaceNode'; import { TreeNode } from '../nodes/treeNode'; import { HelmReleaseNode } from '../nodes/workload/helmReleaseNode'; import { KustomizationNode } from '../nodes/workload/kustomizationNode'; import { WorkloadNode } from '../nodes/workload/workloadNode'; -import { refreshWorkloadsTreeView } from '../treeViews'; -import { DataProvider } from './dataProvider'; import { KubernetesObjectDataProvider } from './kubernetesObjectDataProvider'; -/** +/**- * Defines data provider for loading Kustomizations * and Helm Releases in Workloads Tree View. */ export class WorkloadDataProvider extends KubernetesObjectDataProvider { + protected viewData(contextData: ContextData) { + return contextData.viewData.workload; + } + /** * Creates Workload tree nodes for the currently selected kubernetes cluster. - * @returns Workload tree nodes to display. */ - async buildTree(): Promise { + async loadRootNodes() { statusBar.startLoadingTree(); const workloadNodes: WorkloadNode[] = []; - setVSCodeContext(ContextId.LoadingWorkloads, true); - const [kustomizations, helmReleases, _] = await Promise.all([ // Fetch all workloads getKustomizations(), @@ -53,13 +50,9 @@ export class WorkloadDataProvider extends KubernetesObjectDataProvider { this.updateWorkloadChildren(node); } - setVSCodeContext(ContextId.LoadingWorkloads, false); - setVSCodeContext(ContextId.NoWorkloads, workloadNodes.length === 0); statusBar.stopLoadingTree(); - const [groupedNodes] = await groupNodesByNamespace(workloadNodes, this.expandNewTree); - this.expandNewTree = false; - + const [groupedNodes] = await groupNodesByNamespace(workloadNodes, false, true); return groupedNodes; } @@ -69,6 +62,8 @@ export class WorkloadDataProvider extends KubernetesObjectDataProvider { * @param workloadNode target workload node */ async updateWorkloadChildren(workloadNode: WorkloadNode) { + workloadNode.children = [new TreeNode('Loading...')]; + if (workloadNode instanceof KustomizationNode) { this.updateKustomizationChildren(workloadNode); } else if (workloadNode instanceof HelmReleaseNode) { @@ -81,14 +76,21 @@ export class WorkloadDataProvider extends KubernetesObjectDataProvider { const namespace = node.resource.metadata?.namespace || ''; const resourceTree = await fluxTools.tree(name, namespace); - if (!resourceTree || !resourceTree.resources) { + if (!resourceTree) { + node.children = infoNodes(InfoNode.FailedToLoad); + this.redraw(node); + return; + } + + if (!resourceTree.resources) { node.children = [new TreeNode('No Resources')]; - refreshWorkloadsTreeView(node); + this.redraw(node); return; } - addFluxTreeToNode(node, resourceTree.resources); - refreshWorkloadsTreeView(node); + node.children = []; + await addFluxTreeToNode(node, resourceTree.resources); + this.redraw(node); } @@ -96,13 +98,17 @@ export class WorkloadDataProvider extends KubernetesObjectDataProvider { const name = node.resource.metadata?.name || ''; const namespace = node.resource.metadata?.namespace || ''; - // const targetNamespace = node.resource.spec.targetNamespace; const workloadChildren = await getChildrenOfWorkload('helm', name, namespace); + if (!workloadChildren) { + node.children = infoNodes(InfoNode.FailedToLoad); + this.redraw(node); + return; + } - if (!workloadChildren || workloadChildren.length === 0) { + if (workloadChildren.length === 0) { node.children = [new TreeNode('No Resources')]; - refreshWorkloadsTreeView(node); + this.redraw(node); return; } @@ -110,23 +116,7 @@ export class WorkloadDataProvider extends KubernetesObjectDataProvider { const [groupedNodes, clusterScopedNodes] = await groupNodesByNamespace(childrenNodes); node.children = [...groupedNodes, ...clusterScopedNodes]; - refreshWorkloadsTreeView(node); - } - - /** - * This is called when the tree node is being expanded. - * @param workloadNode target node or undefined when at the root level. - */ - async getChildren(workloadNode?: KustomizationNode | HelmReleaseNode) { - if (workloadNode) { - if (workloadNode.children.length) { - return workloadNode.children; - } else { - return [new TreeNode('Loading...')]; - } - } else { - this.treeItems = await this.buildTree(); - return this.treeItems; - } + this.redraw(node); } } + diff --git a/src/ui/treeviews/nodes/cluster/clusterNode.ts b/src/ui/treeviews/nodes/cluster/clusterNode.ts index 26c0c17a..19dd6f8e 100644 --- a/src/ui/treeviews/nodes/cluster/clusterNode.ts +++ b/src/ui/treeviews/nodes/cluster/clusterNode.ts @@ -3,17 +3,20 @@ import { ExtensionMode, MarkdownString } from 'vscode'; import { fluxVersion } from 'cli/checkVersions'; +import { fluxTools } from 'cli/flux/fluxTools'; +import { ApiState } from 'cli/kubernetes/apiResources'; import { detectClusterProvider } from 'cli/kubernetes/clusterProvider'; +import { getFluxControllers } from 'cli/kubernetes/kubectlGet'; import { kubeConfig } from 'cli/kubernetes/kubernetesConfig'; +import { currentContextData } from 'data/contextData'; import { extensionContext, globalState, setVSCodeContext } from 'extension'; -import { result } from 'types/errorable'; import { CommandId, ContextId } from 'types/extensionIds'; import { ClusterProvider } from 'types/kubernetes/clusterProvider'; import { NodeContext } from 'types/nodeContext'; +import { clusterDataProvider, revealClusterNode } from 'ui/treeviews/treeViews'; +import { InfoNode, infoNodes } from 'utils/makeTreeviewInfoNode'; import { createContextMarkdownTable, createMarkdownHr } from 'utils/markdownUtils'; import { TreeNode } from '../treeNode'; -import { clusterDataProvider, revealClusterNode } from 'ui/treeviews/treeViews'; -import { getFluxControllers } from 'cli/kubernetes/kubectlGet'; import { ClusterDeploymentNode } from './clusterDeploymentNode'; /** @@ -69,23 +72,10 @@ export class ClusterNode extends TreeNode { * - Whether or not GitOps is enabled * - Cluster provider. */ - async updateNodeContext() { - const fluxControllers = await getFluxControllers(this.context.name); - this.isGitOpsEnabled = fluxControllers.length !== 0; - - if(this.isGitOpsEnabled) { - // load flux system deployments - this.expand(); - revealClusterNode(this, { - expand: true, - }); - for (const deployment of fluxControllers) { - this.addChild(new ClusterDeploymentNode(deployment)); - } - } else { - this.addChild(new TreeNode('Flux controllers not found')); - } + async updateNodeChildren() { + this.updateControllersNodes(); + // set cluster provider const clusterMetadata = globalState.getClusterMetadata(this.cluster?.name || this.context.name); if (clusterMetadata?.clusterProvider) { this.clusterProviderManuallyOverridden = true; @@ -97,15 +87,92 @@ export class ClusterNode extends TreeNode { setVSCodeContext(ContextId.CurrentClusterGitOpsNotEnabled, !this.isGitOpsEnabled); } + // icon if (this.isGitOpsEnabled) { this.setIcon('cloud-gitops'); } else { this.setIcon('cloud'); } - clusterDataProvider.refresh(this); + clusterDataProvider.redraw(); + this.updateControllersStatus(); + } + + private async updateControllersNodes() { + const contextData = currentContextData(); + if(contextData.contextName !== this.context.name) { + return; + } + + if(contextData.apiState === ApiState.ClusterUnreachable) { + this.children = infoNodes(InfoNode.ClusterUnreachable); + return; + } + if(contextData.apiState === ApiState.Loading) { + this.children = infoNodes(InfoNode.LoadingApi); + return; + } + + const fluxControllers = await getFluxControllers(this.context.name); + this.isGitOpsEnabled = fluxControllers.length !== 0; + + this.children = []; + if (this.isGitOpsEnabled) { + // load flux system deployments + this.expand(); + revealClusterNode(this, { + expand: false, + }); + for (const deployment of fluxControllers) { + this.addChild(new ClusterDeploymentNode(deployment)); + } + } else { + const notFound = new TreeNode('Flux controllers not found'); + notFound.setIcon('warning'); + this.addChild(notFound); + } + } + + /** + * Update deployment status for flux controllers. + * Get status from running flux commands instead of kubectl. + */ + private async updateControllersStatus() { + const contextData = currentContextData(); + if(contextData.contextName !== this.context.name) { + return; + } + + if (this.children.length === 0 || contextData.apiState === ApiState.ClusterUnreachable) { + return; + } + const fluxCheckResult = await fluxTools.check(this.context.name); + if (!fluxCheckResult) { + return; + } + + const deploymentNodes: ClusterDeploymentNode[] = this.children.filter(node => node instanceof ClusterDeploymentNode) as ClusterDeploymentNode[]; + // Match controllers fetched with flux with controllers + // fetched with kubectl and update tree nodes. + for (const clusterController of deploymentNodes) { + for (const controller of fluxCheckResult.controllers) { + const clusterControllerName = clusterController.resource.metadata.name?.trim(); + const deploymentName = controller.name.trim(); + + if (clusterControllerName === deploymentName) { + clusterController.description = controller.status; + if (controller.success) { + clusterController.setStatus('success'); + } else { + clusterController.setStatus('failure'); + } + } + } + clusterDataProvider.redraw(this); + } } + get isCurrent(): boolean { return this.context.name === kubeConfig.getCurrentContext(); } diff --git a/src/ui/treeviews/nodes/gitOpsTemplateNode.ts b/src/ui/treeviews/nodes/gitOpsTemplateNode.ts index 588430a8..00f8c166 100644 --- a/src/ui/treeviews/nodes/gitOpsTemplateNode.ts +++ b/src/ui/treeviews/nodes/gitOpsTemplateNode.ts @@ -1,4 +1,4 @@ -import { MarkdownString, ThemeColor, ThemeIcon } from 'vscode'; +import { MarkdownString, ThemeColor, ThemeIcon, TreeItemCollapsibleState } from 'vscode'; import { GitOpsTemplate } from 'types/flux/gitOpsTemplate'; import { Kind } from 'types/kubernetes/kubernetesTypes'; @@ -17,6 +17,7 @@ export class GitOpsTemplateNode extends TreeNode { this.resource = template; this.setIcon(new ThemeIcon('notebook-render-output', new ThemeColor('editorWidget.foreground'))); + this.collapsibleState = TreeItemCollapsibleState.None; } get tooltip() { diff --git a/src/ui/treeviews/nodes/namespaceNode.ts b/src/ui/treeviews/nodes/namespaceNode.ts index 17c349aa..e17701b2 100644 --- a/src/ui/treeviews/nodes/namespaceNode.ts +++ b/src/ui/treeviews/nodes/namespaceNode.ts @@ -1,6 +1,8 @@ import { Kind, Namespace } from 'types/kubernetes/kubernetesTypes'; -import { TreeNode } from './treeNode'; import { TreeItemCollapsibleState } from 'vscode'; +import { SourceNode } from './source/sourceNode'; +import { TreeNode, TreeNodeIcon } from './treeNode'; +import { WorkloadNode } from './workload/workloadNode'; /** * Defines any kubernetes resourse. @@ -24,9 +26,29 @@ export class NamespaceNode extends TreeNode { return [Kind.Namespace]; } - updateLabel() { + updateLabel(withIcons = true) { if(this.collapsibleState === TreeItemCollapsibleState.Collapsed) { - this.label = `${this.resource.metadata?.name} (${this.children.length})`; + const totalLength = this.children.length; + let readyLength = 0; + for(const child of this.children) { + if(child instanceof SourceNode || child instanceof WorkloadNode) { + if(!child.isReconcileFailed) { + readyLength++; + } + } else { + readyLength++; + } + } + const lengthLabel = totalLength === readyLength ? `${totalLength}` : `${readyLength}/${totalLength}`; + this.label = `${this.resource.metadata?.name} (${lengthLabel})`; + + if(withIcons) { + if(readyLength === totalLength) { + this.setIcon(TreeNodeIcon.Success); + } else { + this.setIcon(TreeNodeIcon.Warning); + } + } } else { this.label = `${this.resource.metadata?.name}`; } diff --git a/src/ui/treeviews/nodes/treeNode.ts b/src/ui/treeviews/nodes/treeNode.ts index 1477fed2..94bfef6d 100644 --- a/src/ui/treeviews/nodes/treeNode.ts +++ b/src/ui/treeviews/nodes/treeNode.ts @@ -2,7 +2,7 @@ import { Command, MarkdownString, ThemeColor, ThemeIcon, TreeItem, TreeItemColla import { CommandId } from 'types/extensionIds'; import { FileTypes } from 'types/fileTypes'; -import { KubernetesObject } from 'types/kubernetes/kubernetesTypes'; +import { KubernetesObject, qualifyToolkitKind } from 'types/kubernetes/kubernetesTypes'; import { asAbsolutePath } from 'utils/asAbsolutePath'; import { getResourceUri } from 'utils/getResourceUri'; import { KnownTreeNodeResources, createMarkdownTable } from 'utils/markdownUtils'; @@ -11,6 +11,7 @@ export const enum TreeNodeIcon { Error = 'error', Warning = 'warning', Success = 'success', + Disconnected = 'disconnected', Unknown = 'unknown', } @@ -74,6 +75,8 @@ export class TreeNode extends TreeItem { this.iconPath = new ThemeIcon('error', new ThemeColor('editorError.foreground')); } else if (icon === TreeNodeIcon.Warning) { this.iconPath = new ThemeIcon('warning', new ThemeColor('editorWarning.foreground')); + } else if (icon === TreeNodeIcon.Disconnected) { + this.iconPath = new ThemeIcon('sync-ignored', new ThemeColor('editorError.foreground')); } else if (icon === TreeNodeIcon.Success) { this.iconPath = new ThemeIcon('pass', new ThemeColor('terminal.ansiGreen')); } else if (icon === TreeNodeIcon.Unknown) { @@ -138,6 +141,10 @@ export class TreeNode extends TreeItem { .join(''); } + fullyQualifyKind(): string { + return qualifyToolkitKind(this.resource?.kind || ''); + } + // @ts-ignore get tooltip(): string | MarkdownString { if (this.resource) { @@ -149,9 +156,10 @@ export class TreeNode extends TreeItem { get command(): Command | undefined { // Set click event handler to load kubernetes resource as yaml file in editor. if (this.resource) { + let stringKind = this.fullyQualifyKind(); const resourceUri = getResourceUri( this.resource.metadata?.namespace, - `${this.resource.kind}/${this.resource.metadata?.name}`, + `${stringKind}/${this.resource.metadata?.name}`, FileTypes.Yaml, ); diff --git a/src/ui/treeviews/treeViews.ts b/src/ui/treeviews/treeViews.ts index 0ed8c29d..7374cb12 100644 --- a/src/ui/treeviews/treeViews.ts +++ b/src/ui/treeviews/treeViews.ts @@ -9,7 +9,6 @@ import { DocumentationDataProvider } from './dataProviders/documentationDataProv import { SourceDataProvider } from './dataProviders/sourceDataProvider'; import { WorkloadDataProvider } from './dataProviders/workloadDataProvider'; import { ClusterNode } from './nodes/cluster/clusterNode'; -import { TreeNode } from './nodes/treeNode'; import { detectClusterProvider } from 'cli/kubernetes/clusterProvider'; import { kubeConfig } from 'cli/kubernetes/kubernetesConfig'; @@ -64,7 +63,7 @@ export function createTreeViews() { showCollapseAll: true, }); - + documentationDataProvider.reload(); } function listenCollapsableState() { @@ -72,7 +71,7 @@ function listenCollapsableState() { if (e.element instanceof NamespaceNode) { e.element.collapsibleState = TreeItemCollapsibleState.Collapsed; e.element.updateLabel(); - sourceDataProvider.refresh(e.element); + sourceDataProvider.redraw(e.element); } }); @@ -80,7 +79,7 @@ function listenCollapsableState() { if (e.element instanceof NamespaceNode) { e.element.collapsibleState = TreeItemCollapsibleState.Expanded; e.element.updateLabel(); - sourceDataProvider.refresh(e.element); + sourceDataProvider.redraw(e.element); } }); @@ -89,7 +88,7 @@ function listenCollapsableState() { if (e.element instanceof NamespaceNode) { e.element.collapsibleState = TreeItemCollapsibleState.Collapsed; e.element.updateLabel(); - workloadDataProvider.refresh(e.element); + workloadDataProvider.redraw(e.element); } }); @@ -97,7 +96,7 @@ function listenCollapsableState() { if (e.element instanceof NamespaceNode) { e.element.collapsibleState = TreeItemCollapsibleState.Expanded; e.element.updateLabel(); - workloadDataProvider.refresh(e.element); + workloadDataProvider.redraw(e.element); } }); } @@ -107,33 +106,29 @@ function listenCollapsableState() { * When an argument is passed - only that tree item * and its children are updated. */ -export function refreshClustersTreeView(node?: TreeNode) { - if (node && !clusterDataProvider.includesTreeNode(node)) { - // Trying to refresh old (non-existent) cluster context node - return; - } - clusterDataProvider.refresh(node); +export function reloadClustersTreeView() { + clusterDataProvider.reload(); } /** * Reloads sources tree view for the selected cluster. */ -export function refreshSourcesTreeView(node?: TreeNode) { - sourceDataProvider.refresh(node); +export function reloadSourcesTreeView() { + sourceDataProvider.reload(); } /** * Reloads workloads tree view for the selected cluster. */ -export function refreshWorkloadsTreeView(node?: TreeNode) { - workloadDataProvider.refresh(node); +export function reloadWorkloadsTreeView() { + workloadDataProvider.reload(); } /** * Reloads workloads tree view for the selected cluster. */ -export function refreshTemplatesTreeView(node?: TreeNode) { - templateDateProvider.refresh(node); +export function reloadTemplatesTreeView() { + templateDateProvider.reload(); } /** diff --git a/src/ui/webviews/configureGitOps/lib/createAzure.ts b/src/ui/webviews/configureGitOps/lib/createAzure.ts index 5ac9e233..35bd8405 100644 --- a/src/ui/webviews/configureGitOps/lib/createAzure.ts +++ b/src/ui/webviews/configureGitOps/lib/createAzure.ts @@ -1,13 +1,13 @@ -import { AzureClusterProvider, azureTools, CreateSourceBucketAzureArgs, CreateSourceGitAzureArgs } from 'cli/azure/azureTools'; +import { AzureClusterProvider, azureTools, CreateSourceGitAzureArgs } from 'cli/azure/azureTools'; +import { kubeConfig } from 'cli/kubernetes/kubernetesConfig'; import { showDeployKeyNotificationIfNeeded } from 'commands/createSource'; import { telemetry } from 'extension'; +import { ClusterInfo } from 'types/kubernetes/clusterProvider'; import { Kind } from 'types/kubernetes/kubernetesTypes'; import { TelemetryEvent } from 'types/telemetryEventNames'; -import { ParamsDictionary } from 'utils/typeUtils'; -import { refreshSourcesTreeView, refreshWorkloadsTreeView } from 'ui/treeviews/treeViews'; -import { ClusterInfo } from 'types/kubernetes/clusterProvider'; -import { kubeConfig } from 'cli/kubernetes/kubernetesConfig'; +import { reloadSourcesTreeView, reloadWorkloadsTreeView } from 'ui/treeviews/treeViews'; import { splitNamespacedFluxObject } from 'utils/namespacedFluxObject'; +import { ParamsDictionary } from 'utils/typeUtils'; export async function createConfigurationAzure(data: ParamsDictionary) { const clusterInfo = data.clusterInfo as ClusterInfo; @@ -51,8 +51,8 @@ async function createGitSourceAzure(source: ParamsDictionary, kustomization: Par setTimeout(() => { // Wait a bit for the repository to have a failed state in case of SSH url - refreshSourcesTreeView(); - refreshWorkloadsTreeView(); + reloadSourcesTreeView(); + reloadWorkloadsTreeView(); }, 1000); showDeployKeyNotificationIfNeeded(args.url, deployKey); @@ -83,8 +83,8 @@ async function createBucketSourceAzure(source: ParamsDictionary, kustomization: await azureTools.createSourceBucket(args); setTimeout(() => { - refreshSourcesTreeView(); - refreshWorkloadsTreeView(); + reloadSourcesTreeView(); + reloadWorkloadsTreeView(); }, 1000); } diff --git a/src/ui/webviews/configureGitOps/lib/createGeneric.ts b/src/ui/webviews/configureGitOps/lib/createGeneric.ts index 7d219f7b..85a06e39 100644 --- a/src/ui/webviews/configureGitOps/lib/createGeneric.ts +++ b/src/ui/webviews/configureGitOps/lib/createGeneric.ts @@ -1,10 +1,10 @@ +import { fluxTools } from 'cli/flux/fluxTools'; import { showDeployKeyNotificationIfNeeded } from 'commands/createSource'; +import { refreshAllTreeViewsCommand } from 'commands/refreshTreeViews'; import { telemetry } from 'extension'; -import { fluxTools } from 'cli/flux/fluxTools'; import { TelemetryEvent } from 'types/telemetryEventNames'; +import { reloadSourcesTreeView } from 'ui/treeviews/treeViews'; import { ParamsDictionary } from 'utils/typeUtils'; -import { refreshSourcesTreeView } from 'ui/treeviews/treeViews'; -import { refreshAllTreeViewsCommand } from 'commands/refreshTreeViews'; export async function createConfigurationGeneric(data: ParamsDictionary) { telemetry.send(TelemetryEvent.CreateSource, { @@ -17,7 +17,7 @@ export async function createConfigurationGeneric(data: ParamsDictionary) { showDeployKeyNotificationIfNeeded(data.source.url, deployKey); setTimeout(() => { // Wait a bit for the repository to have a failed state in case of SSH url - refreshSourcesTreeView(); + reloadSourcesTreeView(); }, 1000); } diff --git a/src/ui/webviews/createFromTemplate/receiveMessage.ts b/src/ui/webviews/createFromTemplate/receiveMessage.ts index 94d4ae7b..cca11a9b 100644 --- a/src/ui/webviews/createFromTemplate/receiveMessage.ts +++ b/src/ui/webviews/createFromTemplate/receiveMessage.ts @@ -5,8 +5,6 @@ import { Uri, WebviewPanel, workspace } from 'vscode'; export async function receiveMessage(message: any, panel: WebviewPanel) { switch (message.action) { case 'show-yaml': - // actionYAML(message.data); - console.log(message.data); const data = message.data; renderTemplates(data.template, data.values); diff --git a/src/utils/kubeConfigCompare.ts b/src/utils/kubeConfigCompare.ts index ddfc1d9b..67b8e84d 100644 --- a/src/utils/kubeConfigCompare.ts +++ b/src/utils/kubeConfigCompare.ts @@ -1,5 +1,5 @@ -import deepEqual from 'lite-deep-equal'; import * as k8s from '@kubernetes/client-node'; +import deepEqual from 'lite-deep-equal'; export function kcTextChanged(kc1: k8s.KubeConfig, kc2: k8s.KubeConfig): boolean { // exportConfig() will omit tokens and certs diff --git a/src/utils/makeTreeviewInfoNode.ts b/src/utils/makeTreeviewInfoNode.ts new file mode 100644 index 00000000..1e71a9b7 --- /dev/null +++ b/src/utils/makeTreeviewInfoNode.ts @@ -0,0 +1,38 @@ +import { kubeConfig } from 'cli/kubernetes/kubernetesConfig'; +import { TreeNode, TreeNodeIcon } from '../ui/treeviews/nodes/treeNode'; + + +export enum InfoNode { + FailedToLoad, + NoResources, + Loading, + LoadingApi, + ClusterUnreachable, +} + +export function infoNodes(type: InfoNode) { + return [infoNode(type)]; +} + +export function infoNode(type: InfoNode) { + let node; + + switch(type) { + case InfoNode.FailedToLoad: + node = new TreeNode('Failed to load'); + node.setIcon(TreeNodeIcon.Disconnected); + return node; + case InfoNode.NoResources: + return new TreeNode('No Resources'); + case InfoNode.Loading: + return new TreeNode('Loading...'); + case InfoNode.LoadingApi: + return new TreeNode('Loading API...'); + case InfoNode.ClusterUnreachable: + const name = kubeConfig.currentContext; + node = new TreeNode(`Cluster ${name} unreachable`); + node.setIcon(TreeNodeIcon.Disconnected); + return node; + } +} + diff --git a/src/utils/treeNodeUtils.ts b/src/utils/treeNodeUtils.ts index afd314a9..81f851c7 100644 --- a/src/utils/treeNodeUtils.ts +++ b/src/utils/treeNodeUtils.ts @@ -34,7 +34,7 @@ export async function addFluxTreeToNode(node: TreeNode, resourceTree: FluxTreeRe } // returns grouped by namespace, and ugroupable (cluster scoped) nodes -export async function groupNodesByNamespace(nodes: TreeNode[], expandAll = false): Promise<[NamespaceNode[], TreeNode[]]> { +export async function groupNodesByNamespace(nodes: TreeNode[], expandAll = false, withIcons = false): Promise<[NamespaceNode[], TreeNode[]]> { const namespaces: Namespace[] = getCachedNamespaces(); const namespaceNodes: NamespaceNode[] = []; @@ -51,7 +51,7 @@ export async function groupNodesByNamespace(nodes: TreeNode[], expandAll = false } }); nsNode.collapsibleState = expandAll ? TreeItemCollapsibleState.Expanded : TreeItemCollapsibleState.Collapsed; - nsNode.updateLabel(); + nsNode.updateLabel(withIcons); namespaceNodes.push(nsNode); } diff --git a/tslint-imports.json b/tslint-imports.json new file mode 100644 index 00000000..7929f6ec --- /dev/null +++ b/tslint-imports.json @@ -0,0 +1,11 @@ +{ + "extends": [ + "tslint-etc" + ], + "rules": { + "no-unused-declaration": true + } +} +// https://wesleygrimes.com/angular/2019/02/14/how-to-use-tslint-to-autoremove-all-unused-imports-in-a-typescript-project +// npm install -g typescript tslint tslint-etc +// tslint --config tslint-imports.json --fix --project . \ No newline at end of file diff --git a/webview-ui/configureGitOps/package-lock.json b/webview-ui/configureGitOps/package-lock.json index 0d34e9b9..4cb7449d 100644 --- a/webview-ui/configureGitOps/package-lock.json +++ b/webview-ui/configureGitOps/package-lock.json @@ -1410,9 +1410,9 @@ "dev": true }, "node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -1541,15 +1541,15 @@ } }, "node_modules/vite": { - "version": "2.9.13", - "resolved": "https://registry.npmjs.org/vite/-/vite-2.9.13.tgz", - "integrity": "sha512-AsOBAaT0AD7Mhe8DuK+/kE4aWYFMx/i0ZNi98hJclxb4e0OhQcZYUrvLjIaQ8e59Ui7txcvKMiJC1yftqpQoDw==", + "version": "2.9.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-2.9.16.tgz", + "integrity": "sha512-X+6q8KPyeuBvTQV8AVSnKDvXoBMnTx8zxh54sOwmmuOdxkjMmEJXH2UEchA+vTMps1xw9vL64uwJOWryULg7nA==", "dev": true, "dependencies": { "esbuild": "^0.14.27", "postcss": "^8.4.13", "resolve": "^1.22.0", - "rollup": "^2.59.0" + "rollup": ">=2.59.0 <2.78.0" }, "bin": { "vite": "bin/vite.js" @@ -2510,9 +2510,9 @@ "dev": true }, "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true }, "solid-collapse": { @@ -2597,16 +2597,16 @@ "dev": true }, "vite": { - "version": "2.9.13", - "resolved": "https://registry.npmjs.org/vite/-/vite-2.9.13.tgz", - "integrity": "sha512-AsOBAaT0AD7Mhe8DuK+/kE4aWYFMx/i0ZNi98hJclxb4e0OhQcZYUrvLjIaQ8e59Ui7txcvKMiJC1yftqpQoDw==", + "version": "2.9.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-2.9.16.tgz", + "integrity": "sha512-X+6q8KPyeuBvTQV8AVSnKDvXoBMnTx8zxh54sOwmmuOdxkjMmEJXH2UEchA+vTMps1xw9vL64uwJOWryULg7nA==", "dev": true, "requires": { "esbuild": "^0.14.27", "fsevents": "~2.3.2", "postcss": "^8.4.13", "resolve": "^1.22.0", - "rollup": "^2.59.0" + "rollup": ">=2.59.0 <2.78.0" } }, "vite-plugin-solid": { diff --git a/webview-ui/createFromTemplate/package-lock.json b/webview-ui/createFromTemplate/package-lock.json index 5d384218..75d188e3 100644 --- a/webview-ui/createFromTemplate/package-lock.json +++ b/webview-ui/createFromTemplate/package-lock.json @@ -1008,9 +1008,10 @@ "license": "MIT" }, "node_modules/semver": { - "version": "6.3.0", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "license": "ISC", "bin": { "semver": "bin/semver.js" } @@ -1126,14 +1127,15 @@ } }, "node_modules/vite": { - "version": "2.9.13", + "version": "2.9.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-2.9.16.tgz", + "integrity": "sha512-X+6q8KPyeuBvTQV8AVSnKDvXoBMnTx8zxh54sOwmmuOdxkjMmEJXH2UEchA+vTMps1xw9vL64uwJOWryULg7nA==", "dev": true, - "license": "MIT", "dependencies": { "esbuild": "^0.14.27", "postcss": "^8.4.13", "resolve": "^1.22.0", - "rollup": "^2.59.0" + "rollup": ">=2.59.0 <2.78.0" }, "bin": { "vite": "bin/vite.js"