Skip to content

Commit 6a7a658

Browse files
replaced odo delete component configuration (#5772)
* replaced odo delete component configuration * fixed test file * moved the functions from odo to oc * fixed test case failure * fixed test failures * fixing the import
1 parent 504b698 commit 6a7a658

6 files changed

Lines changed: 305 additions & 27 deletions

File tree

src/oc/devfileUtils.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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+
import * as fs from 'fs/promises';
6+
import * as path from 'path';
7+
import * as yaml from 'yaml';
8+
9+
export type DevfileInfo = {
10+
path: string;
11+
name: string;
12+
};
13+
14+
export async function parseDevfile(filePath: string): Promise<DevfileInfo | undefined> {
15+
try {
16+
const content = await fs.readFile(filePath, 'utf-8');
17+
const parsed = yaml.parse(content);
18+
19+
if (parsed && typeof parsed === 'object' && parsed.schemaVersion && parsed.metadata?.name) {
20+
return {
21+
path: filePath,
22+
name: parsed.metadata.name,
23+
};
24+
}
25+
} catch {
26+
// ignore invalid YAML
27+
}
28+
29+
return undefined;
30+
}
31+
32+
export async function findDevfiles(componentDir: string): Promise<DevfileInfo[]> {
33+
const files = await fs.readdir(componentDir);
34+
35+
const results = await Promise.all(
36+
files
37+
.filter((f) => f.endsWith('.yaml') || f.endsWith('.yml'))
38+
.map((f) => parseDevfile(path.join(componentDir, f))),
39+
);
40+
41+
return results.filter(Boolean) as DevfileInfo[];
42+
}
43+
44+
export async function getComponentName(componentDir: string): Promise<string | undefined> {
45+
const devfiles = await findDevfiles(componentDir);
46+
return devfiles[0]?.name;
47+
}

src/oc/ocWrapper.ts

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import { CliExitData } from '../util/childProcessUtil';
1414
import { isOpenShiftCluster, KubeConfigInfo, loadKubeConfig, serializeKubeConfig } from '../util/kubeUtils';
1515
import { Project } from './project';
1616
import { ClusterType, KubernetesConsole } from './types';
17+
import { findDevfiles, getComponentName } from './devfileUtils';
18+
import path from 'path';
1719

1820
/**
1921
* A wrapper around the `oc` CLI tool.
@@ -895,4 +897,238 @@ export class Oc {
895897
undefined, true, config);
896898
return result.stdout;
897899
}
900+
901+
async devWorkspaceExists(componentName: string): Promise<boolean> {
902+
try {
903+
const result = await CliChannel.getInstance().executeTool(
904+
new CommandText('oc', 'get', [
905+
new CommandOption('devworkspace'),
906+
new CommandOption(componentName),
907+
new CommandOption('-o'),
908+
new CommandOption('name'),
909+
]),
910+
);
911+
912+
return result?.stdout?.includes('devworkspace');
913+
} catch {
914+
return false;
915+
}
916+
}
917+
918+
async deleteOdoFiles(componentDir: string, componentName?: string): Promise<void> {
919+
const devfiles = await findDevfiles(componentDir);
920+
921+
for (const devfile of devfiles) {
922+
if (!componentName || devfile.name === componentName) {
923+
await fs.rm(devfile.path, { force: true });
924+
}
925+
}
926+
927+
// Delete .odo directory
928+
await fs.rm(path.join(componentDir, '.odo'), {
929+
recursive: true,
930+
force: true,
931+
});
932+
}
933+
934+
async findDevWorkspaceByLabel(componentName: string): Promise<string | null> {
935+
try {
936+
const result = await CliChannel.getInstance().executeTool(
937+
new CommandText('oc', 'get', [
938+
new CommandOption('devworkspace'),
939+
new CommandOption('-l'),
940+
new CommandOption(`app.kubernetes.io/component=${componentName}`),
941+
new CommandOption('-o'),
942+
new CommandOption('jsonpath={.items[0].metadata.name}'),
943+
]),
944+
);
945+
const name = result?.stdout?.trim();
946+
return name || null;
947+
}
948+
catch {
949+
return null;
950+
}
951+
952+
}
953+
/**
954+
* Deletes all the odo configuration files associated with the component (`.odo`, `devfile.yaml`) located at the given path.
955+
*
956+
* @param componentPath the path to the component
957+
*/
958+
public async deleteComponentConfiguration(componentPath: string): Promise<void> {
959+
const componentName = await getComponentName(componentPath);
960+
961+
if (!componentName) {
962+
throw new Error('Component name is missing. Cannot delete resources safely.');
963+
}
964+
965+
const cli = CliChannel.getInstance();
966+
967+
/**
968+
* Try to delete the DevWorkspace resource with the component label -
969+
* this should trigger the cleanup of all associated resources by the controller and is the safest way to delete a component.
970+
* If this fails (e.g. due to RBAC issues), we will try to delete resources by label in the next steps
971+
*/
972+
try {
973+
await cli.executeTool(
974+
new CommandText('oc', 'delete', [
975+
new CommandOption('devworkspace'),
976+
new CommandOption(componentName),
977+
new CommandOption('--ignore-not-found'),
978+
]),
979+
);
980+
} catch {
981+
// Ignore RBAC / not found
982+
}
983+
984+
/**
985+
* Get all pods with the component label to find dynamic labels (like devworkspace_id) that we can use for safe deletion
986+
*/
987+
let podJson: any = {};
988+
try {
989+
const podResult = await cli.executeTool(
990+
new CommandText('oc', 'get', [
991+
new CommandOption('pod'),
992+
new CommandOption('-l'),
993+
new CommandOption(`app.kubernetes.io/instance=${componentName}`),
994+
new CommandOption('-o'),
995+
new CommandOption('json'),
996+
]),
997+
);
998+
999+
podJson = JSON.parse(podResult.stdout || '{}');
1000+
} catch {
1001+
podJson = {};
1002+
}
1003+
1004+
const items = podJson.items || [];
1005+
1006+
/**
1007+
* Collect unique devworkspace IDs and instance labels from the pods to build label selectors for deletion.
1008+
*/
1009+
const devworkspaceIds = new Set<string>();
1010+
const instanceLabels = new Set<string>();
1011+
1012+
for (const item of items) {
1013+
const labels = item?.metadata?.labels || {};
1014+
1015+
if (labels['controller.devfile.io/devworkspace_id']) {
1016+
devworkspaceIds.add(labels['controller.devfile.io/devworkspace_id']);
1017+
}
1018+
1019+
if (labels['app.kubernetes.io/instance']) {
1020+
instanceLabels.add(labels['app.kubernetes.io/instance']);
1021+
}
1022+
}
1023+
1024+
instanceLabels.add(componentName);
1025+
1026+
/**
1027+
* Build label selectors for deletion based on collected labels. We will use these selectors to delete resources in the next steps,
1028+
* ensuring we only target resources associated with our component.
1029+
*/
1030+
const selectors: string[] = [];
1031+
1032+
instanceLabels.forEach((val) => {
1033+
selectors.push(`app.kubernetes.io/instance=${val}`);
1034+
selectors.push(`component=${val}`);
1035+
selectors.push(`app=${val}`);
1036+
});
1037+
1038+
/**
1039+
* Delete all resources associated with the component using the built label selectors.
1040+
*/
1041+
try {
1042+
for (const selector of selectors) {
1043+
await cli.executeTool(
1044+
new CommandText('oc', 'delete', [
1045+
new CommandOption('all'),
1046+
new CommandOption('-l'),
1047+
new CommandOption(selector),
1048+
new CommandOption('--ignore-not-found'),
1049+
]),
1050+
);
1051+
}
1052+
} catch {
1053+
// Ignore errors
1054+
}
1055+
1056+
/**
1057+
* To ensure we cover resources that might not have the common labels but are still associated with the component
1058+
* (like ConfigMaps, Secrets, PVCs, Routes, etc.), we will also attempt to delete these resources using the same label selectors.
1059+
* This is a safety net to catch any resources that might have been missed in the previous step.
1060+
*/
1061+
const extraResources = [
1062+
'configmap',
1063+
'secret',
1064+
'pvc',
1065+
'route', // OpenShift
1066+
'ingress',
1067+
'serviceaccount',
1068+
'role',
1069+
'rolebinding',
1070+
];
1071+
1072+
try {
1073+
for (const resource of extraResources) {
1074+
await cli.executeTool(
1075+
new CommandText('oc', 'delete', [
1076+
new CommandOption('-l'),
1077+
new CommandOption(resource),
1078+
new CommandOption('--ignore-not-found'),
1079+
]),
1080+
);
1081+
}
1082+
} catch {
1083+
// Ignore errors
1084+
}
1085+
1086+
/**
1087+
* Delete all resources associated with each DevWorkspace ID.
1088+
*/
1089+
try {
1090+
for (const dwId of devworkspaceIds) {
1091+
const dwSelector = `controller.devfile.io/devworkspace_id=${dwId}`;
1092+
1093+
await cli.executeTool(
1094+
new CommandText('oc', 'delete', [
1095+
new CommandOption('all'),
1096+
new CommandOption('-l'),
1097+
new CommandOption(dwSelector),
1098+
new CommandOption('--ignore-not-found'),
1099+
]),
1100+
);
1101+
1102+
await cli.executeTool(
1103+
new CommandText('oc', 'delete', [
1104+
new CommandOption('route'),
1105+
new CommandOption('-l'),
1106+
new CommandOption(dwSelector),
1107+
new CommandOption('--ignore-not-found'),
1108+
]),
1109+
);
1110+
}
1111+
} catch {
1112+
// Ignore errors
1113+
}
1114+
1115+
/**
1116+
* As a final safety measure, we will also attempt to delete any remaining resources that have the component instance label,
1117+
* even if they don't have the other common labels.
1118+
*/
1119+
try {
1120+
await cli.executeTool(
1121+
new CommandText('oc', 'delete', [
1122+
new CommandOption('all'),
1123+
new CommandOption('--selector'),
1124+
new CommandOption(`app.kubernetes.io/instance=${componentName}`),
1125+
new CommandOption('--ignore-not-found'),
1126+
]),
1127+
);
1128+
} catch {
1129+
// ignore
1130+
}
1131+
1132+
await this.deleteOdoFiles(componentPath, componentName);
1133+
}
8981134
}

