Skip to content

Commit 771d39a

Browse files
committed
Migrate Kubernetes extension commands #3990
Fixes: #3990 Signed-off-by: Victor Rubezhny <vrubezhny@redhat.com>
1 parent 49d16c5 commit 771d39a

File tree

9 files changed

+289
-24
lines changed

9 files changed

+289
-24
lines changed

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,8 @@
255255
"onCommand:openshift.component.deleteConfigurationFiles",
256256
"onCommand:openshift.component.deleteSourceFolder",
257257
"onWalkthrough:openshiftWalkthrough",
258-
"onWalkthrough:serverlessFunctionWalkthrough"
258+
"onWalkthrough:serverlessFunctionWalkthrough",
259+
"onFileSystem:k8smsx"
259260
],
260261
"contributes": {
261262
"configurationDefaults": {
@@ -2311,8 +2312,7 @@
23112312
]
23122313
},
23132314
"extensionDependencies": [
2314-
"redhat.vscode-redhat-account",
2315-
"ms-kubernetes-tools.vscode-kubernetes-tools"
2315+
"redhat.vscode-redhat-account"
23162316
],
23172317
"__metadata": {
23182318
"id": "8fea1f1f-b45c-4eea-b479-3a92c6e697d3",

src/explorer.ts

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import { CustomResourceDefinitionStub } from './webview/common/createServiceType
3737
import { OpenShiftTerminalManager } from './webview/openshift-terminal/openShiftTerminal';
3838
import { LoginUtil } from './util/loginUtil';
3939
import { PortForward } from './port-forward';
40+
import { kubefsUri } from './k8s/vfs/kuberesources.virtualfs';
4041

4142
type ExplorerItem = KubernetesObject | Helm.HelmRelease | Context | TreeItem | OpenShiftObject | HelmRepo;
4243

@@ -529,7 +530,7 @@ export class OpenShiftExplorer implements TreeDataProvider<ExplorerItem>, Dispos
529530
*/
530531
loadKubernetesCore(namespace: string | null, value: string) {
531532
const outputFormat = this.getOutputFormat();
532-
const uri = this.kubefsUri(namespace, value, outputFormat);
533+
const uri = kubefsUri(namespace, value, outputFormat);
533534

534535
const query = this.getComparableQuery(uri);
535536
const openUri = workspace.textDocuments.map((doc) => doc.uri)
@@ -564,20 +565,6 @@ export class OpenShiftExplorer implements TreeDataProvider<ExplorerItem>, Dispos
564565
return 'yaml'
565566
}
566567

567-
kubefsUri(namespace: string | null | undefined, value: string, outputFormat: string, action?: string): Uri {
568-
const K8S_RESOURCE_SCHEME = 'k8smsx';
569-
const K8S_RESOURCE_SCHEME_READONLY = 'k8smsxro';
570-
const KUBECTL_RESOURCE_AUTHORITY = 'loadkubernetescore';
571-
const KUBECTL_DESCRIBE_AUTHORITY = 'kubernetesdescribe';
572-
const docname = `${value.replace('/', '-')}${outputFormat && outputFormat !== '' ? `.${outputFormat}` : ''}`;
573-
const nonce = new Date().getTime();
574-
const nsquery = namespace ? `ns=${namespace}&` : '';
575-
const scheme = action === 'describe' ? K8S_RESOURCE_SCHEME_READONLY : K8S_RESOURCE_SCHEME;
576-
const authority = action === 'describe' ? KUBECTL_DESCRIBE_AUTHORITY : KUBECTL_RESOURCE_AUTHORITY;
577-
const uri = `${scheme}://${authority}/${docname}?${nsquery}value=${value}&_=${nonce}`;
578-
return Uri.parse(uri);
579-
}
580-
581568
/*
582569
* Returns the query string of the specified Uri without "nonce" param,
583570
* so the query strings can be compared.
@@ -640,7 +627,7 @@ export class OpenShiftExplorer implements TreeDataProvider<ExplorerItem>, Dispos
640627

641628
@vsCommand('openshift.resource.openInConsole')
642629
public static openInConsole(component: KubernetesObject) {
643-
void commands.executeCommand('extension.vsKubernetesLoad', { namespace: component.metadata.namespace, kindName: `${component.kind}/${component.metadata.name}` });
630+
void commands.executeCommand('openshift.resource.load', { namespace: component.metadata.namespace, kindName: `${component.kind}/${component.metadata.name}` });
644631
}
645632

646633
@vsCommand('openshift.explorer.reportIssue')

src/extension.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import { registerYamlHandlers } from './yaml/yamlDocumentFeatures';
3636

3737
import fsx = require('fs-extra');
3838
import { Oc } from './oc/ocWrapper';
39+
import { K8S_RESOURCE_SCHEME, K8S_RESOURCE_SCHEME_READONLY, KubernetesResourceVirtualFileSystemProvider } from './k8s/vfs/kuberesources.virtualfs';
3940

4041
// eslint-disable-next-line @typescript-eslint/no-empty-function
4142
// this method is called when your extension is deactivated
@@ -76,6 +77,12 @@ export async function activate(extensionContext: ExtensionContext): Promise<unkn
7677
Cluster.extensionContext = extensionContext;
7778
TokenStore.extensionContext = extensionContext;
7879

80+
// Temporarily loaded resource providers
81+
const resourceDocProvider = new KubernetesResourceVirtualFileSystemProvider();
82+
83+
// Link from resources to referenced resources
84+
// const resourceLinkProvider = new KubernetesResourceLinkProvider();
85+
7986
// pick kube config in case multiple are configured
8087
await setKubeConfig();
8188

@@ -104,9 +111,9 @@ export async function activate(extensionContext: ExtensionContext): Promise<unkn
104111
'./feedback',
105112
'./deployment'
106113
)),
107-
commands.registerCommand('clusters.openshift.useProject', (context) =>
108-
commands.executeCommand('extension.vsKubernetesUseNamespace', context),
109-
),
114+
// commands.registerCommand('clusters.openshift.useProject', (context) =>
115+
// commands.executeCommand('extension.vsKubernetesUseNamespace', context),
116+
// ),
110117
crcStatusItem,
111118
activeNamespaceStatusBarItem,
112119
activeContextStatusBarItem,
@@ -119,6 +126,13 @@ export async function activate(extensionContext: ExtensionContext): Promise<unkn
119126
setupWorkspaceDevfileContext(),
120127
window.registerWebviewViewProvider('openShiftTerminalView', OpenShiftTerminalManager.getInstance(), { webviewOptions: { retainContextWhenHidden: true, } }),
121128
...registerYamlHandlers(),
129+
// Temporarily loaded resource providers
130+
workspace.registerFileSystemProvider(K8S_RESOURCE_SCHEME, resourceDocProvider, { /* TODO: case sensitive? */ }),
131+
workspace.registerFileSystemProvider(K8S_RESOURCE_SCHEME_READONLY, resourceDocProvider, { isReadonly: true }),
132+
133+
// Link from resources to referenced resources
134+
// languages.registerDocumentLinkProvider({ scheme: K8S_RESOURCE_SCHEME }, resourceLinkProvider),
135+
122136
];
123137
disposable.forEach((value) => extensionContext.subscriptions.push(value));
124138

