Skip to content

Commit 37e329f

Browse files
feat: Delete for community modules (#3957)
* Add initial changes for delete * Checking if index is from community * Checking if is it community module in DeleteBox * Add additional checking while deleting for community * Add useful comments * Add backend for fetching yaml * Add creating urls and delting resources * Check link * Move community enpoint to modules * Make separate functions for delete * Check if url is safe * Show catched error * Code correction added * Test deleting * Remove unusuful import from test * Remove console.log
1 parent 874da8e commit 37e329f

File tree

9 files changed

+323
-73
lines changed

9 files changed

+323
-73
lines changed

backend/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { makeHandleRequest, serveStaticApp, serveMonaco } from './common';
22
import { handleTracking } from './tracking.js';
33
import { proxyHandler, proxyRateLimiter } from './proxy.js';
44
import companionRouter from './companion/companionRouter';
5+
import communityRouter from './modules/communityRouter';
56
//import { requestLogger } from './utils/other'; //uncomment this to log the outgoing traffic
67

78
const express = require('express');
@@ -73,12 +74,14 @@ if (isDocker) {
7374
// yup, order matters here
7475
serveMonaco(app);
7576
app.use('/backend/ai-chat', companionRouter);
77+
app.use('/backend/modules', communityRouter);
7678
app.use('/backend', handleRequest);
7779
serveStaticApp(app, '/', '/core-ui');
7880
} else {
7981
// Running in prod mode
8082
handleTracking(app);
8183
app.use('/backend/ai-chat', companionRouter);
84+
app.use('/backend/modules', communityRouter);
8285
app.use('/backend', handleRequest);
8386
}
8487

backend/modules/communityRouter.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import express from 'express';
2+
import cors from 'cors';
3+
import jsyaml from 'js-yaml';
4+
5+
const router = express.Router();
6+
router.use(express.json());
7+
router.use(cors());
8+
9+
async function handleGetCommunityResource(req, res) {
10+
const { link } = JSON.parse(req.body.toString());
11+
12+
// Validate that link is a string and a valid HTTPS URL, and restrict to allowed domains.
13+
if (typeof link !== 'string') {
14+
return res.status(400).json('Link must be a string.');
15+
}
16+
17+
try {
18+
const url = new URL(link);
19+
// Only allow HTTPS protocol and restrict to specific trusted domains.
20+
const allowedDomains = ['github.com'];
21+
if (
22+
url.protocol !== 'https:' ||
23+
!allowedDomains.some(domain => url.hostname.endsWith(domain))
24+
) {
25+
return res.status(400).json('Invalid or untrusted link provided.');
26+
} else {
27+
const response = await fetch(link);
28+
const data = await response.text();
29+
res.json(jsyaml.loadAll(data));
30+
}
31+
} catch (error) {
32+
res.status(500).json(`Failed to fetch community resource. ${error}`);
33+
}
34+
}
35+
36+
router.post('/community-resource', handleGetCommunityResource);
37+
38+
export default router;

src/components/KymaModules/KymaModulesList.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export default function KymaModulesList({ namespaced }) {
4141
installedCommunityModules,
4242
communityModulesLoading,
4343
setOpenedModuleIndex: setOpenedCommunityModuleIndex,
44+
handleResourceDelete: handleCommunityModuleDelete,
4445
} = useContext(CommunityModuleContext);
4546

4647
const [selectedEntry, setSelectedEntry] = useState(null);
@@ -94,7 +95,7 @@ export default function KymaModulesList({ namespaced }) {
9495
modulesLoading={communityModulesLoading}
9596
namespaced={namespaced}
9697
setOpenedModuleIndex={setOpenedCommunityModuleIndex}
97-
handleResourceDelete={handleResourceDelete}
98+
handleResourceDelete={handleCommunityModuleDelete}
9899
customSelectedEntry={selectedEntry}
99100
setSelectedEntry={setSelectedEntry}
100101
/>

src/components/KymaModules/components/ModulesDeleteBox.tsx

Lines changed: 92 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import {
1414
fetchResourceCounts,
1515
generateAssociatedResourcesUrls,
1616
getAssociatedResources,
17+
getCommunityResourceUrls,
18+
getCommunityResources,
1719
getCRResource,
1820
handleItemClick,
1921
} from '../deleteModulesHelpers';
@@ -26,15 +28,17 @@ import { cloneDeep } from 'lodash';
2628
import { KymaResourceType, ModuleTemplateListType } from '../support';
2729
import { SetterOrUpdater } from 'recoil';
2830
import { ColumnLayoutState } from 'state/columnLayoutAtom';
31+
import { usePost } from 'shared/hooks/BackendAPI/usePost';
2932

3033
type ModulesListDeleteBoxProps = {
3134
DeleteMessageBox: React.FC<any>;
3235
moduleTemplates: ModuleTemplateListType;
3336
selectedModules: { name: string }[];
3437
chosenModuleIndex: number | null;
3538
kymaResource: KymaResourceType;
36-
kymaResourceState: KymaResourceType;
39+
kymaResourceState?: KymaResourceType;
3740
detailsOpen: boolean;
41+
isCommunity?: boolean;
3842
setLayoutColumn: SetterOrUpdater<ColumnLayoutState>;
3943
handleModuleUninstall: () => void;
4044
setChosenModuleIndex: React.Dispatch<React.SetStateAction<number | null>>;
@@ -50,6 +54,7 @@ export const ModulesDeleteBox = ({
5054
kymaResource,
5155
kymaResourceState,
5256
detailsOpen,
57+
isCommunity,
5358
setLayoutColumn,
5459
handleModuleUninstall,
5560
setChosenModuleIndex,
@@ -62,10 +67,14 @@ export const ModulesDeleteBox = ({
6267
const { clusterUrl, namespaceUrl } = useUrl();
6368
const deleteResourceMutation = useDelete();
6469
const fetchFn = useSingleGet();
70+
const post = usePost();
6571

6672
const [resourceCounts, setResourceCounts] = useState<Record<string, any>>({});
6773
const [forceDeleteUrls, setForceDeleteUrls] = useState<string[]>([]);
6874
const [crUrls, setCrUrls] = useState<string[]>([]);
75+
const [communityResourcesUrls, setCommunityResourcesUrls] = useState<
76+
string[]
77+
>([]);
6978
const [allowForceDelete, setAllowForceDelete] = useState(false);
7079
const [associatedResourceLeft, setAssociatedResourceLeft] = useState(false);
7180

@@ -99,16 +108,31 @@ export const ModulesDeleteBox = ({
99108
selectedModules,
100109
kymaResource,
101110
moduleTemplates,
111+
isCommunity,
102112
);
103113

104-
const crUrl = await generateAssociatedResourcesUrls(
105-
crUResources,
106-
fetchFn,
107-
clusterUrl,
108-
getScope,
109-
namespaceUrl,
110-
navigate,
111-
);
114+
const crUrl = isCommunity
115+
? getCommunityResourceUrls(crUResources)
116+
: await generateAssociatedResourcesUrls(
117+
crUResources,
118+
fetchFn,
119+
clusterUrl,
120+
getScope,
121+
namespaceUrl,
122+
navigate,
123+
);
124+
125+
if (isCommunity) {
126+
const communityResources = await getCommunityResources(
127+
chosenModuleIndex,
128+
selectedModules,
129+
kymaResource,
130+
moduleTemplates,
131+
post,
132+
);
133+
const communityUrls = getCommunityResourceUrls(communityResources);
134+
setCommunityResourcesUrls(communityUrls);
135+
}
112136

113137
setResourceCounts(counts);
114138
setForceDeleteUrls(urls);
@@ -130,6 +154,61 @@ export const ModulesDeleteBox = ({
130154
// eslint-disable-next-line react-hooks/exhaustive-deps
131155
}, [resourceCounts, associatedResources]);
132156

157+
const deleteAllResources = () => {
158+
if (allowForceDelete && forceDeleteUrls.length > 0) {
159+
deleteAssociatedResources(deleteResourceMutation, forceDeleteUrls);
160+
}
161+
if (chosenModuleIndex != null) {
162+
selectedModules.splice(chosenModuleIndex, 1);
163+
}
164+
if (!isCommunity && kymaResource) {
165+
setKymaResourceState({
166+
...kymaResource,
167+
spec: {
168+
...kymaResource.spec,
169+
modules: selectedModules,
170+
},
171+
});
172+
handleModuleUninstall();
173+
setInitialUnchangedResource(cloneDeep(kymaResourceState));
174+
}
175+
176+
if (detailsOpen) {
177+
setLayoutColumn({
178+
layout: 'OneColumn',
179+
startColumn: null,
180+
midColumn: null,
181+
endColumn: null,
182+
});
183+
}
184+
if (allowForceDelete && forceDeleteUrls.length > 0) {
185+
deleteCrResources(deleteResourceMutation, crUrls);
186+
}
187+
};
188+
189+
const deleteCommunityResources = async () => {
190+
if (allowForceDelete && forceDeleteUrls.length) {
191+
// Delete associated resources.
192+
await deleteAssociatedResources(deleteResourceMutation, forceDeleteUrls);
193+
}
194+
if (allowForceDelete && crUrls?.length) {
195+
// Delete spec.data.
196+
await deleteCrResources(deleteResourceMutation, crUrls);
197+
}
198+
if (allowForceDelete && communityResourcesUrls?.length) {
199+
// Delete community resources.
200+
await deleteCrResources(deleteResourceMutation, communityResourcesUrls);
201+
}
202+
if (detailsOpen) {
203+
setLayoutColumn({
204+
layout: 'OneColumn',
205+
startColumn: null,
206+
midColumn: null,
207+
endColumn: null,
208+
});
209+
}
210+
};
211+
133212
return (
134213
<DeleteMessageBox
135214
disableDeleteButton={associatedResourceLeft ? !allowForceDelete : false}
@@ -230,31 +309,10 @@ export const ModulesDeleteBox = ({
230309
: ''
231310
}
232311
deleteFn={() => {
233-
if (allowForceDelete && forceDeleteUrls.length > 0) {
234-
deleteAssociatedResources(deleteResourceMutation, forceDeleteUrls);
235-
}
236-
if (chosenModuleIndex != null) {
237-
selectedModules.splice(chosenModuleIndex, 1);
238-
}
239-
setKymaResourceState({
240-
...kymaResource,
241-
spec: {
242-
...kymaResource.spec,
243-
modules: selectedModules,
244-
},
245-
});
246-
handleModuleUninstall();
247-
setInitialUnchangedResource(cloneDeep(kymaResourceState));
248-
if (detailsOpen) {
249-
setLayoutColumn({
250-
layout: 'OneColumn',
251-
startColumn: null,
252-
midColumn: null,
253-
endColumn: null,
254-
});
255-
}
256-
if (allowForceDelete && forceDeleteUrls.length > 0) {
257-
deleteCrResources(deleteResourceMutation, crUrls);
312+
if (!isCommunity && kymaResource) {
313+
deleteAllResources();
314+
} else if (isCommunity) {
315+
deleteCommunityResources();
258316
}
259317
}}
260318
/>

src/components/KymaModules/deleteModulesHelpers.tsx

Lines changed: 91 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
findModuleTemplate,
55
ModuleTemplateListType,
66
} from './support';
7+
import { PostFn } from 'shared/hooks/BackendAPI/usePost';
78

89
interface Counts {
910
[key: string]: number;
@@ -44,6 +45,7 @@ export const getCRResource = (
4445
selectedModules: any,
4546
kymaResource: any,
4647
moduleTemplates: ModuleTemplateListType,
48+
isCommunity: boolean = false,
4749
) => {
4850
if (chosenModuleIndex == null) {
4951
return [];
@@ -63,15 +65,54 @@ export const getCRResource = (
6365

6466
let resource: Resource | null = null;
6567
if (module?.spec?.data) {
66-
resource = {
67-
group: module.spec.data.apiVersion.split('/')[0],
68-
version: module.spec.data.apiVersion.split('/')[1],
69-
kind: module.spec.data.kind,
70-
};
68+
resource = isCommunity
69+
? module?.spec?.data
70+
: {
71+
group: module.spec.data.apiVersion.split('/')[0],
72+
version: module.spec.data.apiVersion.split('/')[1],
73+
kind: module.spec.data.kind,
74+
};
7175
}
7276
return resource ? [resource] : [];
7377
};
7478

79+
export const getCommunityResources = async (
80+
chosenModuleIndex: number | null,
81+
selectedModules: any,
82+
kymaResource: any,
83+
moduleTemplates: ModuleTemplateListType,
84+
post: PostFn,
85+
) => {
86+
if (chosenModuleIndex == null) {
87+
return [];
88+
}
89+
const selectedModule = selectedModules[chosenModuleIndex];
90+
const moduleChannel = selectedModule?.channel || kymaResource?.spec?.channel;
91+
const moduleVersion =
92+
selectedModule?.version ||
93+
findModuleStatus(kymaResource, selectedModule?.name)?.version;
94+
95+
const module = findModuleTemplate(
96+
moduleTemplates,
97+
selectedModule?.name,
98+
moduleChannel,
99+
moduleVersion,
100+
);
101+
102+
const resources = (module?.spec as any)?.resources;
103+
if (resources?.length) {
104+
const yamlRes = await Promise.all(
105+
resources.map(async (res: any) => {
106+
if (res.link) {
107+
return await postForCommunityResources(post, res.link);
108+
}
109+
}),
110+
);
111+
return yamlRes.flat();
112+
}
113+
return [];
114+
};
115+
75116
export const handleItemClick = async (
76117
kind: string,
77118
group: string,
@@ -235,3 +276,48 @@ export const deleteCrResources = async (
235276
return 'Error while deleting Custom Resource';
236277
}
237278
};
279+
280+
export default async function postForCommunityResources(
281+
post: PostFn,
282+
link: string,
283+
) {
284+
if (!link) {
285+
console.error('No link provided for community resource');
286+
return false;
287+
}
288+
289+
try {
290+
const response = await post('/modules/community-resource', { link });
291+
if (response?.length) {
292+
return response;
293+
}
294+
console.error('Empty or invalid response:', response);
295+
return false;
296+
} catch (error) {
297+
console.error('Error fetching data:', error);
298+
return false;
299+
}
300+
}
301+
302+
export const getCommunityResourceUrls = (resources: any) => {
303+
if (!resources?.length) return [];
304+
305+
return resources.map((resource: any) => {
306+
if (!resource) return '';
307+
308+
const apiVersion =
309+
resource?.apiVersion || `${resource?.group}/${resource?.version}`;
310+
const resourceName = resource?.metadata?.name || resource?.name;
311+
const resourceNamespace =
312+
resource?.metadata?.namespace || resource?.namespace;
313+
const api = apiVersion === 'v1' ? 'api' : 'apis';
314+
315+
return resourceNamespace
316+
? `/${api}/${apiVersion}/namespaces/${resourceNamespace}/${pluralize(
317+
resource.kind,
318+
).toLowerCase()}/${resourceName}`
319+
: `/${api}/${apiVersion}/${pluralize(
320+
resource.kind || '',
321+
).toLowerCase()}/${resourceName}`;
322+
});
323+
};

0 commit comments

Comments
 (0)