src/odo/odoWrapper.ts

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*-----------------------------------------------------------------------------------------------*/
55

66
import { Uri, WorkspaceFolder, workspace } from 'vscode';
7-
import { CommandOption, CommandText } from '../base/command';
7+
import { CommandText } from '../base/command';
88
import * as cliInstance from '../cli';
99
import { ToolsConfig } from '../tools';
1010
import { ChildProcessUtil, CliExitData } from '../util/childProcessUtil';
@@ -80,7 +80,7 @@ export class Odo {
8080
location: Uri,
8181
starter: string = undefined,
8282
useExistingDevfile = false,
83-
customDevfilePath = ''
83+
customDevfilePath = '',
8484
): Promise<void> {
8585
await this.execute(
8686
Command.createLocalComponent(
@@ -91,7 +91,7 @@ export class Odo {
9191
undefined,
9292
starter,
9393
useExistingDevfile,
94-
customDevfilePath
94+
customDevfilePath,
9595
),
9696
location.fsPath,
9797
);
@@ -134,7 +134,7 @@ export class Odo {
134134
portNumber,
135135
undefined,
136136
false,
137-
''
137+
'',
138138
),
139139
location.fsPath,
140140
);
@@ -173,18 +173,4 @@ export class Odo {
173173
);
174174
}
175175

