From 2034802f1ad051fdd2af7f5e404e9744104ef32e Mon Sep 17 00:00:00 2001 From: msivasubramaniaan Date: Mon, 27 Apr 2026 15:48:37 +0530 Subject: [PATCH 01/11] replaced odo delete component configuration --- src/odo/odoWrapper.ts | 70 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 64 insertions(+), 6 deletions(-) diff --git a/src/odo/odoWrapper.ts b/src/odo/odoWrapper.ts index 452fc046c..5955f6113 100644 --- a/src/odo/odoWrapper.ts +++ b/src/odo/odoWrapper.ts @@ -11,6 +11,9 @@ import { ChildProcessUtil, CliExitData } from '../util/childProcessUtil'; import { VsCommandError } from '../vscommand'; import { Command } from './command'; import { ComponentDescription } from './componentTypeDescription'; +import * as fs from 'fs/promises'; +import * as path from 'path'; +import * as yaml from 'yaml'; /** * Wraps the `odo` cli tool. @@ -179,12 +182,67 @@ export class Odo { * @param componentPath the path to the component */ public async deleteComponentConfiguration(componentPath: string): Promise { - await this.execute( - new CommandText('odo', 'delete component', [ - new CommandOption('--files'), - new CommandOption('-f'), - ]), - componentPath, + + const componentName = path.basename(componentPath); + + // Delete core workload resources + await this.execute(new CommandText('oc', 'delete', [ + new CommandOption('pod,service,deployment,replicaset'), + new CommandOption('-l', `app=${componentName}`), + new CommandOption('--ignore-not-found'), + ])); + + // Delete routes (OpenShift) + await this.execute(new CommandText('oc', 'delete', [ + new CommandOption('route'), + new CommandOption('-l', `app=${componentName}`), + new CommandOption('--ignore-not-found'), + ])); + + // Delete configmaps (optional) + await this.execute(new CommandText('oc', 'delete', [ + new CommandOption('configmap'), + new CommandOption('-l', `app=${componentName}`), + new CommandOption('--ignore-not-found'), + ])); + + await deleteOdoFiles(componentPath, componentName); + + } +} + +async function isDevfile(filePath: string, componentName: string): Promise { + try { + const content = await fs.readFile(filePath, 'utf-8'); + const parsed = yaml.parse(content); + + return ( + parsed && + typeof parsed === 'object' && + parsed.schemaVersion && + parsed.metadata && + parsed.metadata.name && parsed.metadata.name === componentName ); + } catch { + return false; } } + +async function deleteOdoFiles(componentDir: string, componentName: string): Promise { + const files = await fs.readdir(componentDir); + + for (const file of files) { + const fullPath = path.join(componentDir, file); + + // Only check YAML files + if (file.endsWith('.yaml') || file.endsWith('.yml')) { + if (await isDevfile(fullPath, componentName)) { + await fs.rm(fullPath, { force: true }); + } + } + } + + // Delete .odo directory + const odoDirPath = path.join(componentDir, '.odo'); + await fs.rm(odoDirPath, { recursive: true, force: true }); +} From b378e423b60d36e5337718495e0fd5a6bf61deaf Mon Sep 17 00:00:00 2001 From: msivasubramaniaan Date: Mon, 27 Apr 2026 16:20:46 +0530 Subject: [PATCH 02/11] fixed test file --- test/integration/odoWrapper.test.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/integration/odoWrapper.test.ts b/test/integration/odoWrapper.test.ts index 2df11662c..a4f5e0ec2 100644 --- a/test/integration/odoWrapper.test.ts +++ b/test/integration/odoWrapper.test.ts @@ -168,14 +168,14 @@ suite('./odo/odoWrapper.ts', function () { }); test('deleteComponentConfiguration()', async function() { - await Odo.Instance.deleteComponentConfiguration(tmpFolder); - try { - await fs.access(path.join(tmpFolder, 'devfile.yaml')); - fail('devfile.yaml should have been deleted') - } catch { - // deleted successfully - } - }); + await Odo.Instance.deleteComponentConfiguration(tmpFolder); + + const files = await fs.readdir(tmpFolder); + + if (files.includes('devfile.yaml')) { + fail('devfile.yaml should have been deleted'); + } + }); test('createComponentFromLocation()', async function() { // the project already exists from the previous step, From 0e0d27b18d71ac20035fa59825fdb9fd2588cb20 Mon Sep 17 00:00:00 2001 From: msivasubramaniaan Date: Tue, 28 Apr 2026 13:38:52 +0530 Subject: [PATCH 03/11] moved the functions from odo to oc --- src/oc/devfileUtils.ts | 47 ++++++ src/oc/ocWrapper.ts | 236 ++++++++++++++++++++++++++++ src/odo/odoWrapper.ts | 80 +--------- src/openshift/component.ts | 2 +- test/integration/odoWrapper.test.ts | 16 +- 5 files changed, 296 insertions(+), 85 deletions(-) create mode 100644 src/oc/devfileUtils.ts diff --git a/src/oc/devfileUtils.ts b/src/oc/devfileUtils.ts new file mode 100644 index 000000000..df1d69b92 --- /dev/null +++ b/src/oc/devfileUtils.ts @@ -0,0 +1,47 @@ +/*----------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat, Inc. All rights reserved. + * Licensed under the MIT License. See LICENSE file in the project root for license information. + *-----------------------------------------------------------------------------------------------*/ +import * as fs from 'fs/promises'; +import * as path from 'path'; +import * as yaml from 'yaml'; + +export type DevfileInfo = { + path: string; + name: string; +}; + +export async function parseDevfile(filePath: string): Promise { + try { + const content = await fs.readFile(filePath, 'utf-8'); + const parsed = yaml.parse(content); + + if (parsed && typeof parsed === 'object' && parsed.schemaVersion && parsed.metadata?.name) { + return { + path: filePath, + name: parsed.metadata.name, + }; + } + } catch { + // ignore invalid YAML + } + + return undefined; +} + +export async function findDevfiles(componentDir: string): Promise { + const files = await fs.readdir(componentDir); + + const results = await Promise.all( + files + .filter((f) => f.endsWith('.yaml') || f.endsWith('.yml')) + .map((f) => parseDevfile(path.join(componentDir, f))), + ); + + return results.filter(Boolean) as DevfileInfo[]; +} + +export async function getComponentName(componentDir: string): Promise { + const devfiles = await findDevfiles(componentDir); + return devfiles[0]?.name; +} diff --git a/src/oc/ocWrapper.ts b/src/oc/ocWrapper.ts index 91ecec161..ae372e21b 100644 --- a/src/oc/ocWrapper.ts +++ b/src/oc/ocWrapper.ts @@ -14,6 +14,8 @@ import { CliExitData } from '../util/childProcessUtil'; import { isOpenShiftCluster, KubeConfigInfo, loadKubeConfig, serializeKubeConfig } from '../util/kubeUtils'; import { Project } from './project'; import { ClusterType, KubernetesConsole } from './types'; +import { findDevfiles, getComponentName } from './devfileUtils'; +import path from 'path'; /** * A wrapper around the `oc` CLI tool. @@ -895,4 +897,238 @@ export class Oc { undefined, true, config); return result.stdout; } + + async devWorkspaceExists(componentName: string): Promise { + try { + const result = await CliChannel.getInstance().executeTool( + new CommandText('oc', 'get', [ + new CommandOption('devworkspace'), + new CommandOption(componentName), + new CommandOption('-o'), + new CommandOption('name'), + ]), + ); + + return result?.stdout?.includes('devworkspace'); + } catch { + return false; + } + } + + async deleteOdoFiles(componentDir: string, componentName?: string): Promise { + const devfiles = await findDevfiles(componentDir); + + for (const devfile of devfiles) { + if (!componentName || devfile.name === componentName) { + await fs.rm(devfile.path, { force: true }); + } + } + + // Delete .odo directory + await fs.rm(path.join(componentDir, '.odo'), { + recursive: true, + force: true, + }); + } + + async findDevWorkspaceByLabel(componentName: string): Promise { + try { + const result = await CliChannel.getInstance().executeTool( + new CommandText('oc', 'get', [ + new CommandOption('devworkspace'), + new CommandOption('-l'), + new CommandOption(`app.kubernetes.io/component=${componentName}`), + new CommandOption('-o'), + new CommandOption('jsonpath={.items[0].metadata.name}'), + ]), + ); + const name = result?.stdout?.trim(); + return name || null; + } + catch { + return null; + } + + } + /** + * Deletes all the odo configuration files associated with the component (`.odo`, `devfile.yaml`) located at the given path. + * + * @param componentPath the path to the component + */ + public async deleteComponentConfiguration(componentPath: string): Promise { + const componentName = await getComponentName(componentPath); + + if (!componentName) { + throw new Error('Component name is missing. Cannot delete resources safely.'); + } + + const cli = CliChannel.getInstance(); + + /** + * Try to delete the DevWorkspace resource with the component label - + * this should trigger the cleanup of all associated resources by the controller and is the safest way to delete a component. + * If this fails (e.g. due to RBAC issues), we will try to delete resources by label in the next steps + */ + try { + await cli.executeTool( + new CommandText('oc', 'delete', [ + new CommandOption('devworkspace'), + new CommandOption(componentName), + new CommandOption('--ignore-not-found'), + ]), + ); + } catch { + // Ignore RBAC / not found + } + + /** + * Get all pods with the component label to find dynamic labels (like devworkspace_id) that we can use for safe deletion + */ + let podJson: any = {}; + try { + const podResult = await cli.executeTool( + new CommandText('oc', 'get', [ + new CommandOption('pod'), + new CommandOption('-l'), + new CommandOption(`app.kubernetes.io/instance=${componentName}`), + new CommandOption('-o'), + new CommandOption('json'), + ]), + ); + + podJson = JSON.parse(podResult.stdout || '{}'); + } catch { + podJson = {}; + } + + const items = podJson.items || []; + + /** + * Collect unique devworkspace IDs and instance labels from the pods to build label selectors for deletion. + */ + const devworkspaceIds = new Set(); + const instanceLabels = new Set(); + + for (const item of items) { + const labels = item?.metadata?.labels || {}; + + if (labels['controller.devfile.io/devworkspace_id']) { + devworkspaceIds.add(labels['controller.devfile.io/devworkspace_id']); + } + + if (labels['app.kubernetes.io/instance']) { + instanceLabels.add(labels['app.kubernetes.io/instance']); + } + } + + instanceLabels.add(componentName); + + /** + * Build label selectors for deletion based on collected labels. We will use these selectors to delete resources in the next steps, + * ensuring we only target resources associated with our component. + */ + const selectors: string[] = []; + + instanceLabels.forEach((val) => { + selectors.push(`app.kubernetes.io/instance=${val}`); + selectors.push(`component=${val}`); + selectors.push(`app=${val}`); + }); + + /** + * Delete all resources associated with the component using the built label selectors. + */ + try { + for (const selector of selectors) { + await cli.executeTool( + new CommandText('oc', 'delete', [ + new CommandOption('all'), + new CommandOption('-l'), + new CommandOption(selector), + new CommandOption('--ignore-not-found'), + ]), + ); + } + } catch { + // Ignore errors + } + + /** + * To ensure we cover resources that might not have the common labels but are still associated with the component + * (like ConfigMaps, Secrets, PVCs, Routes, etc.), we will also attempt to delete these resources using the same label selectors. + * This is a safety net to catch any resources that might have been missed in the previous step. + */ + const extraResources = [ + 'configmap', + 'secret', + 'pvc', + 'route', // OpenShift + 'ingress', + 'serviceaccount', + 'role', + 'rolebinding', + ]; + + try { + for (const resource of extraResources) { + await cli.executeTool( + new CommandText('oc', 'delete', [ + new CommandOption('-l'), + new CommandOption(resource), + new CommandOption('--ignore-not-found'), + ]), + ); + } + } catch { + // Ignore errors + } + + /** + * Delete all resources associated with each DevWorkspace ID. + */ + try { + for (const dwId of devworkspaceIds) { + const dwSelector = `controller.devfile.io/devworkspace_id=${dwId}`; + + await cli.executeTool( + new CommandText('oc', 'delete', [ + new CommandOption('all'), + new CommandOption('-l'), + new CommandOption(dwSelector), + new CommandOption('--ignore-not-found'), + ]), + ); + + await cli.executeTool( + new CommandText('oc', 'delete', [ + new CommandOption('route'), + new CommandOption('-l'), + new CommandOption(dwSelector), + new CommandOption('--ignore-not-found'), + ]), + ); + } + } catch { + // Ignore errors + } + + /** + * As a final safety measure, we will also attempt to delete any remaining resources that have the component instance label, + * even if they don't have the other common labels. + */ + try { + await cli.executeTool( + new CommandText('oc', 'delete', [ + new CommandOption('all'), + new CommandOption('--selector'), + new CommandOption(`app.kubernetes.io/instance=${componentName}`), + new CommandOption('--ignore-not-found'), + ]), + ); + } catch { + // ignore + } + + await this.deleteOdoFiles(componentPath, componentName); + } } diff --git a/src/odo/odoWrapper.ts b/src/odo/odoWrapper.ts index 5955f6113..1b001daa9 100644 --- a/src/odo/odoWrapper.ts +++ b/src/odo/odoWrapper.ts @@ -4,16 +4,13 @@ *-----------------------------------------------------------------------------------------------*/ import { Uri, WorkspaceFolder, workspace } from 'vscode'; -import { CommandOption, CommandText } from '../base/command'; +import { CommandText } from '../base/command'; import * as cliInstance from '../cli'; import { ToolsConfig } from '../tools'; import { ChildProcessUtil, CliExitData } from '../util/childProcessUtil'; import { VsCommandError } from '../vscommand'; import { Command } from './command'; import { ComponentDescription } from './componentTypeDescription'; -import * as fs from 'fs/promises'; -import * as path from 'path'; -import * as yaml from 'yaml'; /** * Wraps the `odo` cli tool. @@ -83,7 +80,7 @@ export class Odo { location: Uri, starter: string = undefined, useExistingDevfile = false, - customDevfilePath = '' + customDevfilePath = '', ): Promise { await this.execute( Command.createLocalComponent( @@ -94,7 +91,7 @@ export class Odo { undefined, starter, useExistingDevfile, - customDevfilePath + customDevfilePath, ), location.fsPath, ); @@ -137,7 +134,7 @@ export class Odo { portNumber, undefined, false, - '' + '', ), location.fsPath, ); @@ -176,73 +173,4 @@ export class Odo { ); } - /** - * Deletes all the odo configuration files associated with the component (`.odo`, `devfile.yaml`) located at the given path. - * - * @param componentPath the path to the component - */ - public async deleteComponentConfiguration(componentPath: string): Promise { - - const componentName = path.basename(componentPath); - - // Delete core workload resources - await this.execute(new CommandText('oc', 'delete', [ - new CommandOption('pod,service,deployment,replicaset'), - new CommandOption('-l', `app=${componentName}`), - new CommandOption('--ignore-not-found'), - ])); - - // Delete routes (OpenShift) - await this.execute(new CommandText('oc', 'delete', [ - new CommandOption('route'), - new CommandOption('-l', `app=${componentName}`), - new CommandOption('--ignore-not-found'), - ])); - - // Delete configmaps (optional) - await this.execute(new CommandText('oc', 'delete', [ - new CommandOption('configmap'), - new CommandOption('-l', `app=${componentName}`), - new CommandOption('--ignore-not-found'), - ])); - - await deleteOdoFiles(componentPath, componentName); - - } -} - -async function isDevfile(filePath: string, componentName: string): Promise { - try { - const content = await fs.readFile(filePath, 'utf-8'); - const parsed = yaml.parse(content); - - return ( - parsed && - typeof parsed === 'object' && - parsed.schemaVersion && - parsed.metadata && - parsed.metadata.name && parsed.metadata.name === componentName - ); - } catch { - return false; - } -} - -async function deleteOdoFiles(componentDir: string, componentName: string): Promise { - const files = await fs.readdir(componentDir); - - for (const file of files) { - const fullPath = path.join(componentDir, file); - - // Only check YAML files - if (file.endsWith('.yaml') || file.endsWith('.yml')) { - if (await isDevfile(fullPath, componentName)) { - await fs.rm(fullPath, { force: true }); - } - } - } - - // Delete .odo directory - const odoDirPath = path.join(componentDir, '.odo'); - await fs.rm(odoDirPath, { recursive: true, force: true }); } diff --git a/src/openshift/component.ts b/src/openshift/component.ts index a19027c98..07dd587c8 100644 --- a/src/openshift/component.ts +++ b/src/openshift/component.ts @@ -568,7 +568,7 @@ export class Component extends OpenShiftItem { const CANCEL = 'Cancel'; const response = await window.showWarningMessage(`Are you sure you want to delete the configuration for the component ${context.contextPath}?\nOpenShift Toolkit will no longer recognize the project as a component.`, DELETE_CONFIGURATION, CANCEL); if (response === DELETE_CONFIGURATION) { - await Odo.Instance.deleteComponentConfiguration(context.contextPath); + await Oc.Instance.deleteComponentConfiguration(context.contextPath); void commands.executeCommand('openshift.componentsView.refresh'); } } diff --git a/test/integration/odoWrapper.test.ts b/test/integration/odoWrapper.test.ts index a4f5e0ec2..a37265bc0 100644 --- a/test/integration/odoWrapper.test.ts +++ b/test/integration/odoWrapper.test.ts @@ -168,14 +168,14 @@ suite('./odo/odoWrapper.ts', function () { }); test('deleteComponentConfiguration()', async function() { - await Odo.Instance.deleteComponentConfiguration(tmpFolder); - - const files = await fs.readdir(tmpFolder); - - if (files.includes('devfile.yaml')) { - fail('devfile.yaml should have been deleted'); - } - }); + await Oc.Instance.deleteComponentConfiguration(tmpFolder); + try { + await fs.access(path.join(tmpFolder, 'devfile.yaml')); + fail('devfile.yaml should have been deleted') + } catch { + // deleted successfully + } + }); test('createComponentFromLocation()', async function() { // the project already exists from the previous step, From 204f220aa31e8097f42f44e8db7f597bc55059ae Mon Sep 17 00:00:00 2001 From: msivasubramaniaan Date: Tue, 28 Apr 2026 13:55:24 +0530 Subject: [PATCH 04/11] fixed test case failure --- test/unit/openshift/component.test.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/test/unit/openshift/component.test.ts b/test/unit/openshift/component.test.ts index 9f34b7ef3..f4bb84893 100644 --- a/test/unit/openshift/component.test.ts +++ b/test/unit/openshift/component.test.ts @@ -179,12 +179,15 @@ suite('OpenShift/Component', function () { showWarningMessageStub.resolves('Delete Configuration'); await Component.deleteConfigurationFiles({ component: { - // these fields aren't used + name: 'comp1', }, contextPath: wsFolder1.uri.fsPath - } as ComponentWorkspaceFolder); - expect(execStub.called).is.true; - expect(execStub.lastCall.args[0].toString().endsWith('odo component delete -f --force')); + } as unknown as ComponentWorkspaceFolder); + expect(execStub.called).to.be.true; + + const commands = execStub.getCalls().map(call => call.args[0].toString()); + + expect(commands.some(cmd => cmd.includes('oc delete'))).to.be.true; }); test('cancel delete', async function () { @@ -234,10 +237,10 @@ suite('OpenShift/Component', function () { showWarningMessageStub.resolves('Delete Source Folder'); await Component.deleteSourceFolder({ component: { - // these fields aren't used + name: 'comp1', }, contextPath: wsFolder1.uri.fsPath - } as ComponentWorkspaceFolder); + } as unknown as ComponentWorkspaceFolder); expect(rmStub).to.be.called; expect(rmStub.lastCall.args[0]).to.equal(wsFolder1.uri.fsPath); }); From 2ccb0471f33b1bc3f43cd1b192e17d9eaa6d6b95 Mon Sep 17 00:00:00 2001 From: msivasubramaniaan Date: Tue, 28 Apr 2026 17:02:30 +0530 Subject: [PATCH 05/11] fixed test failures --- test/unit/openshift/component.test.ts | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/test/unit/openshift/component.test.ts b/test/unit/openshift/component.test.ts index f4bb84893..b856c08d6 100644 --- a/test/unit/openshift/component.test.ts +++ b/test/unit/openshift/component.test.ts @@ -25,6 +25,7 @@ import { Util } from '../../../src/util/async'; import { Util as fsp } from '../../../src/util/utils'; import { OpenShiftTerminalManager } from '../../../src/webview/openshift-terminal/openShiftTerminal'; import { comp1Folder } from '../../fixtures'; +import { CliChannel } from 'src/cli'; const { expect } = chai; @@ -32,7 +33,7 @@ chai.use(sinonChai); suite('OpenShift/Component', function () { let sandbox: sinon.SinonSandbox; - let termStub: sinon.SinonStub; let execStub: sinon.SinonStub; + let termStub: sinon.SinonStub; let execStub: sinon.SinonStub; let ocExecStub: sinon.SinonStub; const fixtureFolder = path.join(__dirname, '..', '..', '..', 'test', 'fixtures').normalize(); const comp1Uri = vscode.Uri.file(path.join(fixtureFolder, 'components', 'comp1')); const comp2Uri = vscode.Uri.file(path.join(fixtureFolder, 'components', 'comp2')); @@ -134,6 +135,11 @@ suite('OpenShift/Component', function () { termStub = sandbox.stub(OpenShiftTerminalManager.prototype, 'executeInTerminal'); execStub = sandbox.stub(Odo.prototype, 'execute').resolves({ stdout: '', stderr: undefined, error: undefined }); + ocExecStub = sandbox.stub(CliChannel.prototype, 'executeTool').resolves({ + stdout: '', + error: undefined, + stderr: '' + }); sandbox.stub(Oc.prototype, 'getProjects').resolves([projectItem]); sandbox.stub(Odo.prototype, 'describeComponent').resolves(componentItem1.component); sandbox.stub(OdoWorkspace.prototype, 'getComponents').resolves([componentItem1]); @@ -179,15 +185,20 @@ suite('OpenShift/Component', function () { showWarningMessageStub.resolves('Delete Configuration'); await Component.deleteConfigurationFiles({ component: { - name: 'comp1', + name: 'comp1', }, contextPath: wsFolder1.uri.fsPath } as unknown as ComponentWorkspaceFolder); - expect(execStub.called).to.be.true; - const commands = execStub.getCalls().map(call => call.args[0].toString()); + expect(ocExecStub.called).to.be.true; + + const commands = ocExecStub.getCalls().map(call => + call.args[0].toString() + ); - expect(commands.some(cmd => cmd.includes('oc delete'))).to.be.true; + expect(commands.some(cmd => + cmd.includes('oc') && cmd.includes('delete') + )).to.be.true; }); test('cancel delete', async function () { @@ -237,10 +248,10 @@ suite('OpenShift/Component', function () { showWarningMessageStub.resolves('Delete Source Folder'); await Component.deleteSourceFolder({ component: { - name: 'comp1', - }, + // these fields aren't used + }, contextPath: wsFolder1.uri.fsPath - } as unknown as ComponentWorkspaceFolder); + } as ComponentWorkspaceFolder); expect(rmStub).to.be.called; expect(rmStub.lastCall.args[0]).to.equal(wsFolder1.uri.fsPath); }); From 01bd650b90ed38baf43fca90d262d9abe469c51c Mon Sep 17 00:00:00 2001 From: msivasubramaniaan Date: Tue, 28 Apr 2026 17:13:06 +0530 Subject: [PATCH 06/11] fixing the import --- test/unit/openshift/component.test.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/test/unit/openshift/component.test.ts b/test/unit/openshift/component.test.ts index b856c08d6..9d6603158 100644 --- a/test/unit/openshift/component.test.ts +++ b/test/unit/openshift/component.test.ts @@ -25,8 +25,7 @@ import { Util } from '../../../src/util/async'; import { Util as fsp } from '../../../src/util/utils'; import { OpenShiftTerminalManager } from '../../../src/webview/openshift-terminal/openShiftTerminal'; import { comp1Folder } from '../../fixtures'; -import { CliChannel } from 'src/cli'; - +import { CliChannel } from '../../../src/cli'; const { expect } = chai; chai.use(sinonChai); @@ -135,11 +134,7 @@ suite('OpenShift/Component', function () { termStub = sandbox.stub(OpenShiftTerminalManager.prototype, 'executeInTerminal'); execStub = sandbox.stub(Odo.prototype, 'execute').resolves({ stdout: '', stderr: undefined, error: undefined }); - ocExecStub = sandbox.stub(CliChannel.prototype, 'executeTool').resolves({ - stdout: '', - error: undefined, - stderr: '' - }); + ocExecStub = sandbox.stub(CliChannel.prototype, 'executeTool').resolves({ stdout: '', stderr: '', error: undefined }); sandbox.stub(Oc.prototype, 'getProjects').resolves([projectItem]); sandbox.stub(Odo.prototype, 'describeComponent').resolves(componentItem1.component); sandbox.stub(OdoWorkspace.prototype, 'getComponents').resolves([componentItem1]); From 126725e1752f33908130990eac8388a5e29a2f5e Mon Sep 17 00:00:00 2001 From: msivasubramaniaan Date: Sat, 30 May 2026 20:04:31 +0530 Subject: [PATCH 07/11] replaced odo run command --- src/devfile/commandResolver.ts | 29 +++++++ src/devfile/compositeCommand.ts | 52 +++++++++++ src/devfile/execCommand.ts | 110 ++++++++++++++++++++++++ src/devfile/parallelCompositeCommand.ts | 61 +++++++++++++ src/devfile/variableResolver.ts | 68 +++++++++++++++ src/oc/ocWrapper.ts | 28 ++++++ src/odo/command.ts | 4 - src/openshift/component.ts | 25 ++++-- test/integration/command.test.ts | 72 +++++++--------- 9 files changed, 398 insertions(+), 51 deletions(-) create mode 100644 src/devfile/commandResolver.ts create mode 100644 src/devfile/compositeCommand.ts create mode 100644 src/devfile/execCommand.ts create mode 100644 src/devfile/parallelCompositeCommand.ts create mode 100644 src/devfile/variableResolver.ts diff --git a/src/devfile/commandResolver.ts b/src/devfile/commandResolver.ts new file mode 100644 index 000000000..6212ad9aa --- /dev/null +++ b/src/devfile/commandResolver.ts @@ -0,0 +1,29 @@ +/*----------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat, Inc. All rights reserved. + * Licensed under the MIT License. See LICENSE file in the project root for license information. + *-----------------------------------------------------------------------------------------------*/ +import { Command, Data } from '../odo/componentTypeDescription'; + +export class CommandResolver { + public static getCommand(devfile: Data, commandId: string): Command { + const command = devfile.commands.find( + (c) => c.id.toLowerCase() === commandId.toLowerCase(), + ); + + if (!command) { + throw new Error(`Command '${commandId}' not found`); + } + + return command; + } + + public static getAllCommandsMap(devfile: Data): Map { + const map = new Map(); + + for (const command of devfile.commands) { + map.set(command.id.toLowerCase(), command); + } + + return map; + } +} diff --git a/src/devfile/compositeCommand.ts b/src/devfile/compositeCommand.ts new file mode 100644 index 000000000..893206e7d --- /dev/null +++ b/src/devfile/compositeCommand.ts @@ -0,0 +1,52 @@ +/*----------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat, Inc. All rights reserved. + * Licensed under the MIT License. See LICENSE file in the project root for license information. + *-----------------------------------------------------------------------------------------------*/ + +import { ComponentWorkspaceFolder } from '../odo/workspace'; +import { Command } from '../odo/componentTypeDescription'; +import { CommandResolver } from './commandResolver'; +import { ExecCommandExecutor } from './execCommand'; + +export class CompositeCommand { + + public static async execute( + componentFolder: ComponentWorkspaceFolder, + command: Command, + ): Promise { + + const devfile = + componentFolder.component.devfileData.devfile; + + const commandMap = + CommandResolver.getAllCommandsMap( + devfile, + ); + + for (const childId of command.composite.commands) { + + const child = + commandMap.get( + childId.toLowerCase(), + ); + + if (!child) { + throw new Error( + `Command '${childId}' not found`, + ); + } + + if (child.exec) { + await ExecCommandExecutor.execute( + componentFolder, + child.exec, + ); + } else if (child.composite) { + await CompositeCommand.execute( + componentFolder, + child, + ); + } + } + } +} diff --git a/src/devfile/execCommand.ts b/src/devfile/execCommand.ts new file mode 100644 index 000000000..c9085ad65 --- /dev/null +++ b/src/devfile/execCommand.ts @@ -0,0 +1,110 @@ +/*----------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat, Inc. All rights reserved. + * Licensed under the MIT License. See LICENSE file in the project root for license information. + *-----------------------------------------------------------------------------------------------*/ + +import { ComponentWorkspaceFolder } from '../odo/workspace'; +import { Exec } from '../odo/componentTypeDescription'; +import { VariableResolver } from './variableResolver'; +import { Oc } from '../oc/ocWrapper'; +import { CommandText } from '../base/command'; +import { OpenShiftTerminalManager } from '../webview/openshift-terminal/openShiftTerminal'; +import { CommandResolver } from './commandResolver'; +import { CompositeCommand } from './compositeCommand'; +import { ParallelCompositeCommand } from './parallelCompositeCommand'; + +export class ExecCommandExecutor { + + public static async execute( + componentFolder: ComponentWorkspaceFolder, + exec: Exec, + ): Promise { + + const devfile = + componentFolder.component.devfileData.devfile; + + const resolvedExec = + VariableResolver.resolveExec( + devfile, + exec, + ); + + const componentName = + devfile.metadata.name; + + const podName = + await Oc.Instance.getComponentPod( + componentName, + ); + + const command = new CommandText( + 'oc', + `exec ${podName} -c ${resolvedExec.component} -- sh -c "cd ${resolvedExec.workingDir} && ${resolvedExec.commandLine}"`, + ); + + await OpenShiftTerminalManager + .getInstance() + .createTerminal( + command, + `Run '${resolvedExec.commandLine}'`, + componentFolder.contextPath, + ); + } +} + +export class DevfileCommandRunner { + + public static async execute( + componentFolder: ComponentWorkspaceFolder, + commandId: string, + ): Promise { + + const devfile = + componentFolder.component.devfileData.devfile; + + const command = + CommandResolver.getCommand( + devfile, + commandId, + ); + + if (command.exec) { + + await ExecCommandExecutor.execute( + componentFolder, + command.exec, + ); + + return; + } + + if (command.composite) { + + const isParallel = + (command.composite as { + parallel?: boolean; + }).parallel === true; + + if (isParallel) { + + await ParallelCompositeCommand.execute( + componentFolder, + command, + ); + + } else { + + await CompositeCommand.execute( + componentFolder, + command, + ); + } + + return; + } + + throw new Error( + `Unsupported command type '${commandId}'`, + ); + } +} diff --git a/src/devfile/parallelCompositeCommand.ts b/src/devfile/parallelCompositeCommand.ts new file mode 100644 index 000000000..89846e93a --- /dev/null +++ b/src/devfile/parallelCompositeCommand.ts @@ -0,0 +1,61 @@ +/*----------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat, Inc. All rights reserved. + * Licensed under the MIT License. See LICENSE file in the project root for license information. + *-----------------------------------------------------------------------------------------------*/ + +import { ComponentWorkspaceFolder } from '../odo/workspace'; +import { Command } from '../odo/componentTypeDescription'; +import { CommandResolver } from './commandResolver'; +import { CompositeCommand } from './compositeCommand'; +import { ExecCommandExecutor } from './execCommand'; + +export class ParallelCompositeCommand { + + public static async execute( + componentFolder: ComponentWorkspaceFolder, + command: Command, + ): Promise { + + const devfile = + componentFolder.component.devfileData.devfile; + + const commandMap = + CommandResolver.getAllCommandsMap( + devfile, + ); + + const promises: Promise[] = []; + + for (const childId of command.composite.commands) { + + const child = + commandMap.get( + childId.toLowerCase(), + ); + + if (!child) { + throw new Error( + `Command '${childId}' not found`, + ); + } + + if (child.exec) { + promises.push( + ExecCommandExecutor.execute( + componentFolder, + child.exec, + ), + ); + } else if (child.composite) { + promises.push( + CompositeCommand.execute( + componentFolder, + child, + ), + ); + } + } + + await Promise.all(promises); + } +} diff --git a/src/devfile/variableResolver.ts b/src/devfile/variableResolver.ts new file mode 100644 index 000000000..f0778e8ce --- /dev/null +++ b/src/devfile/variableResolver.ts @@ -0,0 +1,68 @@ +/*----------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat, Inc. All rights reserved. + * Licensed under the MIT License. See LICENSE file in the project root for license information. + *-----------------------------------------------------------------------------------------------*/ + +import { Container, Data, Exec } from '../odo/componentTypeDescription'; + +export class VariableResolver { + private static readonly VARIABLE_REGEX = /\$\{([^}]+)\}/g; + + public static resolveExec(devfile: Data, exec: Exec): Exec { + return { + ...exec, + workingDir: this.resolveValue(devfile, exec.workingDir ?? '/projects', exec.component), + commandLine: this.resolveValue(devfile, exec.commandLine, exec.component), + }; + } + + public static resolveValue(devfile: Data, value: string, componentName?: string): string { + if (!value) { + return value; + } + + return value.replace(this.VARIABLE_REGEX, (_, variable) => + this.resolveVariable(devfile, variable, componentName), + ); + } + + private static resolveVariable( + devfile: Data, + variable: string, + componentName?: string, + ): string { + if (variable === 'PROJECT_SOURCE') { + return '/projects'; + } + + if (componentName) { + const component = devfile.components.find((c) => c.name === componentName); + + const container = component?.container; + + const envValue = this.findEnvValue(container, variable); + + if (envValue) { + return envValue; + } + } + + return process.env[variable] ?? `\${${variable}}`; + } + + private static findEnvValue( + container: Container | undefined, + variable: string, + ): string | undefined { + const env = ( + container as unknown as { + env?: { + name: string; + value: string; + }[]; + } + )?.env; + + return env?.find((e) => e.name === variable)?.value; + } +} diff --git a/src/oc/ocWrapper.ts b/src/oc/ocWrapper.ts index ae372e21b..a85077ed4 100644 --- a/src/oc/ocWrapper.ts +++ b/src/oc/ocWrapper.ts @@ -1131,4 +1131,32 @@ export class Oc { await this.deleteOdoFiles(componentPath, componentName); } + + public async getComponentPod(componentName: string): Promise { + + const selectors = [ + `app.kubernetes.io/instance=${componentName}`, + `app.kubernetes.io/component=${componentName}`, + `component=${componentName}`, + `app=${componentName}` + ]; + + for (const selector of selectors) { + + const pods = + await this.getKubernetesObjects( + 'pods', + undefined, + selector + ); + + if (pods.length > 0) { + return pods[0].metadata.name as string; + } + } + + throw new Error( + `No running pod found for component '${componentName}'` + ); + } } diff --git a/src/odo/command.ts b/src/odo/command.ts index c406e157b..fd7192679 100644 --- a/src/odo/command.ts +++ b/src/odo/command.ts @@ -84,8 +84,4 @@ export class Command { } return cTxt; } - - static runComponentCommand(commandId : string): CommandText { - return new CommandText('odo', `run ${commandId}`); - } } diff --git a/src/openshift/component.ts b/src/openshift/component.ts index 07dd587c8..aeb8074a9 100644 --- a/src/openshift/component.ts +++ b/src/openshift/component.ts @@ -19,6 +19,7 @@ import { vsCommand, VsCommandError } from '../vscommand'; import CreateComponentLoader from '../webview/create-component/createComponentLoader'; import { OpenShiftTerminalApi, OpenShiftTerminalManager } from '../webview/openshift-terminal/openShiftTerminal'; import OpenShiftItem, { clusterRequired, projectRequired } from './openshiftItem'; +import { DevfileCommandRunner } from '../devfile/execCommand'; function createStartDebuggerResult(language: string, message = '') { const result: any = new String(message); @@ -593,20 +594,28 @@ export class Component extends OpenShiftItem { } } - @vsCommand('openshift.component.commands.command.run', true) + @vsCommand('openshift.component.commands.command.run', true) static runComponentCommand(componentFolder: ComponentWorkspaceFolder): Promise { const componentName = componentFolder.component.devfileData.devfile.metadata.name; if ('getCommand' in componentFolder) { const componentCommand = (componentFolder).getCommand(); - const command = Command.runComponentCommand(componentCommand.id); - void OpenShiftTerminalManager.getInstance().createTerminal( - command, - `Component ${componentName}: Run '${componentCommand.id}' Command`, - componentFolder.contextPath, + + if (!componentCommand) { + void window.showErrorMessage( + `No Command found in Component '${componentName}'`, + ); + return; + } + + void DevfileCommandRunner.execute( + componentFolder, + componentCommand.id, ); } else { - void window.showErrorMessage(`No Command found in Component '${componentName}`); + void window.showErrorMessage( + `No Command found in Component '${componentName}'`, + ); } return; - } + } } diff --git a/test/integration/command.test.ts b/test/integration/command.test.ts index 31237d3e2..9254c2a04 100644 --- a/test/integration/command.test.ts +++ b/test/integration/command.test.ts @@ -22,6 +22,8 @@ import { OdoPreference } from '../../src/odo/odoPreference'; import { Odo } from '../../src/odo/odoWrapper'; import { LoginUtil } from '../../src/util/loginUtil'; import { YAML_STRINGIFY_OPTIONS } from '../../src/util/utils'; +import { ComponentWorkspaceFolder } from 'src/odo/workspace'; +import { DevfileCommandRunner } from 'src/devfile/execCommand'; const ODO = Odo.Instance; @@ -290,34 +292,6 @@ suite('odo commands integration', function () { const helloWorldCommandOutput = 'Hello, World!'; const helloWorldCommandExecCommandLine = `echo "${helloWorldCommandOutput}"`; - async function runComponentCommandInTerminal(commandId: string, cwd?) : Promise { - let termOutput = ''; - let termError = ''; - const term = executeCommandInTerminal(Command.runComponentCommand(commandId), cwd, { - onOutput(data) { - termOutput = termOutput.concat(data); - }, - onError(data) { - termError = termError.concat(data); - } - }); - - let hopesLeft = 30; - let commandIdRunning = false; - do { - hopesLeft--; - await new Promise(resolve => setTimeout(resolve, 2000)); - commandIdRunning = termOutput.indexOf(helloWorldCommandOutput) >= -1; - } while (hopesLeft > 0 && !commandIdRunning); - if (!commandIdRunning) { - if (termError.trim().length > 0) { - fail(`Run Component Command failed: ${termError}`); - } - fail('Waiting for command to start executing is timed out'); - } - return term; - } - async function fixupDevFile(devfilePath: string): Promise { // Parse YAML into an Object, add: // @@ -366,7 +340,7 @@ suite('odo commands integration', function () { } test('runComponentCommand()', async function () { - await ODO.execute( + await ODO.execute( Command.createLocalComponent( componentType, '2.1.1', @@ -397,20 +371,40 @@ suite('odo commands integration', function () { } } if (!helloCommand) { - fail(`Command '${helloWorldCommandId}' doesn't exist in Component '${componentName}'`); + fail( + `Command '${helloWorldCommandId}' doesn't exist in Component '${componentName}'` + ); } - let devTerm : Terminal; - let runCommandTerm : Terminal; + let devTerm: Terminal; + try { - devTerm = await startDevInTerminal(componentLocation); - runCommandTerm = await runComponentCommandInTerminal(helloWorldCommandId, componentLocation); + + devTerm = + await startDevInTerminal( + componentLocation + ); + + const componentFolder: ComponentWorkspaceFolder = { + contextPath: componentLocation, + component: componentDescription + }; + + await DevfileCommandRunner.execute( + componentFolder, + helloWorldCommandId + ); + + } catch (err) { + + fail( + err instanceof Error + ? err.message + : String(err) + ); + } finally { - // we instruct the pseudo terminals to close the dev session when any text is sent - if (runCommandTerm) { - runCommandTerm.sendText('exit'); - runCommandTerm.dispose(); - } + if (devTerm) { devTerm.sendText('exit'); devTerm.dispose(); From 735b8a4fdbfdf7c4fba01d17f8e6b0eae365f48c Mon Sep 17 00:00:00 2001 From: msivasubramaniaan Date: Sun, 31 May 2026 00:25:45 +0530 Subject: [PATCH 08/11] resolved cyclic dependency --- src/devfile/compositeCommand.ts | 38 ++------- src/devfile/devfileCommandRunner.ts | 109 ++++++++++++++++++++++++ src/devfile/execCommand.ts | 60 ------------- src/devfile/parallelCompositeCommand.ts | 52 ++--------- src/odo/componentTypeDescription.ts | 1 + src/openshift/component.ts | 2 +- test/integration/command.test.ts | 2 +- 7 files changed, 126 insertions(+), 138 deletions(-) create mode 100644 src/devfile/devfileCommandRunner.ts diff --git a/src/devfile/compositeCommand.ts b/src/devfile/compositeCommand.ts index 893206e7d..186d329d2 100644 --- a/src/devfile/compositeCommand.ts +++ b/src/devfile/compositeCommand.ts @@ -5,8 +5,7 @@ import { ComponentWorkspaceFolder } from '../odo/workspace'; import { Command } from '../odo/componentTypeDescription'; -import { CommandResolver } from './commandResolver'; -import { ExecCommandExecutor } from './execCommand'; +import { DevfileCommandRunner } from './devfileCommandRunner'; export class CompositeCommand { @@ -15,38 +14,11 @@ export class CompositeCommand { command: Command, ): Promise { - const devfile = - componentFolder.component.devfileData.devfile; - - const commandMap = - CommandResolver.getAllCommandsMap( - devfile, - ); - for (const childId of command.composite.commands) { - - const child = - commandMap.get( - childId.toLowerCase(), - ); - - if (!child) { - throw new Error( - `Command '${childId}' not found`, - ); - } - - if (child.exec) { - await ExecCommandExecutor.execute( - componentFolder, - child.exec, - ); - } else if (child.composite) { - await CompositeCommand.execute( - componentFolder, - child, - ); - } + await DevfileCommandRunner.execute( + componentFolder, + childId, + ); } } } diff --git a/src/devfile/devfileCommandRunner.ts b/src/devfile/devfileCommandRunner.ts new file mode 100644 index 000000000..a555f787d --- /dev/null +++ b/src/devfile/devfileCommandRunner.ts @@ -0,0 +1,109 @@ +/*----------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat, Inc. All rights reserved. + * Licensed under the MIT License. See LICENSE file in the project root for license information. + *-----------------------------------------------------------------------------------------------*/ + +import { ComponentWorkspaceFolder } from '../odo/workspace'; +import { Command } from '../odo/componentTypeDescription'; +import { CommandResolver } from './commandResolver'; +import { ExecCommandExecutor } from './execCommand'; + +export class DevfileCommandRunner { + + public static async execute( + componentFolder: ComponentWorkspaceFolder, + commandId: string, + ): Promise { + + const devfile = + componentFolder.component.devfileData.devfile; + + const command = + CommandResolver.getCommand( + devfile, + commandId, + ); + + await this.executeCommand( + componentFolder, + command, + ); + } + + private static async executeCommand( + componentFolder: ComponentWorkspaceFolder, + command: Command, + ): Promise { + + if (command.exec) { + + await ExecCommandExecutor.execute( + componentFolder, + command.exec, + ); + + return; + } + + if (command.composite) { + + const devfile = + componentFolder.component.devfileData.devfile; + + const commandMap = + CommandResolver.getAllCommandsMap( + devfile, + ); + + const children = + command.composite.commands.map(id => { + + const child = + commandMap.get( + id.toLowerCase(), + ); + + if (!child) { + throw new Error( + `Command '${id}' not found`, + ); + } + + return child; + }); + + const isParallel = + (command.composite as { + parallel?: boolean; + }).parallel === true; + + if (isParallel) { + + await Promise.all( + children.map(child => + this.executeCommand( + componentFolder, + child, + ), + ), + ); + + } else { + + for (const child of children) { + + await this.executeCommand( + componentFolder, + child, + ); + } + } + + return; + } + + throw new Error( + `Unsupported command '${command.id}'`, + ); + } +} diff --git a/src/devfile/execCommand.ts b/src/devfile/execCommand.ts index c9085ad65..994adad33 100644 --- a/src/devfile/execCommand.ts +++ b/src/devfile/execCommand.ts @@ -9,9 +9,6 @@ import { VariableResolver } from './variableResolver'; import { Oc } from '../oc/ocWrapper'; import { CommandText } from '../base/command'; import { OpenShiftTerminalManager } from '../webview/openshift-terminal/openShiftTerminal'; -import { CommandResolver } from './commandResolver'; -import { CompositeCommand } from './compositeCommand'; -import { ParallelCompositeCommand } from './parallelCompositeCommand'; export class ExecCommandExecutor { @@ -51,60 +48,3 @@ export class ExecCommandExecutor { ); } } - -export class DevfileCommandRunner { - - public static async execute( - componentFolder: ComponentWorkspaceFolder, - commandId: string, - ): Promise { - - const devfile = - componentFolder.component.devfileData.devfile; - - const command = - CommandResolver.getCommand( - devfile, - commandId, - ); - - if (command.exec) { - - await ExecCommandExecutor.execute( - componentFolder, - command.exec, - ); - - return; - } - - if (command.composite) { - - const isParallel = - (command.composite as { - parallel?: boolean; - }).parallel === true; - - if (isParallel) { - - await ParallelCompositeCommand.execute( - componentFolder, - command, - ); - - } else { - - await CompositeCommand.execute( - componentFolder, - command, - ); - } - - return; - } - - throw new Error( - `Unsupported command type '${commandId}'`, - ); - } -} diff --git a/src/devfile/parallelCompositeCommand.ts b/src/devfile/parallelCompositeCommand.ts index 89846e93a..d03c67bad 100644 --- a/src/devfile/parallelCompositeCommand.ts +++ b/src/devfile/parallelCompositeCommand.ts @@ -3,11 +3,9 @@ * Licensed under the MIT License. See LICENSE file in the project root for license information. *-----------------------------------------------------------------------------------------------*/ -import { ComponentWorkspaceFolder } from '../odo/workspace'; +import { ComponentWorkspaceFolder } from 'src/odo/workspace'; import { Command } from '../odo/componentTypeDescription'; -import { CommandResolver } from './commandResolver'; -import { CompositeCommand } from './compositeCommand'; -import { ExecCommandExecutor } from './execCommand'; +import { DevfileCommandRunner } from './devfileCommandRunner'; export class ParallelCompositeCommand { @@ -16,46 +14,14 @@ export class ParallelCompositeCommand { command: Command, ): Promise { - const devfile = - componentFolder.component.devfileData.devfile; - - const commandMap = - CommandResolver.getAllCommandsMap( - devfile, - ); - - const promises: Promise[] = []; - - for (const childId of command.composite.commands) { - - const child = - commandMap.get( - childId.toLowerCase(), - ); - - if (!child) { - throw new Error( - `Command '${childId}' not found`, - ); - } - - if (child.exec) { - promises.push( - ExecCommandExecutor.execute( - componentFolder, - child.exec, - ), - ); - } else if (child.composite) { - promises.push( - CompositeCommand.execute( + await Promise.all( + command.composite.commands.map( + childId => + DevfileCommandRunner.execute( componentFolder, - child, + childId, ), - ); - } - } - - await Promise.all(promises); + ), + ); } } diff --git a/src/odo/componentTypeDescription.ts b/src/odo/componentTypeDescription.ts index 65d727fdf..68d2beedf 100644 --- a/src/odo/componentTypeDescription.ts +++ b/src/odo/componentTypeDescription.ts @@ -164,6 +164,7 @@ export interface Exec { export type Composite = { commands: string[]; + parallel?: boolean; group: Group; } diff --git a/src/openshift/component.ts b/src/openshift/component.ts index f63d3fb9e..627421fc2 100644 --- a/src/openshift/component.ts +++ b/src/openshift/component.ts @@ -20,7 +20,7 @@ import { vsCommand, VsCommandError } from '../vscommand'; import CreateComponentLoader from '../webview/create-component/createComponentLoader'; import { OpenShiftTerminalApi, OpenShiftTerminalManager } from '../webview/openshift-terminal/openShiftTerminal'; import OpenShiftItem, { clusterRequired, projectRequired } from './openshiftItem'; -import { DevfileCommandRunner } from '../devfile/execCommand'; +import { DevfileCommandRunner } from '../devfile/devfileCommandRunner'; function createStartDebuggerResult(language: string, message = '') { const result: any = new String(message); diff --git a/test/integration/command.test.ts b/test/integration/command.test.ts index a837ab1b0..a5299b3e3 100644 --- a/test/integration/command.test.ts +++ b/test/integration/command.test.ts @@ -22,7 +22,7 @@ import { Odo } from '../../src/odo/odoWrapper'; import { LoginUtil } from '../../src/util/loginUtil'; import { YAML_STRINGIFY_OPTIONS } from '../../src/util/utils'; import { ComponentWorkspaceFolder } from 'src/odo/workspace'; -import { DevfileCommandRunner } from 'src/devfile/execCommand'; +import { DevfileCommandRunner } from 'src/devfile/devfileCommandRunner'; const ODO = Odo.Instance; From 009f566f1cdae14fca0e63467959acefdc4ea3d6 Mon Sep 17 00:00:00 2001 From: msivasubramaniaan Date: Sun, 31 May 2026 00:36:39 +0530 Subject: [PATCH 09/11] fix test failure --- test/unit/openshift/component.test.ts | 92 ++++++++++++++++++++++++++- 1 file changed, 89 insertions(+), 3 deletions(-) diff --git a/test/unit/openshift/component.test.ts b/test/unit/openshift/component.test.ts index dbbd47ac8..eae2d1c1b 100644 --- a/test/unit/openshift/component.test.ts +++ b/test/unit/openshift/component.test.ts @@ -25,7 +25,7 @@ import * as openShiftComponent from '../../../src/openshift/component'; import { Util } from '../../../src/util/async'; import { Util as fsp } from '../../../src/util/utils'; import { OpenShiftTerminalManager } from '../../../src/webview/openshift-terminal/openShiftTerminal'; -import { comp1Folder } from '../../fixtures'; +import { comp1Folder, comp2Folder } from '../../fixtures'; const { expect } = chai; chai.use(sinonChai); @@ -125,6 +125,92 @@ suite('OpenShift/Component', function () { devForwardedPorts: [] } }; + const componentItem2: ComponentWorkspaceFolder = { + contextPath: comp2Folder, + component: { + devfilePath: `${path.join(fixtureFolder, 'components', 'comp2', 'devfile.yaml')}`, + devfileData: { + devfile: { + schemaVersion: '2.1.0', + metadata: { + name: 'comp2', + version: '2.0.1', + displayName: 'React', + description: 'React is a free and open-source front-end JavaScript library for building user interfaces based on UI components. It is maintained by Meta and a community of individual developers and companies.', + tags: [ + 'Node.js', + 'React' + ], + icon: 'https://raw.githubusercontent.com/devfile-samples/devfile-stack-icons/main/react.svg', + projectType: 'React', + language: 'Typescript', + }, + parent: null, + starterProjects: [ + { + name: 'nodejs-react-starter' + } + ], + components: [ + { + name: 'runtime', + container: { + image: 'registry.access.redhat.com/ubi8/nodejs-16:latest', + memoryLimit: '1024Mi', + endpoints: [ + { + name: 'http-react', + targetPort: 3000 + } + ], + mountSources: false, + volumeMounts: [], + } + } + ], + commands: [ + { + id: 'install', + exec: { + group: { + kind: 'build', + isDefault: true + }, + commandLine: 'npm install', + component: 'runtime', + workingDir: '${PROJECT_SOURCE}' + } + }, + { + id: 'run', + exec: { + group: { + kind: 'run', + isDefault: true + }, + commandLine: 'npm run dev', + component: 'runtime', + workingDir: '${PROJECT_SOURCE}' + } + } + ], + events: { + postStart: [] + } + }, + commands: [], + supportedOdoFeatures: { + debug: true, + deploy: true, + dev: true + } + }, + runningIn: null, + runningOn: null, + managedBy: 'odo', + devForwardedPorts: [] + } + }; let Component: typeof openShiftComponent.Component; let commandStub: sinon.SinonStub; @@ -273,14 +359,14 @@ suite('OpenShift/Component', function () { test('calls the correct odo command', async function () { // As `odo describe` has removed, now we need to check the OpenShift terminal output // instead of stubbing 'executeInTerminal' expecting the command to be invoked on the terminal - const compName = componentItem1.component.devfileData.devfile.metadata.name + const compName = componentItem2.component.devfileData.devfile.metadata.name const writeStub = sinon.stub(); sinon.stub(OpenShiftTerminalManager, 'getInstance').returns({ writeToTerminal: writeStub } as any); - await Component.describe(componentItem1); + await Component.describe(componentItem2); const output = writeStub.firstCall.args[0]; expect(writeStub).calledOnce; From ac44a2551c07488ae1b188dec40c19658fc71117 Mon Sep 17 00:00:00 2001 From: msivasubramaniaan Date: Sun, 31 May 2026 01:48:25 +0530 Subject: [PATCH 10/11] fixed test failure --- test/integration/command.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/command.test.ts b/test/integration/command.test.ts index a5299b3e3..86bc4fe84 100644 --- a/test/integration/command.test.ts +++ b/test/integration/command.test.ts @@ -22,7 +22,7 @@ import { Odo } from '../../src/odo/odoWrapper'; import { LoginUtil } from '../../src/util/loginUtil'; import { YAML_STRINGIFY_OPTIONS } from '../../src/util/utils'; import { ComponentWorkspaceFolder } from 'src/odo/workspace'; -import { DevfileCommandRunner } from 'src/devfile/devfileCommandRunner'; +import { DevfileCommandRunner } from '../../src/devfile/devfileCommandRunner'; const ODO = Odo.Instance; From 7bfb97e55dac871acd4298162828c9533ddb1ca6 Mon Sep 17 00:00:00 2001 From: msivasubramaniaan Date: Sun, 31 May 2026 11:42:51 +0530 Subject: [PATCH 11/11] fixed UI test case --- src/devfile/execCommand.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/devfile/execCommand.ts b/src/devfile/execCommand.ts index 994adad33..4df5a82da 100644 --- a/src/devfile/execCommand.ts +++ b/src/devfile/execCommand.ts @@ -39,12 +39,10 @@ export class ExecCommandExecutor { `exec ${podName} -c ${resolvedExec.component} -- sh -c "cd ${resolvedExec.workingDir} && ${resolvedExec.commandLine}"`, ); - await OpenShiftTerminalManager - .getInstance() - .createTerminal( - command, - `Run '${resolvedExec.commandLine}'`, - componentFolder.contextPath, - ); + void OpenShiftTerminalManager.getInstance().createTerminal( + command, + `Component ${componentName}: Run '${resolvedExec.commandLine}' Command`, + componentFolder.contextPath + ); } }