Skip to content

Commit

Permalink
(Prerelease) WGE integration (#495)
Browse files Browse the repository at this point in the history
Support for Template, Canary, Pipeline and GitOpsSet resources.

Integration with WGE web portal and documentation.

TreeNode code improvements

weave-gitops-interop cluster ConfigMap support
  • Loading branch information
juozasg authored Oct 31, 2023
1 parent 1f09314 commit a0e4446
Show file tree
Hide file tree
Showing 76 changed files with 2,016 additions and 647 deletions.
32 changes: 26 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 setting:

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)



Expand Down
Binary file added docs/images/config-enable-wge.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed docs/images/vscode-templates-config.png
Binary file not shown.
Binary file added docs/images/weave-gitops-treeview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
86 changes: 69 additions & 17 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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": {
Expand All @@ -285,8 +305,8 @@
"name": "Workloads"
},
{
"id": "gitops.views.templates",
"name": "Templates",
"id": "gitops.views.wge",
"name": "Weave GitOps",
"when": "config.gitops.weaveGitopsEnterprise"
},
{
Expand All @@ -311,7 +331,7 @@
"gitops.weaveGitopsEnterprise": {
"type": "boolean",
"default": false,
"description": "Enable WGE GitOpsTemplates feature"
"description": "Enable WGE features"
},
"gitops.kubectlRequestTimeout": {
"type": "string",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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": [
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -614,6 +662,10 @@
{
"command": "gitops.dev.showGlobalState",
"when": "gitops:isDev"
},
{
"command": "gitops.views.setContextToGitopsCluster",
"when": "never"
}
]
}
Expand Down
3 changes: 2 additions & 1 deletion src/cli/kubernetes/apiResources.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -102,5 +102,6 @@ export async function loadAvailableResourceKinds() {
// give proxy init callbacks time to fire
setTimeout(() => {
refreshResourcesTreeViews();
loadContextData();
}, 100);
}
63 changes: 54 additions & 9 deletions src/cli/kubernetes/kubectlGet.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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<undefined | KubernetesObject> {
const shellResult = await invokeKubectlCommand(`get ${kind}/${name} --namespace=${namespace} -o json`);
export async function getResource<T extends KubernetesObject>(name: string, namespace: string, kind: Kind): Promise<undefined | T> {
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<T extends KubernetesObject>(kind: Kind, telemetryError: TelemetryError): Promise<T[]> {
Expand Down Expand Up @@ -89,6 +99,18 @@ export async function getGitOpsTemplates(): Promise<GitOpsTemplate[]> {
return getResourcesAllNamespaces(Kind.GitOpsTemplate, TelemetryError.FAILED_TO_GET_GITOPSTEMPLATES);
}

export async function getCanaries(): Promise<Canary[]> {
return getResourcesAllNamespaces(Kind.Canary, TelemetryError.FAILED_TO_GET_HELM_RELEASES);
}

export async function getPipelines(): Promise<Pipeline[]> {
return getResourcesAllNamespaces(Kind.Pipeline, TelemetryError.FAILED_TO_GET_HELM_RELEASES);
}

export async function getGitOpsSet(): Promise<GitOpsSet[]> {
return getResourcesAllNamespaces(Kind.GitOpsSet, TelemetryError.FAILED_TO_GET_HELM_RELEASES);
}


/**
* Get all flux system deployments.
Expand Down Expand Up @@ -118,8 +140,7 @@ export async function getFluxControllers(context?: string): Promise<Deployment[]
* @param name name of the kustomize/helmRelease object
* @param namespace namespace of the kustomize/helmRelease object
*/
export async function getChildrenOfWorkload(
workload: 'kustomize' | 'helm',
export async function getHelmReleaseChildren(
name: string,
namespace: string,
): Promise<KubernetesObject[] | undefined> {
Expand All @@ -129,22 +150,46 @@ 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;
}

return parseJsonItems(shellResult.stdout);
}


export async function getCanaryChildren(
name: string,
): Promise<KubernetesObject[]> {
// 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
Expand Down
14 changes: 14 additions & 0 deletions src/cli/kubernetes/kubernetesToolsKubectl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand Down Expand Up @@ -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';
Expand Down
Loading

0 comments on commit a0e4446

Please sign in to comment.