src/helm/helm.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* Copyright (c) Red Hat, Inc. All rights reserved.
33
* Licensed under the MIT License. See LICENSE file in the project root for license information.
44
*-----------------------------------------------------------------------------------------------*/
5+
import { CommandText } from '../base/command';
56
import { CliChannel } from '../cli';
67
import { CliExitData } from '../util/childProcessUtil';
78
import { HelmRepo } from './helmChartType';
@@ -108,3 +109,35 @@ export function ascRepoName(oldRepo: HelmRepo, newRepo: HelmRepo) {
108109
}
109110
return oldRepo.name.localeCompare(newRepo.name);
110111
}
112+
113+
// This file contains utilities for executing command line tools, notably Helm.
114+
115+
export enum HelmSyntaxVersion {
116+
Unknown = 1,
117+
V2 = 2,
118+
V3 = 3,
119+
}
120+
121+
let cachedVersion: HelmSyntaxVersion | undefined = undefined;
122+
123+
export async function helmSyntaxVersion(): Promise<HelmSyntaxVersion> {
124+
if (cachedVersion === undefined) {
125+
const srHelm2 = await CliChannel.getInstance().executeTool(new CommandText('helm version --short -c'));
126+
if (CliExitData.failed(srHelm2)) {
127+
// failed to run Helm; do not cache result
128+
return HelmSyntaxVersion.Unknown;
129+
}
130+
131+
if (srHelm2.stdout.indexOf('v2') >= 0) {
132+
cachedVersion = HelmSyntaxVersion.V2;
133+
} else {
134+
const srHelm3 = await CliChannel.getInstance().executeTool(new CommandText('helm version --short'));
135+
if (!CliExitData.failed(srHelm3) && srHelm3.stdout.indexOf('v3') >= 0) {
136+
cachedVersion = HelmSyntaxVersion.V3;
137+
} else {
138+
return HelmSyntaxVersion.Unknown;
139+
}
140+
}
141+
}
142+
return cachedVersion;
143+
}

src/k8s/node.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export class Node implements ClusterExplorerV1.Node, ClusterExplorerV1.ClusterEx
3737
item.contextValue = `openShift.resource.${this.node}`;
3838
item.command = {
3939
arguments: [this],
40-
command: 'extension.vsKubernetesLoad',
40+
command: 'openshift.resource.load',
4141
title: 'Load'
4242
};
4343
return item;

src/k8s/vfs/errorable.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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+
export interface Succeeded<T> {
7+
readonly succeeded: true;
8+
readonly result: T;
9+
}
10+
11+
export interface Failed {
12+
readonly succeeded: false;
13+
readonly error: string[];
14+
}
15+
16+
export type Errorable<T> = Succeeded<T> | Failed;
17+
18+
export function succeeded<T>(e: Errorable<T>): e is Succeeded<T> {
19+
return e.succeeded;
20+
}
21+
22+
export function failed<T>(e: Errorable<T>): e is Failed {
23+
return !e.succeeded;
24+
}
25+
26+
// eslint-disable-next-line @typescript-eslint/no-namespace
27+
export namespace Errorable {
28+
export function succeeded<T>(e: Errorable<T>): e is Succeeded<T> {
29+
return e.succeeded;
30+
}
31+
32+
export function failed<T>(e: Errorable<T>): e is Failed {
33+
return !e.succeeded;
34+
}
35+
}
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
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

Comments
 (0)