diff --git a/README.md b/README.md index 292cbbfd..07dd4c53 100644 --- a/README.md +++ b/README.md @@ -125,17 +125,37 @@ The GitOps Tools Extension depends on the [Kubernetes Tools](https://marketplace - Make sure you have [successfully authenticated](https://docs.microsoft.com/en-us/cli/azure/authenticate-azure-cli) on your `az` CLI and have access to the [correct subscription](https://docs.microsoft.com/en-us/cli/azure/account?view=azure-cli-latest#az_account_set) for your AKS or ARC cluster. - The easiest way to get your AKS or Arc cluster visible by the GitOps and Kubernetes Extensions, is to use the `az` CLI to merge the kubeconfig for accessing your cluster onto the default `kubectl` config. Use `get-credentials` as shown in the [official CLI documentation](https://docs.microsoft.com/en-us/cli/azure/aks?view=azure-cli-latest#az_aks_get_credentials). In order to enable GitOps in a cluster you will likely need the `--admin` credentials. -## Weave GitOps Enterprise (WGE) Templates +## Weave GitOps Enterprise (WGE) Integration -WGE users can access GitOpsTemplates directly from this extensions. Templates are provided by cluster administrators (Platform Teams) and can be used to quickly create cluster and configure applications with GitOps. +WGE users can access GitOpsTemplates directly from this extensions. WGE integration adds another treeview that shows `GitOpsTemplate`, `Canary`, `Pipeline`, and `GitOpsSet` resources and adds new interactions to each type of resource. All WGE resources have a right-click action to open them in WGE portal. -Templates are an opt-in feature that must be enabled in setting: +GitOpsTemplates are provided by cluster administrators (Platform Teams) and can be used to quickly create cluster and configure applications with GitOps. Flagger Canaries status can be visualized and their progress tracked. Pipelines are listed with their targets and each `GitopsCluster` attached to a Pipeline can be set as the current selected cluster for quick navigation between clusters. -![Enable GitOpsTemplates](docs/images/vscode-templates-config.png) +WGE integration is an opt-in feature that must be enabled in settings: -After that they can be seen in a new 'Templates' view. Right-click a template to use it: +![Enable WGE Features](docs/images/config-enable-wge.png) + +![Weave GitOps Treeview](docs/images/weave-gitops-treeview.png) + +![Create GitOps Template](docs/images/vscode-templates-view.png) + +### WGE Configuration + +For the integration to work, this extension needs a ConfigMap that provides WGE information and settings: + +`kubectl create configmap weave-gitops-interop --from-literal=portalUrl='https://WGE-CLUSTER-HOST' --from-literal=wgeClusterName='WGE-CLUSTER-NAME' -n flux-system`` + +``` +apiVersion: v1 +kind: ConfigMap +metadata: + namespace: flux-system + name: weave-gitops-interop +data: + portalUrl: https://mccp.howard.moomboo.space + wgeClusterName: howard-moomboo-space +``` -![Use GitOpsTemplates](docs/images/vscode-templates-view.png) diff --git a/docs/images/config-enable-wge.png b/docs/images/config-enable-wge.png new file mode 100644 index 00000000..5c943227 Binary files /dev/null and b/docs/images/config-enable-wge.png differ diff --git a/docs/images/vscode-templates-config.png b/docs/images/vscode-templates-config.png deleted file mode 100644 index 503016ef..00000000 Binary files a/docs/images/vscode-templates-config.png and /dev/null differ diff --git a/docs/images/weave-gitops-treeview.png b/docs/images/weave-gitops-treeview.png new file mode 100644 index 00000000..53a5874e Binary files /dev/null and b/docs/images/weave-gitops-treeview.png differ diff --git a/package-lock.json b/package-lock.json index 0c52c952..7100a899 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "vscode-gitops-tools", - "version": "0.25.4", + "version": "0.25.5-edge.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "vscode-gitops-tools", - "version": "0.25.4", + "version": "0.25.5-edge.0", "license": "MPL-2.0", "dependencies": { "@kubernetes/client-node": "^0.18.1", diff --git a/package.json b/package.json index 06d476d5..545a236f 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "vscode-gitops-tools", "displayName": "GitOps Tools for Flux", "description": "GitOps automation tools for continuous delivery of Kubernetes and Cloud Native applications", - "version": "0.25.4", + "version": "0.25.5-edge.0", "author": "Kingdon Barrett ", "contributors": [ "Kingdon Barrett ", @@ -98,6 +98,16 @@ "title": "Resume", "category": "GitOps" }, + { + "command": "gitops.manualPromotion", + "title": "Disable Automatic Promotion", + "category": "GitOps" + }, + { + "command": "gitops.autoPromotion", + "title": "Enable Automatic Promotion", + "category": "GitOps" + }, { "command": "gitops.flux.checkPrerequisites", "title": "Flux Check Prerequisites", @@ -259,6 +269,16 @@ "command": "gitops.views.createFromTemplate", "title": "Create from Template", "category": "GitOps" + }, + { + "command": "gitops.views.openInWgePortal", + "title": "Open in Weave Gitops Enterprise...", + "category": "GitOps" + }, + { + "command": "gitops.views.setContextToGitopsCluster", + "title": "Set as Current Context", + "category": "GitOps" } ], "viewsContainers": { @@ -285,8 +305,8 @@ "name": "Workloads" }, { - "id": "gitops.views.templates", - "name": "Templates", + "id": "gitops.views.wge", + "name": "Weave GitOps", "when": "config.gitops.weaveGitopsEnterprise" }, { @@ -311,7 +331,7 @@ "gitops.weaveGitopsEnterprise": { "type": "boolean", "default": false, - "description": "Enable WGE GitOpsTemplates feature" + "description": "Enable WGE features" }, "gitops.kubectlRequestTimeout": { "type": "string", @@ -388,7 +408,7 @@ { "command": "gitops.views.refreshResourcesTreeView", "group": "navigation@1", - "when": "view == gitops.views.templates" + "when": "view == gitops.views.wge" }, { "command": "gitops.views.showWorkloadsHelpMessage", @@ -434,57 +454,67 @@ }, { "command": "gitops.flux.reconcileSource", - "when": "view == gitops.views.sources && viewItem =~ /(GitRepository;|OCIRepository;|HelmRepository;|Bucket;)/", + "when": "viewItem =~ /(GitRepository;|OCIRepository;|HelmRepository;|Bucket;)/", "group": "navigation@0" }, { "command": "gitops.flux.reconcileWorkloadWithSource", - "when": "view == gitops.views.workloads && viewItem =~ /(Kustomization;|HelmRelease;)/", + "when": "viewItem =~ /(Kustomization;|HelmRelease;)/", "group": "navigation@0" }, { "command": "gitops.flux.reconcileWorkload", - "when": "view == gitops.views.workloads && viewItem =~ /(Kustomization;|HelmRelease;)/", + "when": "viewItem =~ /(Kustomization;|HelmRelease;)/", "group": "navigation@1" }, { "command": "gitops.suspend", - "when": "view =~ /(gitops.views.sources|gitops.views.workloads)/ && viewItem =~ /(GitRepository;|OCIRepository;|Kustomization;|HelmRelease;|HelmRepository;)/ && viewItem =~ /notSuspend;/", + "when": "viewItem =~ /notSuspend;/", "group": "navigation@1" }, { "command": "gitops.resume", - "when": "view =~ /(gitops.views.sources|gitops.views.workloads)/ && viewItem =~ /(GitRepository;|OCIRepository;|Kustomization;|HelmRelease;|HelmRepository;)/ && viewItem =~ /suspend;/", + "when": "viewItem =~ /suspend;/", "group": "navigation@1" }, + { + "command": "gitops.manualPromotion", + "when": "viewItem =~ /autoPromotion;/", + "group": "1" + }, + { + "command": "gitops.autoPromotion", + "when": "viewItem =~ /manualPromotion;/", + "group": "1" + }, { "command": "gitops.views.deleteWorkload", - "when": "view == gitops.views.workloads && viewItem =~ /(Kustomization;|HelmRelease;)/", + "when": "viewItem =~ /(Kustomization;|HelmRelease;)/", "group": "navigation@2" }, { "command": "gitops.views.deleteSource", - "when": "view == gitops.views.sources && viewItem =~ /(GitRepository;|OCIRepository;|HelmRepository;|Bucket;)/", + "when": "viewItem =~ /(GitRepository;|OCIRepository;|HelmRepository;|Bucket;)/", "group": "navigation@2" }, { "command": "gitops.views.pullGitRepository", - "when": "view == gitops.views.sources && viewItem =~ /GitRepository;/", + "when": "viewItem =~ /GitRepository;/", "group": "navigation@3" }, { "command": "gitops.addKustomization", - "when": "view == gitops.views.sources && viewItem =~ /GitRepository;|OCIRepository;|Bucket;/", + "when": "viewItem =~ /GitRepository;|OCIRepository;|Bucket;/", "group": "navigation@3" }, { "command": "gitops.editor.showLogs", - "when": "view =~ /^(gitops.views.clusters)$/ && viewItem =~ /(Deployment;)/" + "when": "viewItem =~ /(Deployment;)/" }, { "command": "gitops.copyResourceName", - "when": "view =~ /^(gitops.views.sources|gitops.views.workloads)$/", - "group": "navigation@9" + "when": "view =~ /^(gitops.views.sources|gitops.views.workloads||gitops.views.wge)$/ && !(viewItem =~ /Container;/)", + "group": "9" }, { "command": "gitops.flux.trace", @@ -494,7 +524,17 @@ { "command": "gitops.views.createFromTemplate", "group": "1", - "when": "view == gitops.views.templates" + "when": "viewItem =~ /GitOpsTemplate;/" + }, + { + "command": "gitops.views.openInWgePortal", + "group": "1", + "when": "viewItem =~ /hasWgePortal;/" + }, + { + "command": "gitops.views.setContextToGitopsCluster", + "group": "1", + "when": "viewItem =~ /GitopsCluster;/" } ], "gitops.explorer": [ @@ -543,6 +583,14 @@ "command": "gitops.resume", "when": "never" }, + { + "command": "gitops.manualPromotion", + "when": "never" + }, + { + "command": "gitops.autoPromotion", + "when": "never" + }, { "command": "gitops.flux.check", "when": "never" @@ -614,6 +662,10 @@ { "command": "gitops.dev.showGlobalState", "when": "gitops:isDev" + }, + { + "command": "gitops.views.setContextToGitopsCluster", + "when": "never" } ] } diff --git a/src/cli/kubernetes/apiResources.ts b/src/cli/kubernetes/apiResources.ts index 059dff31..d5a0e6da 100644 --- a/src/cli/kubernetes/apiResources.ts +++ b/src/cli/kubernetes/apiResources.ts @@ -1,5 +1,5 @@ import { redrawResourcesTreeViews, refreshResourcesTreeViews } from 'commands/refreshTreeViews'; -import { currentContextData } from 'data/contextData'; +import { currentContextData, loadContextData } from 'data/contextData'; import { setVSCodeContext, telemetry } from 'extension'; import { ContextId } from 'types/extensionIds'; import { Kind } from 'types/kubernetes/kubernetesTypes'; @@ -102,5 +102,6 @@ export async function loadAvailableResourceKinds() { // give proxy init callbacks time to fire setTimeout(() => { refreshResourcesTreeViews(); + loadContextData(); }, 100); } diff --git a/src/cli/kubernetes/kubectlGet.ts b/src/cli/kubernetes/kubectlGet.ts index 15758741..f352fa1d 100644 --- a/src/cli/kubernetes/kubectlGet.ts +++ b/src/cli/kubernetes/kubectlGet.ts @@ -1,14 +1,17 @@ import safesh from 'shell-escape-tag'; import { telemetry } from 'extension'; -import { k8sList } from 'k8s/list'; +import { k8sGet, k8sList } from 'k8s/list'; import { Bucket } from 'types/flux/bucket'; +import { Canary } from 'types/flux/canary'; import { GitOpsTemplate } from 'types/flux/gitOpsTemplate'; import { GitRepository } from 'types/flux/gitRepository'; +import { GitOpsSet } from 'types/flux/gitopsset'; import { HelmRelease } from 'types/flux/helmRelease'; import { HelmRepository } from 'types/flux/helmRepository'; import { Kustomization } from 'types/flux/kustomization'; import { OCIRepository } from 'types/flux/ociRepository'; +import { Pipeline } from 'types/flux/pipeline'; import { Deployment, Kind, KubernetesObject, Pod, qualifyToolkitKind } from 'types/kubernetes/kubernetesTypes'; import { TelemetryError } from 'types/telemetryEventNames'; import { parseJson, parseJsonItems } from 'utils/jsonUtils'; @@ -29,14 +32,21 @@ export const notAnErrorServerNotRunning = /no connection could be made because t * @param namespace namespace of the target resource * @param kind kind of the target resource */ -export async function getResource(name: string, namespace: string, kind: string): Promise { - const shellResult = await invokeKubectlCommand(`get ${kind}/${name} --namespace=${namespace} -o json`); +export async function getResource(name: string, namespace: string, kind: Kind): Promise { + const item = await k8sGet(name, namespace, kind); + if(item) { + return item as T; + } + + let fqKind = qualifyToolkitKind(kind); + + const shellResult = await invokeKubectlCommand(`get ${fqKind}/${name} --namespace=${namespace} -o json`); if (shellResult?.code !== 0) { telemetry.sendError(TelemetryError.FAILED_TO_GET_RESOURCE); return; } - return parseJson(shellResult.stdout); + return parseJson(shellResult.stdout) as T; } export async function getResourcesAllNamespaces(kind: Kind, telemetryError: TelemetryError): Promise { @@ -89,6 +99,18 @@ export async function getGitOpsTemplates(): Promise { return getResourcesAllNamespaces(Kind.GitOpsTemplate, TelemetryError.FAILED_TO_GET_GITOPSTEMPLATES); } +export async function getCanaries(): Promise { + return getResourcesAllNamespaces(Kind.Canary, TelemetryError.FAILED_TO_GET_HELM_RELEASES); +} + +export async function getPipelines(): Promise { + return getResourcesAllNamespaces(Kind.Pipeline, TelemetryError.FAILED_TO_GET_HELM_RELEASES); +} + +export async function getGitOpsSet(): Promise { + return getResourcesAllNamespaces(Kind.GitOpsSet, TelemetryError.FAILED_TO_GET_HELM_RELEASES); +} + /** * Get all flux system deployments. @@ -118,8 +140,7 @@ export async function getFluxControllers(context?: string): Promise { @@ -129,15 +150,15 @@ export async function getChildrenOfWorkload( return; } - const labelNameSelector = `-l ${workload}.toolkit.fluxcd.io/name=${name}`; - const labelNamespaceSelector = `-l ${workload}.toolkit.fluxcd.io/namespace=${namespace}`; + const labelNameSelector = `-l helm.toolkit.fluxcd.io/name=${name}`; + const labelNamespaceSelector = `-l helm.toolkit.fluxcd.io/namespace=${namespace}`; const query = `get ${resourceKinds.join(',')} ${labelNameSelector} ${labelNamespaceSelector} -A -o json`; const shellResult = await invokeKubectlCommand(query); 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}`); + window.showErrorMessage(`Failed to get HelmRelease created resources: ${shellResult?.stderr}`); return; } @@ -145,6 +166,30 @@ export async function getChildrenOfWorkload( } +export async function getCanaryChildren( + name: string, +): Promise { + // return []; + const resourceKinds = getAvailableResourcePlurals(); + if (!resourceKinds) { + return []; + } + + const labelNameSelector = `-l app=${name}`; + + const query = `get ${resourceKinds.join(',')} ${labelNameSelector} -A -o json`; + const shellResult = await invokeKubectlCommand(query); + + if (!shellResult || shellResult.code !== 0) { + telemetry.sendError(TelemetryError.FAILED_TO_GET_CHILDREN_OF_A_WORKLOAD); + window.showErrorMessage(`Failed to get HelmRelease created resources: ${shellResult?.stderr}`); + return []; + } + + return parseJsonItems(shellResult.stdout); +} + + /** * Get pods by a deployment name. * @param name pod target name diff --git a/src/cli/kubernetes/kubernetesToolsKubectl.ts b/src/cli/kubernetes/kubernetesToolsKubectl.ts index 9dd41fd5..0a79d32a 100644 --- a/src/cli/kubernetes/kubernetesToolsKubectl.ts +++ b/src/cli/kubernetes/kubernetesToolsKubectl.ts @@ -4,6 +4,7 @@ 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 { KubernetesObject, qualifyToolkitKind } from 'types/kubernetes/kubernetesTypes'; import { TelemetryError } from 'types/telemetryEventNames'; /** @@ -73,6 +74,19 @@ export async function invokeKubectlCommand(command: string, printOutput = true): return kubectlShellResult; } +export async function kubectlPatchNamespacedResource(resource: KubernetesObject, patch: string) { + const namespace = resource.metadata.namespace; + if(!namespace) { + return; + } + + const name = resource.metadata.name; + const kind = qualifyToolkitKind(resource.kind); + + const cmd = `kubectl patch ${kind} ${name} -n ${namespace} -p '${patch}' --type=merge`; + return shell.execWithOutput(cmd); +} + function getRequestTimeout(): string { return workspace.getConfiguration('gitops').get('kubectlRequestTimeout') || '20s'; diff --git a/src/commands/commands.ts b/src/commands/commands.ts index 04f08463..307b8b5c 100644 --- a/src/commands/commands.ts +++ b/src/commands/commands.ts @@ -22,10 +22,13 @@ import { fluxReconcileSourceCommand } from './fluxReconcileSource'; import { fluxReconcileWorkload, fluxReconcileWorkloadWithSource } from './fluxReconcileWorkload'; import { installFluxCli } from './installFluxCli'; import { kubectlApplyKustomization, kubectlApplyPath, kubectlDeletePath } from './kubectlApply'; +import { openInWgePortal } from './openInWgePortal'; import { openKubeconfig, openResource } from './openResource'; +import { setPipelineAutoPromotion, setPipelineManualPromotion } from './pipelineAutoPromotion'; import { pullGitRepository } from './pullGitRepository'; import { resume } from './resume'; import { setClusterProvider } from './setClusterProvider'; +import { setContextToGitopsCluster } from './setContextToGops'; import { setCurrentKubernetesContext } from './setCurrentKubernetesContext'; import { showGlobalState } from './showGlobalState'; import { showInstalledVersions } from './showInstalledVersions'; @@ -82,6 +85,8 @@ export function registerCommands(context: ExtensionContext) { registerCommand(CommandId.KubectlApplyKustomization, kubectlApplyKustomization); + + registerCommand(CommandId.ExpandAllSources, expandAllSources); registerCommand(CommandId.ExpandAllWorkloads, expandAllWorkloads); @@ -101,7 +106,13 @@ export function registerCommands(context: ExtensionContext) { registerCommand(CommandId.ShowInstalledVersions, showInstalledVersions); registerCommand(CommandId.InstallFluxCli, installFluxCli); registerCommand(CommandId.ShowGlobalState, showGlobalState); + + // wget registerCommand(CommandId.CreateFromTemplate, createFromTemplate); + registerCommand(CommandId.OpenInWgePortal, openInWgePortal); + registerCommand(CommandId.EnableAutoPromotion, setPipelineAutoPromotion); + registerCommand(CommandId.DisableAutoPromotion, setPipelineManualPromotion); + registerCommand(CommandId.SetContextToGitopsCluster, setContextToGitopsCluster); } /** diff --git a/src/commands/copyResourceName.ts b/src/commands/copyResourceName.ts index 65bccf45..0c68dfa0 100644 --- a/src/commands/copyResourceName.ts +++ b/src/commands/copyResourceName.ts @@ -7,7 +7,7 @@ import { WorkloadNode } from 'ui/treeviews/nodes/workload/workloadNode'; * Copy to clipboard any resource node name. */ export function copyResourceName(resourceNode: SourceNode | WorkloadNode) { - const name = resourceNode.resource.metadata?.name; + const name = resourceNode.resource.metadata.name; if (!name) { return; diff --git a/src/commands/createFromTemplate.ts b/src/commands/createFromTemplate.ts index 91d9ae63..6aaabb9c 100644 --- a/src/commands/createFromTemplate.ts +++ b/src/commands/createFromTemplate.ts @@ -1,4 +1,4 @@ -import { GitOpsTemplateNode } from 'ui/treeviews/nodes/gitOpsTemplateNode'; +import { GitOpsTemplateNode } from 'ui/treeviews/nodes/wge/gitOpsTemplateNode'; import { openCreateFromTemplatePanel } from 'ui/webviews/createFromTemplate/openWebview'; export async function createFromTemplate(templateNode?: GitOpsTemplateNode) { diff --git a/src/commands/deleteSource.ts b/src/commands/deleteSource.ts index dd35af83..b4eb4583 100644 --- a/src/commands/deleteSource.ts +++ b/src/commands/deleteSource.ts @@ -21,8 +21,8 @@ import { getCurrentClusterInfo, reloadSourcesTreeView, reloadWorkloadsTreeView } */ export async function deleteSource(sourceNode: GitRepositoryNode | OCIRepositoryNode | HelmRepositoryNode | BucketNode) { - const sourceName = sourceNode.resource.metadata?.name || ''; - const sourceNamespace = sourceNode.resource.metadata?.namespace || ''; + const sourceName = sourceNode.resource.metadata.name; + const sourceNamespace = sourceNode.resource.metadata.namespace || ''; const confirmButton = 'Delete'; const sourceType: FluxSource | 'unknown' = sourceNode.resource.kind === Kind.GitRepository ? 'source git' : diff --git a/src/commands/deleteWorkload.ts b/src/commands/deleteWorkload.ts index c908404d..947f3bc5 100644 --- a/src/commands/deleteWorkload.ts +++ b/src/commands/deleteWorkload.ts @@ -20,8 +20,8 @@ import { getCurrentClusterInfo, reloadWorkloadsTreeView } from 'ui/treeviews/tre */ export async function deleteWorkload(workloadNode: KustomizationNode | HelmReleaseNode) { - const workloadName = workloadNode.resource.metadata?.name || ''; - const workloadNamespace = workloadNode.resource.metadata?.namespace || ''; + const workloadName = workloadNode.resource.metadata.name; + const workloadNamespace = workloadNode.resource.metadata.namespace || ''; const confirmButton = 'Delete'; let workloadType: FluxWorkload; diff --git a/src/commands/fluxReconcileGitRepositoryForPath.ts b/src/commands/fluxReconcileGitRepositoryForPath.ts index 3ce31377..0e4de782 100644 --- a/src/commands/fluxReconcileGitRepositoryForPath.ts +++ b/src/commands/fluxReconcileGitRepositoryForPath.ts @@ -14,7 +14,7 @@ export async function fluxReconcileRepositoryForPath(fileExplorerUri?: Uri) { const gitInfo = await getFolderGitInfo(fileExplorerUri.fsPath); const gr = await getGitRepositoryforGitInfo(gitInfo); - if(!gr?.metadata?.name || !gr.metadata?.namespace) { + if(!gr?.metadata.name || !gr.metadata.namespace) { window.showWarningMessage(`No GitRepository with url '${gitInfo?.url}'`); return; } diff --git a/src/commands/fluxReconcileSource.ts b/src/commands/fluxReconcileSource.ts index 940ed460..57d53841 100644 --- a/src/commands/fluxReconcileSource.ts +++ b/src/commands/fluxReconcileSource.ts @@ -24,5 +24,5 @@ export async function fluxReconcileSourceCommand(source: GitRepositoryNode | OCI return; } - await fluxTools.reconcile(sourceType, source.resource.metadata?.name || '', source.resource.metadata?.namespace || ''); + await fluxTools.reconcile(sourceType, source.resource.metadata.name, source.resource.metadata.namespace || ''); } diff --git a/src/commands/fluxReconcileWorkload.ts b/src/commands/fluxReconcileWorkload.ts index acd15f7e..b7c8cffd 100644 --- a/src/commands/fluxReconcileWorkload.ts +++ b/src/commands/fluxReconcileWorkload.ts @@ -22,7 +22,7 @@ export async function fluxReconcileWorkload(workload: KustomizationNode | HelmRe return; } - await fluxTools.reconcile(workloadType, workload.resource.metadata?.name || '', workload.resource.metadata?.namespace || '', withSource); + await fluxTools.reconcile(workloadType, workload.resource.metadata.name, workload.resource.metadata.namespace || '', withSource); } diff --git a/src/commands/openInWgePortal.ts b/src/commands/openInWgePortal.ts new file mode 100644 index 00000000..6e129a5e --- /dev/null +++ b/src/commands/openInWgePortal.ts @@ -0,0 +1,24 @@ +import { currentContextData } from 'data/contextData'; +import { CanaryNode } from 'ui/treeviews/nodes/wge/canaryNode'; +import { GitOpsSetNode } from 'ui/treeviews/nodes/wge/gitOpsSetNode'; +import { GitOpsTemplateNode } from 'ui/treeviews/nodes/wge/gitOpsTemplateNode'; +import { PipelineNode } from 'ui/treeviews/nodes/wge/pipelineNode'; +import { WgeContainerNode } from 'ui/treeviews/nodes/wge/wgeNodes'; +import { env, Uri } from 'vscode'; + + +type WgePortalNode = GitOpsTemplateNode | PipelineNode | CanaryNode | GitOpsSetNode | WgeContainerNode; + +export function openInWgePortal(node: WgePortalNode) { + const portalUrl = currentContextData().portalUrl; + if(!portalUrl) { + return; + } + + const query = node.wgePortalQuery; + const url = `${portalUrl}/${query}`; + + // const url = `https://${portalHost}/canary_details/details?clusterName=vcluster-howard-moomboo-stage%2Fhoward-moomboo-staging&name=${name}&namespace=${namespace}`; + env.openExternal(Uri.parse(url)); +} + diff --git a/src/commands/pipelineAutoPromotion.ts b/src/commands/pipelineAutoPromotion.ts new file mode 100644 index 00000000..0e0d69d6 --- /dev/null +++ b/src/commands/pipelineAutoPromotion.ts @@ -0,0 +1,14 @@ +import { kubectlPatchNamespacedResource } from 'cli/kubernetes/kubernetesToolsKubectl'; +import { PipelineNode } from 'ui/treeviews/nodes/wge/pipelineNode'; + +export async function setPipelineAutoPromotion(node: PipelineNode) { + await kubectlPatchNamespacedResource(node.resource, '{"spec": {"promotion": {"manual": false}}}'); + + node.dataProvider.reload(); +} + +export async function setPipelineManualPromotion(node: PipelineNode) { + await kubectlPatchNamespacedResource(node.resource, '{"spec": {"promotion": {"manual": true}}}'); + + node.dataProvider.reload(); +} diff --git a/src/commands/pullGitRepository.ts b/src/commands/pullGitRepository.ts index 82f47f24..a5d88e31 100644 --- a/src/commands/pullGitRepository.ts +++ b/src/commands/pullGitRepository.ts @@ -34,7 +34,7 @@ export async function pullGitRepository(sourceNode: GitRepositoryNode): Promise< return; } - const pickedFolderFsPath = path.join(pickedFolder[0].fsPath, sourceNode.resource.metadata?.name || 'gitRepository'); + const pickedFolderFsPath = path.join(pickedFolder[0].fsPath, sourceNode.resource.metadata.name); // precedence - commit > semver > tag > branch const url = safesh.escape(sourceNode.resource.spec.url); diff --git a/src/commands/refreshTreeViews.ts b/src/commands/refreshTreeViews.ts index 4c6fdbe3..5c63e3b9 100644 --- a/src/commands/refreshTreeViews.ts +++ b/src/commands/refreshTreeViews.ts @@ -1,5 +1,5 @@ import { syncKubeConfig } from 'cli/kubernetes/kubernetesConfig'; -import { reloadClustersTreeView, reloadSourcesTreeView, reloadTemplatesTreeView, reloadWorkloadsTreeView, sourceDataProvider, templateDateProvider, workloadDataProvider } from '../ui/treeviews/treeViews'; +import { reloadClustersTreeView, reloadSourcesTreeView, reloadWgeTreeView, reloadWorkloadsTreeView, sourceDataProvider, wgeDataProvider, workloadDataProvider } from '../ui/treeviews/treeViews'; /** * Clicked button on the cluster tree view @@ -27,11 +27,11 @@ export function refreshResourcesTreeViewsCommand() { export function refreshResourcesTreeViews() { reloadSourcesTreeView(); reloadWorkloadsTreeView(); - reloadTemplatesTreeView(); + reloadWgeTreeView(); } export function redrawResourcesTreeViews() { sourceDataProvider.redraw(); workloadDataProvider.redraw(); - templateDateProvider.redraw(); + wgeDataProvider.redraw(); } diff --git a/src/commands/resume.ts b/src/commands/resume.ts index 023c82d4..7a08e5b8 100644 --- a/src/commands/resume.ts +++ b/src/commands/resume.ts @@ -1,57 +1,14 @@ -import { window } from 'vscode'; -import { AzureClusterProvider, azureTools } from 'cli/azure/azureTools'; -import { fluxTools } from 'cli/flux/fluxTools'; -import { kubeConfig } from 'cli/kubernetes/kubernetesConfig'; -import { failed } from 'types/errorable'; -import { FluxSource, FluxWorkload } from 'types/fluxCliTypes'; +import { kubectlPatchNamespacedResource } from 'cli/kubernetes/kubernetesToolsKubectl'; import { GitRepositoryNode } from 'ui/treeviews/nodes/source/gitRepositoryNode'; import { HelmRepositoryNode } from 'ui/treeviews/nodes/source/helmRepositoryNode'; -import { OCIRepositoryNode } from 'ui/treeviews/nodes/source/ociRepositoryNode'; +import { GitOpsSetNode } from 'ui/treeviews/nodes/wge/gitOpsSetNode'; import { HelmReleaseNode } from 'ui/treeviews/nodes/workload/helmReleaseNode'; import { KustomizationNode } from 'ui/treeviews/nodes/workload/kustomizationNode'; -import { getCurrentClusterInfo, reloadSourcesTreeView, reloadWorkloadsTreeView } from 'ui/treeviews/treeViews'; -/** - * Resume source or workload reconciliation and refresh its Tree View. - * - * @param node sources tree view node - */ -export async function resume(node: GitRepositoryNode | HelmReleaseNode | HelmRepositoryNode | KustomizationNode) { - const contextName = kubeConfig.getCurrentContext(); - const currentClusterInfo = await getCurrentClusterInfo(); - if (failed(currentClusterInfo) || !contextName) { - return; - } +export async function resume(node: GitRepositoryNode | HelmReleaseNode | KustomizationNode | HelmRepositoryNode | GitOpsSetNode) { + await kubectlPatchNamespacedResource(node.resource, '{"spec": {"suspend": false}}'); - const fluxResourceType: FluxSource | FluxWorkload | 'unknown' = node instanceof GitRepositoryNode ? - 'source git' : node instanceof HelmRepositoryNode ? - 'source helm' : node instanceof OCIRepositoryNode ? - 'source oci' : node instanceof HelmReleaseNode ? - 'helmrelease' : node instanceof KustomizationNode ? - 'kustomization' : 'unknown'; - if (fluxResourceType === 'unknown') { - window.showErrorMessage(`Unknown object kind ${fluxResourceType}`); - return; - } - - if (currentClusterInfo.result.isAzure) { - // TODO: implement - if (fluxResourceType === 'helmrelease' || fluxResourceType === 'kustomization') { - window.showInformationMessage('Not implemented on AKS/ARC', { modal: true }); - return; - } - await azureTools.resume(node.resource.metadata?.name || '', contextName, currentClusterInfo.result.clusterProvider as AzureClusterProvider); - } else { - await fluxTools.resume(fluxResourceType, node.resource.metadata?.name || '', node.resource.metadata?.namespace || ''); - } - - if (node instanceof GitRepositoryNode || node instanceof OCIRepositoryNode || node instanceof HelmRepositoryNode) { - reloadSourcesTreeView(); - if (currentClusterInfo.result.isAzure) { - reloadWorkloadsTreeView(); - } - } else { - reloadWorkloadsTreeView(); - } + node.dataProvider.reload(); } + diff --git a/src/commands/setContextToGops.ts b/src/commands/setContextToGops.ts new file mode 100644 index 00000000..bf6f5f1a --- /dev/null +++ b/src/commands/setContextToGops.ts @@ -0,0 +1,40 @@ +import { Context } from '@kubernetes/client-node'; +import { kubeConfig, setCurrentContext, syncKubeConfig } from 'cli/kubernetes/kubernetesConfig'; +import { GitOpsCluster } from 'types/flux/gitOpsCluster'; +import { AnyResourceNode } from 'ui/treeviews/nodes/anyResourceNode'; +import { window } from 'vscode'; + +export async function setContextToGitopsCluster(gopsNode: AnyResourceNode) { + const resource = gopsNode.resource as GitOpsCluster; + const clusterName = resource.metadata.name; + + let matchingContext: Context | undefined; + kubeConfig.getContexts().forEach(context => { + if (context.cluster === clusterName) { + matchingContext = context; + window.showInformationMessage(`Found cluster name matching '${clusterName}'`); + + } + }); + + if(!matchingContext) { + kubeConfig.getContexts().forEach(context => { + if (context.name === clusterName) { + matchingContext = context; + window.showInformationMessage(`Found context name matching '${clusterName}'`); + + } + }); + } + + if(!matchingContext) { + window.showWarningMessage(`Could not find context name or cluster name matching '${clusterName}'`); + return; + } + + const setContextResult = await setCurrentContext(matchingContext.name); + if (setContextResult?.isChanged) { + await syncKubeConfig(); + } +} + diff --git a/src/commands/showLogs.ts b/src/commands/showLogs.ts index 6c547387..5fc514f6 100644 --- a/src/commands/showLogs.ts +++ b/src/commands/showLogs.ts @@ -10,11 +10,11 @@ import { getResourceUri } from 'utils/getResourceUri'; */ export async function showLogs(deploymentNode: ClusterDeploymentNode): Promise { - const pods = await getPodsOfADeployment(deploymentNode.resource.metadata?.name, deploymentNode.resource.metadata?.namespace); + const pods = await getPodsOfADeployment(deploymentNode.resource.metadata.name, deploymentNode.resource.metadata.namespace); const pod = pods[0]; if (!pod) { - window.showErrorMessage(`No pods were found from ${deploymentNode.resource.metadata?.name} deployment.`); + window.showErrorMessage(`No pods were found from ${deploymentNode.resource.metadata.name} deployment.`); return; } diff --git a/src/commands/suspend.ts b/src/commands/suspend.ts index 26d65cf1..922666e4 100644 --- a/src/commands/suspend.ts +++ b/src/commands/suspend.ts @@ -1,59 +1,18 @@ -import { window } from 'vscode'; -import { AzureClusterProvider, azureTools } from 'cli/azure/azureTools'; -import { fluxTools } from 'cli/flux/fluxTools'; -import { kubeConfig } from 'cli/kubernetes/kubernetesConfig'; -import { failed } from 'types/errorable'; -import { FluxSource, FluxWorkload } from 'types/fluxCliTypes'; +import { kubectlPatchNamespacedResource } from 'cli/kubernetes/kubernetesToolsKubectl'; import { GitRepositoryNode } from 'ui/treeviews/nodes/source/gitRepositoryNode'; import { HelmRepositoryNode } from 'ui/treeviews/nodes/source/helmRepositoryNode'; -import { OCIRepositoryNode } from 'ui/treeviews/nodes/source/ociRepositoryNode'; +import { GitOpsSetNode } from 'ui/treeviews/nodes/wge/gitOpsSetNode'; import { HelmReleaseNode } from 'ui/treeviews/nodes/workload/helmReleaseNode'; import { KustomizationNode } from 'ui/treeviews/nodes/workload/kustomizationNode'; -import { getCurrentClusterInfo, reloadSourcesTreeView, reloadWorkloadsTreeView } from 'ui/treeviews/treeViews'; /** * Suspend source or workload reconciliation and refresh its Tree View. * * @param node sources tree view node */ -export async function suspend(node: GitRepositoryNode | HelmReleaseNode | KustomizationNode | HelmRepositoryNode) { - const contextName = kubeConfig.getCurrentContext(); - const currentClusterInfo = await getCurrentClusterInfo(); - if (failed(currentClusterInfo) || !contextName) { - return; - } +export async function suspend(node: GitRepositoryNode | HelmReleaseNode | KustomizationNode | HelmRepositoryNode | GitOpsSetNode) { + await kubectlPatchNamespacedResource(node.resource, '{"spec": {"suspend": true}}'); - const fluxResourceType: FluxSource | FluxWorkload | 'unknown' = node instanceof GitRepositoryNode ? - 'source git' : node instanceof HelmRepositoryNode ? - 'source helm' : node instanceof OCIRepositoryNode ? - 'source oci' : node instanceof HelmReleaseNode ? - 'helmrelease' : node instanceof KustomizationNode ? - 'kustomization' : 'unknown'; - - if (fluxResourceType === 'unknown') { - window.showErrorMessage(`Unknown object kind ${fluxResourceType}`); - return; - } - - if (currentClusterInfo.result.isAzure) { - // TODO: implement - if (fluxResourceType === 'helmrelease' || fluxResourceType === 'kustomization') { - window.showInformationMessage('Not implemented on AKS/ARC', { modal: true }); - return; - } - - await azureTools.suspend(node.resource.metadata?.name || '', contextName, currentClusterInfo.result.clusterProvider as AzureClusterProvider); - } else { - await fluxTools.suspend(fluxResourceType, node.resource.metadata?.name || '', node.resource.metadata?.namespace || ''); - } - - if (node instanceof GitRepositoryNode || node instanceof OCIRepositoryNode || node instanceof HelmRepositoryNode) { - reloadSourcesTreeView(); - if (currentClusterInfo.result.isAzure) { - reloadWorkloadsTreeView(); - } - } else { - reloadWorkloadsTreeView(); - } + node.dataProvider.reload(); } diff --git a/src/commands/trace.ts b/src/commands/trace.ts index 255504fb..f0e3ce9c 100644 --- a/src/commands/trace.ts +++ b/src/commands/trace.ts @@ -1,19 +1,20 @@ import { window } from 'vscode'; import { fluxTools } from 'cli/flux/fluxTools'; +import { getResource } from 'cli/kubernetes/kubectlGet'; import { telemetry } from 'extension'; +import { Kind } from 'types/kubernetes/kubernetesTypes'; import { AnyResourceNode } from 'ui/treeviews/nodes/anyResourceNode'; import { WorkloadNode } from 'ui/treeviews/nodes/workload/workloadNode'; -import { getResource } from 'cli/kubernetes/kubectlGet'; /** * Run flux trace for the Workloads tree view node. */ export async function trace(node: AnyResourceNode | WorkloadNode) { - const resourceName = node.resource?.metadata?.name || ''; - const resourceNamespace = node.resource?.metadata?.namespace || 'flux-system'; - const resourceKind = node.resource?.kind || ''; - let resourceApiVersion = node.resource?.apiVersion || ''; + const resourceName = node.resource.metadata.name; + const resourceNamespace = node.resource.metadata.namespace || 'flux-system'; + const resourceKind = node.resource.kind; + let resourceApiVersion = node.resource.apiVersion; if (!resourceName) { window.showErrorMessage('"name" is required to run `flux trace`.'); @@ -28,7 +29,7 @@ export async function trace(node: AnyResourceNode | WorkloadNode) { // flux tree fetched items don't have the "apiVersion" property if (!resourceApiVersion) { - const resource = await getResource(resourceName, resourceNamespace, resourceKind); + const resource = await getResource(resourceName, resourceNamespace, resourceKind as Kind); const apiVersion = resource?.apiVersion; if (!apiVersion && !apiVersion) { window.showErrorMessage('"apiVersion" is required to run `flux trace`'); diff --git a/src/data/contextData.ts b/src/data/contextData.ts index a987a52b..d6ad7563 100644 --- a/src/data/contextData.ts +++ b/src/data/contextData.ts @@ -1,10 +1,16 @@ +import { getResource } from 'cli/kubernetes/kubectlGet'; import { kubeConfig } from 'cli/kubernetes/kubernetesConfig'; -import { Kind } from 'types/kubernetes/kubernetesTypes'; +import { HelmRelease } from 'types/flux/helmRelease'; +import { ConfigMap, Kind } from 'types/kubernetes/kubernetesTypes'; +import { NamespaceNode } from 'ui/treeviews/nodes/namespaceNode'; import { TreeNode } from 'ui/treeviews/nodes/treeNode'; +import { WgeContainerNode } from 'ui/treeviews/nodes/wge/wgeNodes'; import { TreeItemCollapsibleState } from 'vscode'; import { ApiState, KindApiParams } from '../cli/kubernetes/apiResources'; - +// a data store for each context defined in kubeconfig. +// view data is stored here. +// allows to safely switch contexts without laggy queries from previous context overwriting data in the global tree view export class ContextData { public viewData: { [key: string]: ViewData; }; public contextName = ''; @@ -12,12 +18,15 @@ export class ContextData { // Current cluster supported kubernetes resource kinds. public apiResources: Map | undefined; + public portalUrl?: string; + public wgeClusterName?: string; + constructor(contextName: string) { this.contextName = contextName; this.viewData = { 'source': new ViewData(), 'workload': new ViewData(), - 'template': new ViewData(), + 'wge': new ViewData(), }; } @@ -39,26 +48,67 @@ export class ViewData { public collapsibleStates = new Map(); public loading = false; + + get savedNodes() { + let nodes: TreeNode[] = []; + this.nodes.forEach(node => { + nodes = nodes.concat(node.children); + }); + return nodes.concat(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); + for (const node of this.savedNodes) { + const key = node.viewStateKey; + if (key) { + this.collapsibleStates.set(key, 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); + for (const node of this.savedNodes) { + const key = node.viewStateKey; + if (key) { + const state = this.collapsibleStates.get(key); if (state) { node.collapsibleState = state; + if(node instanceof NamespaceNode) { + const withIcons = !node.parent || node.parent instanceof WgeContainerNode; + node.updateLabel(withIcons); + } } } } } } + +export async function loadContextData() { + const context = currentContextData(); + const config = await getResource('weave-gitops-interop', 'flux-system', Kind.ConfigMap) as ConfigMap; + + if(config) { + context.portalUrl = config.data.portalUrl; + context.wgeClusterName = config.data.wgeClusterName; + } + + context.portalUrl ??= await wgeHelmReleasePortalUrl(); + context.wgeClusterName ??= kubeConfig.getCurrentCluster()?.name || kubeConfig.currentContext; +} + +async function wgeHelmReleasePortalUrl() { + const wgeHelmRelease = await getResource('weave-gitops-enterprise', 'flux-system', Kind.HelmRelease); + if(!wgeHelmRelease) { + return; + } + + const values = wgeHelmRelease.spec?.values as any; + const hosts = values?.ingress?.hosts; + const host = hosts?.[0]; + + if(host) { + return `https://${host.host}`; + } +} diff --git a/src/k8s/informers.ts b/src/k8s/informers.ts index 51dd31ab..b7d6ffef 100644 --- a/src/k8s/informers.ts +++ b/src/k8s/informers.ts @@ -1,11 +1,11 @@ import * as k8s from '@kubernetes/client-node'; import { getAPIParams } from 'cli/kubernetes/apiResources'; import { GitRepository } from 'types/flux/gitRepository'; +import { FluxSourceKinds, FluxWorkloadKinds } from 'types/flux/object'; import { Kind, KubernetesListObject, KubernetesObject } from 'types/kubernetes/kubernetesTypes'; import { KubernetesObjectDataProvider } from 'ui/treeviews/dataProviders/kubernetesObjectDataProvider'; import { sourceDataProvider, workloadDataProvider } from 'ui/treeviews/treeViews'; import { k8sCustomApi } from './client'; -import { FluxSourceKinds, FluxWorkloadKinds } from 'types/flux/object'; let informers: k8s.Informer[] = []; @@ -18,7 +18,6 @@ export function createInformers(kc: k8s.KubeConfig) { FluxWorkloadKinds.forEach(kind => { createInformer(kc, workloadDataProvider, kind); }); - } export function destroyInformers() { diff --git a/src/k8s/list.ts b/src/k8s/list.ts index 7464e5ac..a82e11ee 100644 --- a/src/k8s/list.ts +++ b/src/k8s/list.ts @@ -1,9 +1,9 @@ import { getAPIParams } from 'cli/kubernetes/apiResources'; -import { FluxObject } from 'types/flux/object'; +import { ToolkitObject } from 'types/flux/object'; import { Kind, KubernetesListObject, Namespace } from 'types/kubernetes/kubernetesTypes'; import { k8sCoreApi, k8sCustomApi } from './client'; -export async function k8sList(kind: Kind): Promise { +export async function k8sList(kind: Kind): Promise { const api = getAPIParams(kind); if(!api) { return; @@ -22,6 +22,29 @@ export async function k8sList(kind: Kind): Promise(name: string, namespace: string, kind: Kind): Promise { + const api = getAPIParams(kind); + if(!api) { + return; + } + + if(!k8sCustomApi) { + return; + } + + try { + const result = await k8sCustomApi.getNamespacedCustomObject(api.group, api.version, namespace, api.plural, name); + const kbody = result.body as KubernetesListObject; + if (kbody.items && kbody.items.length > 0) { + return kbody.items[0]; + } + } catch (error) { + return; + } +} + + export async function k8sListNamespaces(): Promise { if(!k8sCoreApi) { return; diff --git a/src/types/extensionIds.ts b/src/types/extensionIds.ts index 9cc934f4..a19279e2 100644 --- a/src/types/extensionIds.ts +++ b/src/types/extensionIds.ts @@ -9,7 +9,7 @@ export const enum TreeViewId { ClustersView = 'gitops.views.clusters', SourcesView = 'gitops.views.sources', WorkloadsView = 'gitops.views.workloads', - TemplatesView = 'gitops.views.templates', + WgeView = 'gitops.views.wge', DocumentationView = 'gitops.views.documentation', } @@ -79,7 +79,13 @@ export const enum CommandId { ShowInstalledVersions = 'gitops.showInstalledVersions', InstallFluxCli = 'gitops.installFluxCli', ShowGlobalState = 'gitops.dev.showGlobalState', + + // wge + OpenInWgePortal = 'gitops.views.openInWgePortal', CreateFromTemplate = 'gitops.views.createFromTemplate', + EnableAutoPromotion = 'gitops.autoPromotion', + DisableAutoPromotion = 'gitops.manualPromotion', + SetContextToGitopsCluster = 'gitops.views.setContextToGitopsCluster', } diff --git a/src/types/flux/canary.ts b/src/types/flux/canary.ts new file mode 100644 index 00000000..2881ad40 --- /dev/null +++ b/src/types/flux/canary.ts @@ -0,0 +1,231 @@ +import { Condition, Kind, KubernetesObject } from 'types/kubernetes/kubernetesTypes'; + +export interface Canary extends KubernetesObject { + readonly kind: Kind.Canary; + readonly spec: CanarySpec; + readonly status: CanaryStatus; +} + +declare interface CanarySpec { + provider?: string; + metricsServer?: string; + targetRef: LocalObjectReference; + autoscalerRef?: AutoscalerRefernce; + ingressRef?: LocalObjectReference; + routeRef?: LocalObjectReference; + upstreamRef?: CrossNamespaceObjectReference; + service: CanaryService; + analysis?: CanaryAnalysis; + canaryAnalysis?: CanaryAnalysis; + progressDeadlineSeconds?: number; + skipAnalysis?: boolean; + revertOnDeletion?: boolean; + suspend?: boolean; +} + +declare interface CanaryService { + name?: string; + port: number; + portName?: string; + targetPort?: number | string; + appProtocol?: string; + portDiscovery: boolean; + timeout?: string; + gateways?: string[]; + gatewayRefs?: any[]; + hosts?: string[]; + delegation?: boolean; + trafficPolicy?: any; + match?: any; + rewrite?: any; + retries?: any; + headers?: any; + mirror?: any[]; + corsPolicy?: any; + meshName?: string; + backends?: string[]; + apex?: CustomMetadata; + primary?: CustomMetadata; + canary?: CustomMetadata; +} + +declare interface CanaryAnalysis { + interval: string; + iterations?: number; + mirror?: boolean; + mirrorWeight?: number; + maxWeight?: number; + stepWeight?: number; + stepWeights?: number[]; + stepWeightPromotion?: number; + threshold: number; + primaryReadyThreshold?: number; + canaryReadyThreshold?: number; + alerts?: CanaryAlert[]; + metrics?: CanaryMetric[]; + webhooks?: CanaryWebhook[]; + match?: any[]; + sessionAffinity?: SessionAffinity; +} + +declare interface SessionAffinity { + cookieName?: string; + maxAge?: number; +} + +declare interface CanaryMetric { + name: string; + interval?: string; + threshold?: number; + thresholdRange?: CanaryThresholdRange; + query?: string; + templateRef?: CrossNamespaceObjectReference; + templateVariables?: { [key: string]: string;}; +} + +declare interface CanaryThresholdRange { + min?: number; + max?: number; +} + +declare interface CanaryAlert { + name: string; + severity?: AlertSeverity; + providerRef: CrossNamespaceObjectReference; +} + +declare interface CanaryWebhook { + type: HookType; + name: string; + url: string; + muteAlert: boolean; + timeout?: string; + metadata?: { [key: string]: string;}; +} + +declare interface CanaryWebhookPayload { + name: string; + namespace: string; + phase: CanaryPhase; + checksum: string; + metadata?: { [key: string]: string;}; +} + +declare interface CrossNamespaceObjectReference { + apiVersion?: string; + kind?: string; + name: string; + namespace?: string; +} + +declare interface LocalObjectReference { + apiVersion?: string; + kind?: string; + name: string; +} + +declare interface AutoscalerRefernce { + apiVersion?: string; + kind?: string; + name: string; + primaryScalerQueries: { [key: string]: string;}; + primaryScalerReplicas?: ScalerReplicas; +} + +declare interface ScalerReplicas { + minReplicas?: number; + maxReplicas?: number; +} + +declare interface CustomMetadata { + labels?: { [key: string]: string;}; + annotations?: { [key: string]: string;}; +} + +declare interface HTTPRewrite { + uri?: string; + authority?: string; + type?: string; +} + +// CanaryStatus is used for state persistence (read-only) +interface CanaryStatus { + phase: CanaryPhase; + failedChecks: number; + canaryWeight: number; + iterations: number; + + previousSessionAffinityCookie?: string; + sessionAffinityCookie?: string; + trackedConfigs?: any; + + lastAppliedSpec?: string; + lastPromotedSpec?: string; + lastTransitionTime: string; + conditions?: CanaryCondition[]; +} + +export type CanaryCondition = Required & { + // Type of this condition + type: 'Promoted'; +}; + + +export const enum CanaryPhase { + // Initializing means the canary initializing is underway + Initializing = 'Initializing', + // Initialized means the primary deployment, hpa and ClusterIP services + // have been created along with the service mesh or ingress objects + Initialized = 'Initialized', + // Waiting means the canary rollout is paused (waiting for confirmation to proceed) + Waiting = 'Waiting', + // Progressing means the canary analysis is underway + Progressing = 'Progressing', + // WaitingPromotion means the canary promotion is paused (waiting for confirmation to proceed) + WaitingPromotion = 'WaitingPromotion', + // Promoting means the canary analysis is finished and the primary spec has been updated + Promoting = 'Promoting', + // Finalising means the canary promotion is finished and traffic has been routed back to primary + Finalising = 'Finalising', + // Succeeded means the canary analysis has been successful + // and the canary deployment has been promoted + Succeeded = 'Succeeded', + // Failed means the canary analysis failed + // and the canary deployment has been scaled to zero + Failed = 'Failed', + // Terminating means the canary has been marked + // for deletion and in the finalizing state + Terminating = 'Terminating', + // Terminated means the canary has been finalized + // and successfully deleted + Terminated = 'Terminated', +} + + +// // HookType can be pre, post or during rollout +export enum HookType { + // RolloutHook execute webhook during the canary analysis + RolloutHook = 'rollout', + // PreRolloutHook execute webhook before routing traffic to canary + PreRolloutHook = 'pre-rollout', + // PostRolloutHook execute webhook after the canary analysis + PostRolloutHook = 'post-rollout', + // ConfirmRolloutHook halt canary analysis until webhook returns HTTP 200 + ConfirmRolloutHook = 'confirm-rollout', + // ConfirmPromotionHook halt canary promotion until webhook returns HTTP 200 + ConfirmPromotionHook = 'confirm-promotion', + // EventHook dispatches Flagger events to the specified endpoint + EventHook = 'event', + // RollbackHook rollback canary analysis if webhook returns HTTP 200 + RollbackHook = 'rollback', + // ConfirmTrafficIncreaseHook increases traffic weight if webhook returns HTTP 200 + ConfirmTrafficIncreaseHook = 'confirm-traffic-increase', +} + + +export enum AlertSeverity { + SeverityInfo = 'info', + SeverityWarn = 'warn', + SeverityError = 'error', +} + diff --git a/src/types/flux/gitOpsCluster.ts b/src/types/flux/gitOpsCluster.ts new file mode 100644 index 00000000..621e1407 --- /dev/null +++ b/src/types/flux/gitOpsCluster.ts @@ -0,0 +1,13 @@ +import { Kind, KubernetesObject } from 'types/kubernetes/kubernetesTypes'; + +export interface GitOpsCluster extends KubernetesObject { + readonly kind: Kind.GitopsCluster; + readonly spec: GitOpsClusterSpec; + readonly status: GitOpsClusterStatus; +} + +export type GitOpsClusterSpec = any; +export type GitOpsClusterStatus = any; + + + diff --git a/src/types/flux/gitopsset.ts b/src/types/flux/gitopsset.ts new file mode 100644 index 00000000..cca46d98 --- /dev/null +++ b/src/types/flux/gitopsset.ts @@ -0,0 +1,133 @@ +import * as k8s from '@kubernetes/client-node'; +import { Condition, Kind, KubernetesObject, LocalObjectReference } from 'types/kubernetes/kubernetesTypes'; + + +export interface GitOpsSet extends KubernetesObject { + readonly kind: Kind.GitOpsSet; + readonly spec: GitOpsSetSpec; + readonly status: GitOpsSetStatus; +} + +declare interface GitOpsSetSpec { + suspend?: boolean; + generators?: GitOpsSetGenerator[]; + templates?: GitOpsSetTemplate[]; + serviceAccountName?: string; +} + +declare interface GitOpsSetTemplate { + repeat?: string; + content: any; +} + +declare interface ClusterGenerator { + selector?: k8s.V1LabelSelector; +} + +declare interface ConfigGenerator { + kind: string; + name: string; +} + +declare interface ListGenerator { + elements?: any[]; +} + +declare interface PullRequestGenerator { + interval: any; + driver: string; + serverURL?: string; + repo: string; + secretRef?: LocalObjectReference; + labels?: string[]; + forks?: boolean; +} + +declare interface APIClientGenerator { + interval: any; + endpoint?: string; + method?: string; + jsonPath?: string; + headersRef?: HeadersReference; + body?: any; + singleElement?: boolean; + secretRef?: LocalObjectReference; +} + +declare interface HeadersReference { + kind: string; + name: string; +} + +declare interface RepositoryGeneratorFileItem { + path: string; +} + +declare interface RepositoryGeneratorDirectoryItem { + path: string; + exclude?: boolean; +} + +declare interface GitRepositoryGenerator { + repositoryRef?: string; + files?: RepositoryGeneratorFileItem[]; + directories?: RepositoryGeneratorDirectoryItem[]; +} + +declare interface OCIRepositoryGenerator { + repositoryRef?: string; + files?: RepositoryGeneratorFileItem[]; + directories?: RepositoryGeneratorDirectoryItem[]; +} + +declare interface MatrixGenerator { + generators?: GitOpsSetNestedGenerator[]; + singleElement?: boolean; +} + +declare interface GitOpsSetNestedGenerator { + name?: string; + list?: ListGenerator; + gitRepository?: GitRepositoryGenerator; + ociRepository?: OCIRepositoryGenerator; + pullRequests?: PullRequestGenerator; + cluster?: ClusterGenerator; + apiClient?: APIClientGenerator; + imagePolicy?: ImagePolicyGenerator; + config?: ConfigGenerator; +} + +declare interface ImagePolicyGenerator { + policyRef?: string; +} + +declare interface GitOpsSetGenerator { + list?: ListGenerator; + pullRequests?: PullRequestGenerator; + gitRepository?: GitRepositoryGenerator; + ociRepository?: OCIRepositoryGenerator; + matrix?: MatrixGenerator; + cluster?: ClusterGenerator; + apiClient?: APIClientGenerator; + imagePolicy?: ImagePolicyGenerator; + config?: ConfigGenerator; +} + +declare interface GitOpsSetStatus { + observedGeneration?: number; + conditions?: Condition[]; + inventory?: ResourceInventory; +} + + +declare interface ResourceInventory { + entries: ResourceRef[]; +} + +declare interface ResourceRef { + // ID is the string representation of the Kubernetes resource object’s metadata, + // in the format ‘namespace_name_group_kind’. + id: string; + // Version is the API version of the Kubernetes resource object’s kind. + v: string; +} diff --git a/src/types/flux/object.ts b/src/types/flux/object.ts index 1547c721..eccbd653 100644 --- a/src/types/flux/object.ts +++ b/src/types/flux/object.ts @@ -1,14 +1,17 @@ import { Kind } from 'types/kubernetes/kubernetesTypes'; import { Bucket } from './bucket'; +import { Canary } from './canary'; import { GitRepository } from './gitRepository'; +import { GitOpsSet } from './gitopsset'; import { HelmRelease } from './helmRelease'; import { HelmRepository } from './helmRepository'; import { Kustomization } from './kustomization'; import { OCIRepository } from './ociRepository'; +import { Pipeline } from './pipeline'; export type FluxSourceObject = GitRepository | OCIRepository | HelmRepository | Bucket; export type FluxWorkloadObject = Kustomization | HelmRelease; -export type FluxObject = FluxSourceObject | FluxWorkloadObject; +export type ToolkitObject = FluxSourceObject | FluxWorkloadObject | GitOpsSet | Pipeline | Canary; export const FluxSourceKinds: Kind[] = [ Kind.GitRepository, @@ -20,5 +23,6 @@ export const FluxSourceKinds: Kind[] = [ export const FluxWorkloadKinds: Kind[] = [ Kind.Kustomization, Kind.HelmRelease, + Kind.Canary, ]; diff --git a/src/types/flux/pipeline.ts b/src/types/flux/pipeline.ts new file mode 100644 index 00000000..b86b5aa8 --- /dev/null +++ b/src/types/flux/pipeline.ts @@ -0,0 +1,109 @@ +import { Condition, Kind, KubernetesObject, LocalObjectReference } from 'types/kubernetes/kubernetesTypes'; + +export interface Pipeline extends KubernetesObject { + readonly kind: Kind.Pipeline; + readonly spec: PipelineSpec; + readonly status: PipelineStatus; +} + +declare interface PipelineSpec { + environments: PipelineEnvironment[]; + appRef: LocalAppReference; + promotion?: Promotion; +} + +declare interface Promotion { + manual?: boolean; + strategy: Strategy; +} + +declare interface Strategy { + pullRequest?: PullRequestPromotion; + notification?: NotificationPromotion; + secretRef?: LocalObjectReference; +} + +declare interface PullRequestPromotion { + type: GitProviderType; + url: string; + baseBranch: string; + secretRef: LocalObjectReference; +} + + +export enum GitProviderType { + Github = 'github', + Gitlab = 'gitlab', + BitBucketServer = 'bitbucket-server', + AzureDevOps = 'azure-devops', +} + +type NotificationPromotion = unknown; + +export declare interface PipelineStatus { + observedGeneration?: number; + conditions?: Condition[]; + environments: { [key: string]: EnvironmentStatus | undefined;}; +} + +export declare interface EnvironmentStatus { + waitingApproval?: WaitingApproval; + targets?: TargetStatus[]; +} + +declare interface WaitingApproval { + revision: string; +} + +export declare interface ClusterAppReference { + clusterRef?: CrossNamespaceClusterReference; +} + +export declare interface TargetStatus { + clusterAppRef: ClusterAppReference; + ready: boolean; + revision?: string; + error?: string; +} + +export declare interface PipelineEnvironment { + name: string; + targets: PipelineTarget[]; + promotion?: Promotion; +} + +export declare interface PipelineTarget { + namespace: string; + clusterRef?: CrossNamespaceClusterReference; +} + +export declare interface LocalAppReference { + apiVersion: string; + kind: string; + name: string; +} + +export declare interface CrossNamespaceClusterReference { + apiVersion?: string; + kind: string; + name: string; + namespace?: string; +} + + +export enum PipelineReasons { + // Reasons used by the original controller. + // TargetClusterNotFoundReason signals a failure to locate a cluster resource on the management cluster. + TargetClusterNotFoundReason = 'TargetClusterNotFound', + // TargetClusterNotReadyReason signals that a cluster pointed to by a Pipeline is not ready. + TargetClusterNotReadyReason = 'TargetClusterNotReady', + // ReconciliationSucceededReason signals that a Pipeline has been successfully reconciled. + ReconciliationSucceededReason = 'ReconciliationSucceeded', + + // Reasons used by the level-triggered controller. + // TargetNotReadableReason signals that an app object pointed to by a Pipeline cannot be read, either because it is not found, or it's on a cluster that cannot be reached. + TargetNotReadableReason = 'TargetNotReadable', +} + + + diff --git a/src/types/kubernetes/kubernetesTypes.ts b/src/types/kubernetes/kubernetesTypes.ts index 1999960f..482d8695 100644 --- a/src/types/kubernetes/kubernetesTypes.ts +++ b/src/types/kubernetes/kubernetesTypes.ts @@ -1,32 +1,47 @@ import * as k8s from '@kubernetes/client-node'; +export type KubernetesListObject = k8s.KubernetesListObject; + +export type Metadata = k8s.V1ObjectMeta & { + // required + name: string; + uid: string; +}; + export type KubernetesObject = k8s.KubernetesObject & { spec?: unknown; status?: unknown; + // required + kind: string; + metadata: Metadata; }; -export type KubernetesListObject = k8s.KubernetesListObject; export type Condition = k8s.V1Condition; // Specify types from `@kubernetes/client-node` export type Namespace = Required & { readonly kind: Kind.Namespace; + metadata: Metadata; }; export type Deployment = Required & { readonly kind: Kind.Deployment; + metadata: Metadata; }; export type ConfigMap = Required & { readonly kind: Kind.ConfigMap; + metadata: Metadata; }; export type Node = Required & { readonly kind: Kind.Node; + metadata: Metadata; }; export type Pod = Required & { readonly kind: Kind.Pod; + metadata: Metadata; }; /** @@ -40,6 +55,10 @@ export const enum Kind { HelmRelease = 'HelmRelease', Kustomization = 'Kustomization', GitOpsTemplate = 'GitOpsTemplate', + Canary = 'Canary', + Pipeline = 'Pipeline', + GitOpsSet = 'GitOpsSet', + GitopsCluster = 'GitopsCluster', Namespace = 'Namespace', Deployment = 'Deployment', @@ -57,7 +76,12 @@ const fullKinds: Record = { HelmRepository: 'HelmRepositories.source.toolkit.fluxcd.io', HelmRelease: 'HelmReleases.helm.toolkit.fluxcd.io', Kustomization: 'Kustomizations.kustomize.toolkit.fluxcd.io', + GitOpsTemplate: 'GitOpsTemplates.templates.weave.works', + Canary: 'Canaries.flagger.app', + Pipeline: 'Pipelines.pipelines.weave.works', + GitOpsSet: 'GitOpsSets.templates.weave.works', + GitOpsCluster: 'GitOpsClusters.gitops.weave.works', }; export function qualifyToolkitKind(kind: string): string { @@ -65,13 +89,6 @@ export function qualifyToolkitKind(kind: string): string { } -export const enum SourceKind { - Bucket = 'Bucket', - GitRepository = 'GitRepository', - OCIRepository = 'OCIRepository', - HelmRepository = 'HelmRepository', -} - /* * LocalObjectReference contains enough information * to let you locate the referenced object inside the same namespace. diff --git a/src/types/nodeContext.ts b/src/types/nodeContext.ts index 7912524e..9ed953db 100644 --- a/src/types/nodeContext.ts +++ b/src/types/nodeContext.ts @@ -11,11 +11,15 @@ export const enum NodeContext { ClusterGitOpsEnabled = 'clusterGitOpsEnabled', ClusterGitOpsNotEnabled = 'clusterGitOpsNotEnabled', - // resource contexts - AzureFluxConfig = 'azureFluxConfig', - NotAzureFluxConfig = 'NotAzureFluxConfig', // Generic context values Suspend = 'suspend', NotSuspend = 'notSuspend', + + // WGE + HasWgePortal = 'hasWgePortal', + + // Pipeline + ManualPromotion = 'manualPromotion', + AutoPromotion = 'autoPromotion', } diff --git a/src/types/telemetryEventNames.ts b/src/types/telemetryEventNames.ts index e51370f9..0bd17288 100644 --- a/src/types/telemetryEventNames.ts +++ b/src/types/telemetryEventNames.ts @@ -22,6 +22,7 @@ export const enum TelemetryError { FAILED_TO_GET_PODS_OF_A_DEPLOYMENT = 'FAILED_TO_GET_PODS_OF_A_DEPLOYMENT', FAILED_TO_GET_KUSTOMIZATIONS = 'FAILED_TO_GET_KUSTOMIZATIONS', FAILED_TO_GET_HELM_RELEASES = 'FAILED_TO_GET_HELM_RELEASES', + FAILED_TO_GET_CANARIES = 'FAILED_TO_GET_CANARIES', FAILED_TO_GET_GIT_REPOSITORIES = 'FAILED_TO_GET_GIT_REPOSITORIES', FAILED_TO_GET_OCI_REPOSITORIES = 'FAILED_TO_GET_OCI_REPOSITORIES', FAILED_TO_GET_HELM_REPOSITORIES = 'FAILED_TO_GET_HELM_REPOSITORIES', diff --git a/src/ui/icons.ts b/src/ui/icons.ts new file mode 100644 index 00000000..cc6f83ad --- /dev/null +++ b/src/ui/icons.ts @@ -0,0 +1,42 @@ +import { ThemeColor, ThemeIcon } from 'vscode'; + +export const enum CommonIcon { + Error = 'error', + Warning = 'warning', + Success = 'success', + Disconnected = 'disconnected', + Progressing = 'progressing', + Loading = 'loading', + Unknown = 'unknown', +} + +export const IconColors: Record = { + 'error': 'editorError.foreground', + 'warning': 'editorWarning.foreground', + 'pass': 'terminal.ansiGreen', + 'green': 'terminal.ansiGreen', + 'foreground': 'foreground', +}; + +const IconDefinitions: Record = { + [CommonIcon.Error]: ['error', 'editorError.foreground'], + [CommonIcon.Warning]: ['warning', 'editorWarning.foreground'], + [CommonIcon.Success]: ['pass', 'terminal.ansiGreen'], + [CommonIcon.Disconnected]: ['sync-ignored', 'editorError.foreground'], + [CommonIcon.Progressing]: ['sync~spin', 'terminal.ansiGreen'], + [CommonIcon.Loading]: ['loading~spin', 'foreground'], + [CommonIcon.Unknown]: ['circle-large-outline', 'foreground'], +}; + +export function commonIcon(icon: CommonIcon) { + const [id, color] = IconDefinitions[icon]; + return new ThemeIcon(id, new ThemeColor(color)); +} + + +export function themeIcon(icon: string, color?: string) { + color = color ?? 'foreground'; + color = IconColors[color] ?? color; + return new ThemeIcon(icon, new ThemeColor(color)); +} + diff --git a/src/ui/treeviews/dataProviders/asyncDataProvider.ts b/src/ui/treeviews/dataProviders/asyncDataProvider.ts index 4afbbd71..07011238 100644 --- a/src/ui/treeviews/dataProviders/asyncDataProvider.ts +++ b/src/ui/treeviews/dataProviders/asyncDataProvider.ts @@ -1,7 +1,7 @@ import { ApiState } from 'cli/kubernetes/apiResources'; import { KubeConfigState, kubeConfigState } from 'cli/kubernetes/kubernetesConfig'; import { ContextData, ViewData, currentContextData } from 'data/contextData'; -import { InfoNode, infoNodes } from 'utils/makeTreeviewInfoNode'; +import { InfoLabel, infoNodes } from 'utils/makeTreeviewInfoNode'; import { NamespaceNode } from '../nodes/namespaceNode'; import { TreeNode } from '../nodes/treeNode'; import { clusterDataProvider } from '../treeViews'; @@ -10,7 +10,7 @@ import { SimpleDataProvider } from './simpleDataProvider'; /**` * Defines tree view data provider base class for all GitOps tree views. */ -export class AsyncDataProvider extends SimpleDataProvider{ +export class AsyncDataProvider extends SimpleDataProvider { get nodes() { return this.viewData(currentContextData()).nodes; } @@ -30,11 +30,11 @@ export class AsyncDataProvider extends SimpleDataProvider{ const context = currentContextData(); if(context.apiState === ApiState.Loading) { - return infoNodes(InfoNode.LoadingApi); + return infoNodes(InfoLabel.LoadingApi, this); } if(context.apiState === ApiState.ClusterUnreachable) { - return infoNodes(InfoNode.ClusterUnreachable); + return infoNodes(InfoLabel.ClusterUnreachable, this); } // return empty array so that vscode welcome view with embedded link "Enable Gitops ..." is shown @@ -43,11 +43,11 @@ export class AsyncDataProvider extends SimpleDataProvider{ } if (this.currentViewData().loading || kubeConfigState === KubeConfigState.Loading) { - return infoNodes(InfoNode.Loading); + return infoNodes(InfoLabel.Loading, this); } if(this.currentViewData().nodes.length === 0) { - return infoNodes(InfoNode.NoResources); + return infoNodes(InfoLabel.NoResources, this); } return this.currentViewData().nodes; @@ -81,8 +81,4 @@ export class AsyncDataProvider extends SimpleDataProvider{ }); this.redraw(); } - - } - - diff --git a/src/ui/treeviews/dataProviders/kubernetesObjectDataProvider.ts b/src/ui/treeviews/dataProviders/kubernetesObjectDataProvider.ts index 46e1ed5d..6de01160 100644 --- a/src/ui/treeviews/dataProviders/kubernetesObjectDataProvider.ts +++ b/src/ui/treeviews/dataProviders/kubernetesObjectDataProvider.ts @@ -19,26 +19,26 @@ export abstract class KubernetesObjectDataProvider extends AsyncDataProvider { if(!nsName) { return; } - return this.namespaceNodeTreeItems().find(node => node.resource?.metadata?.name === nsName); + return this.namespaceNodeTreeItems().find(node => node.resource.metadata.name === nsName); } private findParentNamespaceNode(object: KubernetesObject): NamespaceNode | undefined { - const nsName = object.metadata?.namespace; + const nsName = object.metadata.namespace; return this.findNamespaceNode(nsName); } public async add(object: KubernetesObject) { - if(!object.metadata?.namespace) { + if(!object.metadata.namespace) { return; } let namespaceNode = this.findParentNamespaceNode(object); if(!namespaceNode) { - const ns = await getNamespace(object.metadata?.namespace); + const ns = await getNamespace(object.metadata.namespace); if(!ns) { return; } - namespaceNode = new NamespaceNode(ns); + namespaceNode = new NamespaceNode(ns, this); this.nodes?.push(namespaceNode); sortNodes(this.nodes); setTimeout(() => { @@ -56,7 +56,7 @@ export abstract class KubernetesObjectDataProvider extends AsyncDataProvider { return; } - const resourceNode = makeTreeNode(object); + const resourceNode = makeTreeNode(object, this); if(!resourceNode) { return; } @@ -124,4 +124,11 @@ export abstract class KubernetesObjectDataProvider extends AsyncDataProvider { this.redraw(); } + + // async updateNodeChildren(node: TreeNode) { + // if(node instanceof Canary) { + // await node.updateChildren(); + // } + // } + } diff --git a/src/ui/treeviews/dataProviders/simpleDataProvider.ts b/src/ui/treeviews/dataProviders/simpleDataProvider.ts index 8a567086..b5916a66 100644 --- a/src/ui/treeviews/dataProviders/simpleDataProvider.ts +++ b/src/ui/treeviews/dataProviders/simpleDataProvider.ts @@ -1,5 +1,5 @@ import { KubeConfigState, kubeConfigState } from 'cli/kubernetes/kubernetesConfig'; -import { InfoNode, infoNodes } from 'utils/makeTreeviewInfoNode'; +import { InfoLabel, infoNodes } from 'utils/makeTreeviewInfoNode'; import { Event, EventEmitter, TreeDataProvider, TreeItem } from 'vscode'; import { TreeNode } from '../nodes/treeNode'; @@ -10,11 +10,16 @@ import { TreeNode } from '../nodes/treeNode'; export class SimpleDataProvider implements TreeDataProvider { private _nodes: TreeNode[] = []; + guid = ''; + + constructor() { + this.guid = Math.random().toString(36); + } + get nodes() { return this._nodes; } - protected loading = false; protected _onDidChangeTreeData: EventEmitter = new EventEmitter(); @@ -61,10 +66,10 @@ export class SimpleDataProvider implements TreeDataProvider { // give nodes for vscode to render based on async data loading state protected async getRootNodes(): Promise { if (this.loading || kubeConfigState === KubeConfigState.Loading) { - return infoNodes(InfoNode.Loading); + return infoNodes(InfoLabel.Loading, this); } if(this.nodes.length === 0) { - return infoNodes(InfoNode.NoResources); + return infoNodes(InfoLabel.NoResources, this); } return this.nodes; diff --git a/src/ui/treeviews/dataProviders/sourceDataProvider.ts b/src/ui/treeviews/dataProviders/sourceDataProvider.ts index 78743c5e..88cbd4cc 100644 --- a/src/ui/treeviews/dataProviders/sourceDataProvider.ts +++ b/src/ui/treeviews/dataProviders/sourceDataProvider.ts @@ -4,6 +4,7 @@ import { ContextData } from 'data/contextData'; import { statusBar } from 'ui/statusBar'; import { sortByMetadataName } from 'utils/sortByMetadataName'; import { groupNodesByNamespace } from 'utils/treeNodeUtils'; +import { makeTreeNode } from '../nodes/makeTreeNode'; import { BucketNode } from '../nodes/source/bucketNode'; import { GitRepositoryNode } from '../nodes/source/gitRepositoryNode'; import { HelmRepositoryNode } from '../nodes/source/helmRepositoryNode'; @@ -27,7 +28,7 @@ export class SourceDataProvider extends KubernetesObjectDataProvider { async loadRootNodes() { statusBar.startLoadingTree(); - const sourceNodes: SourceNode[] = []; + const nodes: SourceNode[] = []; // Fetch all sources asynchronously and at once const [gitRepositories, ociRepositories, helmRepositories, buckets, _] = await Promise.all([ @@ -40,27 +41,26 @@ export class SourceDataProvider extends KubernetesObjectDataProvider { // add git repositories to the tree for (const gitRepository of sortByMetadataName(gitRepositories)) { - sourceNodes.push(new GitRepositoryNode(gitRepository)); + nodes.push(makeTreeNode(gitRepository, this) as GitRepositoryNode); } // add oci repositories to the tree for (const ociRepository of sortByMetadataName(ociRepositories)) { - sourceNodes.push(new OCIRepositoryNode(ociRepository)); + nodes.push(makeTreeNode(ociRepository, this) as OCIRepositoryNode); } for (const helmRepository of sortByMetadataName(helmRepositories)) { - sourceNodes.push(new HelmRepositoryNode(helmRepository)); - const x = new HelmRepositoryNode(helmRepository); + nodes.push(makeTreeNode(helmRepository, this) as HelmRepositoryNode); } // add buckets to the tree for (const bucket of sortByMetadataName(buckets)) { - sourceNodes.push(new BucketNode(bucket)); + nodes.push(makeTreeNode(bucket, this) as BucketNode); } statusBar.stopLoadingTree(); - const [groupedNodes] = await groupNodesByNamespace(sourceNodes, false, true); + const [groupedNodes] = await groupNodesByNamespace(nodes, false, true); return groupedNodes; } } diff --git a/src/ui/treeviews/dataProviders/templateDataProvider.ts b/src/ui/treeviews/dataProviders/templateDataProvider.ts deleted file mode 100644 index 37579b1f..00000000 --- a/src/ui/treeviews/dataProviders/templateDataProvider.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { getGitOpsTemplates } from 'cli/kubernetes/kubectlGet'; -import { ContextData } from 'data/contextData'; -import { sortByMetadataName } from 'utils/sortByMetadataName'; -import { GitOpsTemplateNode } from '../nodes/gitOpsTemplateNode'; -import { AsyncDataProvider } from './asyncDataProvider'; - -export class TemplateDataProvider extends AsyncDataProvider { - protected viewData(contextData: ContextData) { - return contextData.viewData.template; - } - - async loadRootNodes() { - const nodes = []; - - const templates = await getGitOpsTemplates(); - - for (const template of sortByMetadataName(templates)) { - nodes.push(new GitOpsTemplateNode(template)); - } - - return nodes; - } -} diff --git a/src/ui/treeviews/dataProviders/wgeDataProvider.ts b/src/ui/treeviews/dataProviders/wgeDataProvider.ts new file mode 100644 index 00000000..cb119d48 --- /dev/null +++ b/src/ui/treeviews/dataProviders/wgeDataProvider.ts @@ -0,0 +1,78 @@ +import { getCanaries, getGitOpsSet, getGitOpsTemplates, getPipelines } from 'cli/kubernetes/kubectlGet'; +import { ContextData } from 'data/contextData'; +import { sortByMetadataName } from 'utils/sortByMetadataName'; +import { groupNodesByNamespace } from 'utils/treeNodeUtils'; +import { CanaryNode } from '../nodes/wge/canaryNode'; +import { GitOpsSetNode } from '../nodes/wge/gitOpsSetNode'; +import { GitOpsTemplateNode } from '../nodes/wge/gitOpsTemplateNode'; +import { PipelineNode } from '../nodes/wge/pipelineNode'; +import { CanariesContainerNode, GitOpsSetsContainerNode, PipelinesContainerNode, TemplatesContainerNode } from '../nodes/wge/wgeNodes'; +import { AsyncDataProvider } from './asyncDataProvider'; + +export class WgeDataProvider extends AsyncDataProvider { + protected viewData(contextData: ContextData) { + return contextData.viewData.wge; + } + + async loadRootNodes() { + const nodes = []; + + const [templates, canaries, pipelines, gitopssets] = await Promise.all([ + getGitOpsTemplates(), + getCanaries(), + getPipelines(), + getGitOpsSet(), + ]); + + + // TEMPLATES + const ts = new TemplatesContainerNode(); + nodes.push(ts); + + for (const t of sortByMetadataName(templates)) { + const node = new GitOpsTemplateNode(t); + ts.addChild(node); + } + + // CANARIES + const cs = new CanariesContainerNode(); + nodes.push(cs); + + for (const c of sortByMetadataName(canaries)) { + const node = new CanaryNode(c); + cs.addChild(node); + node.updateChildren(); + } + [cs.children] = await groupNodesByNamespace(cs.children, false, true); + + // PIPELINES + const ps = new PipelinesContainerNode(); + nodes.push(ps); + + for (const p of sortByMetadataName(pipelines)) { + const node = new PipelineNode(p); + ps.addChild(node); + node.updateChildren(); + } + [ps.children] = await groupNodesByNamespace(ps.children, false, true); + + + // GITOPSSETS + const gops = new GitOpsSetsContainerNode(); + nodes.push(gops); + + for (const g of sortByMetadataName(gitopssets)) { + const node = new GitOpsSetNode(g); + gops.addChild(node); + } + // log(gops.children); + [gops.children] = await groupNodesByNamespace(gops.children, false, true); + // log(gops.children); + + return nodes; + } + + +} + + diff --git a/src/ui/treeviews/dataProviders/workloadDataProvider.ts b/src/ui/treeviews/dataProviders/workloadDataProvider.ts index bd7bc7f0..7419673b 100644 --- a/src/ui/treeviews/dataProviders/workloadDataProvider.ts +++ b/src/ui/treeviews/dataProviders/workloadDataProvider.ts @@ -1,13 +1,10 @@ -import { fluxTools } from 'cli/flux/fluxTools'; -import { getChildrenOfWorkload, getHelmReleases, getKustomizations } from 'cli/kubernetes/kubectlGet'; +import { getHelmReleases, getKustomizations } from 'cli/kubernetes/kubectlGet'; import { getNamespaces } from 'cli/kubernetes/kubectlGetNamespace'; import { ContextData } from 'data/contextData'; import { statusBar } from 'ui/statusBar'; -import { InfoNode, infoNodes } from 'utils/makeTreeviewInfoNode'; import { sortByMetadataName } from 'utils/sortByMetadataName'; -import { addFluxTreeToNode, groupNodesByNamespace } from 'utils/treeNodeUtils'; -import { AnyResourceNode } from '../nodes/anyResourceNode'; -import { TreeNode } from '../nodes/treeNode'; +import { groupNodesByNamespace } from 'utils/treeNodeUtils'; +import { makeTreeNode } from '../nodes/makeTreeNode'; import { HelmReleaseNode } from '../nodes/workload/helmReleaseNode'; import { KustomizationNode } from '../nodes/workload/kustomizationNode'; import { WorkloadNode } from '../nodes/workload/workloadNode'; @@ -28,7 +25,7 @@ export class WorkloadDataProvider extends KubernetesObjectDataProvider { async loadRootNodes() { statusBar.startLoadingTree(); - const workloadNodes: WorkloadNode[] = []; + const nodes: WorkloadNode[] = []; const [kustomizations, helmReleases, _] = await Promise.all([ // Fetch all workloads @@ -38,85 +35,23 @@ export class WorkloadDataProvider extends KubernetesObjectDataProvider { getNamespaces(), ]); - for (const kustomizeWorkload of sortByMetadataName(kustomizations)) { - workloadNodes.push(new KustomizationNode(kustomizeWorkload)); + for (const k of sortByMetadataName(kustomizations)) { + nodes.push(makeTreeNode(k, this) as KustomizationNode); } - for (const helmRelease of sortByMetadataName(helmReleases)) { - workloadNodes.push(new HelmReleaseNode(helmRelease)); + for (const hr of sortByMetadataName(helmReleases)) { + nodes.push(makeTreeNode(hr, this) as HelmReleaseNode); } - for (const node of workloadNodes) { - this.updateWorkloadChildren(node); + for (const node of nodes) { + node.updateChildren(); } statusBar.stopLoadingTree(); - const [groupedNodes] = await groupNodesByNamespace(workloadNodes, false, true); + const [groupedNodes] = await groupNodesByNamespace(nodes, false, true); return groupedNodes; } - /** - * Fetch all kubernetes resources that were created by a kustomize/helmRelease - * and add them as child nodes of the workload. - * @param workloadNode target workload node - */ - async updateWorkloadChildren(workloadNode: WorkloadNode) { - workloadNode.children = infoNodes(InfoNode.Loading); - - if (workloadNode instanceof KustomizationNode) { - this.updateKustomizationChildren(workloadNode); - } else if (workloadNode instanceof HelmReleaseNode) { - this.updateHelmReleaseChildren(workloadNode); - } - } - - async updateKustomizationChildren(node: KustomizationNode) { - const name = node.resource.metadata?.name || ''; - const namespace = node.resource.metadata?.namespace || ''; - const resourceTree = await fluxTools.tree(name, namespace); - - if (!resourceTree) { - node.children = infoNodes(InfoNode.FailedToLoad); - this.redraw(node); - return; - } - - if (!resourceTree.resources) { - node.children = [new TreeNode('No Resources')]; - this.redraw(node); - return; - } - - node.children = []; - await addFluxTreeToNode(node, resourceTree.resources); - this.redraw(node); - } - - - async updateHelmReleaseChildren(node: HelmReleaseNode) { - const name = node.resource.metadata?.name || ''; - const namespace = node.resource.metadata?.namespace || ''; - - const workloadChildren = await getChildrenOfWorkload('helm', name, namespace); - - if (!workloadChildren) { - node.children = infoNodes(InfoNode.FailedToLoad); - this.redraw(node); - return; - } - - if (workloadChildren.length === 0) { - node.children = [new TreeNode('No Resources')]; - this.redraw(node); - return; - } - - const childrenNodes = workloadChildren.map(child => new AnyResourceNode(child)); - const [groupedNodes, clusterScopedNodes] = await groupNodesByNamespace(childrenNodes); - node.children = [...groupedNodes, ...clusterScopedNodes]; - - this.redraw(node); - } } diff --git a/src/ui/treeviews/nodes/anyResourceNode.ts b/src/ui/treeviews/nodes/anyResourceNode.ts index d87dcebd..31c6599d 100644 --- a/src/ui/treeviews/nodes/anyResourceNode.ts +++ b/src/ui/treeviews/nodes/anyResourceNode.ts @@ -1,24 +1,20 @@ import { KubernetesObject } from 'types/kubernetes/kubernetesTypes'; -import { TreeNode } from './treeNode'; +import { SimpleDataProvider } from '../dataProviders/simpleDataProvider'; +import { KubernetesObjectNode } from './kubernetesObjectNode'; /** * Defines any kubernetes resourse. */ -export class AnyResourceNode extends TreeNode { - resource: KubernetesObject; - - constructor(anyResource: KubernetesObject) { - super(anyResource.metadata?.name || ''); +export class AnyResourceNode extends KubernetesObjectNode { + constructor(anyResource: KubernetesObject, dataProvider: SimpleDataProvider) { + super(anyResource, anyResource.metadata.name || '', dataProvider); this.description = anyResource.kind; - - // save metadata reference - this.resource = anyResource; } get tooltip() { - if(this.resource?.metadata?.namespace) { + if(this.resource.metadata.namespace) { return `Namespace: ${this.resource.metadata.namespace}`; } else { return ''; diff --git a/src/ui/treeviews/nodes/cluster/clusterDeploymentNode.ts b/src/ui/treeviews/nodes/cluster/clusterDeploymentNode.ts index 0c5b5618..d5702bda 100644 --- a/src/ui/treeviews/nodes/cluster/clusterDeploymentNode.ts +++ b/src/ui/treeviews/nodes/cluster/clusterDeploymentNode.ts @@ -1,10 +1,11 @@ -import { Deployment, Kind } from 'types/kubernetes/kubernetesTypes'; -import { TreeNode, TreeNodeIcon } from '../treeNode'; +import { Deployment } from 'types/kubernetes/kubernetesTypes'; +import { CommonIcon } from 'ui/icons'; +import { ClusterTreeNode } from './clusterTreeNode'; /** * Defines deployment tree view item for display in GitOps Clusters tree view. */ -export class ClusterDeploymentNode extends TreeNode { +export class ClusterDeploymentNode extends ClusterTreeNode { /** * Cluster deployment kubernetes resource object @@ -12,13 +13,13 @@ export class ClusterDeploymentNode extends TreeNode { resource: Deployment; constructor(deployment: Deployment) { - super(deployment.metadata.name || ''); + super(deployment.metadata.name); this.resource = deployment; this.label = this.getImageName(deployment); - this.setIcon(TreeNodeIcon.Unknown); + this.setCommonIcon(CommonIcon.Unknown); } /** @@ -37,14 +38,10 @@ export class ClusterDeploymentNode extends TreeNode { */ setStatus(status: 'success' | 'failure') { if (status === 'success') { - this.setIcon(TreeNodeIcon.Success); + this.setCommonIcon(CommonIcon.Success); } else if (status === 'failure') { - this.setIcon(TreeNodeIcon.Warning); + this.setCommonIcon(CommonIcon.Warning); } } - - get contexts() { - return [Kind.Deployment]; - } } diff --git a/src/ui/treeviews/nodes/cluster/clusterNode.ts b/src/ui/treeviews/nodes/cluster/clusterNode.ts index f4f9c03f..0e990adc 100644 --- a/src/ui/treeviews/nodes/cluster/clusterNode.ts +++ b/src/ui/treeviews/nodes/cluster/clusterNode.ts @@ -14,16 +14,16 @@ import { CommandId, ContextId } from 'types/extensionIds'; import { ClusterProvider } from 'types/kubernetes/clusterProvider'; import { NodeContext } from 'types/nodeContext'; import { clusterDataProvider, revealClusterNode } from 'ui/treeviews/treeViews'; -import { InfoNode, infoNodes } from 'utils/makeTreeviewInfoNode'; +import { InfoLabel } from 'utils/makeTreeviewInfoNode'; import { createContextMarkdownTable, createMarkdownHr } from 'utils/markdownUtils'; -import { TreeNode } from '../treeNode'; import { ClusterDeploymentNode } from './clusterDeploymentNode'; +import { ClusterTreeNode } from './clusterTreeNode'; /** * Defines Cluster context tree view item for displaying * kubernetes contexts inside the Clusters tree view. */ -export class ClusterNode extends TreeNode { +export class ClusterNode extends ClusterTreeNode { /** * Whether cluster is managed by AKS or Azure ARC @@ -105,11 +105,11 @@ export class ClusterNode extends TreeNode { } if(contextData.apiState === ApiState.ClusterUnreachable) { - this.children = infoNodes(InfoNode.ClusterUnreachable); + this.children = this.infoNodes(InfoLabel.ClusterUnreachable); return; } if(contextData.apiState === ApiState.Loading) { - this.children = infoNodes(InfoNode.LoadingApi); + this.children = this.infoNodes(InfoLabel.LoadingApi); return; } @@ -125,7 +125,7 @@ export class ClusterNode extends TreeNode { this.addChild(new ClusterDeploymentNode(deployment)); } } else { - const notFound = new TreeNode('Flux controllers not found'); + const notFound = new ClusterTreeNode('Flux controllers not found'); notFound.setIcon('warning'); this.addChild(notFound); } @@ -214,7 +214,7 @@ export class ClusterNode extends TreeNode { } get contexts() { - const cs = [NodeContext.Cluster]; + const cs = []; if (typeof this.isGitOpsEnabled === 'boolean') { cs.push( @@ -226,6 +226,8 @@ export class ClusterNode extends TreeNode { this.isCurrent ? NodeContext.CurrentCluster : NodeContext.NotCurrentCluster, ); + cs.push(NodeContext.Cluster); + return cs; } diff --git a/src/ui/treeviews/nodes/cluster/clusterTreeNode.ts b/src/ui/treeviews/nodes/cluster/clusterTreeNode.ts new file mode 100644 index 00000000..3e57e0bb --- /dev/null +++ b/src/ui/treeviews/nodes/cluster/clusterTreeNode.ts @@ -0,0 +1,8 @@ +import { clusterDataProvider } from 'ui/treeviews/treeViews'; +import { TreeNode } from '../treeNode'; + +export class ClusterTreeNode extends TreeNode { + constructor(label: string) { + super(label, clusterDataProvider); + } +} diff --git a/src/ui/treeviews/nodes/documentationNode.ts b/src/ui/treeviews/nodes/documentationNode.ts index c21c5b5a..ff499d0d 100644 --- a/src/ui/treeviews/nodes/documentationNode.ts +++ b/src/ui/treeviews/nodes/documentationNode.ts @@ -3,6 +3,7 @@ import { Uri } from 'vscode'; import { CommandId } from 'types/extensionIds'; import { asAbsolutePath } from 'utils/asAbsolutePath'; import { DocumentationLink } from '../documentationConfig'; +import { documentationDataProvider } from '../treeViews'; import { TreeNode } from './treeNode'; /** @@ -18,7 +19,7 @@ export class DocumentationNode extends TreeNode { newUserGuide?: boolean; constructor(link: DocumentationLink, isParent = false) { - super(link.title); + super(link.title, documentationDataProvider); this.title = link.title; this.newUserGuide = link.newUserGuide; diff --git a/src/ui/treeviews/nodes/gitOpsTemplateNode.ts b/src/ui/treeviews/nodes/gitOpsTemplateNode.ts deleted file mode 100644 index 00f8c166..00000000 --- a/src/ui/treeviews/nodes/gitOpsTemplateNode.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { MarkdownString, ThemeColor, ThemeIcon, TreeItemCollapsibleState } from 'vscode'; - -import { GitOpsTemplate } from 'types/flux/gitOpsTemplate'; -import { Kind } from 'types/kubernetes/kubernetesTypes'; -import { createMarkdownTable } from 'utils/markdownUtils'; -import { TreeNode } from './treeNode'; - -/** - * Base class for all the Source tree view items. - */ -export class GitOpsTemplateNode extends TreeNode { - resource: GitOpsTemplate; - - constructor(template: GitOpsTemplate) { - super(template.metadata?.name || 'No name'); - - this.resource = template; - - this.setIcon(new ThemeIcon('notebook-render-output', new ThemeColor('editorWidget.foreground'))); - this.collapsibleState = TreeItemCollapsibleState.None; - } - - get tooltip() { - return this.getMarkdownHover(this.resource); - } - - // @ts-ignore - get description() { - // return 'Description'; - return false; - } - - getMarkdownHover(template: GitOpsTemplate): MarkdownString { - const markdown: MarkdownString = createMarkdownTable(template); - return markdown; - } - - get contexts() { - return [Kind.GitOpsTemplate]; - } -} diff --git a/src/ui/treeviews/nodes/kubernetesObjectNode.ts b/src/ui/treeviews/nodes/kubernetesObjectNode.ts new file mode 100644 index 00000000..4a68b182 --- /dev/null +++ b/src/ui/treeviews/nodes/kubernetesObjectNode.ts @@ -0,0 +1,74 @@ +import { CommandId } from 'types/extensionIds'; +import { FileTypes } from 'types/fileTypes'; +import { KubernetesObject, qualifyToolkitKind } from 'types/kubernetes/kubernetesTypes'; +import { getResourceUri } from 'utils/getResourceUri'; +import { KnownTreeNodeResources, createMarkdownTable } from 'utils/markdownUtils'; +import { SimpleDataProvider } from '../dataProviders/simpleDataProvider'; +import { TreeNode } from './treeNode'; + + + +export class KubernetesObjectNode extends TreeNode { + /** + * Kubernetes resource. + */ + resource: KubernetesObject; + + constructor(resource: KubernetesObject, label: string, dataProvider: SimpleDataProvider) { + super(label, dataProvider); + + this.resource = resource; + } + + fullyQualifyKind(): string { + return qualifyToolkitKind(this.resource.kind); + } + + // @ts-ignore + get tooltip(): string | MarkdownString { + if (this.resource) { + return createMarkdownTable(this.resource as KnownTreeNodeResources); + } + } + + // @ts-ignore + get command(): Command | undefined { + // Set click event handler to load kubernetes resource as yaml file in editor. + if (this.resource) { + let stringKind = this.fullyQualifyKind(); + const resourceUri = getResourceUri( + this.resource.metadata.namespace, + `${stringKind}/${this.resource.metadata.name}`, + FileTypes.Yaml, + ); + + return { + command: CommandId.EditorOpenResource, + arguments: [resourceUri], + title: 'View Resource', + }; + } + } + + findChildByResource(resource: KubernetesObject): KubernetesObjectNode | undefined { + const found = this.children.find(child => { + if (child instanceof KubernetesObjectNode) { + return child.resource.metadata.name === resource.metadata.name && + child.resource.kind === resource.kind && + child.resource.metadata.namespace === resource.metadata.namespace; + } + }); + if (found) { + return found as KubernetesObjectNode; + } + } + + get viewStateKey(): string { + const parentViewKey = this.parent?.viewStateKey; + return this.resource.metadata.uid + parentViewKey + this.dataProvider.guid; + } + + get contextType(): string | undefined { + return this.resource.kind; + } +} diff --git a/src/ui/treeviews/nodes/makeTreeNode.ts b/src/ui/treeviews/nodes/makeTreeNode.ts index 4ffd26f0..ffbd6e1f 100644 --- a/src/ui/treeviews/nodes/makeTreeNode.ts +++ b/src/ui/treeviews/nodes/makeTreeNode.ts @@ -1,13 +1,16 @@ import { KubernetesObject } from '@kubernetes/client-node'; import { Kind } from 'types/kubernetes/kubernetesTypes'; +import { SimpleDataProvider } from '../dataProviders/simpleDataProvider'; 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 { CanaryNode } from './wge/canaryNode'; +import { GitOpsSetNode } from './wge/gitOpsSetNode'; +import { GitOpsTemplateNode } from './wge/gitOpsTemplateNode'; import { HelmReleaseNode } from './workload/helmReleaseNode'; import { KustomizationNode } from './workload/kustomizationNode'; @@ -19,7 +22,10 @@ const nodeConstructors = { 'HelmRepository': HelmRepositoryNode, 'HelmRelease': HelmReleaseNode, 'Kustomization': KustomizationNode, + 'Canary': CanaryNode, 'GitOpsTemplate': GitOpsTemplateNode, + 'GitOpsSet': GitOpsSetNode, + 'Pipeline': GitOpsSetNode, 'Namespace': NamespaceNode, @@ -27,15 +33,13 @@ const nodeConstructors = { 'Node': AnyResourceNode, 'Pod': AnyResourceNode, 'ConfigMap': AnyResourceNode, + 'GitopsCluster': 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); +export function makeTreeNode(object: KubernetesObject, dataProvider: SimpleDataProvider): TreeNode { + let constructor = nodeConstructors[object.kind as Kind]; + if(!constructor) { + constructor = AnyResourceNode; } + return new constructor(object as any, dataProvider); } diff --git a/src/ui/treeviews/nodes/namespaceNode.ts b/src/ui/treeviews/nodes/namespaceNode.ts index 60560837..5189c3d9 100644 --- a/src/ui/treeviews/nodes/namespaceNode.ts +++ b/src/ui/treeviews/nodes/namespaceNode.ts @@ -1,37 +1,36 @@ import { Kind, Namespace } from 'types/kubernetes/kubernetesTypes'; +import { CommonIcon } from 'ui/icons'; import { TreeItemCollapsibleState } from 'vscode'; +import { SimpleDataProvider } from '../dataProviders/simpleDataProvider'; +import { KubernetesObjectNode } from './kubernetesObjectNode'; import { SourceNode } from './source/sourceNode'; -import { TreeNode, TreeNodeIcon } from './treeNode'; +import { WgeNode } from './wge/wgeNodes'; import { WorkloadNode } from './workload/workloadNode'; /** * Defines any kubernetes resourse. */ -export class NamespaceNode extends TreeNode { +export class NamespaceNode extends KubernetesObjectNode { /** * kubernetes resource metadata */ resource: Namespace; - constructor(namespace: Namespace) { - super(namespace.metadata?.name || ''); + constructor(namespace: Namespace, dataProvider: SimpleDataProvider) { + super(namespace, namespace.metadata.name, dataProvider); this.description = Kind.Namespace; this.resource = namespace; } - get contexts() { - return [Kind.Namespace]; - } - updateLabel(withIcons = true) { const totalLength = this.children.length; let readyLength = 0; let loadingLength = 0; for(const child of this.children) { - if(child instanceof SourceNode || child instanceof WorkloadNode) { + if(child instanceof SourceNode || child instanceof WorkloadNode || child instanceof WgeNode) { if(child.resourceIsReady) { readyLength++; } else if(child.resourceIsProgressing) { @@ -45,11 +44,11 @@ export class NamespaceNode extends TreeNode { const validLength = readyLength + loadingLength; if(withIcons) { if(readyLength === totalLength) { - this.setIcon(TreeNodeIcon.Success); + this.setCommonIcon(CommonIcon.Success); } else if(validLength === totalLength) { - this.setIcon(TreeNodeIcon.Progressing); + this.setCommonIcon(CommonIcon.Progressing); } else { - this.setIcon(TreeNodeIcon.Warning); + this.setCommonIcon(CommonIcon.Warning); } } else { this.setIcon(undefined); @@ -57,9 +56,9 @@ export class NamespaceNode extends TreeNode { if(this.collapsibleState === TreeItemCollapsibleState.Collapsed) { const lengthLabel = totalLength === validLength ? `${totalLength}` : `${validLength}/${totalLength}`; - this.label = `${this.resource.metadata?.name} (${lengthLabel})`; + this.label = `${this.resource.metadata.name} (${lengthLabel})`; } else { - this.label = `${this.resource.metadata?.name}`; + this.label = `${this.resource.metadata.name}`; } } } diff --git a/src/ui/treeviews/nodes/source/bucketNode.ts b/src/ui/treeviews/nodes/source/bucketNode.ts index 3754c156..8c2e7e1a 100644 --- a/src/ui/treeviews/nodes/source/bucketNode.ts +++ b/src/ui/treeviews/nodes/source/bucketNode.ts @@ -1,5 +1,4 @@ import { Bucket } from 'types/flux/bucket'; -import { Kind } from 'types/kubernetes/kubernetesTypes'; import { SourceNode } from './sourceNode'; /** @@ -10,8 +9,4 @@ export class BucketNode extends SourceNode { * Bucket kubernetes resource object */ resource!: Bucket; - - get contexts() { - return [Kind.Bucket]; - } } diff --git a/src/ui/treeviews/nodes/source/gitRepositoryNode.ts b/src/ui/treeviews/nodes/source/gitRepositoryNode.ts index a3535c6e..0ae35e41 100644 --- a/src/ui/treeviews/nodes/source/gitRepositoryNode.ts +++ b/src/ui/treeviews/nodes/source/gitRepositoryNode.ts @@ -1,6 +1,4 @@ import { GitRepository } from 'types/flux/gitRepository'; -import { Kind } from 'types/kubernetes/kubernetesTypes'; -import { NodeContext } from 'types/nodeContext'; import { SourceNode } from './sourceNode'; /** @@ -8,13 +6,4 @@ import { SourceNode } from './sourceNode'; */ export class GitRepositoryNode extends SourceNode { resource!: GitRepository; - - - get contexts() { - const contextsArr: string[] = [Kind.GitRepository]; - contextsArr.push( - this.resource.spec.suspend ? NodeContext.Suspend : NodeContext.NotSuspend, - ); - return contextsArr; - } } diff --git a/src/ui/treeviews/nodes/source/helmRepositoryNode.ts b/src/ui/treeviews/nodes/source/helmRepositoryNode.ts index 9358dd7d..4b35e8eb 100644 --- a/src/ui/treeviews/nodes/source/helmRepositoryNode.ts +++ b/src/ui/treeviews/nodes/source/helmRepositoryNode.ts @@ -1,6 +1,4 @@ import { HelmRepository } from 'types/flux/helmRepository'; -import { Kind } from 'types/kubernetes/kubernetesTypes'; -import { NodeContext } from 'types/nodeContext'; import { SourceNode } from './sourceNode'; /** @@ -8,13 +6,4 @@ import { SourceNode } from './sourceNode'; */ export class HelmRepositoryNode extends SourceNode { resource!: HelmRepository; - - - get contexts() { - const contextsArr: string[] = [Kind.HelmRepository]; - contextsArr.push( - this.resource.spec.suspend ? NodeContext.Suspend : NodeContext.NotSuspend, - ); - return contextsArr; - } } diff --git a/src/ui/treeviews/nodes/source/ociRepositoryNode.ts b/src/ui/treeviews/nodes/source/ociRepositoryNode.ts index 09cc9fdb..bd98df64 100644 --- a/src/ui/treeviews/nodes/source/ociRepositoryNode.ts +++ b/src/ui/treeviews/nodes/source/ociRepositoryNode.ts @@ -1,6 +1,4 @@ import { OCIRepository } from 'types/flux/ociRepository'; -import { Kind } from 'types/kubernetes/kubernetesTypes'; -import { NodeContext } from 'types/nodeContext'; import { SourceNode } from './sourceNode'; /** @@ -17,11 +15,4 @@ export class OCIRepositoryNode extends SourceNode { super(ociRepository); } - get contexts() { - const contextsArr: string[] = [Kind.OCIRepository]; - contextsArr.push( - this.resource.spec.suspend ? NodeContext.Suspend : NodeContext.NotSuspend, - ); - return contextsArr; - } } diff --git a/src/ui/treeviews/nodes/source/sourceNode.ts b/src/ui/treeviews/nodes/source/sourceNode.ts index d7a2ae04..67bbfa9a 100644 --- a/src/ui/treeviews/nodes/source/sourceNode.ts +++ b/src/ui/treeviews/nodes/source/sourceNode.ts @@ -1,15 +1,29 @@ import { FluxSourceObject } from 'types/flux/object'; +import { NodeContext } from 'types/nodeContext'; +import { SimpleDataProvider } from 'ui/treeviews/dataProviders/simpleDataProvider'; +import { sourceDataProvider } from 'ui/treeviews/treeViews'; import { shortenRevision } from 'utils/stringUtils'; -import { ToolkitNode } from './toolkitNode'; +import { ToolkitNode } from '../toolkitNode'; /** * Base class for all the Source tree view items. */ export class SourceNode extends ToolkitNode { resource!: FluxSourceObject; + dataProvider!: SimpleDataProvider; + + + constructor(resource: FluxSourceObject) { + super(resource, sourceDataProvider); + } get revision() { return shortenRevision(this.resource.status.artifact?.revision); } + get contexts() { + return this.resource.spec.suspend ? [NodeContext.Suspend] : [NodeContext.NotSuspend]; + } + + } diff --git a/src/ui/treeviews/nodes/source/toolkitNode.ts b/src/ui/treeviews/nodes/toolkitNode.ts similarity index 62% rename from src/ui/treeviews/nodes/source/toolkitNode.ts rename to src/ui/treeviews/nodes/toolkitNode.ts index 2e2491f7..0c612c3f 100644 --- a/src/ui/treeviews/nodes/source/toolkitNode.ts +++ b/src/ui/treeviews/nodes/toolkitNode.ts @@ -1,7 +1,9 @@ -import { FluxObject } from 'types/flux/object'; -import { Condition } from 'types/kubernetes/kubernetesTypes'; +import { ToolkitObject } from 'types/flux/object'; +import { Condition, Kind } from 'types/kubernetes/kubernetesTypes'; +import { CommonIcon } from 'ui/icons'; import { createMarkdownError, createMarkdownHr, createMarkdownTable } from 'utils/markdownUtils'; -import { TreeNode, TreeNodeIcon } from '../treeNode'; +import { SimpleDataProvider } from '../dataProviders/simpleDataProvider'; +import { KubernetesObjectNode } from './kubernetesObjectNode'; export enum ReconcileState { Ready, @@ -9,14 +11,13 @@ export enum ReconcileState { Progressing, } -export class ToolkitNode extends TreeNode { - resource: FluxObject; +export class ToolkitNode extends KubernetesObjectNode { + resource!: ToolkitObject; reconcileState: ReconcileState = ReconcileState.Progressing; - constructor(resource: FluxObject) { - super(`${resource.kind}: ${resource.metadata?.name || 'unknown'}`); + constructor(resource: ToolkitObject, dataProvider: SimpleDataProvider) { + super(resource, `${resource.kind}: ${resource.metadata.name}`, dataProvider); - this.resource = resource; this.updateStatus(); } @@ -28,13 +29,13 @@ export class ToolkitNode extends TreeNode { const condition = this.readyOrFirstCondition; if (condition?.status === 'True') { this.reconcileState = ReconcileState.Ready; - this.setIcon(TreeNodeIcon.Success); - } else if (condition?.reason === 'Progressing') { + this.setCommonIcon(CommonIcon.Success); + } else if (condition?.reason === 'Progressing' || condition?.reason === 'Promoting' || condition?.reason === 'Finalising') { this.reconcileState = ReconcileState.Progressing; - this.setIcon(TreeNodeIcon.Progressing); + this.setCommonIcon(CommonIcon.Progressing); } else { this.reconcileState = ReconcileState.Failed; - this.setIcon(TreeNodeIcon.Error); + this.setCommonIcon(CommonIcon.Error); } } @@ -74,19 +75,29 @@ export class ToolkitNode extends TreeNode { // @ts-ignore get description() { - const isSuspendIcon = this.resource.spec?.suspend ? '⏸ ' : ''; let revisionOrError = ''; if (!this.resourceIsReady) { revisionOrError = `${this.readyOrFirstCondition?.reason || ''}`; + if(this.resource.kind === Kind.Canary) { + revisionOrError = `${revisionOrError} ${this.resource.status?.canaryWeight}%`; + } } else { revisionOrError = this.revision; } - return `${isSuspendIcon}${revisionOrError}`; + return `${this.isSuspendIcon}${revisionOrError}`; } get revision(): string { return 'unknown'; } + + get isSuspendIcon(): string { + if(this.resource.kind !== Kind.Pipeline) { + return this.resource.spec?.suspend ? '⏸ ' : ''; + } + return ''; + } + } diff --git a/src/ui/treeviews/nodes/treeNode.ts b/src/ui/treeviews/nodes/treeNode.ts index 9dcac84c..8d4375df 100644 --- a/src/ui/treeviews/nodes/treeNode.ts +++ b/src/ui/treeviews/nodes/treeNode.ts @@ -1,31 +1,15 @@ -import { Command, MarkdownString, ThemeColor, ThemeIcon, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode'; +import { ThemeIcon, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode'; -import { CommandId } from 'types/extensionIds'; -import { FileTypes } from 'types/fileTypes'; -import { KubernetesObject, qualifyToolkitKind } from 'types/kubernetes/kubernetesTypes'; +import { CommonIcon, commonIcon } from 'ui/icons'; import { asAbsolutePath } from 'utils/asAbsolutePath'; -import { getResourceUri } from 'utils/getResourceUri'; -import { KnownTreeNodeResources, createMarkdownTable } from 'utils/markdownUtils'; - -export const enum TreeNodeIcon { - Error = 'error', - Warning = 'warning', - Success = 'success', - Disconnected = 'disconnected', - Progressing = 'progressing', - Loading = 'loading', - Unknown = 'unknown', -} +import { InfoLabel, infoNode } from 'utils/makeTreeviewInfoNode'; +import { SimpleDataProvider } from '../dataProviders/simpleDataProvider'; /** * Defines tree view item base class used by all GitOps tree views. */ export class TreeNode extends TreeItem { - - /** - * Kubernetes resource. - */ - resource?: KubernetesObject; + resource?: any; /** * Reference to the parent node (if exists). @@ -35,14 +19,33 @@ export class TreeNode extends TreeItem { /** * Reference to all the child nodes. */ - children: TreeNode[] = []; + private _children: TreeNode[] = []; + + get children(): TreeNode[] { + return this._children; + } + + set children(cs: TreeNode[]) { + this._children = cs; + this._children.forEach(c => c.parent = this); + } + + dataProvider: SimpleDataProvider; + + /* + * async load children for the node + */ + async updateChildren() { + // no-op + } /** * Creates new tree node. * @param label Tree node label */ - constructor(label: string) { + constructor(label: string, dataProvider: SimpleDataProvider) { super(label, TreeItemCollapsibleState.None); + this.dataProvider = dataProvider; } /** @@ -52,6 +55,11 @@ export class TreeNode extends TreeItem { this.collapsibleState = TreeItemCollapsibleState.Collapsed; } + makeUncollapsible() { + this.collapsibleState = TreeItemCollapsibleState.None; + } + + /** * Expands a tree node and shows its children. */ @@ -64,6 +72,11 @@ export class TreeNode extends TreeItem { */ updateStatus(): void {} + redraw() { + if(this.dataProvider) { + this.dataProvider.redraw(this); + } + } /** * Sets tree view item icon. @@ -72,22 +85,8 @@ 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 | undefined) { - if (icon === TreeNodeIcon.Error) { - 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.Progressing) { - this.iconPath = new ThemeIcon('sync~spin', new ThemeColor('terminal.ansiGreen')); - } else if (icon === TreeNodeIcon.Loading) { - this.iconPath = new ThemeIcon('loading~spin', new ThemeColor('foreground')); - } else if (icon === TreeNodeIcon.Success) { - this.iconPath = new ThemeIcon('pass', new ThemeColor('terminal.ansiGreen')); - } else if (icon === TreeNodeIcon.Unknown) { - this.iconPath = new ThemeIcon('circle-large-outline'); - } else if (typeof icon === 'string') { + setIcon(icon: string | ThemeIcon | Uri |undefined) { + if (typeof icon === 'string') { this.iconPath = { light: asAbsolutePath(`resources/icons/light/${icon}.svg`), dark: asAbsolutePath(`resources/icons/dark/${icon}.svg`), @@ -97,6 +96,11 @@ export class TreeNode extends TreeItem { } } + setCommonIcon(icon: CommonIcon) { + this.iconPath = commonIcon(icon); + } + + /** * Add new tree view item to the children collection. * @param child Child tree view item to add. @@ -118,16 +122,6 @@ export class TreeNode extends TreeItem { this.children = this.children.filter(c => c !== child); } - findChildByResource(resource: KubernetesObject): TreeNode | undefined { - return this.children.find(child => { - if (child.resource) { - return child.resource.metadata?.name === resource.metadata?.name && - child.resource.kind === resource.kind && - child.resource.metadata?.namespace === resource.metadata?.namespace; - } - }); - } - /** * @@ -147,48 +141,57 @@ export class TreeNode extends TreeItem { .join(''); } - fullyQualifyKind(): string { - return qualifyToolkitKind(this.resource?.kind || ''); - } - - // @ts-ignore - get tooltip(): string | MarkdownString { - if (this.resource) { - return createMarkdownTable(this.resource as KnownTreeNodeResources); - } - } - - // @ts-ignore - get command(): Command | undefined { - // Set click event handler to load kubernetes resource as yaml file in editor. - if (this.resource) { - let stringKind = this.fullyQualifyKind(); - const resourceUri = getResourceUri( - this.resource.metadata?.namespace, - `${stringKind}/${this.resource.metadata?.name}`, - FileTypes.Yaml, - ); - - return { - command: CommandId.EditorOpenResource, - arguments: [resourceUri], - title: 'View Resource', - }; - } - } - /** * VSCode contexts to use for setting {@link contextValue} * of this tree node. Used for context/inline menus. + * + * Contexts are used to enable/disable menu items. */ get contexts(): string[] { return []; } + /** + * + * Context for types of resources. + */ + get contextType(): string | undefined { + return; + } + // @ts-ignore get contextValue() { - if (this.contexts.length) { - return this.joinContexts(this.contexts); + const cs = [...this.contexts]; + if(this.contextType) { + cs.push(this.contextType); + } + if(cs.length) { + return this.joinContexts(cs); } } + + // @ts-ignore + get tooltip(): string | MarkdownString { + return ''; + } + + // @ts-ignore + get command(): Command | undefined {} + + + get viewStateKey(): string { + return ''; + } + + + infoNodes(type: InfoLabel) { + return [this.infoNode(type)]; + } + + infoNode(type: InfoLabel) { + return infoNode(type, this.dataProvider); + } } + + + diff --git a/src/ui/treeviews/nodes/wge/canaryNode.ts b/src/ui/treeviews/nodes/wge/canaryNode.ts new file mode 100644 index 00000000..c58dbd75 --- /dev/null +++ b/src/ui/treeviews/nodes/wge/canaryNode.ts @@ -0,0 +1,64 @@ +import { getCanaryChildren } from 'cli/kubernetes/kubectlGet'; +import { currentContextData } from 'data/contextData'; +import { Canary } from 'types/flux/canary'; +import { NodeContext } from 'types/nodeContext'; +import { InfoLabel } from 'utils/makeTreeviewInfoNode'; +import { groupNodesByNamespace } from 'utils/treeNodeUtils'; +import { AnyResourceNode } from '../anyResourceNode'; +import { WgeNode } from './wgeNodes'; + +export class CanaryNode extends WgeNode { + resource!: Canary; + + get revision() { + // return shortenRevision(this.resource.status.lastAppliedRevision); + return `${this.resource.status.phase} ${this.resource.status.lastAppliedSpec || ''}`; + } + + get contexts() { + return [NodeContext.HasWgePortal]; + } + + get wgePortalQuery() { + const name = this.resource.metadata.name; + const namespace = this.resource.metadata.namespace || 'default'; + const clusterName = currentContextData().wgeClusterName; + + return `canary_details/details?clusterName=${clusterName}&name=${name}&namespace=${namespace}`; + } + + + + async updateChildren() { + // deployment/-primary + if(!this.resource.metadata.name) { + return; + } + + this.children = this.infoNodes(InfoLabel.Loading); + this.redraw(); + + const [children, primary] = await Promise.all([getCanaryChildren(this.resource.metadata.name), getCanaryChildren(`${this.resource.metadata.name}-primary`)]); + const canaryChildren = [...children, ...primary]; + + if (!canaryChildren) { + this.children = this.infoNodes(InfoLabel.FailedToLoad); + this.redraw(); + return; + } + + if (canaryChildren.length === 0) { + this.children = this.infoNodes(InfoLabel.NoResources); + this.redraw(); + return; + } + + const childrenNodes = canaryChildren.map(child => new AnyResourceNode(child, this.dataProvider)); + const [groupedNodes, clusterScopedNodes] = await groupNodesByNamespace(childrenNodes); + this.children = [...groupedNodes, ...clusterScopedNodes]; + + this.redraw(); + return; + } + +} diff --git a/src/ui/treeviews/nodes/wge/environmentNode.ts b/src/ui/treeviews/nodes/wge/environmentNode.ts new file mode 100644 index 00000000..d99d896d --- /dev/null +++ b/src/ui/treeviews/nodes/wge/environmentNode.ts @@ -0,0 +1,94 @@ +import { getResource } from 'cli/kubernetes/kubectlGet'; +import { GitOpsCluster } from 'types/flux/gitOpsCluster'; +import { LocalAppReference, Pipeline, PipelineEnvironment, PipelineTarget } from 'types/flux/pipeline'; +import { Kind, KubernetesObject } from 'types/kubernetes/kubernetesTypes'; +import { themeIcon } from 'ui/icons'; +import { wgeDataProvider } from 'ui/treeviews/treeViews'; +import { makeTreeNode } from '../makeTreeNode'; +import { TreeNode } from '../treeNode'; + +export class PipelineEnvironmentNode extends TreeNode { + environment: PipelineEnvironment; + pipepine: Pipeline; + + constructor(environment: PipelineEnvironment, pipeline: Pipeline) { + super(environment.name, wgeDataProvider); + + this.makeCollapsible(); + this.environment = environment; + this.pipepine = pipeline; + + this.description = 'Environment'; + } + + async updateChildren() { + const targets = this.environment.targets; + for(const target of targets) { + const targetCluster = await this.getTargetCluster(target); + const appRef = this.pipepine.spec.appRef; + const targetNode = new PipelineTargetNode(target, appRef, targetCluster); + targetNode.updateChildren(); + this.addChild(targetNode); + } + this.redraw(); + } + + async getTargetCluster(target: PipelineTarget): Promise { + if(target.clusterRef) { + const namespace = target.clusterRef.namespace || this.pipepine.metadata.namespace || 'default'; + const cluster = await getResource(target.clusterRef.name, namespace, target.clusterRef.kind as Kind); + return cluster; + } + // if no clusterRef is set then the current cluster is the target cluster + return undefined; + } +} + + +export class PipelineTargetNode extends TreeNode { + target: PipelineTarget; + targetCluster?: GitOpsCluster; + appRef: LocalAppReference; + + constructor(target: PipelineTarget, appRef: LocalAppReference, targetCluster?: GitOpsCluster) { + const clusterLabel = targetCluster ? `${targetCluster.metadata.name}.${targetCluster.metadata.namespace}` : '(this cluster)'; + super(`${clusterLabel} ${appRef.name}.${target.namespace}`, wgeDataProvider); + + this.target = target; + this.targetCluster = targetCluster; + this.appRef = appRef; + + this.makeCollapsible(); + + this.description = 'Target'; + } + + async updateChildren() { + if(this.targetCluster) { + const gopsClusterNode = makeTreeNode(this.targetCluster, wgeDataProvider); + this.addChild(gopsClusterNode); + + const crossClusterAppNode = new TreeNode(`${this.appRef.name}.${this.target.namespace}`, wgeDataProvider); + crossClusterAppNode.description = `${this.appRef.kind} (in ${this.targetCluster.metadata.name})`; + crossClusterAppNode.setIcon(themeIcon('link-external', 'descriptionForeground')); + this.addChild(crossClusterAppNode); + } else { + const localHr = await getResource(this.appRef.name, this.target.namespace, this.appRef.kind as Kind); + if(localHr) { + const localAppNode = makeTreeNode(localHr, wgeDataProvider); + localAppNode.label = `${this.appRef.kind}: ${this.appRef.name}.${this.target.namespace}`; + localAppNode.updateChildren(); + this.addChild(localAppNode); + } + } + + this.redraw(); + } + + + + + + +} + diff --git a/src/ui/treeviews/nodes/wge/gitOpsSetNode.ts b/src/ui/treeviews/nodes/wge/gitOpsSetNode.ts new file mode 100644 index 00000000..40441796 --- /dev/null +++ b/src/ui/treeviews/nodes/wge/gitOpsSetNode.ts @@ -0,0 +1,36 @@ +import { currentContextData } from 'data/contextData'; +import { GitOpsSet } from 'types/flux/gitopsset'; +import { NodeContext } from 'types/nodeContext'; +import { WgeNode } from './wgeNodes'; + +export class GitOpsSetNode extends WgeNode { + resource!: GitOpsSet; + + constructor(gitOpsSet: GitOpsSet) { + super(gitOpsSet); + + this.makeUncollapsible(); + } + + get revision() { + const condition = this.readyOrFirstCondition; + return condition?.lastTransitionTime ? `${condition?.lastTransitionTime.toLocaleString()}` : ''; + } + + get contexts() { + const cs = this.resource.spec.suspend ? [NodeContext.Suspend] : [NodeContext.NotSuspend]; + cs.push(NodeContext.HasWgePortal); + return cs; + } + + + get wgePortalQuery() { + const name = this.resource.metadata.name; + const namespace = this.resource.metadata.namespace || 'default'; + const clusterName = currentContextData().wgeClusterName; + + return `gitopssets/object/details?clusterName=${clusterName}&name=${name}&namespace=${namespace}`; + } + + +} diff --git a/src/ui/treeviews/nodes/wge/gitOpsTemplateNode.ts b/src/ui/treeviews/nodes/wge/gitOpsTemplateNode.ts new file mode 100644 index 00000000..6ee82cc0 --- /dev/null +++ b/src/ui/treeviews/nodes/wge/gitOpsTemplateNode.ts @@ -0,0 +1,76 @@ +import { MarkdownString } from 'vscode'; + +import { GitOpsTemplate } from 'types/flux/gitOpsTemplate'; +import { NodeContext } from 'types/nodeContext'; +import { themeIcon } from 'ui/icons'; +import { wgeDataProvider } from 'ui/treeviews/treeViews'; +import { createMarkdownTable } from 'utils/markdownUtils'; +import { KubernetesObjectNode } from '../kubernetesObjectNode'; + +export enum TemplateType { + Cluster = 'cluster', + Application = 'application', + Pipeline = 'pipeline', +} + +export class GitOpsTemplateNode extends KubernetesObjectNode { + resource: GitOpsTemplate; + + constructor(template: GitOpsTemplate) { + super(template, template.metadata.name, wgeDataProvider); + + this.resource = template; + + if(this.templateType === 'cluster') { + this.setIcon(themeIcon('server-environment', 'descriptionForeground')); + } else if (this.templateType === 'application') { + this.setIcon(themeIcon('preview', 'descriptionForeground')); + } else if (this.templateType === 'pipeline') { + this.setIcon(themeIcon('rocket', 'descriptionForeground')); + + } + this.makeUncollapsible(); + } + + get tooltip() { + return this.getMarkdownHover(this.resource); + } + + get templateType(): TemplateType { + switch(this.resource.metadata.labels?.['weave.works/template-type']){ + case 'cluster': + return TemplateType.Cluster; + case 'application': + return TemplateType.Application; + case 'pipeline': + return TemplateType.Pipeline; + default: + return TemplateType.Application; + } + } + + // @ts-ignore + get description() { + return false; + } + + getMarkdownHover(template: GitOpsTemplate): MarkdownString { + const markdown: MarkdownString = createMarkdownTable(template); + return markdown; + } + + get contexts() { + return [NodeContext.HasWgePortal]; + } + + + get wgePortalQuery() { + const name = this.resource.metadata.name; + const namespace = this.resource.metadata.namespace || 'default'; + + + return `templates/create?name=${name}&namespace=${namespace}`; + } + + +} diff --git a/src/ui/treeviews/nodes/wge/pipelineNode.ts b/src/ui/treeviews/nodes/wge/pipelineNode.ts new file mode 100644 index 00000000..a1d43e36 --- /dev/null +++ b/src/ui/treeviews/nodes/wge/pipelineNode.ts @@ -0,0 +1,62 @@ +import { Pipeline } from 'types/flux/pipeline'; +import { NodeContext } from 'types/nodeContext'; +import { TreeNode } from '../treeNode'; +import { PipelineEnvironmentNode } from './environmentNode'; +import { WgeNode } from './wgeNodes'; + +export class PipelineNode extends WgeNode { + resource!: Pipeline; + + constructor(pipeline: Pipeline) { + super(pipeline); + } + + get revision() { + const condition = this.readyOrFirstCondition; + return condition?.lastTransitionTime ? `${condition?.lastTransitionTime.toLocaleString()}` : ''; + } + + get contexts() { + const promotionContext = this.isManualPromotion ? NodeContext.ManualPromotion : NodeContext.AutoPromotion; + return [NodeContext.HasWgePortal, promotionContext]; + } + + get wgePortalQuery() { + const name = this.resource.metadata.name; + const namespace = this.resource.metadata.namespace || 'default'; + + return `pipelines/details/status?kind=Pipeline&name=${name}&namespace=${namespace}`; + } + + + async updateChildren() { + this.children = await this.createEnvNodes(); + this.redraw(); + } + + async createEnvNodes(): Promise { + const envNodes = []; + for(const env of this.resource.spec.environments) { + const envNode = new PipelineEnvironmentNode(env, this.resource); + envNode.updateChildren(); + envNodes.push(envNode); + } + + return envNodes; + } + + + get isManualPromotion() { + return !!this.resource.spec?.promotion?.manual; + } + + get isSuspendIcon(): string { + return this.isManualPromotion ? '⏸ ' : ''; + } + + + async createPromotionNodes() { + return []; + } + +} diff --git a/src/ui/treeviews/nodes/wge/wgeNodes.ts b/src/ui/treeviews/nodes/wge/wgeNodes.ts new file mode 100644 index 00000000..6d2bc443 --- /dev/null +++ b/src/ui/treeviews/nodes/wge/wgeNodes.ts @@ -0,0 +1,106 @@ + +import { ToolkitObject } from 'types/flux/object'; +import { NodeContext } from 'types/nodeContext'; +import { themeIcon } from 'ui/icons'; +import { SimpleDataProvider } from 'ui/treeviews/dataProviders/simpleDataProvider'; +import { wgeDataProvider } from 'ui/treeviews/treeViews'; +import { ToolkitNode } from '../toolkitNode'; +import { TreeNode } from '../treeNode'; + +export class WgeNode extends ToolkitNode { + dataProvider!: SimpleDataProvider; + + constructor(object: ToolkitObject) { + super(object, wgeDataProvider); + + // this.label = `${object.kind}: ${object.metadata.name}.${object.metadata.namespace}`; + this.label = `${object.metadata.name}`; + this.makeCollapsible(); + } +} + +export class WgeContainerNode extends TreeNode { + constructor(label: any) { + super(label, wgeDataProvider); + } + + get contexts() { + return [NodeContext.HasWgePortal, 'Container']; + } + + get wgePortalQuery() { + return ''; + } +} + + +export class TemplatesContainerNode extends WgeContainerNode { + constructor() { + super('Templates'); + + this.setIcon(themeIcon('notebook-render-output')); + this.makeCollapsible(); + } + + get wgePortalQuery() { + return 'templates'; + } + + get viewStateKey() { + return 'TemplatesContainer'; + } +} + + +export class CanariesContainerNode extends WgeContainerNode { + constructor() { + super('Canaries'); + + this.setIcon(themeIcon('symbol-null')); + this.makeCollapsible(); + } + + get wgePortalQuery() { + return 'delivery'; + } + + get viewStateKey() { + return 'CanariesContainer'; + } +} + + +export class PipelinesContainerNode extends WgeContainerNode { + constructor() { + super('Pipelines'); + + this.setIcon(themeIcon('rocket')); + this.makeCollapsible(); + } + + get wgePortalQuery() { + return 'pipelines'; + } + + get viewStateKey() { + return 'PipelinesContainer'; + } +} + + +export class GitOpsSetsContainerNode extends WgeContainerNode { + constructor() { + super('GitOpsSets'); + + this.setIcon(themeIcon('outline-view-icon')); + this.makeCollapsible(); + } + + get wgePortalQuery() { + return 'gitopssets'; + } + + get viewStateKey() { + return 'GitOpsSetsContainer'; + } +} diff --git a/src/ui/treeviews/nodes/workload/helmReleaseNode.ts b/src/ui/treeviews/nodes/workload/helmReleaseNode.ts index 38165c67..812c57b9 100644 --- a/src/ui/treeviews/nodes/workload/helmReleaseNode.ts +++ b/src/ui/treeviews/nodes/workload/helmReleaseNode.ts @@ -1,6 +1,11 @@ +import { getHelmReleaseChildren } from 'cli/kubernetes/kubectlGet'; import { HelmRelease } from 'types/flux/helmRelease'; -import { Kind } from 'types/kubernetes/kubernetesTypes'; -import { NodeContext } from 'types/nodeContext'; +import { SimpleDataProvider } from 'ui/treeviews/dataProviders/simpleDataProvider'; +import { InfoLabel } from 'utils/makeTreeviewInfoNode'; +import { shortenRevision } from 'utils/stringUtils'; +import { groupNodesByNamespace } from 'utils/treeNodeUtils'; +import { AnyResourceNode } from '../anyResourceNode'; +import { TreeNode } from '../treeNode'; import { WorkloadNode } from './workloadNode'; /** @@ -8,22 +13,47 @@ import { WorkloadNode } from './workloadNode'; */ export class HelmReleaseNode extends WorkloadNode { resource!: HelmRelease; + dataProvider!: SimpleDataProvider; /** * Creates new helm release tree view item for display. * @param helmRelease Helm release kubernetes object info. */ - constructor(helmRelease: HelmRelease) { - super(helmRelease); + constructor(helmRelease: HelmRelease, dataProvider: SimpleDataProvider) { + super(helmRelease, dataProvider); this.makeCollapsible(); } - get contexts() { - const contextsArr: string[] = [Kind.HelmRelease]; - contextsArr.push( - this.resource.spec.suspend ? NodeContext.Suspend : NodeContext.NotSuspend, - ); - return contextsArr; + get revision() { + return shortenRevision(this.resource.status.lastAppliedRevision); + } + + async updateChildren() { + this.children = this.infoNodes(InfoLabel.Loading); + this.redraw(); + + const name = this.resource.metadata.name; + const namespace = this.resource.metadata.namespace || ''; + + const workloadChildren = await getHelmReleaseChildren(name, namespace); + + if (!workloadChildren) { + this.children = this.infoNodes(InfoLabel.FailedToLoad); + this.redraw(); + return; + } + + if (workloadChildren.length === 0) { + this.children = [new TreeNode('No Resources', this.dataProvider)]; + this.redraw(); + return; + } + + const childrenNodes = workloadChildren.map(child => new AnyResourceNode(child, this.dataProvider)); + const [groupedNodes, clusterScopedNodes] = await groupNodesByNamespace(childrenNodes); + this.children = [...groupedNodes, ...clusterScopedNodes]; + + this.redraw(); } } diff --git a/src/ui/treeviews/nodes/workload/kustomizationNode.ts b/src/ui/treeviews/nodes/workload/kustomizationNode.ts index 586355dc..c297574d 100644 --- a/src/ui/treeviews/nodes/workload/kustomizationNode.ts +++ b/src/ui/treeviews/nodes/workload/kustomizationNode.ts @@ -1,6 +1,9 @@ +import { fluxTools } from 'cli/flux/fluxTools'; import { Kustomization } from 'types/flux/kustomization'; -import { Kind } from 'types/kubernetes/kubernetesTypes'; -import { NodeContext } from 'types/nodeContext'; +import { SimpleDataProvider } from 'ui/treeviews/dataProviders/simpleDataProvider'; +import { InfoLabel } from 'utils/makeTreeviewInfoNode'; +import { shortenRevision } from 'utils/stringUtils'; +import { addFluxTreeToNode } from 'utils/treeNodeUtils'; import { WorkloadNode } from './workloadNode'; /** @@ -8,22 +11,46 @@ import { WorkloadNode } from './workloadNode'; */ export class KustomizationNode extends WorkloadNode { resource!: Kustomization; + dataProvider!: SimpleDataProvider; /** * Creates new app kustomization tree view item for display. * @param kustomization Kustomize kubernetes object info. */ - constructor(kustomization: Kustomization) { - super(kustomization); + constructor(kustomization: Kustomization, dataProvider: SimpleDataProvider) { + super(kustomization, dataProvider); this.makeCollapsible(); } - get contexts() { - const contextsArr: string[] = [Kind.Kustomization]; - contextsArr.push( - this.resource.spec.suspend ? NodeContext.Suspend : NodeContext.NotSuspend, - ); - return contextsArr; + get revision() { + return shortenRevision(this.resource.status.lastAppliedRevision); } + + + async updateChildren() { + this.children = this.infoNodes(InfoLabel.Loading); + this.redraw(); + + const name = this.resource.metadata.name; + const namespace = this.resource.metadata.namespace || ''; + const resourceTree = await fluxTools.tree(name, namespace); + + if (!resourceTree) { + this.children = this.infoNodes(InfoLabel.FailedToLoad); + this.redraw(); + return; + } + + if (!resourceTree.resources) { + this.children = this.infoNodes(InfoLabel.NoResources); + this.redraw(); + return; + } + + this.children = []; + await addFluxTreeToNode(this, resourceTree.resources); + this.redraw(); + } + } diff --git a/src/ui/treeviews/nodes/workload/workloadNode.ts b/src/ui/treeviews/nodes/workload/workloadNode.ts index 02853c22..26ddb3d1 100644 --- a/src/ui/treeviews/nodes/workload/workloadNode.ts +++ b/src/ui/treeviews/nodes/workload/workloadNode.ts @@ -1,6 +1,6 @@ import { FluxWorkloadObject } from 'types/flux/object'; -import { shortenRevision } from 'utils/stringUtils'; -import { ToolkitNode } from '../source/toolkitNode'; +import { NodeContext } from 'types/nodeContext'; +import { ToolkitNode } from '../toolkitNode'; /** * Base class for all Workload tree view items. @@ -8,7 +8,7 @@ import { ToolkitNode } from '../source/toolkitNode'; export class WorkloadNode extends ToolkitNode { resource!: FluxWorkloadObject; - get revision() { - return shortenRevision(this.resource.status.lastAppliedRevision); + get contexts() { + return this.resource.spec.suspend ? [NodeContext.Suspend] : [NodeContext.NotSuspend]; } } diff --git a/src/ui/treeviews/treeViews.ts b/src/ui/treeviews/treeViews.ts index 2f4c3951..c9706264 100644 --- a/src/ui/treeviews/treeViews.ts +++ b/src/ui/treeviews/treeViews.ts @@ -13,20 +13,21 @@ import { ClusterNode } from './nodes/cluster/clusterNode'; import { detectClusterProvider } from 'cli/kubernetes/clusterProvider'; import { kubeConfig } from 'cli/kubernetes/kubernetesConfig'; import { ClusterInfo } from 'types/kubernetes/clusterProvider'; -import { TemplateDataProvider } from './dataProviders/templateDataProvider'; +import { WgeDataProvider } from './dataProviders/wgeDataProvider'; import { NamespaceNode } from './nodes/namespaceNode'; +import { WgeContainerNode } from './nodes/wge/wgeNodes'; export let clusterDataProvider = new ClusterDataProvider(); export let sourceDataProvider = new SourceDataProvider(); export let workloadDataProvider = new WorkloadDataProvider(); export let documentationDataProvider = new DocumentationDataProvider(); -export let templateDateProvider = new TemplateDataProvider(); +export let wgeDataProvider = new WgeDataProvider(); let clusterTreeView: TreeView; export let sourceTreeView: TreeView; let workloadTreeView: TreeView; let documentationTreeView: TreeView; -let templateTreeView: TreeView; +let wgeTreeView: TreeView; /** * Creates tree views for the GitOps sidebar. @@ -48,15 +49,14 @@ export function createTreeViews() { showCollapseAll: true, }); - listenCollapsableState(); - - - // WGE templates - templateTreeView = window.createTreeView(TreeViewId.TemplatesView, { - treeDataProvider: templateDateProvider, + // WGE + wgeTreeView = window.createTreeView(TreeViewId.WgeView, { + treeDataProvider: wgeDataProvider, showCollapseAll: true, }); + listenCollapsableState(); + // create documentation links sidebar tree view documentationTreeView = window.createTreeView(TreeViewId.DocumentationView, { treeDataProvider: documentationDataProvider, @@ -67,6 +67,30 @@ export function createTreeViews() { } function listenCollapsableState() { + + // [workloadTreeView, sourceTreeView, wgeTreeView].forEach(treeview => { + // treeview.onDidCollapseElement(e => { + // if (e.element instanceof NamespaceNode) { + // e.element.collapsibleState = TreeItemCollapsibleState.Collapsed; + // const provider = e.element.dataProvider; + // // top-level namespace nodes should get an icon + // const showIcons = provider.nodes.includes(e.element); + // e.element.updateLabel(showIcons); + // provider.redraw(e.element); + // } + // }); + + // treeview.onDidExpandElement(e => { + // if (e.element instanceof NamespaceNode) { + // e.element.collapsibleState = TreeItemCollapsibleState.Expanded; + // const provider = e.element.dataProvider; + // // top-level namespace nodes should get an icon + // const showIcons = provider.nodes.includes(e.element); + // e.element.updateLabel(showIcons); + // provider.redraw(e.element); + // } + // }); + // }); sourceTreeView.onDidCollapseElement(e => { if (e.element instanceof NamespaceNode) { e.element.collapsibleState = TreeItemCollapsibleState.Collapsed; @@ -83,10 +107,10 @@ function listenCollapsableState() { } }); - workloadTreeView.onDidCollapseElement(e => { if (e.element instanceof NamespaceNode) { e.element.collapsibleState = TreeItemCollapsibleState.Collapsed; + // top-level namespace nodes should get an icon const showIcons = workloadDataProvider.nodes.includes(e.element); e.element.updateLabel(showIcons); workloadDataProvider.redraw(e.element); @@ -96,11 +120,33 @@ function listenCollapsableState() { workloadTreeView.onDidExpandElement(e => { if (e.element instanceof NamespaceNode) { e.element.collapsibleState = TreeItemCollapsibleState.Expanded; + // top-level namespace nodes should get an icon const showIcons = workloadDataProvider.nodes.includes(e.element); e.element.updateLabel(showIcons); workloadDataProvider.redraw(e.element); } }); + + + wgeTreeView.onDidCollapseElement(e => { + if (e.element instanceof NamespaceNode) { + e.element.collapsibleState = TreeItemCollapsibleState.Collapsed; + // top-level namespace nodes should get an icon + const showIcons = e.element.parent instanceof WgeContainerNode; + e.element.updateLabel(showIcons); + wgeDataProvider.redraw(e.element); + } + }); + + wgeTreeView.onDidExpandElement(e => { + if (e.element instanceof NamespaceNode) { + e.element.collapsibleState = TreeItemCollapsibleState.Expanded; + // top-level namespace nodes should get an icon + const showIcons = e.element.parent instanceof WgeContainerNode; + e.element.updateLabel(showIcons); + wgeDataProvider.redraw(e.element); + } + }); } /** @@ -129,8 +175,8 @@ export function reloadWorkloadsTreeView() { /** * Reloads workloads tree view for the selected cluster. */ -export function reloadTemplatesTreeView() { - templateDateProvider.reload(); +export function reloadWgeTreeView() { + wgeDataProvider.reload(); } /** diff --git a/src/ui/webviews/createFromTemplate/openWebview.ts b/src/ui/webviews/createFromTemplate/openWebview.ts index bb07d4c0..dcec1705 100644 --- a/src/ui/webviews/createFromTemplate/openWebview.ts +++ b/src/ui/webviews/createFromTemplate/openWebview.ts @@ -21,8 +21,8 @@ export async function openCreateFromTemplatePanel(template: GitOpsTemplate) { const folders = await window.showOpenDialog({title: 'Clusters manifests folder', canSelectFiles: false, canSelectFolders: true}); const folder = folders ? folders[0].fsPath : ''; const webviewParams = { - name: template.metadata?.name, - namespace: template.metadata?.namespace, + name: template.metadata.name, + namespace: template.metadata.namespace, description: template.spec.description, params: template.spec.params, folder, diff --git a/src/utils/makeTreeviewInfoNode.ts b/src/utils/makeTreeviewInfoNode.ts index f2028821..04a1a5df 100644 --- a/src/utils/makeTreeviewInfoNode.ts +++ b/src/utils/makeTreeviewInfoNode.ts @@ -1,8 +1,10 @@ import { kubeConfig } from 'cli/kubernetes/kubernetesConfig'; -import { TreeNode, TreeNodeIcon } from '../ui/treeviews/nodes/treeNode'; +import { CommonIcon } from 'ui/icons'; +import { SimpleDataProvider } from 'ui/treeviews/dataProviders/simpleDataProvider'; +import { TreeNode } from '../ui/treeviews/nodes/treeNode'; -export enum InfoNode { +export enum InfoLabel { FailedToLoad, NoResources, Loading, @@ -10,32 +12,32 @@ export enum InfoNode { ClusterUnreachable, } -export function infoNodes(type: InfoNode) { - return [infoNode(type)]; +export function infoNodes(type: InfoLabel, provider: SimpleDataProvider) { + return [infoNode(type, provider)]; } -export function infoNode(type: InfoNode) { +export function infoNode(type: InfoLabel, provider: SimpleDataProvider) { let node; switch(type) { - case InfoNode.FailedToLoad: - node = new TreeNode('Failed to load'); - node.setIcon(TreeNodeIcon.Disconnected); + case InfoLabel.FailedToLoad: + node = new TreeNode('Failed to load', provider); + node.setCommonIcon(CommonIcon.Disconnected); return node; - case InfoNode.NoResources: - return new TreeNode('No Resources'); - case InfoNode.Loading: - node = new TreeNode('Loading...'); - node.setIcon(TreeNodeIcon.Loading); + case InfoLabel.NoResources: + return new TreeNode('No Resources', provider); + case InfoLabel.Loading: + node = new TreeNode('Loading...', provider); + node.setCommonIcon(CommonIcon.Loading); return node; - case InfoNode.LoadingApi: - node = new TreeNode('Loading API...'); - node.setIcon(TreeNodeIcon.Loading); + case InfoLabel.LoadingApi: + node = new TreeNode('Loading API...', provider); + node.setCommonIcon(CommonIcon.Loading); return node; - case InfoNode.ClusterUnreachable: + case InfoLabel.ClusterUnreachable: const name = kubeConfig.currentContext; - node = new TreeNode(`Cluster ${name} unreachable`); - node.setIcon(TreeNodeIcon.Disconnected); + node = new TreeNode(`Cluster ${name} unreachable`, provider); + node.setCommonIcon(CommonIcon.Disconnected); return node; } } diff --git a/src/utils/markdownUtils.ts b/src/utils/markdownUtils.ts index 26e7eead..0ae346a9 100644 --- a/src/utils/markdownUtils.ts +++ b/src/utils/markdownUtils.ts @@ -1,17 +1,13 @@ import * as k8s from '@kubernetes/client-node'; import { MarkdownString } from 'vscode'; -import { Bucket } from 'types/flux/bucket'; import { GitOpsTemplate } from 'types/flux/gitOpsTemplate'; -import { GitRepository } from 'types/flux/gitRepository'; -import { HelmRelease } from 'types/flux/helmRelease'; -import { HelmRepository } from 'types/flux/helmRepository'; -import { Kustomization } from 'types/flux/kustomization'; -import { OCIRepository } from 'types/flux/ociRepository'; +import { ToolkitObject } from 'types/flux/object'; +import { Pipeline } from 'types/flux/pipeline'; import { Deployment, Kind, Namespace } from 'types/kubernetes/kubernetesTypes'; import { shortenRevision } from './stringUtils'; -export type KnownTreeNodeResources = Namespace | Bucket | GitRepository | OCIRepository | HelmRepository | HelmRelease | Kustomization | Deployment | GitOpsTemplate; +export type KnownTreeNodeResources = Namespace | Deployment | ToolkitObject | GitOpsTemplate | Pipeline; export function createContextMarkdownTable(context: k8s.Context, cluster?: k8s.Cluster): MarkdownString { @@ -31,10 +27,10 @@ export function createContextMarkdownTable(context: k8s.Context, cluster?: k8s.C /** * Create markdown table for tree view item hovers. * 2 clumns, left aligned. - * @param kubernetesObject Standard kubernetes object + * @param obj Standard kubernetes object * @returns vscode MarkdownString object */ -export function createMarkdownTable(kubernetesObject: KnownTreeNodeResources): MarkdownString { +export function createMarkdownTable(obj: KnownTreeNodeResources): MarkdownString { const markdown = new MarkdownString(undefined, true); markdown.isTrusted = true; // Create table header @@ -42,64 +38,92 @@ export function createMarkdownTable(kubernetesObject: KnownTreeNodeResources): M markdown.appendMarkdown(':--- | :---\n'); // Should exist on every object - createMarkdownTableRow('kind', kubernetesObject.kind, markdown); - createMarkdownTableRow('name', kubernetesObject.metadata?.name, markdown); - createMarkdownTableRow('namespace', kubernetesObject.metadata?.namespace, markdown); + createMarkdownTableRow('kind', obj.kind, markdown); + createMarkdownTableRow('name', obj.metadata.name, markdown); + createMarkdownTableRow('namespace', obj.metadata.namespace, markdown); // Object-specific properties - if (kubernetesObject.kind === Kind.GitRepository) { - createMarkdownTableRow('spec.suspend', kubernetesObject.spec?.suspend === undefined ? false : kubernetesObject.spec?.suspend, markdown); - createMarkdownTableRow('spec.url', kubernetesObject.spec?.url, markdown); - createMarkdownTableRow('spec.ref.commit', kubernetesObject.spec?.ref?.commit, markdown); - createMarkdownTableRow('spec.ref.branch', kubernetesObject.spec?.ref?.branch, markdown); - createMarkdownTableRow('spec.ref.tag', kubernetesObject.spec?.ref?.tag, markdown); - createMarkdownTableRow('spec.ref.semver', kubernetesObject.spec?.ref?.semver, markdown); - } else if (kubernetesObject.kind === Kind.OCIRepository) { - createMarkdownTableRow('spec.url', kubernetesObject.spec?.url, markdown); - createMarkdownTableRow('spec.ref.digest', kubernetesObject.spec?.ref?.digest, markdown); - createMarkdownTableRow('spec.ref.semver', kubernetesObject.spec?.ref?.semver, markdown); - createMarkdownTableRow('spec.ref.tag', kubernetesObject.spec?.ref?.tag, markdown); - } else if (kubernetesObject.kind === Kind.HelmRepository) { - createMarkdownTableRow('spec.url', kubernetesObject.spec?.url, markdown); - createMarkdownTableRow('spec.type', kubernetesObject.spec?.type, markdown); - } else if (kubernetesObject.kind === Kind.Bucket) { - createMarkdownTableRow('spec.bucketName', kubernetesObject.spec?.bucketName, markdown); - createMarkdownTableRow('spec.endpoint', kubernetesObject.spec?.endpoint, markdown); - createMarkdownTableRow('spec.provider', kubernetesObject.spec?.provider, markdown); - createMarkdownTableRow('spec.insecure', kubernetesObject.spec?.insecure, markdown); - } else if (kubernetesObject.kind === Kind.Kustomization) { - const sourceRef = `${kubernetesObject.spec?.sourceRef?.kind}/${kubernetesObject.spec?.sourceRef?.name}.${kubernetesObject.spec?.sourceRef?.namespace || kubernetesObject.metadata?.namespace}`; + if (obj.kind === Kind.GitRepository) { + createMarkdownTableRow('spec.suspend', obj.spec?.suspend === undefined ? false : obj.spec?.suspend, markdown); + createMarkdownTableRow('spec.url', obj.spec?.url, markdown); + createMarkdownTableRow('spec.ref.commit', obj.spec?.ref?.commit, markdown); + createMarkdownTableRow('spec.ref.branch', obj.spec?.ref?.branch, markdown); + createMarkdownTableRow('spec.ref.tag', obj.spec?.ref?.tag, markdown); + createMarkdownTableRow('spec.ref.semver', obj.spec?.ref?.semver, markdown); + } else if (obj.kind === Kind.OCIRepository) { + createMarkdownTableRow('spec.url', obj.spec?.url, markdown); + createMarkdownTableRow('spec.ref.digest', obj.spec?.ref?.digest, markdown); + createMarkdownTableRow('spec.ref.semver', obj.spec?.ref?.semver, markdown); + createMarkdownTableRow('spec.ref.tag', obj.spec?.ref?.tag, markdown); + } else if (obj.kind === Kind.HelmRepository) { + createMarkdownTableRow('spec.url', obj.spec?.url, markdown); + createMarkdownTableRow('spec.type', obj.spec?.type, markdown); + } else if (obj.kind === Kind.Bucket) { + createMarkdownTableRow('spec.bucketName', obj.spec?.bucketName, markdown); + createMarkdownTableRow('spec.endpoint', obj.spec?.endpoint, markdown); + createMarkdownTableRow('spec.provider', obj.spec?.provider, markdown); + createMarkdownTableRow('spec.insecure', obj.spec?.insecure, markdown); + } else if (obj.kind === Kind.Kustomization) { + const sourceRef = `${obj.spec?.sourceRef?.kind}/${obj.spec?.sourceRef?.name}.${obj.spec?.sourceRef?.namespace || obj.metadata.namespace}`; createMarkdownTableRow('source', sourceRef, markdown); - createMarkdownTableRow('spec.suspend', kubernetesObject.spec?.suspend === undefined ? false : kubernetesObject.spec?.suspend, markdown); - createMarkdownTableRow('spec.prune', kubernetesObject.spec?.prune, markdown); - createMarkdownTableRow('spec.force', kubernetesObject.spec?.force, markdown); - createMarkdownTableRow('spec.path', kubernetesObject.spec?.path, markdown); - } else if (kubernetesObject.kind === Kind.HelmRelease) { - const sourceRef = `${kubernetesObject.spec?.chart?.spec?.sourceRef?.kind}/${kubernetesObject.spec?.chart?.spec?.sourceRef?.name}.${kubernetesObject.spec?.chart?.spec?.sourceRef?.namespace || kubernetesObject.metadata?.namespace}`; + createMarkdownTableRow('spec.suspend', obj.spec?.suspend === undefined ? false : obj.spec?.suspend, markdown); + createMarkdownTableRow('spec.prune', obj.spec?.prune, markdown); + createMarkdownTableRow('spec.force', obj.spec?.force, markdown); + createMarkdownTableRow('spec.path', obj.spec?.path, markdown); + } else if (obj.kind === Kind.HelmRelease) { + const sourceRef = `${obj.spec?.chart?.spec?.sourceRef?.kind}/${obj.spec?.chart?.spec?.sourceRef?.name}.${obj.spec?.chart?.spec?.sourceRef?.namespace || obj.metadata.namespace}`; createMarkdownTableRow('source', sourceRef, markdown); - createMarkdownTableRow('spec.suspend', kubernetesObject.spec?.suspend === undefined ? false : kubernetesObject.spec?.suspend, markdown); - createMarkdownTableRow('spec.chart.spec.chart', kubernetesObject.spec?.chart?.spec?.chart, markdown); + createMarkdownTableRow('spec.suspend', obj.spec?.suspend === undefined ? false : obj.spec?.suspend, markdown); + createMarkdownTableRow('spec.chart.spec.chart', obj.spec?.chart?.spec?.chart, markdown); + + createMarkdownTableRow('spec.chart.spec.version', obj.spec?.chart?.spec?.version, markdown); + } else if (obj.kind === Kind.Canary) { + createMarkdownTableRow('spec.suspend', obj.spec?.suspend === undefined ? false : obj.spec?.suspend, markdown); + + createMarkdownTableRow('phase', obj.status.phase, markdown); + createMarkdownTableRow('failedChecks', obj.status.failedChecks, markdown); + createMarkdownTableRow('canaryWeight', obj.status.canaryWeight, markdown); + createMarkdownTableRow('iterations', obj.status.iterations, markdown); + createMarkdownTableRow('lastAppliedSpec', obj.status.lastAppliedSpec, markdown); + createMarkdownTableRow('lastPromotedSpec', obj.status.lastPromotedSpec, markdown); + createMarkdownTableRow('lastTransitionTime', obj.status.lastTransitionTime, markdown); + } else if (obj.kind === Kind.Deployment) { + createMarkdownTableRow('spec.paused', obj.spec?.paused, markdown); + createMarkdownTableRow('spec.minReadySeconds', obj.spec?.minReadySeconds, markdown); + createMarkdownTableRow('spec.progressDeadlineSeconds', obj.spec?.progressDeadlineSeconds, markdown); + } else if (obj.kind === Kind.Pipeline) { + if(obj.spec?.promotion?.manual) { + createMarkdownTableRow('promotion.manual', obj.spec?.promotion?.manual, markdown); + } + + if(obj.spec?.promotion?.strategy.notification) { + createMarkdownTableRow('promotion.strategy.notification', true, markdown); + } + + const strategy = obj.spec?.promotion?.strategy as any; + if(strategy['pull-request']) { + createMarkdownTableRow('pull-request.type', strategy['pull-request'].type, markdown); + createMarkdownTableRow('pull-request.url', strategy['pull-request'].url, markdown); + createMarkdownTableRow('pull-request.baseBranch', strategy['pull-request'].baseBranch, markdown); + } - createMarkdownTableRow('spec.chart.spec.version', kubernetesObject.spec?.chart?.spec?.version, markdown); - } else if (kubernetesObject.kind === Kind.Deployment) { - createMarkdownTableRow('spec.paused', kubernetesObject.spec?.paused, markdown); - createMarkdownTableRow('spec.minReadySeconds', kubernetesObject.spec?.minReadySeconds, markdown); - createMarkdownTableRow('spec.progressDeadlineSeconds', kubernetesObject.spec?.progressDeadlineSeconds, markdown); + createMarkdownTableRow('spec.appRef.kind', obj.spec?.appRef.kind, markdown); + createMarkdownTableRow('spec.appRef.name', obj.spec?.appRef.name, markdown); } // Should exist on multiple objects - if(kubernetesObject.spec) { - if ('interval' in kubernetesObject.spec) { - createMarkdownTableRow('spec.interval', kubernetesObject.spec?.interval, markdown); + if(obj.spec) { + if ('interval' in obj.spec) { + createMarkdownTableRow('spec.interval', obj.spec?.interval, markdown); } - if ('timeout' in kubernetesObject.spec) { - createMarkdownTableRow('spec.timeout', kubernetesObject.spec?.timeout, markdown); + if ('timeout' in obj.spec) { + createMarkdownTableRow('spec.timeout', obj.spec?.timeout, markdown); } } - const fluxStatus = kubernetesObject.status as any; + const fluxStatus = obj.status as any; if(fluxStatus?.lastAttemptedRevision) { createMarkdownTableRow('attempted', shortenRevision(fluxStatus.lastAttemptedRevision), markdown); @@ -110,8 +134,8 @@ export function createMarkdownTable(kubernetesObject: KnownTreeNodeResources): M } - if(kubernetesObject.status?.conditions) { - const conditions = kubernetesObject.status.conditions as any[]; + if(obj.status?.conditions) { + const conditions = obj.status.conditions as any[]; for (const c of conditions) { if(c.type === 'SourceVerified' && c.status === 'True') { const message = `${c.message.replace('verified signature of revision', 'verified').slice(0, 48)}...`; diff --git a/src/utils/namespacedFluxObject.ts b/src/utils/namespacedFluxObject.ts index 998f03c6..bc8f0a20 100644 --- a/src/utils/namespacedFluxObject.ts +++ b/src/utils/namespacedFluxObject.ts @@ -2,7 +2,7 @@ import { FluxSourceObject, FluxWorkloadObject } from '../types/flux/object'; export function namespacedFluxObject(resource?: FluxSourceObject | FluxWorkloadObject): string | undefined { if (resource) { - return `${resource.kind}/${resource.metadata?.name}.${resource.metadata?.namespace}`; + return `${resource.kind}/${resource.metadata.name}.${resource.metadata.namespace}`; } } diff --git a/src/utils/treeNodeUtils.ts b/src/utils/treeNodeUtils.ts index 72cd9178..015dcb7a 100644 --- a/src/utils/treeNodeUtils.ts +++ b/src/utils/treeNodeUtils.ts @@ -18,8 +18,10 @@ export async function addFluxTreeToNode(node: TreeNode, resourceTree: FluxTreeRe metadata: { name: resource.resource.Name, namespace, + uid: JSON.stringify(resource.resource), // fake UID, we're using for treeview indexing only }, - }); + + }, node.dataProvider!); nodes.push(childNode); @@ -43,10 +45,10 @@ export async function groupNodesByNamespace(nodes: TreeNode[], expandAll = false const nsChildNodes = filterNodesForNamespace(nodes, nsName); if (nsChildNodes.length > 0) { - const nsNode = new NamespaceNode(ns); + const nsNode = new NamespaceNode(ns, nsChildNodes[0].dataProvider); nsChildNodes.forEach(childNode => { // Don't add the namespace node as a child of itself - if(!(childNode.resource?.kind === 'Namespace' && childNode.resource.metadata?.name === nsName)) { + if(!(childNode.resource.kind === 'Namespace' && childNode.resource.metadata.name === nsName)) { nsNode.addChild(childNode); } }); @@ -57,13 +59,13 @@ export async function groupNodesByNamespace(nodes: TreeNode[], expandAll = false } }); - const clusterScopedNodes = nodes.filter(node => !node.resource?.metadata?.namespace && node.resource?.kind !== 'Namespace'); + const clusterScopedNodes = nodes.filter(node => !node.resource.metadata.namespace && node.resource.kind !== 'Namespace'); return [namespaceNodes, clusterScopedNodes]; } function filterNodesForNamespace(nodes: TreeNode[], namespace: string): TreeNode[] { - const belongsToNamespace = (node: TreeNode) => node.resource?.metadata?.namespace === namespace; - const isNamespace = (node: TreeNode) => node.resource?.kind === 'Namespace' && node.resource?.metadata?.name === namespace; + const belongsToNamespace = (node: TreeNode) => node.resource.metadata.namespace === namespace; + const isNamespace = (node: TreeNode) => node.resource.kind === 'Namespace' && node.resource.metadata.name === namespace; return nodes.filter(node => belongsToNamespace(node) || isNamespace(node)); }