From c3a9b9109e17519d5d1cb974a51fd99a232e27fe Mon Sep 17 00:00:00 2001 From: Victor Rubezhny Date: Thu, 11 Apr 2024 15:08:07 +0200 Subject: [PATCH] Get rid of using k8s extension provided 'kubectl' binary The 'oc' binary is used instead in non-blocking manner, especially when getting 'api-versions' values. Partially fixes #3987. Issue: #3987 Signed-off-by: Victor Rubezhny --- src/k8s/build.ts | 8 ++++---- src/k8s/clusterExplorer.ts | 19 +++++++++---------- src/k8s/common.ts | 12 ++++++------ src/serverlessFunction/knative.ts | 21 +++++++++------------ src/tekton/tekton.ts | 14 +++++++------- src/util/kubeUtils.ts | 14 +++++++------- test/unit/k8s/build.test.ts | 30 +++++++++++++----------------- test/unit/k8s/deployment.test.ts | 23 ++++++++--------------- 8 files changed, 63 insertions(+), 78 deletions(-) diff --git a/src/k8s/build.ts b/src/k8s/build.ts index 5f34f7e6d..9e3485263 100644 --- a/src/k8s/build.ts +++ b/src/k8s/build.ts @@ -118,8 +118,8 @@ export class Build { } if (buildName) { result = Progress.execFunctionWithProgress('Starting build', () => - Build.cli.executeTool(Build.command.startBuild(buildName)), - ) + Build.cli.executeTool(Build.command.startBuild(buildName)), + ) .then(() => `Build '${buildName}' successfully started`) .catch((err) => Promise.reject( @@ -186,8 +186,8 @@ export class Build { const build = await Build.selectBuild(context, 'Select a build to delete'); if (build) { result = Progress.execFunctionWithProgress('Deleting build', () => - Build.cli.executeTool(Build.command.delete(build)), - ) + Build.cli.executeTool(Build.command.delete(build)) + ) .then(() => `Build '${build}' successfully deleted`) .catch((err) => Promise.reject( diff --git a/src/k8s/clusterExplorer.ts b/src/k8s/clusterExplorer.ts index 265835d7d..9eb8e38bc 100644 --- a/src/k8s/clusterExplorer.ts +++ b/src/k8s/clusterExplorer.ts @@ -10,22 +10,21 @@ import { DeploymentConfig } from './deploymentConfig'; import path = require('path'); import { ClusterServiceVersion } from './csv'; import { isOpenShift } from '../util/kubeUtils'; +import { CommandText } from '../base/command'; +import { CliChannel } from '../cli'; let clusterExplorer: k8s.ClusterExplorerV1 | undefined; let lastNamespace = ''; async function initNamespaceName(node: k8s.ClusterExplorerV1.ClusterExplorerResourceNode): Promise { - const kubectl = await k8s.extension.kubectl.v1; - if (kubectl.available) { - const result = await kubectl.api.invokeCommand('config view -o json'); - const config = JSON.parse(result.stdout); - const currentContext = (config.contexts || []).find((ctx) => ctx.name === node.name); - if (!currentContext) { - return ''; - } - return currentContext.context.namespace || 'default'; - } + const result = await CliChannel.getInstance().executeTool(new CommandText('oc', 'config view -o json')); + const config = JSON.parse(result.stdout); + const currentContext = (config.contexts || []).find((ctx) => ctx.name === node.name); + if (!currentContext) { + return ''; + } + return currentContext.context.namespace || 'default'; } async function customizeAsync(node: k8s.ClusterExplorerV1.ClusterExplorerResourceNode, treeItem: vscode.TreeItem): Promise { diff --git a/src/k8s/common.ts b/src/k8s/common.ts index 8e3ca2c77..841145fe6 100644 --- a/src/k8s/common.ts +++ b/src/k8s/common.ts @@ -19,8 +19,8 @@ function convertItemToQuickPick(item: any): QuickPickItem { export async function getQuickPicks(cmd: CommandText, errorMessage: string, converter: (item: any) => QuickPickItem = convertItemToQuickPick): Promise { const result = await CliChannel.getInstance().executeTool(cmd); - const json = JSON.parse(result.stdout); - if (json.items.length === 0) { + const json = result ? JSON.parse(result.stdout) : undefined; + if (!json || json.items.length === 0) { throw new VsCommandError(errorMessage); } return json.items.map(converter); @@ -32,13 +32,13 @@ export async function selectResourceByName(config: Promise | Qu } export async function getChildrenNode(command: CommandText, kind: string, abbreviation: string): Promise { - const kubectl = await k8s.extension.kubectl.v1; - if (kubectl.available) { - const result = await kubectl.api.invokeCommand(`${command}`); + try { + const result = await CliChannel.getInstance().executeTool(command); const builds = result.stdout.split('\n') .filter((value) => value !== '') .map((item: string) => new Node(item.split(',')[0], item.split(',')[1], Number.parseInt(item.split(',')[2], 10), kind, abbreviation)); return builds; + } catch (error) { + return []; } - return []; } diff --git a/src/serverlessFunction/knative.ts b/src/serverlessFunction/knative.ts index c677fc086..0cd92de67 100644 --- a/src/serverlessFunction/knative.ts +++ b/src/serverlessFunction/knative.ts @@ -3,7 +3,8 @@ * Licensed under the MIT License. See LICENSE file in the project root for license information. *-----------------------------------------------------------------------------------------------*/ -import * as k8s from 'vscode-kubernetes-tools-api'; +import { CommandText } from '../base/command'; +import { CliChannel } from '../cli'; /** * Returns true if the cluster has the Knative Serving CRDs, and false otherwise. @@ -11,16 +12,12 @@ import * as k8s from 'vscode-kubernetes-tools-api'; * @returns true if the cluster has the Knative Serving CRDs, and false otherwise */ export async function isKnativeServingAware(): Promise { - const kubectl = await k8s.extension.kubectl.v1; - let isKnative = false; - if (kubectl.available) { - const sr = await kubectl.api.invokeCommand('api-versions'); - isKnative = - sr && - sr.code === 0 && - (sr.stdout.includes('serving.knative.dev/v1') || - sr.stdout.includes('serving.knative.dev/v1alpha1') || - sr.stdout.includes('serving.knative.dev/v1beta1')); + try { + const stdout = await CliChannel.getInstance().executeSyncTool(new CommandText('oc', 'api-versions'), { timeout: 5000 }); + return stdout.includes('serving.knative.dev/v1') || + stdout.includes('serving.knative.dev/v1alpha1') || + stdout.includes('serving.knative.dev/v1beta1') + } catch(error) { + return false; } - return isKnative; } diff --git a/src/tekton/tekton.ts b/src/tekton/tekton.ts index a738d5431..13edfe4aa 100644 --- a/src/tekton/tekton.ts +++ b/src/tekton/tekton.ts @@ -3,7 +3,8 @@ * Licensed under the MIT License. See LICENSE file in the project root for license information. *-----------------------------------------------------------------------------------------------*/ -import * as k8s from 'vscode-kubernetes-tools-api'; +import { CommandText } from '../base/command'; +import { CliChannel } from '../cli'; /** * Returns true if the cluster has the Tekton CRDs, and false otherwise. @@ -11,11 +12,10 @@ import * as k8s from 'vscode-kubernetes-tools-api'; * @returns true if the cluster has the Tekton CRDs, and false otherwise */ export async function isTektonAware(): Promise { - const kubectl = await k8s.extension.kubectl.v1; - let isTekton = false; - if (kubectl.available) { - const sr = await kubectl.api.invokeCommand('api-versions'); - isTekton = sr && sr.code === 0 && sr.stdout.includes('tekton.dev/v1beta1'); + try { + const stdout = await CliChannel.getInstance().executeSyncTool(new CommandText('oc', 'api-versions'), { timeout: 5000 }); + return stdout.includes('tekton.dev/v1beta1'); + } catch(error) { + return false; } - return isTekton; } diff --git a/src/util/kubeUtils.ts b/src/util/kubeUtils.ts index c197fb453..118dee65e 100644 --- a/src/util/kubeUtils.ts +++ b/src/util/kubeUtils.ts @@ -9,7 +9,8 @@ import * as fs from 'fs'; import * as path from 'path'; import { QuickPickItem, window } from 'vscode'; import { Platform } from './platform'; -import * as k8s from 'vscode-kubernetes-tools-api'; +import { CommandText } from '../base/command'; +import { CliChannel } from '../cli'; function fileExists(file: string): boolean { try { @@ -184,13 +185,12 @@ export async function setKubeConfig(): Promise { } export async function isOpenShift(): Promise { - const kubectl = await k8s.extension.kubectl.v1; - let isOS = false; - if (kubectl.available) { - const sr = await kubectl.api.invokeCommand('api-versions'); - isOS = sr && sr.code === 0 && sr.stdout.includes('apps.openshift.io/v1'); + try { + const stdout = await CliChannel.getInstance().executeSyncTool(new CommandText('oc', 'api-versions'), { timeout: 5000 }); + return stdout.includes('apps.openshift.io/v1'); + } catch(error) { + return false; } - return isOS; } export async function getNamespaceKind(): Promise { diff --git a/test/unit/k8s/build.test.ts b/test/unit/k8s/build.test.ts index b9f86338c..f1d4ce371 100644 --- a/test/unit/k8s/build.test.ts +++ b/test/unit/k8s/build.test.ts @@ -11,6 +11,7 @@ import * as k8s from 'vscode-kubernetes-tools-api'; import { Build } from '../../../src/k8s/build'; import { ChildProcessUtil } from '../../../src/util/childProcessUtil'; import { OpenShiftTerminalManager } from '../../../src/webview/openshift-terminal/openShiftTerminal'; +import { CliChannel } from '../../../src/cli'; const {expect} = chai; chai.use(sinonChai); @@ -20,6 +21,7 @@ suite('K8s/build', () => { let sandbox: sinon.SinonSandbox; let termStub: sinon.SinonStub; let execStub: sinon.SinonStub; + let execToolStub: sinon.SinonStub; const errorMessage = 'FATAL ERROR'; const context = { id: 'dummy', @@ -64,6 +66,7 @@ suite('K8s/build', () => { sandbox = sinon.createSandbox(); termStub = sandbox.stub(OpenShiftTerminalManager.prototype, 'executeInTerminal'); execStub = sandbox.stub(ChildProcessUtil.prototype, 'execute').resolves({ stdout: '', stderr: undefined, error: undefined }); + execToolStub = sandbox.stub(CliChannel.prototype, 'executeTool'); // sandbox.stub(Progress, 'execFunctionWithProgress').yields(); }); @@ -88,14 +91,7 @@ suite('K8s/build', () => { } as k8s.ClusterExplorerV1.ClusterExplorerNode; setup(() => { - const api: k8s.API = { - available: true, - api: { - invokeCommand: sandbox.stub().resolves({ stdout: 'namespace, name, 1', stderr: '', code: 0}), - portForward: sandbox.stub() - } - }; - sandbox.stub(k8s.extension.kubectl, 'v1').value(api); + execToolStub.resolves({ stdout: 'namespace, name, 1', stderr: '', error: undefined}); }); test('should able to get the children node of build Config', async () => { @@ -155,23 +151,23 @@ suite('K8s/build', () => { } }`; setup(() => { - execStub.resolves({ stdout: mockData, stderr: undefined, error: undefined }); quickPickStub = sandbox.stub(vscode.window, 'showQuickPick'); quickPickStub.resolves({label: 'nodejs-comp-nodejs-app'}); + execToolStub.resolves({ stdout: mockData, stderr: '', error: undefined }); }); test('works from context menu', async () => { const result = await Build.startBuild(startBuildCtx); expect(result).equals(`Build '${startBuildCtx.name}' successfully started`); - expect(execStub).calledWith(Build.command.startBuild(startBuildCtx.name).toString()); + expect(execToolStub).calledWith(Build.command.startBuild(startBuildCtx.name)); }); test('works with no context', async () => { const result = await Build.startBuild(null); expect(result).equals(`Build '${startBuildCtx.name}' successfully started`); - expect(execStub).calledWith(Build.command.startBuild(startBuildCtx.name).toString()); + expect(execToolStub).calledWith(Build.command.startBuild(startBuildCtx.name)); }); test('returns null when no BuildConfig selected', async () => { @@ -181,7 +177,7 @@ suite('K8s/build', () => { }); test('wraps errors in additional info', async () => { - execStub.rejects(errorMessage); + execToolStub.rejects(errorMessage); try { await Build.startBuild(startBuildCtx); @@ -192,7 +188,7 @@ suite('K8s/build', () => { test('throws error if there is no BuildConfigs to select', async () => { quickPickStub.restore(); - execStub.resolves({ error: undefined, stdout: noBcData, stderr: '' }); + execToolStub.resolves({ error: undefined, stdout: noBcData, stderr: '' }); let checkError: Error; try { await Build.startBuild(null); @@ -290,7 +286,7 @@ suite('K8s/build', () => { suite('Delete', ()=> { setup(() => { - execStub.resolves({ error: null, stdout: buildData, stderr: '' }); + execToolStub.resolves({ error: null, stdout: buildData, stderr: '' }); sandbox.stub(Build, 'getBuildNames').resolves('nodejs-copm-nodejs-comp'); quickPickStub = sandbox.stub(vscode.window, 'showQuickPick'); quickPickStub.resolves({label: 'nodejs-copm-nodejs-comp-8'}); @@ -300,14 +296,14 @@ suite('K8s/build', () => { const result = await Build.delete(context); expect(result).equals(`Build '${context.impl.name}' successfully deleted`); - expect(execStub).calledWith(Build.command.delete(context.impl.name).toString()); + expect(execToolStub).calledWith(Build.command.delete(context.impl.name)); }); test('works with no context', async () => { const result = await Build.delete(null); expect(result).equals('Build \'nodejs-copm-nodejs-comp-8\' successfully deleted'); - expect(execStub).calledWith(Build.command.delete('nodejs-copm-nodejs-comp-8').toString()); + expect(execToolStub).calledWith(Build.command.delete('nodejs-copm-nodejs-comp-8')); }); test('returns null when no build selected to delete', async () => { @@ -317,7 +313,7 @@ suite('K8s/build', () => { }); test('wraps errors in additional info', async () => { - execStub.rejects(errorMessage); + execToolStub.rejects(errorMessage); try { await Build.delete(context); diff --git a/test/unit/k8s/deployment.test.ts b/test/unit/k8s/deployment.test.ts index a23cc637d..69c548c2a 100644 --- a/test/unit/k8s/deployment.test.ts +++ b/test/unit/k8s/deployment.test.ts @@ -13,6 +13,7 @@ import { DeploymentConfig } from '../../../src/k8s/deploymentConfig'; import { ChildProcessUtil } from '../../../src/util/childProcessUtil'; import { Progress } from '../../../src/util/progress'; import { OpenShiftTerminalManager } from '../../../src/webview/openshift-terminal/openShiftTerminal'; +import { VsCommandError } from '../../../src/vscommand'; const {expect} = chai; chai.use(sinonChai); @@ -91,7 +92,7 @@ suite('K8s/deployment', () => { setup(() => { sandbox = sinon.createSandbox(); termStub = sandbox.stub(OpenShiftTerminalManager.prototype, 'executeInTerminal'); - execStub = sandbox.stub(CliChannel.prototype, 'executeTool').resolves({ stdout: '', stderr: undefined, error: undefined}); + execStub = sandbox.stub(CliChannel.prototype, 'executeTool').resolves({ stdout: '', stderr: '', error: undefined}); sandbox.stub(Progress, 'execFunctionWithProgress').yields(); }); @@ -101,7 +102,6 @@ suite('K8s/deployment', () => { suite('DeploymentConfigNodeContributor', () => { let dcnc; - let kubectlV1Stub: sinon.SinonStub; const parent = { metadata: undefined, name: 'comp1-app', @@ -119,26 +119,20 @@ suite('K8s/deployment', () => { setup(() => { dcnc = DeploymentConfig.getNodeContributor(); - const api: k8s.API = { - available: true, - api: { - invokeCommand: sandbox.stub().resolves({ stdout: 'namespace, name, 1', stderr: '', code: 0}), - portForward: sandbox.stub() - } - }; - kubectlV1Stub = sandbox.stub(k8s.extension.kubectl, 'v1').value(api); }); test('should able to get the children node of deployment Config', async () => { + // Set 'executeTool` stub to return a normal response + execStub.resolves({ stdout: 'namespace, name, 1', stderr: '', error: undefined}); + const result = await dcnc.getChildren(parent); expect(result.length).equals(1); }); test('returns empty children node of deployment Config', async () => { - const api = { - available: false - }; - kubectlV1Stub.onFirstCall().value(api); + // Set 'executeTool` stub to return a random error + execStub.resolves({ stdout: '', stderr: 'Not available', error: new VsCommandError('Not available')}); + const result = await dcnc.getChildren(parent); expect(result.length).equals(0); }); @@ -155,7 +149,6 @@ suite('K8s/deployment', () => { }; setup(() => { - // execStub = sandbox.stub(CliChannel.prototype, 'execute').resolves({ stdout: mockData, stderr: undefined, error: undefined }); execStub.resolves({ error: undefined, stdout: mockData, stderr: '' }); quickPickStub = sandbox.stub(vscode.window, 'showQuickPick'); quickPickStub.resolves({label: 'nodejs-comp-nodejs-app'});