|
| 1 | +/*----------------------------------------------------------------------------------------------- |
| 2 | + * Copyright (c) Red Hat, Inc. All rights reserved. |
| 3 | + * Licensed under the MIT License. See LICENSE file in the project root for license information. |
| 4 | + *-----------------------------------------------------------------------------------------------*/ |
| 5 | + |
| 6 | +import * as vscode from 'vscode'; |
| 7 | + |
| 8 | +import { Uri, FileSystemProvider, FileType, FileStat, FileChangeEvent, Event, EventEmitter, Disposable } from 'vscode'; |
| 9 | +import * as path from 'path'; |
| 10 | +import * as fs from 'fs'; |
| 11 | +import * as querystring from 'querystring'; |
| 12 | + |
| 13 | +import { Errorable } from './errorable'; |
| 14 | +import { CommandText } from '../../base/command'; |
| 15 | +import { CliChannel } from '../../cli'; |
| 16 | +import { CliExitData } from '../../util/childProcessUtil'; |
| 17 | +import { helmSyntaxVersion, HelmSyntaxVersion } from '../../helm/helm'; |
| 18 | +import { Progress } from '../../util/progress'; |
| 19 | + |
| 20 | +export const K8S_RESOURCE_SCHEME = 'osmsx'; // Changed from 'k8smsx' to 'osmsx' to not make a conflict with k8s extension |
| 21 | +export const K8S_RESOURCE_SCHEME_READONLY = 'osmsxro'; // Changed from 'k8smsxro' to 'osmsxro' to not make a conflict with k8s extension |
| 22 | +export const KUBECTL_RESOURCE_AUTHORITY = 'loadkubernetescore'; |
| 23 | +export const KUBECTL_DESCRIBE_AUTHORITY = 'kubernetesdescribe'; |
| 24 | +export const HELM_RESOURCE_AUTHORITY = 'helmget'; |
| 25 | + |
| 26 | +export const OUTPUT_FORMAT_YAML = 'yaml' // Default |
| 27 | +export const OUTPUT_FORMAT_JSON = 'json' |
| 28 | + |
| 29 | +export function kubefsUri(namespace: string | null | undefined /* TODO: rationalise null and undefined */, value: string, outputFormat: string, action?: string): Uri { |
| 30 | + const docname = `${value.replace('/', '-')}${outputFormat !== '' ? `.${outputFormat}` : ''}`; |
| 31 | + const nonce = new Date().getTime(); |
| 32 | + const nsquery = namespace ? `ns=${namespace}&` : ''; |
| 33 | + const scheme = action === 'describe' ? K8S_RESOURCE_SCHEME_READONLY : K8S_RESOURCE_SCHEME; |
| 34 | + const authority = action === 'describe' ? KUBECTL_DESCRIBE_AUTHORITY : KUBECTL_RESOURCE_AUTHORITY; |
| 35 | + const uri = `${scheme}://${authority}/${docname}?${nsquery}value=${value}&_=${nonce}`; |
| 36 | + return Uri.parse(uri); |
| 37 | +} |
| 38 | + |
| 39 | +export class KubernetesResourceVirtualFileSystemProvider implements FileSystemProvider { |
| 40 | + constructor() { } |
| 41 | + |
| 42 | + private readonly onDidChangeFileEmitter: EventEmitter<FileChangeEvent[]> = new EventEmitter<FileChangeEvent[]>(); |
| 43 | + |
| 44 | + onDidChangeFile: Event<FileChangeEvent[]> = this.onDidChangeFileEmitter.event; |
| 45 | + |
| 46 | + watch(_uri: Uri, _options: { recursive: boolean; excludes: string[] }): Disposable { |
| 47 | + // It would be quite neat to implement this to watch for changes |
| 48 | + // in the cluster and update the doc accordingly. But that is very |
| 49 | + // definitely a future enhancement thing! |
| 50 | + return new Disposable(() => {}); |
| 51 | + } |
| 52 | + |
| 53 | + stat(_uri: Uri): FileStat { |
| 54 | + return { |
| 55 | + type: FileType.File, |
| 56 | + ctime: 0, |
| 57 | + mtime: 0, |
| 58 | + size: 65536 // These files don't seem to matter for us |
| 59 | + }; |
| 60 | + } |
| 61 | + |
| 62 | + readDirectory(_uri: Uri): [string, FileType][] | Thenable<[string, FileType][]> { |
| 63 | + return []; |
| 64 | + } |
| 65 | + |
| 66 | + createDirectory(_uri: Uri): void | Thenable<void> { |
| 67 | + // no-op |
| 68 | + } |
| 69 | + |
| 70 | + readFile(uri: Uri): Uint8Array | Thenable<Uint8Array> { |
| 71 | + return this.readFileAsync(uri); |
| 72 | + } |
| 73 | + |
| 74 | + async readFileAsync(uri: Uri): Promise<Uint8Array> { |
| 75 | + const content = await this.loadResource(uri); |
| 76 | + return new Buffer(content, 'utf8'); |
| 77 | + } |
| 78 | + |
| 79 | + async loadResource(uri: Uri): Promise<string> { |
| 80 | + const query = querystring.parse(uri.query); |
| 81 | + |
| 82 | + const outputFormat = OUTPUT_FORMAT_YAML; // k8s had a preference for outputFormat selection |
| 83 | + const value = query.value as string; |
| 84 | + const revision = query.revision as string | undefined; |
| 85 | + const ns = query.ns as string | undefined; |
| 86 | + const resourceAuthority = uri.authority; |
| 87 | + |
| 88 | + const eer = await this.execLoadResource(resourceAuthority, ns, value, revision, outputFormat); |
| 89 | + if (Errorable.failed(eer)) { |
| 90 | + void vscode.window.showErrorMessage(eer.error[0]); |
| 91 | + throw eer.error[0]; |
| 92 | + } |
| 93 | + |
| 94 | + const er = eer.result; |
| 95 | + if (CliExitData.failed(er)) { |
| 96 | + const message = `Get command failed: ${CliExitData.getErrorMessage(er)}`; |
| 97 | + void vscode.window.showErrorMessage(message); |
| 98 | + throw message; |
| 99 | + } |
| 100 | + |
| 101 | + return er.stdout; |
| 102 | + } |
| 103 | + |
| 104 | + async execLoadResource(resourceAuthority: string, ns: string | undefined, value: string, revision: string | undefined, outputFormat: string): Promise<Errorable<CliExitData>> { |
| 105 | + const nsarg = ns ? `--namespace ${ns}` : ''; |
| 106 | + switch (resourceAuthority) { |
| 107 | + case KUBECTL_RESOURCE_AUTHORITY: { |
| 108 | + const ced = await Progress.execFunctionWithProgress(`Loading ${value}...`, async () => { |
| 109 | + return await CliChannel.getInstance().executeTool(new CommandText(`oc -o ${outputFormat} ${nsarg} get ${value}`), undefined, false); |
| 110 | + }); |
| 111 | + return { succeeded: true, result: ced }; |
| 112 | + } |
| 113 | + case HELM_RESOURCE_AUTHORITY: { |
| 114 | + const scopearg = ((await helmSyntaxVersion()) === HelmSyntaxVersion.V2) ? '' : 'all'; |
| 115 | + const revarg = revision ? ` --revision=${revision}` : ''; |
| 116 | + const her = await Progress.execFunctionWithProgress(`Loading ${value}...`, async () => { |
| 117 | + return await CliChannel.getInstance().executeTool(new CommandText(`helm get ${scopearg} ${value}${revarg}`), undefined, false); |
| 118 | + }); |
| 119 | + return { succeeded: true, result: her }; |
| 120 | + } |
| 121 | + case KUBECTL_DESCRIBE_AUTHORITY: { |
| 122 | + const describe = await Progress.execFunctionWithProgress(`Loading ${value}...`, async () => { |
| 123 | + return await CliChannel.getInstance().executeTool(new CommandText(`oc describe ${value} ${nsarg}`), undefined, false); |
| 124 | + }); |
| 125 | + return { succeeded: true, result: describe }; |
| 126 | + } |
| 127 | + default: |
| 128 | + return { succeeded: false, error: [`Internal error: please raise an issue with the error code InvalidObjectLoadURI and report authority ${resourceAuthority}.`] }; |
| 129 | + } |
| 130 | + } |
| 131 | + |
| 132 | + writeFile(uri: Uri, content: Uint8Array, _options: { create: boolean; overwrite: boolean }): void | Thenable<void> { |
| 133 | + return this.saveAsync(uri, content); // TODO: respect options |
| 134 | + } |
| 135 | + |
| 136 | + private async saveAsync(uri: Uri, content: Uint8Array): Promise<void> { |
| 137 | + // This assumes no pathing in the URI - if this changes, we'll need to |
| 138 | + // create subdirectories. |
| 139 | + // TODO: not loving prompting as part of the write when it should really be part of a separate |
| 140 | + // 'save' workflow - but needs must, I think |
| 141 | + const rootPath = await this.selectRootFolder(); |
| 142 | + if (!rootPath) { |
| 143 | + return; |
| 144 | + } |
| 145 | + const fspath = path.join(rootPath, uri.fsPath); |
| 146 | + fs.writeFileSync(fspath, content); |
| 147 | + } |
| 148 | + |
| 149 | + delete(_uri: Uri, _options: { recursive: boolean }): void | Thenable<void> { |
| 150 | + // no-op |
| 151 | + } |
| 152 | + |
| 153 | + rename(_oldUri: Uri, _newUri: Uri, _options: { overwrite: boolean }): void | Thenable<void> { |
| 154 | + // no-op |
| 155 | + } |
| 156 | + |
| 157 | + private async showWorkspaceFolderPick(): Promise<vscode.WorkspaceFolder | undefined> { |
| 158 | + if (!vscode.workspace.workspaceFolders) { |
| 159 | + void vscode.window.showErrorMessage('This command requires an open folder.'); |
| 160 | + return undefined; |
| 161 | + } else if (vscode.workspace.workspaceFolders.length === 1) { |
| 162 | + return vscode.workspace.workspaceFolders[0]; |
| 163 | + } |
| 164 | + return await vscode.window.showWorkspaceFolderPick(); |
| 165 | + } |
| 166 | + |
| 167 | + private async selectRootFolder(): Promise<string | undefined> { |
| 168 | + const folder = await this.showWorkspaceFolderPick(); |
| 169 | + if (!folder) { |
| 170 | + return undefined; |
| 171 | + } |
| 172 | + if (folder.uri.scheme !== 'file') { |
| 173 | + void vscode.window.showErrorMessage('This command requires a filesystem folder'); // TODO: make it not |
| 174 | + return undefined; |
| 175 | + } |
| 176 | + return folder.uri.fsPath; |
| 177 | + } |
| 178 | +} |
0 commit comments