From aa35763dbada218ecffe6ed1bc58ab8a1a0b1244 Mon Sep 17 00:00:00 2001 From: Juozas Gaigalas Date: Tue, 12 Sep 2023 23:12:33 +0300 Subject: [PATCH] Live update for all flux types and UI fixes (#479) * status bar for loading stuff * live update for all flux types * treeview icons fixes * submenus * kubectl context commands --- package.json | 56 +++++++++++++++++-- src/commands/commands.ts | 12 +++- src/commands/kubectlApply.ts | 19 +++++++ src/types/extensionIds.ts | 5 ++ src/types/kubernetes/kubernetesTypes.ts | 8 +-- src/ui/statusBar.ts | 9 +-- .../kubernetesObjectDataProvider.ts | 28 +++++++--- src/ui/treeviews/nodes/cluster/clusterNode.ts | 2 - src/ui/treeviews/nodes/makeTreeNode.ts | 41 ++++++++++++++ src/ui/treeviews/nodes/namespaceNode.ts | 37 ++++++------ src/ui/treeviews/nodes/source/sourceNode.ts | 2 +- src/ui/treeviews/nodes/treeNode.ts | 2 +- .../treeviews/nodes/workload/workloadNode.ts | 2 +- src/utils/treeNodeUtils.ts | 1 + 14 files changed, 172 insertions(+), 52 deletions(-) create mode 100644 src/commands/kubectlApply.ts create mode 100644 src/ui/treeviews/nodes/makeTreeNode.ts diff --git a/package.json b/package.json index 58a75f50..84e8df4f 100644 --- a/package.json +++ b/package.json @@ -156,14 +156,28 @@ }, { "command": "gitops.views.createGitRepository", - "title": "Create GitRepository from Path", + "title": "Create Source from Path", "category": "GitOps" }, { "command": "gitops.createKustomization", "title": "Create Kustomization from Path", "icon": "$(add)", - "enablement": "!gitops:clusterUnreachable && !gitops:currentClusterGitOpsNotEnabled", + "category": "GitOps" + }, + { + "command": "gitops.kubectlApplyPath", + "title": "Apply (kubectl apply -f)", + "category": "GitOps" + }, + { + "command": "gitops.kubectlDeletePath", + "title": "Delete (kubectl delete -f)", + "category": "GitOps" + }, + { + "command": "gitops.kubectlApplyKustomization", + "title": "Apply Kustomization Directory (kubectl apply -k)", "category": "GitOps" }, { @@ -322,6 +336,12 @@ "when": "gitops:currentClusterGitOpsNotEnabled && !gitops:clusterUnreachable" } ], + "submenus": [ + { + "id": "gitops.explorer", + "label": "GitOps" + } + ], "menus": { "view/title": [ { @@ -471,17 +491,41 @@ "when": "view == gitops.views.templates" } ], - "explorer/context": [ + "gitops.explorer": [ { - "command": "gitops.views.createGitRepository" + "command": "gitops.kubectlApplyPath", + "group": "1" }, { - "command": "gitops.flux.reconcileRepository" + "command": "gitops.kubectlDeletePath", + "group": "1" }, { - "command": "gitops.createKustomization" + "command": "gitops.kubectlApplyKustomization", + "group": "1", + "when": "explorerResourceIsFolder" + }, + { + "command": "gitops.views.createGitRepository", + "group": "2" + }, + { + "command": "gitops.flux.reconcileRepository", + "when": "explorerResourceIsFolder", + "group": "2" + }, + { + "command": "gitops.createKustomization", + "group": "2" + + } + ], + "explorer/context": [ + { + "submenu": "gitops.explorer" } ], + "commandPalette": [ { "command": "gitops.editor.openResource", diff --git a/src/commands/commands.ts b/src/commands/commands.ts index 1840d9aa..04f08463 100644 --- a/src/commands/commands.ts +++ b/src/commands/commands.ts @@ -1,10 +1,10 @@ import { commands, Disposable, ExtensionContext, Uri, window } from 'vscode'; import { showOutputChannel } from 'cli/shell/output'; +import { refreshAllTreeViewsCommand, refreshResourcesTreeViewsCommand } from 'commands/refreshTreeViews'; import { telemetry } from 'extension'; import { CommandId } from 'types/extensionIds'; import { TelemetryError } from 'types/telemetryEventNames'; -import { refreshAllTreeViewsCommand, refreshResourcesTreeViewsCommand } from 'commands/refreshTreeViews'; import { addKustomization } from './addKustomization'; import { addSource } from './addSource'; import { copyResourceName } from './copyResourceName'; @@ -14,13 +14,15 @@ import { createKustomizationForPath } from './createKustomizationForPath'; import { deleteSource } from './deleteSource'; import { deleteWorkload } from './deleteWorkload'; import { fluxDisableGitOps, fluxEnableGitOps } from './enableDisableGitOps'; +import { expandAllSources, expandAllWorkloads } from './expandAll'; import { fluxCheck } from './fluxCheck'; import { checkFluxPrerequisites } from './fluxCheckPrerequisites'; import { fluxReconcileRepositoryForPath } from './fluxReconcileGitRepositoryForPath'; import { fluxReconcileSourceCommand } from './fluxReconcileSource'; import { fluxReconcileWorkload, fluxReconcileWorkloadWithSource } from './fluxReconcileWorkload'; import { installFluxCli } from './installFluxCli'; -import { openResource, openKubeconfig } from './openResource'; +import { kubectlApplyKustomization, kubectlApplyPath, kubectlDeletePath } from './kubectlApply'; +import { openKubeconfig, openResource } from './openResource'; import { pullGitRepository } from './pullGitRepository'; import { resume } from './resume'; import { setClusterProvider } from './setClusterProvider'; @@ -32,7 +34,6 @@ import { showNewUserGuide } from './showNewUserGuide'; import { showWorkloadsHelpMessage } from './showWorkloadsHelpMessage'; import { suspend } from './suspend'; import { trace } from './trace'; -import { expandAllSources, expandAllWorkloads } from './expandAll'; let _context: ExtensionContext; @@ -76,6 +77,11 @@ export function registerCommands(context: ExtensionContext) { registerCommand(CommandId.CopyResourceName, copyResourceName); registerCommand(CommandId.AddSource, addSource); registerCommand(CommandId.AddKustomization, addKustomization); + registerCommand(CommandId.KubectlApplyPath, kubectlApplyPath); + registerCommand(CommandId.KubectlDeletePath, kubectlDeletePath); + registerCommand(CommandId.KubectlApplyKustomization, kubectlApplyKustomization); + + registerCommand(CommandId.ExpandAllSources, expandAllSources); registerCommand(CommandId.ExpandAllWorkloads, expandAllWorkloads); diff --git a/src/commands/kubectlApply.ts b/src/commands/kubectlApply.ts new file mode 100644 index 00000000..6300b1b8 --- /dev/null +++ b/src/commands/kubectlApply.ts @@ -0,0 +1,19 @@ +import * as shell from 'cli/shell/exec'; +import { Uri } from 'vscode'; + +export async function kubectlApplyPath(uri?: Uri) { + if(uri) { + return await shell.execWithOutput(`kubectl apply -f ${uri.fsPath}`); + } +} +export async function kubectlDeletePath(uri?: Uri) { + if(uri) { + return await shell.execWithOutput(`kubectl delete -f ${uri.fsPath}`); + } +} + +export async function kubectlApplyKustomization(uri?: Uri) { + if(uri) { + return await shell.execWithOutput(`kubectl apply -k ${uri.fsPath}`); + } +} diff --git a/src/types/extensionIds.ts b/src/types/extensionIds.ts index 26aef5f0..9cc934f4 100644 --- a/src/types/extensionIds.ts +++ b/src/types/extensionIds.ts @@ -59,6 +59,11 @@ export const enum CommandId { AddSource = 'gitops.addSource', AddKustomization = 'gitops.addKustomization', + KubectlApplyPath = 'gitops.kubectlApplyPath', + KubectlDeletePath = 'gitops.kubectlDeletePath', + KubectlApplyKustomization = 'gitops.kubectlApplyKustomization', + + // editor EditorOpenResource = 'gitops.editor.openResource', EditorOpenKubeconfig = 'gitops.editor.openKubeconfig', diff --git a/src/types/kubernetes/kubernetesTypes.ts b/src/types/kubernetes/kubernetesTypes.ts index df9dcee1..1999960f 100644 --- a/src/types/kubernetes/kubernetesTypes.ts +++ b/src/types/kubernetes/kubernetesTypes.ts @@ -33,23 +33,23 @@ export type Pod = Required & { * Defines supported Kubernetes object kinds. */ export const enum Kind { - List = 'List', Bucket = 'Bucket', GitRepository = 'GitRepository', OCIRepository = 'OCIRepository', HelmRepository = 'HelmRepository', HelmRelease = 'HelmRelease', Kustomization = 'Kustomization', - Deployment = 'Deployment', + GitOpsTemplate = 'GitOpsTemplate', + Namespace = 'Namespace', + Deployment = 'Deployment', Node = 'Node', Pod = 'Pod', ConfigMap = 'ConfigMap', - - GitOpsTemplate = 'GitOpsTemplate', } + const fullKinds: Record = { Bucket: 'Buckets.source.toolkit.fluxcd.io', GitRepository: 'GitRepositories.source.toolkit.fluxcd.io', diff --git a/src/ui/statusBar.ts b/src/ui/statusBar.ts index defe86cb..40bdf22f 100644 --- a/src/ui/statusBar.ts +++ b/src/ui/statusBar.ts @@ -6,7 +6,6 @@ class StatusBar { private statusBarItemName = 'gitops'; private numberOfLoadingTreeViews = 0; - private loadingWasHidden = false; constructor() { this.statusBarItem = window.createStatusBarItem( @@ -14,7 +13,7 @@ class StatusBar { StatusBarAlignment.Left, -1e10,// align to the right ); - this.statusBarItem.text = '$(sync~spin) GitOps: Initializing Tree Views'; + this.statusBarItem.text = '$(sync~spin) GitOps: Loading Resources'; } /** @@ -22,10 +21,6 @@ class StatusBar { * (only at the extension initialization (once)) */ startLoadingTree(): void { - if (this.loadingWasHidden) { - return; - } - this.numberOfLoadingTreeViews++; this.statusBarItem.show(); } @@ -38,9 +33,7 @@ class StatusBar { this.numberOfLoadingTreeViews--; if (this.numberOfLoadingTreeViews === 0) { - this.loadingWasHidden = true; this.statusBarItem.hide(); - this.statusBarItem.dispose(); } } diff --git a/src/ui/treeviews/dataProviders/kubernetesObjectDataProvider.ts b/src/ui/treeviews/dataProviders/kubernetesObjectDataProvider.ts index 36a3ddd9..46e1ed5d 100644 --- a/src/ui/treeviews/dataProviders/kubernetesObjectDataProvider.ts +++ b/src/ui/treeviews/dataProviders/kubernetesObjectDataProvider.ts @@ -1,10 +1,9 @@ import { getNamespace } from 'cli/kubernetes/kubectlGetNamespace'; import { currentContextData } from 'data/contextData'; -import { GitRepository } from 'types/flux/gitRepository'; import { KubernetesObject } from 'types/kubernetes/kubernetesTypes'; import { groupNodesByNamespace, sortNodes } from 'utils/treeNodeUtils'; +import { makeTreeNode } from '../nodes/makeTreeNode'; import { NamespaceNode } from '../nodes/namespaceNode'; -import { GitRepositoryNode } from '../nodes/source/gitRepositoryNode'; import { TreeNode } from '../nodes/treeNode'; import { AsyncDataProvider } from './asyncDataProvider'; @@ -42,24 +41,31 @@ export abstract class KubernetesObjectDataProvider extends AsyncDataProvider { namespaceNode = new NamespaceNode(ns); this.nodes?.push(namespaceNode); sortNodes(this.nodes); - namespaceNode.expand(); - this.redraw(); + setTimeout(() => { + namespaceNode!.updateLabel(); + namespaceNode!.expand(); + this.redraw(namespaceNode); + }, 0); } if(namespaceNode.findChildByResource(object)) { this.update(object); namespaceNode.updateLabel(); - this.redraw(namespaceNode); + this.redraw(); + return; + } + + const resourceNode = makeTreeNode(object); + if(!resourceNode) { return; } - const resourceNode = new GitRepositoryNode(object as GitRepository); namespaceNode.addChild(resourceNode); sortNodes(namespaceNode.children); namespaceNode.updateLabel(); - this.redraw(namespaceNode); + this.redraw(); } public update(object: KubernetesObject) { @@ -72,8 +78,12 @@ export abstract class KubernetesObjectDataProvider extends AsyncDataProvider { if(node && node.resource) { node.resource = object; node.updateStatus(); - namespaceNode.updateLabel(); - this.redraw(namespaceNode); + + setTimeout(() => { + namespaceNode!.updateLabel(); + this.redraw(namespaceNode); + }, 0); + } } diff --git a/src/ui/treeviews/nodes/cluster/clusterNode.ts b/src/ui/treeviews/nodes/cluster/clusterNode.ts index 19dd6f8e..f4f9c03f 100644 --- a/src/ui/treeviews/nodes/cluster/clusterNode.ts +++ b/src/ui/treeviews/nodes/cluster/clusterNode.ts @@ -118,8 +118,6 @@ export class ClusterNode extends TreeNode { this.children = []; if (this.isGitOpsEnabled) { - // load flux system deployments - this.expand(); revealClusterNode(this, { expand: false, }); diff --git a/src/ui/treeviews/nodes/makeTreeNode.ts b/src/ui/treeviews/nodes/makeTreeNode.ts new file mode 100644 index 00000000..4ffd26f0 --- /dev/null +++ b/src/ui/treeviews/nodes/makeTreeNode.ts @@ -0,0 +1,41 @@ +import { KubernetesObject } from '@kubernetes/client-node'; +import { Kind } from 'types/kubernetes/kubernetesTypes'; +import { AnyResourceNode } from './anyResourceNode'; +import { GitOpsTemplateNode } from './gitOpsTemplateNode'; +import { NamespaceNode } from './namespaceNode'; +import { BucketNode } from './source/bucketNode'; +import { GitRepositoryNode } from './source/gitRepositoryNode'; +import { HelmRepositoryNode } from './source/helmRepositoryNode'; +import { OCIRepositoryNode } from './source/ociRepositoryNode'; +import { TreeNode } from './treeNode'; +import { HelmReleaseNode } from './workload/helmReleaseNode'; +import { KustomizationNode } from './workload/kustomizationNode'; + +// eslint-disable-next-line @typescript-eslint/ban-types +const nodeConstructors = { + 'Bucket': BucketNode, + 'GitRepository': GitRepositoryNode, + 'OCIRepository': OCIRepositoryNode, + 'HelmRepository': HelmRepositoryNode, + 'HelmRelease': HelmReleaseNode, + 'Kustomization': KustomizationNode, + 'GitOpsTemplate': GitOpsTemplateNode, + + 'Namespace': NamespaceNode, + + 'Deployment': AnyResourceNode, + 'Node': AnyResourceNode, + 'Pod': AnyResourceNode, + 'ConfigMap': AnyResourceNode, +}; + +export function makeTreeNode(object: KubernetesObject): TreeNode | undefined { + if(!object.kind) { + return; + } + + const constructor = nodeConstructors[object.kind as Kind]; + if(constructor) { + return new constructor(object as any); + } +} diff --git a/src/ui/treeviews/nodes/namespaceNode.ts b/src/ui/treeviews/nodes/namespaceNode.ts index e17701b2..3513b81d 100644 --- a/src/ui/treeviews/nodes/namespaceNode.ts +++ b/src/ui/treeviews/nodes/namespaceNode.ts @@ -27,28 +27,31 @@ export class NamespaceNode extends TreeNode { } updateLabel(withIcons = true) { - if(this.collapsibleState === TreeItemCollapsibleState.Collapsed) { - 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 { + 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); - } + if(withIcons) { + if(readyLength === totalLength) { + this.setIcon(TreeNodeIcon.Success); + } else { + this.setIcon(TreeNodeIcon.Warning); } + } else { + this.setIcon(undefined); + } + + if(this.collapsibleState === TreeItemCollapsibleState.Collapsed) { + const lengthLabel = totalLength === readyLength ? `${totalLength}` : `${readyLength}/${totalLength}`; + this.label = `${this.resource.metadata?.name} (${lengthLabel})`; } else { this.label = `${this.resource.metadata?.name}`; } diff --git a/src/ui/treeviews/nodes/source/sourceNode.ts b/src/ui/treeviews/nodes/source/sourceNode.ts index 9ed36c64..365d28d0 100644 --- a/src/ui/treeviews/nodes/source/sourceNode.ts +++ b/src/ui/treeviews/nodes/source/sourceNode.ts @@ -3,12 +3,12 @@ import { MarkdownString } from 'vscode'; import { Bucket } from 'types/flux/bucket'; import { GitRepository } from 'types/flux/gitRepository'; import { HelmRepository } from 'types/flux/helmRepository'; +import { FluxSourceObject } from 'types/flux/object'; import { OCIRepository } from 'types/flux/ociRepository'; import { Condition } from 'types/kubernetes/kubernetesTypes'; import { createMarkdownError, createMarkdownHr, createMarkdownTable } from 'utils/markdownUtils'; import { shortenRevision } from 'utils/stringUtils'; import { TreeNode, TreeNodeIcon } from '../treeNode'; -import { FluxSourceObject } from 'types/flux/object'; /** * Base class for all the Source tree view items. diff --git a/src/ui/treeviews/nodes/treeNode.ts b/src/ui/treeviews/nodes/treeNode.ts index 94bfef6d..9f5fbbfd 100644 --- a/src/ui/treeviews/nodes/treeNode.ts +++ b/src/ui/treeviews/nodes/treeNode.ts @@ -70,7 +70,7 @@ export class TreeNode extends TreeItem { * relative file path `resouces/icons/(dark|light)/${icon}.svg` * @param icon Theme icon, uri or light/dark svg icon path. */ - setIcon(icon: string | ThemeIcon | Uri | TreeNodeIcon) { + setIcon(icon: string | ThemeIcon | Uri | TreeNodeIcon | undefined) { if (icon === TreeNodeIcon.Error) { this.iconPath = new ThemeIcon('error', new ThemeColor('editorError.foreground')); } else if (icon === TreeNodeIcon.Warning) { diff --git a/src/ui/treeviews/nodes/workload/workloadNode.ts b/src/ui/treeviews/nodes/workload/workloadNode.ts index 6b93e9be..d0dfb1d7 100644 --- a/src/ui/treeviews/nodes/workload/workloadNode.ts +++ b/src/ui/treeviews/nodes/workload/workloadNode.ts @@ -2,11 +2,11 @@ import { MarkdownString } from 'vscode'; import { HelmRelease } from 'types/flux/helmRelease'; import { Kustomization } from 'types/flux/kustomization'; +import { FluxWorkloadObject } from 'types/flux/object'; import { Condition } from 'types/kubernetes/kubernetesTypes'; import { createMarkdownError, createMarkdownHr, createMarkdownTable } from 'utils/markdownUtils'; import { shortenRevision } from 'utils/stringUtils'; import { TreeNode, TreeNodeIcon } from '../treeNode'; -import { FluxWorkloadObject } from 'types/flux/object'; /** * Base class for all Workload tree view items. diff --git a/src/utils/treeNodeUtils.ts b/src/utils/treeNodeUtils.ts index 81f851c7..72cd9178 100644 --- a/src/utils/treeNodeUtils.ts +++ b/src/utils/treeNodeUtils.ts @@ -81,3 +81,4 @@ export function sortNodes(nodes?: TreeItem[] | null) { }); } } +