diff --git a/src/extension.ts b/src/extension.ts index 69b68f49b..076575317 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -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; @@ -118,6 +120,7 @@ export async function activate(context): Promise { 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(); @@ -235,6 +238,9 @@ export async function activate(context): Promise { // 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()), @@ -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) { diff --git a/src/helm.exec.ts b/src/helm.exec.ts index 3430bd39a..c583fc9d5 100644 --- a/src/helm.exec.ts +++ b/src/helm.exec.ts @@ -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) => { diff --git a/src/kuberesources.linkprovider.ts b/src/kuberesources.linkprovider.ts new file mode 100644 index 000000000..0353789e4 --- /dev/null +++ b/src/kuberesources.linkprovider.ts @@ -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 { + 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); +} diff --git a/src/yaml-support/yaml-locator.ts b/src/yaml-support/yaml-locator.ts index ff8e9b7da..d6b3afdab 100644 --- a/src/yaml-support/yaml-locator.ts +++ b/src/yaml-support/yaml-locator.ts @@ -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 { @@ -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[]; diff --git a/src/yaml-support/yaml-navigation.ts b/src/yaml-support/yaml-navigation.ts new file mode 100644 index 000000000..87f955190 --- /dev/null +++ b/src/yaml-support/yaml-navigation.ts @@ -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 === '-'; +}