Skip to content

Commit

Permalink
Support navigation on cluster resources (vscode-kubernetes-tools#342)
Browse files Browse the repository at this point in the history
  • Loading branch information
itowlson authored Aug 15, 2018
1 parent 49da2ce commit e16c0b2
Show file tree
Hide file tree
Showing 5 changed files with 226 additions and 31 deletions.
33 changes: 6 additions & 27 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,11 @@ import { showWorkspaceFolderPick } from './hostutils';
import { DraftConfigurationProvider } from './draft/draftConfigurationProvider';
import { installHelm, installDraft, installKubectl, installMinikube } from './components/installer/installer';
import { KubernetesResourceVirtualFileSystemProvider, K8S_RESOURCE_SCHEME, KUBECTL_RESOURCE_AUTHORITY, kubefsUri } from './kuberesources.virtualfs';
import { KubernetesResourceLinkProvider } from './kuberesources.linkprovider';
import { Container, isKubernetesResource, KubernetesCollection, Pod, KubernetesResource } from './kuberesources.objectmodel';
import { setActiveKubeconfig, getKnownKubeconfigs, addKnownKubeconfig } from './components/config/config';
import { HelmDocumentSymbolProvider } from './helm.symbolProvider';
import { findParentYaml } from './yaml-support/yaml-navigation';

let explainActive = false;
let swaggerSpecPromise = null;
Expand Down Expand Up @@ -118,6 +120,7 @@ export async function activate(context): Promise<extensionapi.ExtensionAPI> {
const treeProvider = explorer.create(kubectl, host);
const helmRepoTreeProvider = helmRepoExplorer.create(host);
const resourceDocProvider = new KubernetesResourceVirtualFileSystemProvider(kubectl, host, vscode.workspace.rootPath);
const resourceLinkProvider = new KubernetesResourceLinkProvider();
const previewProvider = new HelmTemplatePreviewDocumentProvider();
const inspectProvider = new HelmInspectDocumentProvider();
const dependenciesProvider = new HelmDependencyDocumentProvider();
Expand Down Expand Up @@ -235,6 +238,9 @@ export async function activate(context): Promise<extensionapi.ExtensionAPI> {
// Temporarily loaded resource providers
vscode.workspace.registerFileSystemProvider(K8S_RESOURCE_SCHEME, resourceDocProvider, { /* TODO: case sensitive? */ }),

// Link from resources to referenced resources
vscode.languages.registerDocumentLinkProvider({ scheme: K8S_RESOURCE_SCHEME }, resourceLinkProvider),

// Code lenses
vscode.languages.registerCodeLensProvider(HELM_REQ_MODE, new HelmRequirementsCodeLensProvider()),

Expand Down Expand Up @@ -428,33 +434,6 @@ function findParentJson(document, line) {
return line;
}

function findParentYaml(document, line) {
const indent = yamlIndentLevel(document.lineAt(line).text);
while (line >= 0) {
const txt = document.lineAt(line);
if (yamlIndentLevel(txt.text) < indent) {
return line;
}
line = line - 1;
}
return line;
}

function yamlIndentLevel(str) {
let i = 0;

while (true) {
if (str.length <= i || !isYamlIndentChar(str.charAt(i))) {
return i;
}
++i;
}
}

function isYamlIndentChar(ch) {
return ch === ' ' || ch === '-';
}

async function explain(obj, field) {
return new Promise((resolve) => {
if (!obj.kind) {
Expand Down
13 changes: 9 additions & 4 deletions src/helm.exec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,16 +200,21 @@ export function helmGet(resourceNode: explorer.ResourceNode) {
return;
}
const releaseName = resourceNode.id.split(':')[1];
const docname = `helmrelease-${releaseName}.txt`;
const nonce = new Date().getTime();
const uri = `${K8S_RESOURCE_SCHEME}://${HELM_RESOURCE_AUTHORITY}/${docname}?value=${releaseName}&_=${nonce}`;
vscode.workspace.openTextDocument(vscode.Uri.parse(uri)).then((doc) => {
const uri = helmfsUri(releaseName);
vscode.workspace.openTextDocument(uri).then((doc) => {
if (doc) {
vscode.window.showTextDocument(doc);
}
});
}

export function helmfsUri(releaseName: string): vscode.Uri {
const docname = `helmrelease-${releaseName}.txt`;
const nonce = new Date().getTime();
const uri = `${K8S_RESOURCE_SCHEME}://${HELM_RESOURCE_AUTHORITY}/${docname}?value=${releaseName}&_=${nonce}`;
return vscode.Uri.parse(uri);
}

// helmPackage runs the Helm package on a chart within your project.
export function helmPackage() {
pickChart((path) => {
Expand Down
166 changes: 166 additions & 0 deletions src/kuberesources.linkprovider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import * as vscode from 'vscode';
import * as querystring from 'querystring';
import * as _ from 'lodash';

import * as kuberesources from './kuberesources';
import { kubefsUri } from './kuberesources.virtualfs';
import { helmfsUri } from './helm.exec';
import * as yl from './yaml-support/yaml-locator';

export class KubernetesResourceLinkProvider implements vscode.DocumentLinkProvider {
provideDocumentLinks(document: vscode.TextDocument, token: vscode.CancellationToken): vscode.ProviderResult<vscode.DocumentLink[]> {
const sourceKind = k8sKind(document);
const yaml = yl.yamlLocator.getYamlDocuments(document);
const leaves: yl.YamlNode[] = getLeafNodes(yaml);
const links = leaves.map((l) => getLink(document, sourceKind, l))
.filter((l) => !!l);
return links;
}
}

function getLeafNodes(yaml: yl.YamlDocument[]): yl.YamlNode[] {
const rootNodes = _.flatMap(yaml, (d) => d.nodes);
const nonRootNodes = _.flatMap(rootNodes, (n) => descendants(n));
const allNodes = rootNodes.concat(nonRootNodes);
const leafNodes = allNodes.filter((n) => isLeaf(n));
return leafNodes;
}

function getLink(document: vscode.TextDocument, sourceKind: string, node: yl.YamlNode): vscode.DocumentLink | undefined {
if (yl.isMappingItem(node)) {
return getLinkFromPair(document, sourceKind, node);
}
return undefined;
}

function range(document: vscode.TextDocument, node: yl.YamlMappingItem) {
return new vscode.Range(
document.positionAt(node.value.startPosition),
document.positionAt(node.value.endPosition)
);
}

function descendants(node: yl.YamlNode): yl.YamlNode[] {
const direct = children(node);
const indirect = direct.map((n) => descendants(n));
const all = direct.concat(...indirect);
return all;
}

function children(node: yl.YamlNode): yl.YamlNode[] {
if (yl.isMapping(node)) {
return node.mappings;
} else if (yl.isSequence(node)) {
return node.items;
} else if (yl.isMappingItem(node)) {
if (yl.isMapping(node.value) || yl.isSequence(node.value)) {
return [node.value];
}
return [];
} else {
return [];
}
}

function isLeaf(node: yl.YamlNode): boolean {
return yl.isMappingItem(node) && node.value.kind === 'SCALAR';
}

function key(node: yl.YamlNode): string | undefined {
if (node && yl.isMappingItem(node)) {
return node.key.raw;
}
return undefined;
}

function parentKey(node: yl.YamlNode): string | undefined {
const parent = node.parent;
if (!parent || !parent.parent || !yl.isMapping(parent.parent)) {
return undefined;
}
const parentPair = parent.parent.mappings.find((mi) => mi.value === parent);
return key(parentPair);
}

function siblings(node: yl.YamlMappingItem): yl.YamlMappingItem[] {
const parent = node.parent;
if (parent && yl.isMapping(parent)) {
return parent.mappings;
}
return [];
}

function sibling(node: yl.YamlMappingItem, name: string): string | undefined {
return siblings(node).filter((n) => n.key.raw === name)
.map((n) => n.value.raw)
[0];
}

function getLinkFromPair(document: vscode.TextDocument, sourceKind: string, node: yl.YamlMappingItem): vscode.DocumentLink | undefined {
const uri = getLinkUri(sourceKind, node);
if (!uri) {
return undefined;
}
return new vscode.DocumentLink(range(document, node), uri);
}

function getLinkUri(sourceKind: string, node: yl.YamlMappingItem): vscode.Uri | undefined {
// Things that apply to all source resource types
if (key(node) === 'release' && parentKey(node) === 'labels') {
return helmfsUri(node.value.raw);
}
if (key(node) === 'namespace' && parentKey(node) === 'metadata') {
return kubefsUri(null, `ns/${node.value.raw}`, 'yaml');
}

// Source=type-specific navigation
switch (sourceKind) {
case kuberesources.allKinds.deployment.abbreviation:
return getLinkUriFromDeployment(node);
case kuberesources.allKinds.persistentVolume.abbreviation:
return getLinkUriFromPV(node);
case kuberesources.allKinds.persistentVolumeClaim.abbreviation:
return getLinkUriFromPVC(node);
default:
return undefined;
}
}

function getLinkUriFromDeployment(node: yl.YamlMappingItem): vscode.Uri | undefined {
if (key(node) === 'claimName' && parentKey(node) === 'persistentVolumeClaim') {
return kubefsUri(null, `pvc/${node.value.raw}`, 'yaml');
} else if (key(node) === 'name' && parentKey(node) === 'configMap') {
return kubefsUri(null, `cm/${node.value.raw}`, 'yaml');
} else if (key(node) === 'name' && parentKey(node) === 'secretKeyRef') {
return kubefsUri(null, `secrets/${node.value.raw}`, 'yaml');
} else {
return undefined;
}
}

function getLinkUriFromPV(node: yl.YamlMappingItem): vscode.Uri | undefined {
if (key(node) === 'storageClassName') {
return kubefsUri(null, `sc/${node.value.raw}`, 'yaml');
} else if (key(node) === 'name' && parentKey(node) === 'claimRef') {
return kubefsUri(sibling(node, 'namespace'), `pvc/${node.value.raw}`, 'yaml');
} else {
return undefined;
}
}

function getLinkUriFromPVC(node: yl.YamlMappingItem): vscode.Uri | undefined {
if (key(node) === 'storageClassName') {
return kubefsUri(null, `sc/${node.value.raw}`, 'yaml');
} else if (key(node) === 'volumeName') {
return kubefsUri(null, `pv/${node.value.raw}`, 'yaml');
} else {
return undefined;
}
}

function k8sKind(document: vscode.TextDocument): string {
const query = querystring.parse(document.uri.query);
const k8sid: string = query.value;
const kindSepIndex = k8sid.indexOf('/');
return k8sid.substring(0, kindSepIndex);
}
17 changes: 17 additions & 0 deletions src/yaml-support/yaml-locator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,24 @@ import * as vscode from 'vscode';

import { parse, findNodeAtPosition } from 'node-yaml-parser';

export function isMapping(node: YamlNode): node is YamlMap {
return node.kind === 'MAPPING';
}

export function isSequence(node: YamlNode): node is YamlSequence {
return node.kind === 'SEQ';
}

export function isMappingItem(node: YamlNode): node is YamlMappingItem {
return node.kind === 'PAIR';
}

export interface YamlNode {
readonly kind: string;
readonly raw: string;
readonly startPosition: number;
readonly endPosition: number;
readonly parent?: YamlNode;
}

export interface YamlMappingItem extends YamlNode {
Expand All @@ -18,6 +31,10 @@ export interface YamlMap extends YamlNode {
readonly mappings: YamlMappingItem[];
}

export interface YamlSequence extends YamlNode {
readonly items: YamlNode[];
}

export interface YamlDocument {
readonly nodes: YamlNode[];
readonly errors: string[];
Expand Down
28 changes: 28 additions & 0 deletions src/yaml-support/yaml-navigation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import * as vscode from 'vscode';

export function findParentYaml(document: vscode.TextDocument, line: number): number {
const indent = yamlIndentLevel(document.lineAt(line).text);
while (line >= 0) {
const txt = document.lineAt(line);
if (yamlIndentLevel(txt.text) < indent) {
return line;
}
line = line - 1;
}
return line;
}

function yamlIndentLevel(str: string): number {
let i = 0;

while (true) {
if (str.length <= i || !isYamlIndentChar(str.charAt(i))) {
return i;
}
++i;
}
}

function isYamlIndentChar(ch: string): boolean {
return ch === ' ' || ch === '-';
}

0 comments on commit e16c0b2

Please sign in to comment.