diff --git a/src/kindhandlers/ConfigMap.handler.ts b/src/kindhandlers/ConfigMap.handler.ts index 1f08f9f4ce..03da00062d 100644 --- a/src/kindhandlers/ConfigMap.handler.ts +++ b/src/kindhandlers/ConfigMap.handler.ts @@ -1,6 +1,7 @@ import * as k8s from '@kubernetes/client-node'; import {ResourceKindHandler} from '@models/resourcekindhandler'; import {NAV_K8S_RESOURCES, SECTION_CONFIGURATION} from '@constants/navigator'; +import {K8sResource} from '@models/k8sresource'; const ConfigMapHandler: ResourceKindHandler = { kind: 'ConfigMap', @@ -18,6 +19,10 @@ const ConfigMapHandler: ResourceKindHandler = { const response = await k8sCoreV1Api.listConfigMapForAllNamespaces(); return response.body.items; }, + async deleteResourceInCluster(kubeconfig: k8s.KubeConfig, resource: K8sResource) { + const k8sCoreV1Api = kubeconfig.makeApiClient(k8s.CoreV1Api); + await k8sCoreV1Api.deleteNamespacedConfigMap(resource.name, resource.namespace || 'default'); + }, }; export default ConfigMapHandler; diff --git a/src/models/resourcekindhandler.ts b/src/models/resourcekindhandler.ts index ad3c4cc21e..d39a7bdd1f 100644 --- a/src/models/resourcekindhandler.ts +++ b/src/models/resourcekindhandler.ts @@ -1,5 +1,6 @@ import * as k8s from '@kubernetes/client-node'; import {monaco} from 'react-monaco-editor'; +import {K8sResource} from './k8sresource'; interface SymbolMatcher { isMatch?(symbols: monaco.languages.DocumentSymbol[]): boolean; @@ -53,6 +54,8 @@ interface ResourceKindHandler { listResourcesInCluster(kubeconfig: k8s.KubeConfig): Promise; + deleteResourceInCluster?: (kubeconfig: k8s.KubeConfig, resource: K8sResource) => Promise; + /** * optional outgoing RefMappers to use for resolving refs in resources of this type */ diff --git a/src/redux/reducers/main.ts b/src/redux/reducers/main.ts index 450aef72d4..55b5cfca98 100644 --- a/src/redux/reducers/main.ts +++ b/src/redux/reducers/main.ts @@ -5,6 +5,7 @@ import {PREVIEW_PREFIX, ROOT_FILE_ENTRY} from '@constants/constants'; import {AppConfig} from '@models/appconfig'; import {AppState, FileMapType, HelmChartMapType, HelmValuesMapType, ResourceMapType} from '@models/appstate'; import {parseDocument} from 'yaml'; +import * as k8s from '@kubernetes/client-node'; import fs from 'fs'; import {previewKustomization} from '@redux/thunks/previewKustomization'; import {previewCluster, repreviewCluster} from '@redux/thunks/previewCluster'; @@ -16,6 +17,7 @@ import {saveUnsavedResource} from '@redux/thunks/saveUnsavedResource'; import {resetSelectionHistory} from '@redux/services/selectionHistory'; import {K8sResource} from '@models/k8sresource'; import {AlertType} from '@models/alert'; +import {getResourceKindHandler} from '@src/kindhandlers'; import initialState from '../initialState'; import {clearResourceSelections, highlightChildrenResources, updateSelectionAndHighlights} from '../services/selection'; import { @@ -31,6 +33,7 @@ import { extractK8sResources, isFileResource, recalculateResourceRanges, + removeResourceFromFile, reprocessResources, saveResource, } from '../services/resource'; @@ -201,6 +204,31 @@ export const mainSlice = createSlice({ return original(state); } }, + removeResource: (state: Draft, action: PayloadAction) => { + const resourceId = action.payload; + const resource = state.resourceMap[resourceId]; + if (!resource) { + return; + } + if (isFileResource(resource)) { + removeResourceFromFile(resource, state.fileMap, state.resourceMap); + return; + } + if (state.previewType === 'cluster' && state.previewResourceId) { + try { + const kubeConfig = new k8s.KubeConfig(); + kubeConfig.loadFromFile(state.previewResourceId); + const kindHandler = getResourceKindHandler(resource.kind); + if (kindHandler?.deleteResourceInCluster) { + kindHandler.deleteResourceInCluster(kubeConfig, resource); + delete state.resourceMap[resource.id]; + } + } catch (err) { + log.error(err); + return original(state); + } + } + }, /** * Marks the specified resource as selected and highlights all related resources */ @@ -452,5 +480,6 @@ export const { clearPreviewAndSelectionHistory, startPreviewLoader, stopPreviewLoader, + removeResource, } = mainSlice.actions; export default mainSlice.reducer; diff --git a/src/redux/services/fileEntry.ts b/src/redux/services/fileEntry.ts index f8058bc227..44e469f885 100644 --- a/src/redux/services/fileEntry.ts +++ b/src/redux/services/fileEntry.ts @@ -380,13 +380,15 @@ export function addPath(absolutePath: string, state: AppState, appConfig: AppCon * Removes the specified fileEntry and its resources from the provided state */ -function removeFile(fileEntry: FileEntry, state: AppState, removalSideEffect: PathRemovalSideEffect) { +export function removeFile(fileEntry: FileEntry, state: AppState, removalSideEffect?: PathRemovalSideEffect) { log.info(`removing file ${fileEntry.filePath}`); getResourcesForPath(fileEntry.filePath, state.resourceMap).forEach(resource => { if (state.selectedResourceId === resource.id) { updateSelectionAndHighlights(state, resource); } - removalSideEffect.removedResources.push(resource); + if (removalSideEffect) { + removalSideEffect.removedResources.push(resource); + } delete state.resourceMap[resource.id]; }); } @@ -395,7 +397,7 @@ function removeFile(fileEntry: FileEntry, state: AppState, removalSideEffect: Pa * Removes the specified fileEntry and its resources from the provided state */ -function removeFolder(fileEntry: FileEntry, state: AppState, removalSideEffect: PathRemovalSideEffect) { +function removeFolder(fileEntry: FileEntry, state: AppState, removalSideEffect?: PathRemovalSideEffect) { log.info(`removing folder ${fileEntry.filePath}`); fileEntry.children?.forEach(child => { const childEntry = state.fileMap[path.join(fileEntry.filePath, child)]; diff --git a/src/redux/services/resource.ts b/src/redux/services/resource.ts index ba3f851513..4172dc7d35 100644 --- a/src/redux/services/resource.ts +++ b/src/redux/services/resource.ts @@ -412,6 +412,59 @@ export function recalculateResourceRanges(resource: K8sResource, state: AppState } } +export function removeResourceFromFile( + removedResource: K8sResource, + fileMap: FileMapType, + resourceMap: ResourceMapType +) { + const fileEntry = fileMap[removedResource.filePath]; + if (!fileEntry) { + throw new Error(`Failed to find fileEntry for resource with path ${removedResource.filePath}`); + } + const absoluteFilePath = getAbsoluteResourcePath(removedResource, fileMap); + + if (!removedResource.range) { + fs.unlinkSync(absoluteFilePath); + return; + } + + // get list of resourceIds in file sorted by startPosition + const resourceIds = getResourcesForPath(removedResource.filePath, resourceMap) + .sort((a, b) => { + return a.range && b.range ? a.range.start - b.range.start : 0; + }) + .map(r => r.id); + + // recalculate ranges for resources below the removed resource + let newRangeStart = 0; + let passedRemovedResource = false; + resourceIds.forEach(resourceId => { + const resource = resourceMap[resourceId]; + if (resourceId === removedResource.id) { + passedRemovedResource = true; + newRangeStart = resource.range?.start || newRangeStart; + return; + } + if (!passedRemovedResource) { + return; + } + if (resource.range) { + resource.range.start = newRangeStart; + newRangeStart = resource.range.start + resource.range.length; + } + }); + + const content = fs.readFileSync(absoluteFilePath, 'utf8'); + fs.writeFileSync( + absoluteFilePath, + content.substr(0, removedResource.range.start) + + content.substr(removedResource.range.start + removedResource.range.length) + ); + fileEntry.timestamp = fs.statSync(absoluteFilePath).mtime.getTime(); + + delete resourceMap[removedResource.id]; +} + /** * Extracts all resources from the specified text content (must be yaml) */