diff --git a/package.json b/package.json index 6569e3ae..e8a674b2 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" }, { @@ -306,74 +311,15 @@ } }, "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 && !gitops:clusterUnreachable" - }, - { - "view": "gitops.views.sources", - "contents": "Loading Sources ...", - "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 && !gitops:clusterUnreachable" - }, - { - "view": "gitops.views.sources", - "contents": "Cluster unreachable", - "when": "gitops:clusterUnreachable" - }, - { - "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 && !gitops:clusterUnreachable" - }, - { - "view": "gitops.views.workloads", - "contents": "Loading Workloads ...", - "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 && !gitops:clusterUnreachable" - }, - { - "view": "gitops.views.workloads", - "contents": "Cluster unreachable", - "when": "gitops:clusterUnreachable" - }, - { - "view": "gitops.views.workloads", - "contents": "Select GitOps Cluster to view Workloads.", - "when": "gitops:noClusterSelected" - }, - { - "view": "gitops.views.documentation", - "contents": "Loading Topics ..." } ], "menus": { @@ -388,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", diff --git a/src/cli/flux/fluxTools.ts b/src/cli/flux/fluxTools.ts index 11369d76..ae86514f 100644 --- a/src/cli/flux/fluxTools.ts +++ b/src/cli/flux/fluxTools.ts @@ -67,7 +67,6 @@ class FluxTools { 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) { @@ -134,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.showErrorMessage(`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 314e47a7..89772452 100644 --- a/src/cli/kubernetes/apiResources.ts +++ b/src/cli/kubernetes/apiResources.ts @@ -4,7 +4,15 @@ import { invokeKubectlCommand } from './kubernetesToolsKubectl'; import { Kind } from 'types/kubernetes/kubernetesTypes'; import { createK8sClients } from 'k8s/client'; import { ContextId } from 'types/extensionIds'; - +import { refreshAllTreeViews, refreshResourcesTreeViews } from 'commands/refreshTreeViews'; +import { restartKubeProxy } from './kubectlProxy'; +import { clusterDataProvider } from 'ui/treeviews/treeViews'; + +export enum ApiState { + Loading, + Loaded, + ClusterUnreachable, +} type KindApiParams = { plural: string; // configmaps, deployments, gitrepositories, ... @@ -12,6 +20,8 @@ type KindApiParams = { version: string; // v1, v1beta2, ... }; +export let apiState: ApiState = ApiState.Loading; + /* * Current cluster supported kubernetes resource kinds. */ @@ -41,12 +51,16 @@ export function getAPIParams(kind: Kind): KindApiParams | undefined { export async function loadAvailableResourceKinds() { apiResources = undefined; + apiState = ApiState.Loading; 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}`); + apiState = ApiState.ClusterUnreachable; setVSCodeContext(ContextId.ClusterUnreachable, true); + clusterDataProvider.updateCurrentContextChildNodes(); + refreshResourcesTreeViews(); return; } @@ -77,6 +91,15 @@ export async function loadAvailableResourceKinds() { console.log('apiResources loaded'); + apiState = ApiState.Loaded; setVSCodeContext(ContextId.ClusterUnreachable, false); + clusterDataProvider.updateCurrentContextChildNodes(); + createK8sClients(); + await restartKubeProxy(); + + // give proxy init callbacks time to fire + setTimeout(() => { + refreshResourcesTreeViews(); + }, 100); } diff --git a/src/cli/kubernetes/kubernetesConfig.ts b/src/cli/kubernetes/kubernetesConfig.ts index 31a08bcd..2ca7f764 100644 --- a/src/cli/kubernetes/kubernetesConfig.ts +++ b/src/cli/kubernetes/kubernetesConfig.ts @@ -10,71 +10,81 @@ import { ContextId } from 'types/extensionIds'; import { TelemetryError } from 'types/telemetryEventNames'; import { refreshClustersTreeView } from 'ui/treeviews/treeViews'; import { kcContextsListChanged, kcCurrentContextChanged, kcTextChanged } from 'utils/kubeConfigCompare'; -import { loadAvailableResourceKinds } from './apiResources'; +import { loadAvailableResourceKinds as loadApiResources } from './apiResources'; import { restartKubeProxy } from './kubectlProxy'; 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)) { await kubeconfigChanged(newKubeConfig, forceReloadResourceKinds); } else if(forceReloadResourceKinds) { - loadAvailableResourceKinds(); + loadApiResources(); } } -async function kubeconfigChanged(newKubeConfig: k8s.KubeConfig, forceReloadResourceKinds: boolean) { + + +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(); + console.log(kubeConfig.currentContext); + if(!currentContextExists()) { + kubeConfigState = KubeConfigState.NoContextSelected; } + if (contextChanged) { console.log('currentContext changed', kubeConfig.getCurrentContext()); - vscodeOnCurrentContextChanged(); - await restartKubeProxy(); - // give proxy a chance to start - setTimeout(() => { - refreshAllTreeViews(); - }, 100); + setVSCodeContext(ContextId.CurrentClusterGitOpsNotEnabled, false); + setVSCodeContext(ContextId.ClusterUnreachable, false); + } + + if (contextChanged || forceReloadResourceKinds) { + refreshClustersTreeView(); + loadApiResources(); } else if (contextsListChanged) { refreshClustersTreeView(); } } -async function vscodeOnCurrentContextChanged() { - setVSCodeContext(ContextId.NoClusterSelected, false); - setVSCodeContext(ContextId.CurrentClusterGitOpsNotEnabled, false); - setVSCodeContext(ContextId.NoSources, false); - setVSCodeContext(ContextId.NoWorkloads, false); - setVSCodeContext(ContextId.FailedToLoadClusterContexts, false); - setVSCodeContext(ContextId.ClusterUnreachable, false); -} - /** * Sets current kubectl context. * @param contextName Kubectl context name to use. @@ -100,3 +110,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/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/openResource.ts b/src/commands/openResource.ts index 58512926..12d754a2 100644 --- a/src/commands/openResource.ts +++ b/src/commands/openResource.ts @@ -2,6 +2,8 @@ import { Uri, window, workspace } from 'vscode'; import { telemetry } from 'extension'; import { TelemetryError } from 'types/telemetryEventNames'; +import { kubeConfig } from 'cli/kubernetes/kubernetesConfig'; +import { kubeConfigPath } from 'cli/kubernetes/kubernetesConfigWatcher'; /** * Open resource in the editor @@ -18,3 +20,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/types/extensionIds.ts b/src/types/extensionIds.ts index 78d609b3..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,19 +82,9 @@ export const enum CommandId { * GitOps context types. */ export const enum ContextId { - NoClusterSelected = 'gitops:noClusterSelected', CurrentClusterGitOpsNotEnabled = 'gitops:currentClusterGitOpsNotEnabled', ClusterUnreachable = 'gitops:clusterUnreachable', - LoadingClusters = 'gitops:loadingClusters', - LoadingSources = 'gitops:loadingSources', - LoadingWorkloads = 'gitops:loadingWorkloads', - - FailedToLoadClusterContexts = 'gitops:failedToLoadClusterContexts', - NoClusters = 'gitops:noClusters', - NoSources = 'gitops:noSources', - NoWorkloads = 'gitops:noWorkloads', - IsDev = 'gitops:isDev', IsWGE = 'gitops:isWGE', } diff --git a/src/types/kubernetes/kubernetesTypes.ts b/src/types/kubernetes/kubernetesTypes.ts index 30756bc1..80322a1b 100644 --- a/src/types/kubernetes/kubernetesTypes.ts +++ b/src/types/kubernetes/kubernetesTypes.ts @@ -34,20 +34,20 @@ export type Pod = Required & { */ export const enum Kind { List = 'List', - Bucket = 'Bucket.source.toolkit.fluxcd.io', - GitRepository = 'GitRepository.source.toolkit.fluxcd.io', - OCIRepository = 'OCIRepository.source.toolkit.fluxcd.io', - HelmRepository = 'HelmRepository.source.toolkit.fluxcd.io', - HelmRelease = 'HelmRelease.helm.toolkit.fluxcd.io', - Kustomization = 'Kustomization.kustomize.toolkit.fluxcd.io', - Deployment = 'Deployment.apps', + Bucket = 'Bucket', + GitRepository = 'GitRepository', + OCIRepository = 'OCIRepository', + HelmRepository = 'HelmRepository', + HelmRelease = 'HelmRelease', + Kustomization = 'Kustomization', + Deployment = 'Deployment', Namespace = 'Namespace', Node = 'Node', Pod = 'Pod', ConfigMap = 'ConfigMap', - GitOpsTemplate = 'GitOpsTemplate.templates.weave.works', + GitOpsTemplate = 'GitOpsTemplate', } diff --git a/src/ui/treeviews/dataProviders/clusterDataProvider.ts b/src/ui/treeviews/dataProviders/clusterDataProvider.ts index 93d223c4..59c1bc9c 100644 --- a/src/ui/treeviews/dataProviders/clusterDataProvider.ts +++ b/src/ui/treeviews/dataProviders/clusterDataProvider.ts @@ -59,25 +59,13 @@ export class ClusterDataProvider extends DataProvider { const t1 = Date.now(); - setVSCodeContext(ContextId.FailedToLoadClusterContexts, false); - setVSCodeContext(ContextId.NoClusters, false); - setVSCodeContext(ContextId.LoadingClusters, true); - statusBar.startLoadingTree(); - this.nodes = []; - - if (!kubeConfig) { - setVSCodeContext(ContextId.NoClusters, false); - setVSCodeContext(ContextId.FailedToLoadClusterContexts, true); - setVSCodeContext(ContextId.LoadingClusters, false); - statusBar.stopLoadingTree(); - return; - } + statusBar.startLoadingTree(); let currentContextTreeItem: ClusterNode | undefined; if (kubeConfig.getContexts().length === 0) { - setVSCodeContext(ContextId.NoClusters, true); + statusBar.stopLoadingTree(); return; } @@ -90,13 +78,23 @@ export class ClusterDataProvider extends DataProvider { this.nodes.push(clusterNode); } - // Update async status of the deployments (flux commands take a while to run) - currentContextTreeItem?.updateNodeContext(); - statusBar.stopLoadingTree(); - setVSCodeContext(ContextId.LoadingClusters, false); const t2 = Date.now(); console.log('+ loadClusterNodes ∆', t2 - t1); } + + public updateCurrentContextChildNodes() { + const currentContextTreeItem = this.getCurrentClusterNode(); + currentContextTreeItem?.updateNodeChildren(); + } + + 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 index ee98c359..690baf3c 100644 --- a/src/ui/treeviews/dataProviders/dataProvider.ts +++ b/src/ui/treeviews/dataProviders/dataProvider.ts @@ -1,5 +1,9 @@ +import { ApiState, apiState } from 'cli/kubernetes/apiResources'; +import { InfoNode, infoNodes } from 'utils/makeTreeviewInfoNode'; import { Event, EventEmitter, TreeDataProvider, TreeItem, TreeItemCollapsibleState } from 'vscode'; import { TreeNode } from '../nodes/treeNode'; +import { KubeConfigState, kubeConfigState } from 'cli/kubernetes/kubernetesConfig'; + /** * Defines tree view data provider base class for all GitOps tree views. @@ -13,10 +17,9 @@ export class DataProvider implements TreeDataProvider { protected _onDidChangeTreeData: EventEmitter = new EventEmitter(); readonly onDidChangeTreeData: Event = this._onDidChangeTreeData.event; - + /* if treeItem is undefined, refresh all tree items */ public async refresh(treeItem?: TreeItem) { - const allStr = treeItem ? 'ALL' : treeItem; - console.log(`## ${this.constructor.name} refresh`, allStr); + console.log(`## ${this.constructor.name} refresh`, treeItem ? treeItem : 'ALL'); if (!treeItem) { this.reloadData(); @@ -24,7 +27,9 @@ export class DataProvider implements TreeDataProvider { this.redraw(treeItem); } + /* if treeItem is undefined, redraw all tree items */ public redraw(treeItem?: TreeItem) { + this._onDidChangeTreeData.fire(treeItem); } @@ -61,9 +66,13 @@ export class DataProvider implements TreeDataProvider { protected async getRootNodes(): Promise { - if (this.loading) { - return []; + if (this.loading || kubeConfigState === KubeConfigState.Loading) { + return infoNodes(InfoNode.Loading); + } + if(this.nodes.length === 0) { + return infoNodes(InfoNode.NoResources); } + return this.nodes; } @@ -115,4 +124,7 @@ export class DataProvider implements TreeDataProvider { } + } + + diff --git a/src/ui/treeviews/dataProviders/kubernetesObjectDataProvider.ts b/src/ui/treeviews/dataProviders/kubernetesObjectDataProvider.ts index 13cdf2ed..7aad3c5e 100644 --- a/src/ui/treeviews/dataProviders/kubernetesObjectDataProvider.ts +++ b/src/ui/treeviews/dataProviders/kubernetesObjectDataProvider.ts @@ -6,12 +6,32 @@ import { NamespaceNode } from '../nodes/namespaceNode'; import { GitRepositoryNode } from '../nodes/source/gitRepositoryNode'; import { TreeNode } from '../nodes/treeNode'; import { DataProvider } from './dataProvider'; +import { ApiState, apiState } from 'cli/kubernetes/apiResources'; +import { InfoNode, infoNodes } from 'utils/makeTreeviewInfoNode'; +import { clusterDataProvider } from '../treeViews'; /** * Superclass for data providers that group objects by namespace: Source and Workload data providers */ export abstract class KubernetesObjectDataProvider extends DataProvider { + protected async getRootNodes(): Promise { + if(apiState === ApiState.Loading) { + return infoNodes(InfoNode.LoadingApi); + } + + if(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 []; + } + + return super.getRootNodes(); + } + public namespaceNodeTreeItems(): NamespaceNode[] { return (this.nodes?.filter(node => node instanceof NamespaceNode) as NamespaceNode[] || []); } diff --git a/src/ui/treeviews/dataProviders/sourceDataProvider.ts b/src/ui/treeviews/dataProviders/sourceDataProvider.ts index 1017552a..8cfbf49a 100644 --- a/src/ui/treeviews/dataProviders/sourceDataProvider.ts +++ b/src/ui/treeviews/dataProviders/sourceDataProvider.ts @@ -4,7 +4,7 @@ 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 { groupNodesByNamespace } from 'utils/treeNodeUtils'; import { BucketNode } from '../nodes/source/bucketNode'; import { GitRepositoryNode } from '../nodes/source/gitRepositoryNode'; import { HelmRepositoryNode } from '../nodes/source/helmRepositoryNode'; @@ -26,8 +26,6 @@ export class SourceDataProvider extends KubernetesObjectDataProvider { const sourceNodes: SourceNode[] = []; - setVSCodeContext(ContextId.LoadingSources, true); - // Fetch all sources asynchronously and at once const [gitRepositories, ociRepositories, helmRepositories, buckets, _] = await Promise.all([ getGitRepositories(), @@ -56,8 +54,6 @@ export class SourceDataProvider extends KubernetesObjectDataProvider { sourceNodes.push(new BucketNode(bucket)); } - setVSCodeContext(ContextId.LoadingSources, false); - setVSCodeContext(ContextId.NoSources, sourceNodes.length === 0); statusBar.stopLoadingTree(); [this.nodes] = await groupNodesByNamespace(sourceNodes, false, true); diff --git a/src/ui/treeviews/dataProviders/workloadDataProvider.ts b/src/ui/treeviews/dataProviders/workloadDataProvider.ts index 55ef3f56..b59e08ab 100644 --- a/src/ui/treeviews/dataProviders/workloadDataProvider.ts +++ b/src/ui/treeviews/dataProviders/workloadDataProvider.ts @@ -12,6 +12,7 @@ import { HelmReleaseNode } from '../nodes/workload/helmReleaseNode'; import { KustomizationNode } from '../nodes/workload/kustomizationNode'; import { WorkloadNode } from '../nodes/workload/workloadNode'; import { KubernetesObjectDataProvider } from './kubernetesObjectDataProvider'; +import { InfoNode, infoNodes } from 'utils/makeTreeviewInfoNode'; /**- * Defines data provider for loading Kustomizations @@ -26,8 +27,6 @@ export class WorkloadDataProvider extends KubernetesObjectDataProvider { const workloadNodes: WorkloadNode[] = []; - setVSCodeContext(ContextId.LoadingWorkloads, true); - const [kustomizations, helmReleases, _] = await Promise.all([ // Fetch all workloads getKustomizations(), @@ -48,8 +47,6 @@ export class WorkloadDataProvider extends KubernetesObjectDataProvider { this.updateWorkloadChildren(node); } - setVSCodeContext(ContextId.LoadingWorkloads, false); - setVSCodeContext(ContextId.NoWorkloads, workloadNodes.length === 0); statusBar.stopLoadingTree(); [this.nodes] = await groupNodesByNamespace(workloadNodes, false, true); @@ -76,7 +73,7 @@ export class WorkloadDataProvider extends KubernetesObjectDataProvider { const resourceTree = await fluxTools.tree(name, namespace); if (!resourceTree) { - node.children = [failedToLoad()]; + node.children = infoNodes(InfoNode.FailedToLoad); this.redraw(node); return; } @@ -100,7 +97,7 @@ export class WorkloadDataProvider extends KubernetesObjectDataProvider { const workloadChildren = await getChildrenOfWorkload('helm', name, namespace); if (!workloadChildren) { - node.children = [failedToLoad()]; + node.children = infoNodes(InfoNode.FailedToLoad); this.redraw(node); return; } @@ -119,8 +116,3 @@ export class WorkloadDataProvider extends KubernetesObjectDataProvider { } } -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 27ddfe39..e0a5cba1 100644 --- a/src/ui/treeviews/nodes/cluster/clusterNode.ts +++ b/src/ui/treeviews/nodes/cluster/clusterNode.ts @@ -13,8 +13,10 @@ 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 { TreeNode, TreeNodeIcon } from '../treeNode'; import { ClusterDeploymentNode } from './clusterDeploymentNode'; +import { ApiState, apiState } from 'cli/kubernetes/apiResources'; +import { InfoNode, infoNodes } from 'utils/makeTreeviewInfoNode'; /** * Defines Cluster context tree view item for displaying @@ -69,25 +71,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: 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); - } + async updateNodeChildren() { + this.updateControllersNodes(); + // set cluster provider const clusterMetadata = globalState.getClusterMetadata(this.cluster?.name || this.context.name); if (clusterMetadata?.clusterProvider) { this.clusterProviderManuallyOverridden = true; @@ -99,6 +86,7 @@ export class ClusterNode extends TreeNode { setVSCodeContext(ContextId.CurrentClusterGitOpsNotEnabled, !this.isGitOpsEnabled); } + // icon if (this.isGitOpsEnabled) { this.setIcon('cloud-gitops'); } else { @@ -106,15 +94,45 @@ export class ClusterNode extends TreeNode { } clusterDataProvider.redraw(); - this.updateDeploymentStatus(); + this.updateControllersStatus(); + } + + private async updateControllersNodes() { + if(apiState === ApiState.ClusterUnreachable) { + this.children = infoNodes(InfoNode.ClusterUnreachable); + return; + } + if(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 updateDeploymentStatus() { - if (this.children.length === 0) { + private async updateControllersStatus() { + if (this.children.length === 0 || apiState === ApiState.ClusterUnreachable) { return; } const fluxCheckResult = await fluxTools.check(this.context.name); @@ -122,9 +140,10 @@ export class ClusterNode extends TreeNode { 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 (this.children as ClusterDeploymentNode[])) { + for (const clusterController of deploymentNodes) { for (const controller of fluxCheckResult.controllers) { const clusterControllerName = clusterController.resource.metadata.name?.trim(); const deploymentName = controller.name.trim(); diff --git a/src/ui/treeviews/treeViews.ts b/src/ui/treeviews/treeViews.ts index 0ed8c29d..1604f34a 100644 --- a/src/ui/treeviews/treeViews.ts +++ b/src/ui/treeviews/treeViews.ts @@ -1,9 +1,9 @@ -import { TreeItem, TreeItemCollapsibleState, TreeView, window } from 'vscode'; +import { TreeItem, TreeItemCollapsibleState, TreeView, commands, window } from 'vscode'; import { isAzureProvider } from 'cli/azure/azureTools'; -import { globalState } from 'extension'; +import { globalState, setVSCodeContext } from 'extension'; import { Errorable } from 'types/errorable'; -import { TreeViewId } from 'types/extensionIds'; +import { CommandId, ContextId, TreeViewId } from 'types/extensionIds'; import { ClusterDataProvider } from './dataProviders/clusterDataProvider'; import { DocumentationDataProvider } from './dataProviders/documentationDataProvider'; import { SourceDataProvider } from './dataProviders/sourceDataProvider'; @@ -119,6 +119,9 @@ export function refreshClustersTreeView(node?: TreeNode) { * Reloads sources tree view for the selected cluster. */ export function refreshSourcesTreeView(node?: TreeNode) { + // const g = await commands.executeCommand('getContext', ContextId.CurrentClusterGitOpsNotEnabled); + // const u = await commands.executeCommand('getContext', ContextId.ClusterUnreachable); + sourceDataProvider.refresh(node); } diff --git a/src/utils/makeTreeviewInfoNode.ts b/src/utils/makeTreeviewInfoNode.ts new file mode 100644 index 00000000..ad802f7e --- /dev/null +++ b/src/utils/makeTreeviewInfoNode.ts @@ -0,0 +1,36 @@ +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: + node = new TreeNode('Cluster unreachable'); + node.setIcon(TreeNodeIcon.Disconnected); + return node; + } +} +