Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions backend/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { handleTracking } from './tracking.js';
import jsyaml from 'js-yaml';
import { proxyHandler, proxyRateLimiter } from './proxy.js';
import companionRouter from './companion/companionRouter';
import communityRouter from './modules/communityRouter';
//import { requestLogger } from './utils/other'; //uncomment this to log the outgoing traffic

const express = require('express');
Expand Down Expand Up @@ -94,12 +95,14 @@ if (isDocker) {
// yup, order matters here
serveMonaco(app);
app.use('/backend/ai-chat', companionRouter);
app.use('/backend/modules', communityRouter);
app.use('/backend', handleRequest);
serveStaticApp(app, '/', '/core-ui');
} else {
// Running in prod mode
handleTracking(app);
app.use('/backend/ai-chat', companionRouter);
app.use('/backend/modules', communityRouter);
app.use('/backend', handleRequest);
}

Expand Down
38 changes: 38 additions & 0 deletions backend/modules/communityRouter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import express from 'express';
import cors from 'cors';
import jsyaml from 'js-yaml';

const router = express.Router();
router.use(express.json());
router.use(cors());

async function handleGetCommunityResource(req, res) {
const { link } = JSON.parse(req.body.toString());

// Validate that link is a string and a valid HTTPS URL, and restrict to allowed domains.
if (typeof link !== 'string') {
return res.status(400).json('Link must be a string.');
}

try {
const url = new URL(link);
// Only allow HTTPS protocol and restrict to specific trusted domains.
const allowedDomains = ['github.com'];
if (
url.protocol !== 'https:' ||
!allowedDomains.some(domain => url.hostname.endsWith(domain))
) {
return res.status(400).json('Invalid or untrusted link provided.');
} else {
const response = await fetch(link);
const data = await response.text();
res.json(jsyaml.loadAll(data));
}
} catch (error) {
res.status(500).json(`Failed to fetch community resource. ${error}`);
}
}

router.post('/community-resource', handleGetCommunityResource);

export default router;
3 changes: 2 additions & 1 deletion src/components/KymaModules/KymaModulesList.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export default function KymaModulesList({ namespaced }) {
installedCommunityModules,
communityModulesLoading,
setOpenedModuleIndex: setOpenedCommunityModuleIndex,
handleResourceDelete: handleCommunityModuleDelete,
} = useContext(CommunityModuleContext);

