diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1eaa6a4f..48ffb3d3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: - uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # pin@v3 with: - node-version: '19' + node-version: '20' - run: npm install diff --git a/README.md b/README.md index b3a4a95c..004f4278 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # GitOps Tools for Visual Studio Code -[![VSCode Marketplace Link](https://vsmarketplacebadges.dev/version-short/weaveworks.vscode-gitops-tools.png)](https://marketplace.visualstudio.com/items?itemName=Weaveworks.vscode-gitops-tools) -[![Install Counter](https://vsmarketplacebadges.dev/installs/weaveworks.vscode-gitops-tools.png)](https://marketplace.visualstudio.com/items?itemName=Weaveworks.vscode-gitops-tools) +[![VSCode Marketplace Link](https://img.shields.io/visual-studio-marketplace/v/weaveworks.vscode-gitops-tools)](https://marketplace.visualstudio.com/items?itemName=Weaveworks.vscode-gitops-tools) +[![Install Counter](https://img.shields.io/visual-studio-marketplace/i/weaveworks.vscode-gitops-tools)](https://marketplace.visualstudio.com/items?itemName=Weaveworks.vscode-gitops-tools) Weaveworks [GitOps Tools Extension](https://marketplace.visualstudio.com/items?itemName=Weaveworks.vscode-gitops-tools) provides an intuitive way to manage, troubleshoot and operate your Kubernetes environment following the GitOps operating model. GitOps accelerates your development lifecycle and simplifies your continuous delivery pipelines. The extension is built on Flux (a CNCF open source project). To learn more about the Flux GitOps toolkit, visit [fluxcd.io] @@ -145,11 +145,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. # Data and Telemetry diff --git a/package-lock.json b/package-lock.json index 6ce051e7..f29456ca 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 b0980281..c7f3a6dd 100644 --- a/package.json +++ b/package.json @@ -293,10 +293,15 @@ "default": false, "description": "Enable WGE GitOpsTemplates feature" }, - "gitops.kubectlTimeout": { + "gitops.kubectlRequestTimeout": { "type": "string", "default": "10s", "description": "kubectl --request-timeout" + }, + "gitops.execTimeout": { + "type": "string", + "default": "60", + "description": "Seconds until SIGTERM for every shell exec (except `kubectl proxy`). Set to 0 for no timeout." } } }, @@ -319,17 +324,22 @@ { "view": "gitops.views.sources", "contents": "[Enable GitOps](command:gitops.flux.install) for the selected Cluster to view Sources.", - "when": "gitops:currentClusterGitOpsNotEnabled && !gitops:noClusterSelected" + "when": "gitops:currentClusterGitOpsNotEnabled && !gitops:noClusterSelected && !gitops:clusterUnreachable" }, { "view": "gitops.views.sources", "contents": "Loading Sources ...", - "when": "gitops:loadingSources && !gitops:noClusterSelected && !gitops:currentClusterGitOpsNotEnabled" + "when": "gitops:loadingSources && !gitops:noClusterSelected && !gitops:currentClusterGitOpsNotEnabled && !gitops:clusterUnreachable" }, { "view": "gitops.views.sources", "contents": "No sources.", - "when": "!gitops:loadingSources && !gitops:currentClusterGitOpsNotEnabled && !gitops:noClusterSelected && gitops:noSources" + "when": "!gitops:loadingSources && !gitops:currentClusterGitOpsNotEnabled && !gitops:noClusterSelected && gitops:noSources && !gitops:clusterUnreachable" + }, + { + "view": "gitops.views.sources", + "contents": "Cluster unreachable", + "when": "gitops:clusterUnreachable" }, { "view": "gitops.views.sources", @@ -339,17 +349,22 @@ { "view": "gitops.views.workloads", "contents": "[Enable GitOps](command:gitops.flux.install) for the selected Cluster to view Workloads.", - "when": "gitops:currentClusterGitOpsNotEnabled" + "when": "gitops:currentClusterGitOpsNotEnabled && !gitops:clusterUnreachable" }, { "view": "gitops.views.workloads", "contents": "Loading Workloads ...", - "when": "gitops:loadingWorkloads && !gitops:noClusterSelected && !gitops:currentClusterGitOpsNotEnabled" + "when": "gitops:loadingWorkloads && !gitops:noClusterSelected && !gitops:currentClusterGitOpsNotEnabled && !gitops:clusterUnreachable" }, { "view": "gitops.views.workloads", "contents": "No workloads.", - "when": "!gitops:loadingWorkloads && !gitops:currentClusterGitOpsNotEnabled && !gitops:noClusterSelected && gitops:noWorkloads" + "when": "!gitops:loadingWorkloads && !gitops:currentClusterGitOpsNotEnabled && !gitops:noClusterSelected && gitops:noWorkloads && !gitops:clusterUnreachable" + }, + { + "view": "gitops.views.workloads", + "contents": "Cluster unreachable", + "when": "gitops:clusterUnreachable" }, { "view": "gitops.views.workloads", @@ -609,6 +624,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", @@ -623,10 +639,10 @@ "eslint": "^8.11.0", "glob": "^7.2.0", "mocha": "^9.2.2", + "tough-cookie": ">=4.1.3", "ts-loader": "^9.2.8", "tsconfig-paths-webpack-plugin": "^4.0.1", "typescript": "^4.5.5", - "tough-cookie": ">=4.1.3", "vite": ">=2.9.16", "webpack": "^5.70.0", "webpack-cli": "^4.9.2", @@ -636,9 +652,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", @@ -648,15 +666,15 @@ "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", "vscode-uri": "^3.0.7", "word-wrap": ">=1.2.4" }, - "activationEvents": [ - "onDebug" - ], + "activationEvents": [], "__metadata": { "id": "61a914ed-c714-4c42-a201-6008038286a4", "publisherDisplayName": "Weaveworks", diff --git a/src/cli/azure/azurePrereqs.ts b/src/cli/azure/azurePrereqs.ts index 82951032..87f36710 100644 --- a/src/cli/azure/azurePrereqs.ts +++ b/src/cli/azure/azurePrereqs.ts @@ -1,6 +1,6 @@ import { window } from 'vscode'; -import { shell } from 'cli/shell/exec'; +import * as shell from 'cli/shell/exec'; import { ClusterProvider } from 'types/kubernetes/clusterProvider'; import { AzureClusterProvider } from './azureTools'; diff --git a/src/cli/azure/azureTools.ts b/src/cli/azure/azureTools.ts index b72c5d51..5fc12d6f 100644 --- a/src/cli/azure/azureTools.ts +++ b/src/cli/azure/azureTools.ts @@ -1,18 +1,17 @@ import { Uri, env, window } from 'vscode'; import { fluxTools } from 'cli/flux/fluxTools'; -import { ShellResult, shell, shellCodeError } from 'cli/shell/exec'; +import { kubeConfig } from 'cli/kubernetes/kubernetesConfig'; +import * as shell from 'cli/shell/exec'; +import { ShellResult, shellCodeError } from 'cli/shell/exec'; +import { refreshAllTreeViewsCommand } from 'commands/refreshTreeViews'; import { ClusterMetadata } from 'data/globalState'; import { globalState, telemetry } from 'extension'; -import { failed } from 'types/errorable'; import { ClusterProvider } from 'types/kubernetes/clusterProvider'; import { TelemetryError } from 'types/telemetryEventNames'; -import { getCurrentClusterInfo } from 'ui/treeviews/treeViews'; -import { refreshAllTreeViewsCommand } from 'commands/refreshTreeViews'; import { parseJson } from 'utils/jsonUtils'; import { checkAzurePrerequisites } from './azurePrereqs'; import { getAzureMetadata } from './getAzureMetadata'; -import { kubeConfig } from 'cli/kubernetes/kubernetesConfig'; export type AzureClusterProvider = ClusterProvider.AKS | ClusterProvider.AzureARC; diff --git a/src/cli/azure/getAzureMetadata.ts b/src/cli/azure/getAzureMetadata.ts index 52e42d5b..f3bf1fc5 100644 --- a/src/cli/azure/getAzureMetadata.ts +++ b/src/cli/azure/getAzureMetadata.ts @@ -1,13 +1,14 @@ import safesh from 'shell-escape-tag'; import { QuickPickItem, window } from 'vscode'; +import { kubeConfig } from 'cli/kubernetes/kubernetesConfig'; import { invokeKubectlCommand } from 'cli/kubernetes/kubernetesToolsKubectl'; -import { ShellResult, shell } from 'cli/shell/exec'; +import * as shell from 'cli/shell/exec'; +import { ShellResult } from 'cli/shell/exec'; import { ClusterProvider } from 'types/kubernetes/clusterProvider'; import { ConfigMap } from 'types/kubernetes/kubernetesTypes'; import { parseJson } from 'utils/jsonUtils'; import { AzureClusterProvider, AzureConstants } from './azureTools'; -import { kubeConfig } from 'cli/kubernetes/kubernetesConfig'; export interface AzureMetadata { resourceGroup: string; diff --git a/src/cli/checkVersions.ts b/src/cli/checkVersions.ts index 66b01b0f..93cdd891 100644 --- a/src/cli/checkVersions.ts +++ b/src/cli/checkVersions.ts @@ -1,12 +1,12 @@ import { commands, Uri, window } from 'vscode'; -import { enabledWGE, telemetry, enabledFluxChecks, suppressDebugMessages } from 'extension'; +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 { parseJson } from 'utils/jsonUtils'; -import { shell, shellCodeError } from './shell/exec'; import { clusterDataProvider } from 'ui/treeviews/treeViews'; +import { parseJson } from 'utils/jsonUtils'; +import { shellCodeError } from './shell/exec'; interface KubectlVersion { major: string; @@ -82,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, @@ -98,27 +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() { - if(enabledFluxChecks()) { - 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); - } - } - } else { - if(!suppressDebugMessages()) { - window.showInformationMessage('DEBUG: not running `flux check`'); - } - } -} /** * Return git version or undefined depending diff --git a/src/cli/flux/fluxTools.ts b/src/cli/flux/fluxTools.ts index 9fd554be..11369d76 100644 --- a/src/cli/flux/fluxTools.ts +++ b/src/cli/flux/fluxTools.ts @@ -1,8 +1,8 @@ import safesh from 'shell-escape-tag'; import { window } from 'vscode'; -import { shell } from 'cli/shell/exec'; -import { telemetry } from 'extension'; +import * as shell from 'cli/shell/exec'; +import { enabledFluxChecks, telemetry } from 'extension'; import { FluxSource, FluxTreeResources, FluxWorkload } from 'types/fluxCliTypes'; import { TelemetryError } from 'types/telemetryEventNames'; import { parseJson } from 'utils/jsonUtils'; @@ -64,15 +64,17 @@ class FluxTools { * https://github.com/fluxcd/flux2/blob/main/cmd/flux/check.go */ async check(context: string): Promise<{ prerequisites: FluxPrerequisite[]; controllers: FluxController[]; } | undefined> { - // cannot observe extension.enabledFluxChecks here, return type is specific; - + if (!enabledFluxChecks()) { + return undefined; + } + console.warn('NOOOOO flux check'); 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; } @@ -136,7 +138,7 @@ class FluxTools { if (treeShellResult.code !== 0) { telemetry.sendError(TelemetryError.FAILED_TO_RUN_FLUX_TREE); - window.showErrorMessage(`Failed to get resources created by the workload ${name}. ERROR: ${treeShellResult?.stderr}`); + window.showErrorMessage(`Failed to get resources created by the kustomization ${name}. ERROR: ${treeShellResult?.stderr}`); return; } diff --git a/src/cli/git/gitInfo.ts b/src/cli/git/gitInfo.ts index f127eebc..492d324a 100644 --- a/src/cli/git/gitInfo.ts +++ b/src/cli/git/gitInfo.ts @@ -3,7 +3,7 @@ import path from 'path'; import { window } from 'vscode'; import { checkGitVersion } from 'cli/checkVersions'; -import { shell } from 'cli/shell/exec'; +import * as shell from 'cli/shell/exec'; import { makeSSHUrlFromGitUrl } from 'commands/createSource'; import { GitRepository } from 'types/flux/gitRepository'; import { getGitRepositories } from 'cli/kubernetes/kubectlGet'; diff --git a/src/cli/kubernetes/apiResources.ts b/src/cli/kubernetes/apiResources.ts index 26702541..314e47a7 100644 --- a/src/cli/kubernetes/apiResources.ts +++ b/src/cli/kubernetes/apiResources.ts @@ -1,7 +1,9 @@ -import { telemetry } from 'extension'; +import { setVSCodeContext, telemetry } from 'extension'; import { TelemetryError } from 'types/telemetryEventNames'; import { invokeKubectlCommand } from './kubernetesToolsKubectl'; import { Kind } from 'types/kubernetes/kubernetesTypes'; +import { createK8sClients } from 'k8s/client'; +import { ContextId } from 'types/extensionIds'; type KindApiParams = { @@ -44,6 +46,7 @@ export async function loadAvailableResourceKinds() { if (kindsShellResult?.code !== 0) { telemetry.sendError(TelemetryError.FAILED_TO_GET_AVAILABLE_RESOURCE_KINDS); console.warn(`Failed to get resource kinds: ${kindsShellResult?.stderr}`); + setVSCodeContext(ContextId.ClusterUnreachable, true); return; } @@ -73,4 +76,7 @@ export async function loadAvailableResourceKinds() { }); console.log('apiResources loaded'); + + setVSCodeContext(ContextId.ClusterUnreachable, false); + createK8sClients(); } diff --git a/src/cli/kubernetes/clusterProvider.ts b/src/cli/kubernetes/clusterProvider.ts index 1d98ab29..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. @@ -102,13 +102,3 @@ async function isClusterAzureARC(context: string): Promise { return ClusterProvider.Generic; } -/** - * Return true if gitops is enabled in the current cluster. - * Function checks if `flux-system` namespace contains flux controllers. - * @param contextName target cluster name - */ -export async function isGitOpsEnabled(contextName: string) { - const fluxControllers = await getFluxControllers(contextName); - - return fluxControllers.length !== 0; -} diff --git a/src/cli/kubernetes/kubectlGet.ts b/src/cli/kubernetes/kubectlGet.ts index 59698ee0..6ad1b478 100644 --- a/src/cli/kubernetes/kubectlGet.ts +++ b/src/cli/kubernetes/kubectlGet.ts @@ -1,6 +1,6 @@ import safesh from 'shell-escape-tag'; -import { telemetry } from 'extension'; +import { setVSCodeContext, telemetry } from 'extension'; import { k8sList } from 'k8s/list'; import { Bucket } from 'types/flux/bucket'; import { GitOpsTemplate } from 'types/flux/gitOpsTemplate'; @@ -15,6 +15,7 @@ import { parseJson, parseJsonItems } from 'utils/jsonUtils'; import { invokeKubectlCommand } from './kubernetesToolsKubectl'; import { getAvailableResourcePlurals } from './apiResources'; import { window } from 'vscode'; +import { ContextId } from 'types/extensionIds'; /** * RegExp for the Error that should not be sent in telemetry. * Server doesn't have a resource type = when GitOps not enabled @@ -104,12 +105,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 +143,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); diff --git a/src/cli/kubernetes/kubectlProxy.ts b/src/cli/kubernetes/kubectlProxy.ts index 11c01661..b1d25cf5 100644 --- a/src/cli/kubernetes/kubectlProxy.ts +++ b/src/cli/kubernetes/kubectlProxy.ts @@ -1,12 +1,12 @@ -import * as k8s from '@kubernetes/client-node'; +import { KubeConfig } from '@kubernetes/client-node'; import { ChildProcess } from 'child_process'; -import { kubeConfig } from 'cli/kubernetes/kubernetesConfig'; -import { shell } from 'cli/shell/exec'; -import { createK8sClients, destroyK8sClients } from 'k8s/client'; +import * as shell from 'cli/shell/exec'; +import { destroyK8sClients } from 'k8s/client'; import { createProxyConfig } from 'k8s/createKubeProxyConfig'; // let isConnecting = false; export let proxyProc: ChildProcess | undefined; +export let kubeProxyConfig: KubeConfig | undefined; // tries to keep alive the `kubectl proxy` process // if process dies or errors out it will be stopped @@ -50,10 +50,8 @@ function procListen(p: ChildProcess) { 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); + console.log('~kubeproxy config ready'); } }); @@ -71,10 +69,11 @@ export async function stopKubeProxy() { proxyProc.kill(); } proxyProc = undefined; + kubeProxyConfig = undefined; destroyK8sClients(); // isConnecting = false; - console.log('stopped kube proxy'); + console.log('~stopped kube proxy'); } } diff --git a/src/cli/kubernetes/kubernetesConfig.ts b/src/cli/kubernetes/kubernetesConfig.ts index d7c9ad95..31a08bcd 100644 --- a/src/cli/kubernetes/kubernetesConfig.ts +++ b/src/cli/kubernetes/kubernetesConfig.ts @@ -34,32 +34,35 @@ export async function syncKubeConfig(forceReloadResourceKinds = false) { const newKubeConfig = new k8s.KubeConfig(); newKubeConfig.loadFromString(configShellResult.stdout, {onInvalidEntry: ActionOnInvalid.FILTER}); + if (kcTextChanged(kubeConfig, newKubeConfig)) { - const contextsListChanged = kcContextsListChanged(kubeConfig, newKubeConfig); - const contextChanged = kcCurrentContextChanged(kubeConfig, newKubeConfig); - - // load the changed kubeconfig globally so that subsequent commands use the new config - kubeConfig.loadFromString(configShellResult.stdout); - - if(contextsListChanged) { - refreshClustersTreeView(); - } - - if(contextChanged || forceReloadResourceKinds) { - await 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(); + loadAvailableResourceKinds(); + } +} + +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 (contextChanged || forceReloadResourceKinds) { + loadAvailableResourceKinds(); + } + + if (contextChanged) { + console.log('currentContext changed', kubeConfig.getCurrentContext()); + vscodeOnCurrentContextChanged(); + await restartKubeProxy(); + // give proxy a chance to start + setTimeout(() => { + refreshAllTreeViews(); + }, 100); + } else if (contextsListChanged) { + refreshClustersTreeView(); } } @@ -69,6 +72,7 @@ async function vscodeOnCurrentContextChanged() { setVSCodeContext(ContextId.NoSources, false); setVSCodeContext(ContextId.NoWorkloads, false); setVSCodeContext(ContextId.FailedToLoadClusterContexts, false); + setVSCodeContext(ContextId.ClusterUnreachable, false); } /** diff --git a/src/cli/kubernetes/kubernetesConfigWatcher.ts b/src/cli/kubernetes/kubernetesConfigWatcher.ts index 91c90bee..808ce676 100644 --- a/src/cli/kubernetes/kubernetesConfigWatcher.ts +++ b/src/cli/kubernetes/kubernetesConfigWatcher.ts @@ -39,7 +39,6 @@ export async function initKubeConfigWatcher() { }); restartFsWatcher(); - } diff --git a/src/cli/kubernetes/kubernetesToolsKubectl.ts b/src/cli/kubernetes/kubernetesToolsKubectl.ts index ae8fb53a..bbef8619 100644 --- a/src/cli/kubernetes/kubernetesToolsKubectl.ts +++ b/src/cli/kubernetes/kubernetesToolsKubectl.ts @@ -1,6 +1,7 @@ import { window, workspace } from 'vscode'; import * as kubernetes from 'vscode-kubernetes-tools-api'; +import * as shell from 'cli/shell/exec'; import { output } from 'cli/shell/output'; import { telemetry } from 'extension'; import { TelemetryError } from 'types/telemetryEventNames'; @@ -45,8 +46,11 @@ export async function invokeKubectlCommand(command: string, printOutput = true): } let kubectlShellResult; - const commandWithArgs = `${command} --request-timeout ${getRequestTimeout()}`; - kubectlShellResult = await kubectl.invokeCommand(commandWithArgs); + const commandWithArgs = `kubectl ${command} --request-timeout ${getRequestTimeout()}`; + const t1 = Date.now(); + kubectlShellResult = await shell.exec(commandWithArgs); + const t2 = Date.now(); + console.log(`exec ${command} ∆`, t2 - t1); if(printOutput) { @@ -75,6 +79,6 @@ export async function invokeKubectlCommand(command: string, printOutput = true): function getRequestTimeout(): string { - return workspace.getConfiguration('gitops').get('kubectlTimeout') || '20s'; + return workspace.getConfiguration('gitops').get('kubectlRequestTimeout') || '20s'; } diff --git a/src/cli/shell/exec.ts b/src/cli/shell/exec.ts index a43639f3..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. @@ -60,14 +63,14 @@ export type ShellHandler = (code: number, stdout: string, stderr: string)=> void /** * Return `true` when user has windows OS. */ -function isWindows(): boolean { +export function isWindows(): boolean { return (process.platform === WINDOWS) && !getUseWsl(); } /** * Return `true` when user has Unix OS. */ -function isUnix(): boolean { +export function isUnix(): boolean { return !isWindows(); } @@ -75,7 +78,7 @@ function isUnix(): boolean { * Return user platform. * For WSL - return Linux. */ -function platform(): Platform { +export function platform(): Platform { if (getUseWsl()) { return Platform.Linux; } @@ -115,7 +118,9 @@ function execOpts({ cwd }: { cwd?: string; } = {}): shelljs.ExecOptions { return opts; } -async function exec(cmd: string, { cwd, callback }: { cwd?: string; callback?: ProcCallback;} = {}): Promise { +export async function exec( + cmd: string, + { cwd, callback }: { cwd?: string; callback?: ProcCallback; } = {}): Promise { try { return await execCore(cmd, execOpts({ cwd }), callback); } catch (e) { @@ -133,7 +138,7 @@ async function exec(cmd: string, { cwd, callback }: { cwd?: string; callback?: P * Execute command in cli and send the text to vscode output view. * @param cmd CLI command string */ -async function execWithOutput( +export async function execWithOutput( cmd: string, { revealOutputView = true, @@ -168,6 +173,7 @@ async function execWithOutput( cwd: cwd, env: execOpts().env, }); + setExecTimeoutKill(childProcess); let stdout = ''; let stderr = ''; @@ -190,6 +196,10 @@ 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, @@ -205,7 +215,11 @@ 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); } @@ -215,7 +229,7 @@ function execCore(cmd: string, opts: any, callback?: ProcCallback, stdin?: strin }); } -function execProc(cmd: string): ChildProcess { +export function execProc(cmd: string): ChildProcess { const opts = execOpts(); if (getUseWsl()) { cmd = `wsl ${cmd}`; @@ -225,12 +239,26 @@ function execProc(cmd: string): ChildProcess { return proc; } -export const shell = { - isWindows : isWindows, - isUnix : isUnix, - platform : platform, - exec : exec, - execProc: execProc, - // execCore : execCore, - execWithOutput: execWithOutput, -}; +function setExecTimeoutKill(proc: ChildProcess) { + const timeout = workspace.getConfiguration('gitops').get('execTimeout') as string; + const timeoutSeconds = parseInt(timeout); + if (isNaN(timeoutSeconds) || timeoutSeconds === 0) { + return; + } + + setTimeout(() => { + if (proc.exitCode === null) { + 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/expandAll.ts b/src/commands/expandAll.ts index 6f9b660b..41361ba2 100644 --- a/src/commands/expandAll.ts +++ b/src/commands/expandAll.ts @@ -3,11 +3,9 @@ import { sourceDataProvider, sourceTreeView, workloadDataProvider } from 'ui/tre import { TreeItemCollapsibleState } from 'vscode'; export async function expandAllSources() { - sourceDataProvider.expandNewTree = true; - sourceDataProvider.refresh(); + sourceDataProvider.expandAll(); } export async function expandAllWorkloads() { - workloadDataProvider.expandNewTree = true; - workloadDataProvider.refresh(); + sourceDataProvider.expandAll(); } diff --git a/src/commands/fluxCheck.ts b/src/commands/fluxCheck.ts index aa775675..1351710b 100644 --- a/src/commands/fluxCheck.ts +++ b/src/commands/fluxCheck.ts @@ -1,6 +1,6 @@ import safesh from 'shell-escape-tag'; -import { shell } from 'cli/shell/exec'; +import * as shell from 'cli/shell/exec'; import { ClusterNode } from 'ui/treeviews/nodes/cluster/clusterNode'; import { enabledFluxChecks, suppressDebugMessages } from 'extension'; import { window } from 'vscode'; @@ -10,12 +10,5 @@ import { window } from 'vscode'; * @param clusterNode target cluster node (from tree node context menu) */ export async function fluxCheck(clusterNode: ClusterNode) { - if(enabledFluxChecks()) { - shell.execWithOutput(safesh`flux check --context ${clusterNode.context.name}`); - } else { - // user called for health checking, notify them it isn't being performed - if(!suppressDebugMessages()) { - window.showInformationMessage('DEBUG: not running `flux check`'); - } - } + shell.execWithOutput(safesh`flux check --context ${clusterNode.context.name}`); } diff --git a/src/commands/fluxCheckPrerequisites.ts b/src/commands/fluxCheckPrerequisites.ts index 94816674..e21ca642 100644 --- a/src/commands/fluxCheckPrerequisites.ts +++ b/src/commands/fluxCheckPrerequisites.ts @@ -1,16 +1,9 @@ -import { shell } from 'cli/shell/exec'; +import * as shell from 'cli/shell/exec'; import { enabledFluxChecks } from 'extension'; /** * Runs `flux check --pre` command in the output view. */ export async function checkFluxPrerequisites() { - if(enabledFluxChecks()) { - // Missing debug message here - // Ref: https://github.com/weaveworks/vscode-gitops-tools/pull/459 - // whenever we're skipping a call to `flux check` we should emit a skippable ... - return await shell.execWithOutput('flux check --pre'); - } else { - return true; - } + return await shell.execWithOutput('flux check --pre'); } diff --git a/src/commands/installFluxCli.ts b/src/commands/installFluxCli.ts index 1b4e9399..f52c893f 100644 --- a/src/commands/installFluxCli.ts +++ b/src/commands/installFluxCli.ts @@ -6,13 +6,14 @@ import path from 'path'; import request from 'request'; import { commands, window } from 'vscode'; -import { Platform, shell } from 'cli/shell/exec'; +import * as shell from 'cli/shell/exec'; +import { Platform } from 'cli/shell/exec'; import { output } from 'cli/shell/output'; import { runTerminalCommand } from 'cli/shell/terminal'; +import { refreshAllTreeViewsCommand } from 'commands/refreshTreeViews'; import { GlobalStateKey } from 'data/globalState'; import { globalState } from 'extension'; import { Errorable, failed } from 'types/errorable'; -import { refreshAllTreeViewsCommand } from 'commands/refreshTreeViews'; import { appendToPathEnvironmentVariableWindows, createDir, deleteFile, downloadFile, getAppdataPath, moveFile, readFile, unzipFile } from 'utils/fsUtils'; const fluxGitHubUserProject = 'fluxcd/flux2'; diff --git a/src/commands/pullGitRepository.ts b/src/commands/pullGitRepository.ts index d61b7115..82f47f24 100644 --- a/src/commands/pullGitRepository.ts +++ b/src/commands/pullGitRepository.ts @@ -5,7 +5,7 @@ import safesh from 'shell-escape-tag'; import { commands, Uri, window } from 'vscode'; import { checkGitVersion } from 'cli/checkVersions'; -import { shell } from 'cli/shell/exec'; +import * as shell from 'cli/shell/exec'; import { telemetry } from 'extension'; import { TelemetryError } from 'types/telemetryEventNames'; import { GitRepositoryNode } from 'ui/treeviews/nodes/source/gitRepositoryNode'; 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/extension.ts b/src/extension.ts index ea891c03..ddd7cc04 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -3,8 +3,8 @@ 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 { shell } from './cli/shell/exec'; +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'; @@ -13,7 +13,7 @@ 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'; @@ -45,19 +45,10 @@ export async function activate(context: ExtensionContext) { telemetry = new Telemetry(context, getExtensionVersion(), GitOpsExtensionConstants.ExtensionId); - await syncKubeConfig(true); - await initKubeConfigWatcher(); - - // schedule load start for tree view data for the event loop - // then k8s proxy client is more likely to be ready - // to avoid the slower kubectl client - setTimeout(() => { - createTreeViews(); - }, 100); + initData(); // register gitops commands registerCommands(context); - kubeProxyKeepAlive(); telemetry.send(TelemetryEvent.Startup); @@ -80,12 +71,8 @@ export async function activate(context: ExtensionContext) { } - // show error notification if flux is not installed - const fluxFoundResult = await promptToInstallFlux(); - if (succeeded(fluxFoundResult)) { - // check flux prerequisites - checkFluxPrerequisites(); - } + // check version and show 'Install Flux?' dialog if flux is not installed + checkInstalledFluxVersion(); checkWGEVersion(); @@ -100,6 +87,17 @@ export async function activate(context: ExtensionContext) { return api; } +async function initData() { + syncKubeConfig(true); + initKubeConfigWatcher(); + kubeProxyKeepAlive(); + + // wait for kubectl proxy to start for faster initial tree view loading + // setTimeout(() => { + createTreeViews(); + // }, 200); +} + function listenExtensionConfigChanged() { workspace.onDidChangeConfiguration(async e => { if(!e.affectsConfiguration('gitops.weaveGitopsEnterprise')) { @@ -119,21 +117,12 @@ export function enabledWGE(): boolean { } export function enabledFluxChecks(): boolean { - let ret = workspace.getConfiguration('gitops').get('doFluxCheck'); - if(ret === false) { - return false; - } else { - return true; - } + return workspace.getConfiguration('gitops').get('doFluxCheck') || false; + } export function suppressDebugMessages(): boolean { - let ret = workspace.getConfiguration('gitops').get('suppressDebugMessages'); - if(ret === true) { - return true; - } else { - return false; - } + return workspace.getConfiguration('gitops').get('suppressDebugMessages') || false; } 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..3a43eba1 100644 --- a/src/k8s/informers.ts +++ b/src/k8s/informers.ts @@ -18,6 +18,8 @@ export function createInformers(kc: k8s.KubeConfig) { FluxWorkloadKinds.forEach(kind => { createInformer(kc, workloadDataProvider, kind); }); + + console.log('*- informers started'); } export function destroyInformers() { @@ -58,6 +60,7 @@ async function createInformer(kc: k8s.KubeConfig, receiver: KubernetesObjectData function registerInformerEvents(informer: k8s.Informer, receiver: KubernetesObjectDataProvider) { informer?.on('add', (obj: KubernetesObject) => { + receiver.add(obj); }); diff --git a/src/k8s/list.ts b/src/k8s/list.ts index 5ec4579a..8922fa84 100644 --- a/src/k8s/list.ts +++ b/src/k8s/list.ts @@ -26,7 +26,7 @@ export async function k8sList(kind: Kind): Promise { if(!k8sCoreApi) { - console.log('k8sList no k8sCustomApi'); + console.log('k8sList no k8sCoreApi'); return; } diff --git a/src/types/extensionIds.ts b/src/types/extensionIds.ts index f461f9ca..78d609b3 100644 --- a/src/types/extensionIds.ts +++ b/src/types/extensionIds.ts @@ -83,6 +83,7 @@ export const enum CommandId { export const enum ContextId { NoClusterSelected = 'gitops:noClusterSelected', CurrentClusterGitOpsNotEnabled = 'gitops:currentClusterGitOpsNotEnabled', + ClusterUnreachable = 'gitops:clusterUnreachable', LoadingClusters = 'gitops:loadingClusters', LoadingSources = 'gitops:loadingSources', 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/clusterDataProvider.ts b/src/ui/treeviews/dataProviders/clusterDataProvider.ts index 2893315d..93d223c4 100644 --- a/src/ui/treeviews/dataProviders/clusterDataProvider.ts +++ b/src/ui/treeviews/dataProviders/clusterDataProvider.ts @@ -1,14 +1,10 @@ -import { fluxTools } from 'cli/flux/fluxTools'; -import { getFluxControllers } from 'cli/kubernetes/kubectlGet'; import { kubeConfig } from 'cli/kubernetes/kubernetesConfig'; -import { enabledFluxChecks, setVSCodeContext, suppressDebugMessages } from 'extension'; +import { setVSCodeContext } from 'extension'; import { ContextId } from 'types/extensionIds'; import { statusBar } from 'ui/statusBar'; -import { TreeItem, window } from 'vscode'; -import { ClusterDeploymentNode } from '../nodes/cluster/clusterDeploymentNode'; +import { TreeItem } from 'vscode'; import { ClusterNode } from '../nodes/cluster/clusterNode'; import { TreeNode } from '../nodes/treeNode'; -import { refreshClustersTreeView, revealClusterNode } from '../treeViews'; import { DataProvider } from './dataProvider'; /** @@ -16,24 +12,32 @@ import { DataProvider } from './dataProvider'; * 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[] = []; + protected nodes: ClusterNode[] = []; public getCurrentClusterNode(): ClusterNode | undefined { - return this.clusterNodes.find(c => c.context.name === kubeConfig?.getCurrentContext()); + return this.nodes.find(c => c.context.name === kubeConfig?.getCurrentContext()); } - public refreshCurrentNode() { - this.refresh(this.getCurrentClusterNode()); + public redrawCurrentNode() { + this.redraw(this.getCurrentClusterNode()); + } + + + public async getChildren(element?: TreeItem): Promise { + if(!element) { + return this.getRootNodes(); + } else if (element instanceof TreeNode) { + return element.children; + } + + return []; } + /** * Check if the cluster node exists or not. */ - public includesTreeNode(treeItem: TreeItem, clusterNodes: TreeNode[] = this.clusterNodes) { + public includesTreeNode(treeItem: TreeItem, clusterNodes: TreeNode[] = this.nodes) { for (const clusterNode of clusterNodes) { if (treeItem === clusterNode) { return true; @@ -46,32 +50,35 @@ export class ClusterDataProvider extends DataProvider { return false; } + /** * Creates Clusters tree view items from local kubernetes config. */ - async buildTree(): Promise { + async loadRootNodes() { + console.log('+ started loadClusterNodes'); + + const t1 = Date.now(); + setVSCodeContext(ContextId.FailedToLoadClusterContexts, false); setVSCodeContext(ContextId.NoClusters, false); setVSCodeContext(ContextId.LoadingClusters, true); statusBar.startLoadingTree(); - this.clusterNodes = []; + this.nodes = []; if (!kubeConfig) { setVSCodeContext(ContextId.NoClusters, false); setVSCodeContext(ContextId.FailedToLoadClusterContexts, true); setVSCodeContext(ContextId.LoadingClusters, false); statusBar.stopLoadingTree(); - return []; + return; } - const clusterNodes: ClusterNode[] = []; - let currentContextTreeItem: ClusterNode | undefined; - process.nextTick(() => {}); + let currentContextTreeItem: ClusterNode | undefined; if (kubeConfig.getContexts().length === 0) { setVSCodeContext(ContextId.NoClusters, true); - return []; + return; } for (const context of kubeConfig.getContexts()) { @@ -79,68 +86,17 @@ export class ClusterDataProvider extends DataProvider { if (context.name === kubeConfig.getCurrentContext()) { currentContextTreeItem = clusterNode; clusterNode.makeCollapsible(); - // load flux system deployments - const fluxControllers = await getFluxControllers(); - if (fluxControllers) { - clusterNode.expand(); - revealClusterNode(clusterNode, { - expand: true, - }); - for (const deployment of fluxControllers) { - clusterNode.addChild(new ClusterDeploymentNode(deployment)); - } - } } - clusterNodes.push(clusterNode); + this.nodes.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; - 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; - } - if(enabledFluxChecks()){ // disable nixes health checking on the cluster - 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(); - - if (clusterControllerName === deploymentName) { - clusterController.description = controller.status; - if (controller.success) { - clusterController.setStatus('success'); - } else { - clusterController.setStatus('failure'); - } - } - } - refreshClustersTreeView(clusterController); - } - } else { - if(!suppressDebugMessages()) { - window.showInformationMessage('DEBUG: not running `flux check`'); - } - } + const t2 = Date.now(); + console.log('+ loadClusterNodes ∆', t2 - t1); } } diff --git a/src/ui/treeviews/dataProviders/dataProvider.ts b/src/ui/treeviews/dataProviders/dataProvider.ts index 4de95e8d..ee98c359 100644 --- a/src/ui/treeviews/dataProviders/dataProvider.ts +++ b/src/ui/treeviews/dataProviders/dataProvider.ts @@ -1,26 +1,30 @@ -import { Event, EventEmitter, TreeDataProvider, TreeItem } from 'vscode'; +import { Event, EventEmitter, TreeDataProvider, TreeItem, TreeItemCollapsibleState } 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 nodes: TreeNode[] = []; + protected collapsibleStates = new Map(); + + protected loading = false; + 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) { + const allStr = treeItem ? 'ALL' : treeItem; + console.log(`## ${this.constructor.name} refresh`, allStr); + if (!treeItem) { - // Only clear all root nodes when no node was passed - this.treeItems = null; + this.reloadData(); } + this.redraw(treeItem); + } + + public redraw(treeItem?: TreeItem) { this._onDidChangeTreeData.fire(treeItem); } @@ -45,34 +49,70 @@ export class DataProvider implements TreeDataProvider { 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) { + return this.getRootNodes(); + } else if (element instanceof TreeNode) { + return element.children; } - if (element instanceof TreeNode) { - return element.children; + return []; + } + + + protected async getRootNodes(): Promise { + if (this.loading) { + return []; } + return this.nodes; + } - if (!element && this.treeItems) { - return this.treeItems; + async reloadData() { + const t1 = Date.now(); + + console.log(`# started ${this.constructor.name} reloadData`); + if(this.loading) { + return; } - return []; + this.loading = true; + this.saveCollapsibleStates(); + this.nodes = []; + await this.loadRootNodes(); + this.loadCollapsibleStates(); + this.loading = false; + this.redraw(); + + const t2 = Date.now(); + console.log(`# finished ${this.constructor.name} reloadData ∆`, t2 - t1); } - /** - * Creates initial tree view items collection. - * @returns - */ - buildTree(): Promise { - return Promise.resolve([]); + async loadRootNodes() { + this.nodes = []; } + 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/ui/treeviews/dataProviders/kubernetesObjectDataProvider.ts b/src/ui/treeviews/dataProviders/kubernetesObjectDataProvider.ts index a48cb031..13cdf2ed 100644 --- a/src/ui/treeviews/dataProviders/kubernetesObjectDataProvider.ts +++ b/src/ui/treeviews/dataProviders/kubernetesObjectDataProvider.ts @@ -1,10 +1,11 @@ import { getNamespace } from 'cli/kubernetes/kubectlGetNamespace'; 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 { TreeNode } from '../nodes/treeNode'; import { DataProvider } from './dataProvider'; -import { sortNodes } from 'utils/treeNodeUtils'; /** * Superclass for data providers that group objects by namespace: Source and Workload data providers @@ -12,7 +13,7 @@ import { sortNodes } from 'utils/treeNodeUtils'; export abstract class KubernetesObjectDataProvider extends DataProvider { 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 { @@ -41,24 +42,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,7 +74,8 @@ export abstract class KubernetesObjectDataProvider extends DataProvider { if(node && node.resource) { node.resource = object; node.updateStatus(); - this._onDidChangeTreeData.fire(node); + namespaceNode.updateLabel(); + this.redraw(namespaceNode); } } @@ -89,13 +93,28 @@ 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 + [this.nodes] = await groupNodesByNamespace(resourceNodes, true, true); + this.redraw(); + } + } diff --git a/src/ui/treeviews/dataProviders/sourceDataProvider.ts b/src/ui/treeviews/dataProviders/sourceDataProvider.ts index a52cb184..1017552a 100644 --- a/src/ui/treeviews/dataProviders/sourceDataProvider.ts +++ b/src/ui/treeviews/dataProviders/sourceDataProvider.ts @@ -1,17 +1,16 @@ import { getBuckets, getGitRepositories, getHelmRepositories, getOciRepositories } from 'cli/kubernetes/kubectlGet'; +import { getNamespaces } from 'cli/kubernetes/kubectlGetNamespace'; import { setVSCodeContext } from 'extension'; import { ContextId } from 'types/extensionIds'; import { statusBar } from 'ui/statusBar'; import { sortByMetadataName } from 'utils/sortByMetadataName'; import { groupNodesByNamespace } from '../../../utils/treeNodeUtils'; -import { NamespaceNode } from '../nodes/namespaceNode'; 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 @@ -21,12 +20,11 @@ export class SourceDataProvider extends KubernetesObjectDataProvider { /** * 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[] = []; + const sourceNodes: SourceNode[] = []; setVSCodeContext(ContextId.LoadingSources, true); @@ -41,29 +39,27 @@ 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); + setVSCodeContext(ContextId.NoSources, sourceNodes.length === 0); statusBar.stopLoadingTree(); - const [groupedNodes] = await groupNodesByNamespace(treeNodes, this.expandNewTree); - this.expandNewTree = false; - return groupedNodes; + [this.nodes] = await groupNodesByNamespace(sourceNodes, false, true); } } diff --git a/src/ui/treeviews/dataProviders/workloadDataProvider.ts b/src/ui/treeviews/dataProviders/workloadDataProvider.ts index f6069cd5..55ef3f56 100644 --- a/src/ui/treeviews/dataProviders/workloadDataProvider.ts +++ b/src/ui/treeviews/dataProviders/workloadDataProvider.ts @@ -3,30 +3,25 @@ import { getChildrenOfWorkload, getHelmReleases, getKustomizations } from 'cli/k import { getNamespaces } from 'cli/kubernetes/kubectlGetNamespace'; import { setVSCodeContext } from 'extension'; import { ContextId } from 'types/extensionIds'; -import { Namespace } from 'types/kubernetes/kubernetesTypes'; import { statusBar } from 'ui/statusBar'; -import { addFluxTreeToNode, groupNodesByNamespace } from 'utils/treeNodeUtils'; 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 { TreeNode, TreeNodeIcon } 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 { /** * 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[] = []; @@ -57,10 +52,7 @@ export class WorkloadDataProvider extends KubernetesObjectDataProvider { setVSCodeContext(ContextId.NoWorkloads, workloadNodes.length === 0); statusBar.stopLoadingTree(); - const [groupedNodes] = await groupNodesByNamespace(workloadNodes, this.expandNewTree); - this.expandNewTree = false; - - return groupedNodes; + [this.nodes] = await groupNodesByNamespace(workloadNodes, false, true); } /** @@ -69,6 +61,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 +75,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 = [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 +97,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 = [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 +115,12 @@ export class WorkloadDataProvider extends KubernetesObjectDataProvider { const [groupedNodes, clusterScopedNodes] = await groupNodesByNamespace(childrenNodes); node.children = [...groupedNodes, ...clusterScopedNodes]; - refreshWorkloadsTreeView(node); + this.redraw(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; - } - } +function failedToLoad() { + const node = new TreeNode('Failed to load'); + node.setIcon(TreeNodeIcon.Disconnected); + return node; } diff --git a/src/ui/treeviews/nodes/cluster/clusterNode.ts b/src/ui/treeviews/nodes/cluster/clusterNode.ts index d7850b9f..27ddfe39 100644 --- a/src/ui/treeviews/nodes/cluster/clusterNode.ts +++ b/src/ui/treeviews/nodes/cluster/clusterNode.ts @@ -3,16 +3,18 @@ import { ExtensionMode, MarkdownString } from 'vscode'; import { fluxVersion } from 'cli/checkVersions'; -import { detectClusterProvider, isGitOpsEnabled } from 'cli/kubernetes/clusterProvider'; +import { fluxTools } from 'cli/flux/fluxTools'; +import { detectClusterProvider } from 'cli/kubernetes/clusterProvider'; +import { getFluxControllers } from 'cli/kubernetes/kubectlGet'; import { kubeConfig } from 'cli/kubernetes/kubernetesConfig'; 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 { createContextMarkdownTable, createMarkdownHr } from 'utils/markdownUtils'; import { TreeNode } from '../treeNode'; -import { clusterDataProvider } from 'ui/treeviews/treeViews'; +import { ClusterDeploymentNode } from './clusterDeploymentNode'; /** * Defines Cluster context tree view item for displaying @@ -68,7 +70,23 @@ export class ClusterNode extends TreeNode { * - Cluster provider. */ async updateNodeContext() { - this.isGitOpsEnabled = await isGitOpsEnabled(this.context.name); + const fluxControllers = await getFluxControllers(this.context.name); + this.isGitOpsEnabled = fluxControllers.length !== 0; + + 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); + } const clusterMetadata = globalState.getClusterMetadata(this.cluster?.name || this.context.name); if (clusterMetadata?.clusterProvider) { @@ -87,9 +105,44 @@ export class ClusterNode extends TreeNode { this.setIcon('cloud'); } - clusterDataProvider.refresh(this); + clusterDataProvider.redraw(); + this.updateDeploymentStatus(); } + /** + * Update deployment status for flux controllers. + * Get status from running flux commands instead of kubectl. + */ + private async updateDeploymentStatus() { + if (this.children.length === 0) { + return; + } + const fluxCheckResult = await fluxTools.check(this.context.name); + if (!fluxCheckResult) { + return; + } + + // Match controllers fetched with flux with controllers + // fetched with kubectl and update tree nodes. + for (const clusterController of (this.children as ClusterDeploymentNode[])) { + 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/namespaceNode.ts b/src/ui/treeviews/nodes/namespaceNode.ts index 17c349aa..a2fbf1eb 100644 --- a/src/ui/treeviews/nodes/namespaceNode.ts +++ b/src/ui/treeviews/nodes/namespaceNode.ts @@ -1,6 +1,9 @@ import { Kind, Namespace } from 'types/kubernetes/kubernetesTypes'; -import { TreeNode } from './treeNode'; +import { TreeNode, TreeNodeIcon } from './treeNode'; import { TreeItemCollapsibleState } from 'vscode'; +import { SourceNode } from './source/sourceNode'; +import { WorkloadNode } from './workload/workloadNode'; +import { read } from 'fs'; /** * Defines any kubernetes resourse. @@ -24,9 +27,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..021bc880 100644 --- a/src/ui/treeviews/nodes/treeNode.ts +++ b/src/ui/treeviews/nodes/treeNode.ts @@ -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) { diff --git a/src/ui/webviews/createFromTemplate/receiveMessage.ts b/src/ui/webviews/createFromTemplate/receiveMessage.ts index 8aaa6381..94d4ae7b 100644 --- a/src/ui/webviews/createFromTemplate/receiveMessage.ts +++ b/src/ui/webviews/createFromTemplate/receiveMessage.ts @@ -1,4 +1,4 @@ -import { shell } from 'cli/shell/exec'; +import * as shell from 'cli/shell/exec'; import { v4 as uuidv4 } from 'uuid'; import { Uri, WebviewPanel, workspace } from 'vscode'; diff --git a/src/utils/fsUtils.ts b/src/utils/fsUtils.ts index 6019bb38..0e1e37dc 100644 --- a/src/utils/fsUtils.ts +++ b/src/utils/fsUtils.ts @@ -4,7 +4,7 @@ import fs from 'fs'; import https from 'https'; import path from 'path'; -import { shell } from 'cli/shell/exec'; +import * as shell from 'cli/shell/exec'; import { Errorable } from 'types/errorable'; /** 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/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"