@@ -14,6 +14,8 @@ import { CliExitData } from '../util/childProcessUtil';
1414import { isOpenShiftCluster , KubeConfigInfo , loadKubeConfig , serializeKubeConfig } from '../util/kubeUtils' ;
1515import { Project } from './project' ;
1616import { 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}
0 commit comments