const [selectedEntry, setSelectedEntry] = useState(null);
Expand Down Expand Up @@ -94,7 +95,7 @@ export default function KymaModulesList({ namespaced }) {
modulesLoading={communityModulesLoading}
namespaced={namespaced}
setOpenedModuleIndex={setOpenedCommunityModuleIndex}
handleResourceDelete={handleResourceDelete}
handleResourceDelete={handleCommunityModuleDelete}
customSelectedEntry={selectedEntry}
setSelectedEntry={setSelectedEntry}
/>
Expand Down
126 changes: 92 additions & 34 deletions src/components/KymaModules/components/ModulesDeleteBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {
fetchResourceCounts,
generateAssociatedResourcesUrls,
getAssociatedResources,
getCommunityResourceUrls,
getCommunityResources,
getCRResource,
handleItemClick,
} from '../deleteModulesHelpers';
Expand All @@ -26,15 +28,17 @@ import { cloneDeep } from 'lodash';
import { KymaResourceType, ModuleTemplateListType } from '../support';
import { SetterOrUpdater } from 'recoil';
import { ColumnLayoutState } from 'state/columnLayoutAtom';
import { usePost } from 'shared/hooks/BackendAPI/usePost';

type ModulesListDeleteBoxProps = {
DeleteMessageBox: React.FC<any>;
moduleTemplates: ModuleTemplateListType;
selectedModules: { name: string }[];
chosenModuleIndex: number | null;
kymaResource: KymaResourceType;
kymaResourceState: KymaResourceType;
kymaResourceState?: KymaResourceType;
detailsOpen: boolean;
isCommunity?: boolean;
setLayoutColumn: SetterOrUpdater<ColumnLayoutState>;
handleModuleUninstall: () => void;
setChosenModuleIndex: React.Dispatch<React.SetStateAction<number | null>>;
Expand All @@ -50,6 +54,7 @@ export const ModulesDeleteBox = ({
kymaResource,
kymaResourceState,
detailsOpen,
isCommunity,
setLayoutColumn,
handleModuleUninstall,
setChosenModuleIndex,
Expand All @@ -62,10 +67,14 @@ export const ModulesDeleteBox = ({
const { clusterUrl, namespaceUrl } = useUrl();
const deleteResourceMutation = useDelete();
const fetchFn = useSingleGet();
const post = usePost();

const [resourceCounts, setResourceCounts] = useState<Record<string, any>>({});
const [forceDeleteUrls, setForceDeleteUrls] = useState<string[]>([]);
const [crUrls, setCrUrls] = useState<string[]>([]);
const [communityResourcesUrls, setCommunityResourcesUrls] = useState<
string[]
>([]);
const [allowForceDelete, setAllowForceDelete] = useState(false);
const [associatedResourceLeft, setAssociatedResourceLeft] = useState(false);

Expand Down Expand Up @@ -99,16 +108,31 @@ export const ModulesDeleteBox = ({
selectedModules,
kymaResource,
moduleTemplates,
isCommunity,
);

const crUrl = await generateAssociatedResourcesUrls(
crUResources,
fetchFn,
clusterUrl,
getScope,
namespaceUrl,
navigate,
);
const crUrl = isCommunity
? getCommunityResourceUrls(crUResources)
: await generateAssociatedResourcesUrls(
crUResources,
fetchFn,
clusterUrl,
getScope,
namespaceUrl,
navigate,
);

if (isCommunity) {
const communityResources = await getCommunityResources(
chosenModuleIndex,
selectedModules,
kymaResource,
moduleTemplates,
post,
);
const communityUrls = getCommunityResourceUrls(communityResources);
setCommunityResourcesUrls(communityUrls);
}

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

const deleteAllResources = () => {
if (allowForceDelete && forceDeleteUrls.length > 0) {
deleteAssociatedResources(deleteResourceMutation, forceDeleteUrls);
}
if (chosenModuleIndex != null) {
selectedModules.splice(chosenModuleIndex, 1);
}
if (!isCommunity && kymaResource) {
setKymaResourceState({
...kymaResource,
spec: {
...kymaResource.spec,
modules: selectedModules,
},
});
handleModuleUninstall();
setInitialUnchangedResource(cloneDeep(kymaResourceState));
}

if (detailsOpen) {
setLayoutColumn({
layout: 'OneColumn',
startColumn: null,
midColumn: null,
endColumn: null,
});
}
if (allowForceDelete && forceDeleteUrls.length > 0) {
deleteCrResources(deleteResourceMutation, crUrls);
}
};

const deleteCommunityResources = async () => {
if (allowForceDelete && forceDeleteUrls.length) {
// Delete associated resources.
await deleteAssociatedResources(deleteResourceMutation, forceDeleteUrls);
}
if (allowForceDelete && crUrls?.length) {
// Delete spec.data.
await deleteCrResources(deleteResourceMutation, crUrls);
}
if (allowForceDelete && communityResourcesUrls?.length) {
// Delete community resources.
await deleteCrResources(deleteResourceMutation, communityResourcesUrls);
}
if (detailsOpen) {
setLayoutColumn({
layout: 'OneColumn',
startColumn: null,
midColumn: null,
endColumn: null,
});
}
};

return (
<DeleteMessageBox
disableDeleteButton={associatedResourceLeft ? !allowForceDelete : false}
Expand Down Expand Up @@ -230,31 +309,10 @@ export const ModulesDeleteBox = ({
: ''
}
deleteFn={() => {
if (allowForceDelete && forceDeleteUrls.length > 0) {
deleteAssociatedResources(deleteResourceMutation, forceDeleteUrls);
}
if (chosenModuleIndex != null) {
selectedModules.splice(chosenModuleIndex, 1);
}
setKymaResourceState({
...kymaResource,
spec: {
...kymaResource.spec,
modules: selectedModules,
},
});
handleModuleUninstall();
setInitialUnchangedResource(cloneDeep(kymaResourceState));
if (detailsOpen) {
setLayoutColumn({
layout: 'OneColumn',
startColumn: null,
midColumn: null,
endColumn: null,
});
}
if (allowForceDelete && forceDeleteUrls.length > 0) {
deleteCrResources(deleteResourceMutation, crUrls);
if (!isCommunity && kymaResource) {
deleteAllResources();
} else if (isCommunity) {
deleteCommunityResources();
}
}}
/>
Expand Down
96 changes: 91 additions & 5 deletions src/components/KymaModules/deleteModulesHelpers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
findModuleTemplate,
ModuleTemplateListType,
} from './support';
import { PostFn } from 'shared/hooks/BackendAPI/usePost';

interface Counts {
[key: string]: number;
Expand Down Expand Up @@ -44,6 +45,7 @@ export const getCRResource = (
selectedModules: any,
kymaResource: any,
moduleTemplates: ModuleTemplateListType,
isCommunity: boolean = false,
) => {
if (chosenModuleIndex == null) {
return [];
Expand All @@ -63,15 +65,54 @@ export const getCRResource = (

let resource: Resource | null = null;
if (module?.spec?.data) {
resource = {
group: module.spec.data.apiVersion.split('/')[0],
version: module.spec.data.apiVersion.split('/')[1],
kind: module.spec.data.kind,
};
resource = isCommunity
? module?.spec?.data
: {
group: module.spec.data.apiVersion.split('/')[0],
version: module.spec.data.apiVersion.split('/')[1],
kind: module.spec.data.kind,
};
}
return resource ? [resource] : [];
};

export const getCommunityResources = async (
chosenModuleIndex: number | null,
selectedModules: any,
kymaResource: any,
moduleTemplates: ModuleTemplateListType,
post: PostFn,
) => {
if (chosenModuleIndex == null) {
return [];
}
const selectedModule = selectedModules[chosenModuleIndex];
const moduleChannel = selectedModule?.channel || kymaResource?.spec?.channel;
const moduleVersion =
selectedModule?.version ||
findModuleStatus(kymaResource, selectedModule?.name)?.version;

const module = findModuleTemplate(
moduleTemplates,
selectedModule?.name,
moduleChannel,
moduleVersion,
);

const resources = (module?.spec as any)?.resources;
if (resources?.length) {
const yamlRes = await Promise.all(
resources.map(async (res: any) => {
if (res.link) {
return await postForCommunityResources(post, res.link);
}
}),
);
return yamlRes.flat();
}
return [];
};

export const handleItemClick = async (
kind: string,
group: string,
Expand Down Expand Up @@ -235,3 +276,48 @@ export const deleteCrResources = async (
return 'Error while deleting Custom Resource';
}
};

export default async function postForCommunityResources(
post: PostFn,
link: string,
) {
if (!link) {
console.error('No link provided for community resource');
return false;
}

try {
const response = await post('/modules/community-resource', { link });
if (response?.length) {
return response;
}
console.error('Empty or invalid response:', response);
return false;
} catch (error) {
console.error('Error fetching data:', error);
return false;
}
}

export const getCommunityResourceUrls = (resources: any) => {
if (!resources?.length) return [];

return resources.map((resource: any) => {
if (!resource) return '';

const apiVersion =
resource?.apiVersion || `${resource?.group}/${resource?.version}`;
const resourceName = resource?.metadata?.name || resource?.name;
const resourceNamespace =
resource?.metadata?.namespace || resource?.namespace;
const api = apiVersion === 'v1' ? 'api' : 'apis';

return resourceNamespace
? `/${api}/${apiVersion}/namespaces/${resourceNamespace}/${pluralize(
resource.kind,
).toLowerCase()}/${resourceName}`
: `/${api}/${apiVersion}/${pluralize(
resource.kind || '',
).toLowerCase()}/${resourceName}`;
});
};
Loading
Loading