176-
/**
177-
* Deletes all the odo configuration files associated with the component (`.odo`, `devfile.yaml`) located at the given path.
178-
*
179-
* @param componentPath the path to the component
180-
*/
181-
public async deleteComponentConfiguration(componentPath: string): Promise<void> {
182-
await this.execute(
183-
new CommandText('odo', 'delete component', [
184-
new CommandOption('--files'),
185-
new CommandOption('-f'),
186-
]),
187-
componentPath,
188-
);
189-
}
190176
}

src/openshift/component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -568,7 +568,7 @@ export class Component extends OpenShiftItem {
568568
const CANCEL = 'Cancel';
569569
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);
570570
if (response === DELETE_CONFIGURATION) {
571-
await Odo.Instance.deleteComponentConfiguration(context.contextPath);
571+
await Oc.Instance.deleteComponentConfiguration(context.contextPath);
572572
void commands.executeCommand('openshift.componentsView.refresh');
573573
}
574574
}

test/integration/odoWrapper.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ suite('./odo/odoWrapper.ts', function () {
168168
});
169169

170170
test('deleteComponentConfiguration()', async function() {
171-
await Odo.Instance.deleteComponentConfiguration(tmpFolder);
171+
await Oc.Instance.deleteComponentConfiguration(tmpFolder);
172172
try {
173173
await fs.access(path.join(tmpFolder, 'devfile.yaml'));
174174
fail('devfile.yaml should have been deleted')

0 commit comments

Comments
 (0)