refactor: launchpad configmap logic (#5650)

* feat: launchpad cm

* add comfigmap key

* update

* support volumeName

* delete log
This commit is contained in:
jingyang
2025-06-18 11:30:16 +08:00
committed by GitHub
parent 8666a91877
commit 2c61ce4e59
11 changed files with 160 additions and 78 deletions
@@ -11,7 +11,6 @@ export default function Document() {
<meta name="apple-mobile-web-app-title" content="Sealos" />
<meta name="description" content="sealos cloud" />
<meta name="format-detection" content="telephone=no" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<body>
@@ -27,7 +27,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
);
if (!ingress.body.items || ingress.body.items.length === 0) {
throw new Error('No ingress found');
throw new Error('Check ready error: No ingress found');
}
const checkResults = await Promise.all(
@@ -75,7 +75,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
data: checkResults
});
} catch (error: any) {
console.error(error);
return jsonRes(res, {
code: 500,
error: error?.message
@@ -40,7 +40,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
})
.filter((item) => item)
.flat();
console.log('responseData', responseData);
const data = adaptAppDetail(responseData as DeployKindsType[], getServerEnv(global.AppConfig));
@@ -21,12 +21,16 @@ export type ConfigMapType = {
id?: string;
mountPath: string;
value: string;
key: string;
volumeName: string;
};
const ConfigmapModal = ({
defaultValue = {
mountPath: '',
value: ''
value: '',
key: '',
volumeName: ''
},
listNames,
successCb,
@@ -68,7 +72,6 @@ const ConfigmapModal = ({
<ModalBody>
<MyFormControl showError errorText={errors.mountPath?.message}>
<Box mb={'8px'} fontSize={'14px'} fontWeight={500} color={'grayModern.900'}>
{' '}
{t('filename')}
</Box>
<Input
@@ -43,6 +43,7 @@ import PriceBox from './PriceBox';
import QuotaBox from './QuotaBox';
import type { StoreType } from './StoreModal';
import styles from './index.module.scss';
import { mountPathToConfigMapKey } from '@/utils/tools';
const CustomAccessModal = dynamic(() => import('./CustomAccessModal'));
const ConfigmapModal = dynamic(() => import('./ConfigmapModal'));
@@ -1147,7 +1148,9 @@ const Form = ({
w={'100%'}
height={'32px'}
variant={'outline'}
onClick={() => setConfigEdit({ mountPath: '', value: '' })}
onClick={() =>
setConfigEdit({ mountPath: '', value: '', key: '', volumeName: '' })
}
leftIcon={<MyIcon name="plus" w={'16px'} fill="#485264" />}
>
{t('form.add_configmap')}
@@ -1341,14 +1344,22 @@ const Form = ({
.map((item) => item.mountPath.toLocaleLowerCase())}
successCb={(e) => {
if (!e.id) {
appendConfigMaps(e);
appendConfigMaps({
...e,
key: mountPathToConfigMapKey(e.mountPath),
volumeName: getValues('appName') + '-cm'
});
} else {
setValue(
'configMapList',
configMaps.map((item) => ({
mountPath: item.id === e.id ? e.mountPath : item.mountPath,
value: item.id === e.id ? e.value : item.value
}))
configMaps.map((item) => {
return {
mountPath: item.id === e.id ? e.mountPath : item.mountPath,
value: item.id === e.id ? e.value : item.value,
key: item.id === e.id ? e.key : item.key,
volumeName: item.id === e.id ? e.volumeName : item.volumeName
};
})
);
}
setConfigEdit(undefined);
@@ -20,7 +20,7 @@ import {
import { useForm } from 'react-hook-form';
import MyFormControl from '@/components/FormControl';
import { useTranslation } from 'next-i18next';
import { pathToNameFormat } from '@/utils/tools';
import { mountPathToConfigMapKey } from '@/utils/tools';
import { MyTooltip } from '@sealos/ui';
import { PVC_STORAGE_MAX } from '@/store/static';
@@ -152,7 +152,7 @@ const StoreModal = ({
return true;
},
onChange(e) {
setValue('name', pathToNameFormat(e.target.value));
setValue('name', mountPathToConfigMapKey(e.target.value));
}
})}
/>
+2
View File
@@ -108,6 +108,8 @@ export interface AppEditType {
configMapList: {
mountPath: string;
value: string;
key: string;
volumeName: string;
}[];
storeList: {
name: string;
@@ -8,7 +8,8 @@ import type {
V1Pod,
SinglePodMetrics,
CoreV1EventList,
V2HorizontalPodAutoscaler
V2HorizontalPodAutoscaler,
V1VolumeMount
} from '@kubernetes/client-node';
import dayjs from 'dayjs';
import type {
@@ -235,7 +236,6 @@ export const adaptAppDetail = (configs: DeployKindsType[], envs: EnvResponse): A
const deployKindsMap: {
[YamlKindEnum.StatefulSet]?: V1StatefulSet;
[YamlKindEnum.Deployment]?: V1Deployment;
// [YamlKindEnum.Service]?: V1Service;
[YamlKindEnum.ConfigMap]?: V1ConfigMap;
[YamlKindEnum.HorizontalPodAutoscaler]?: V2HorizontalPodAutoscaler;
[YamlKindEnum.Secret]?: V1Secret;
@@ -250,10 +250,86 @@ export const adaptAppDetail = (configs: DeployKindsType[], envs: EnvResponse): A
const appDeploy = deployKindsMap.Deployment || deployKindsMap.StatefulSet;
if (!appDeploy) {
if (!appDeploy || !appDeploy?.metadata?.name) {
throw new Error('获取APP异常');
}
const appName = appDeploy.metadata?.name;
const getConfigMapVolumeNames = (): string[] => {
if (!deployKindsMap.ConfigMap) return [];
const configMapName = deployKindsMap.ConfigMap.metadata?.name;
const configMapVolumes =
appDeploy?.spec?.template?.spec?.volumes?.filter(
(volume) => volume.configMap?.name === configMapName
) || [];
return configMapVolumes.map((volume) => volume.name).filter(Boolean) as string[];
};
const configMapVolumeNames = getConfigMapVolumeNames();
const getConfigMapList = (): Array<{
mountPath: string;
value: string;
key: string;
volumeName: string;
}> => {
const configMap = deployKindsMap.ConfigMap;
if (!configMap?.data || !appDeploy) {
return [];
}
const configMapName = configMap.metadata?.name;
const volumes = appDeploy.spec?.template?.spec?.volumes || [];
const configMapVolumes = volumes.filter((volume) => volume.configMap?.name === configMapName);
if (configMapVolumes.length === 0) {
return [];
}
const volumeMounts = appDeploy.spec?.template?.spec?.containers?.[0]?.volumeMounts || [];
const results: Array<{ mountPath: string; value: string; key: string; volumeName: string }> =
[];
configMapVolumes.forEach((volume) => {
const relatedMounts = volumeMounts.filter((mount) => mount.name === volume.name);
if (volume.configMap?.items && volume.configMap.items.length > 0) {
volume.configMap.items.forEach((item) => {
if (!item.key) return;
const matchedMount = relatedMounts.find((mount) => mount.subPath === item.path);
if (matchedMount) {
const value = configMap.data?.[item.key] || '';
results.push({
mountPath: matchedMount.mountPath,
value: value,
key: item.key,
volumeName: volume.name
});
}
});
} else {
relatedMounts.forEach((mount) => {
if (mount.subPath) {
const configMapKey = mount.subPath;
const value = configMap?.data?.[configMapKey] || '';
if (value) {
results.push({
mountPath: mount.mountPath,
value: value,
key: configMapKey,
volumeName: volume.name
});
}
}
});
}
});
return results.filter((item) => item.value);
};
const useGpu = !!Number(
appDeploy.spec?.template?.spec?.containers?.[0]?.resources?.limits?.[gpuResourceKey]
);
@@ -261,14 +337,23 @@ export const adaptAppDetail = (configs: DeployKindsType[], envs: EnvResponse): A
const getFilteredVolumeMounts = () => {
const volumeMounts = appDeploy?.spec?.template?.spec?.containers?.[0]?.volumeMounts || [];
const configMapKeys = Object.keys(deployKindsMap.ConfigMap?.data || {});
const storeNames =
deployKindsMap.StatefulSet?.spec?.volumeClaimTemplates?.map(
(template) => template.metadata?.name
) || [];
return volumeMounts.filter(
(mount) => !configMapKeys.includes(mount.name) && !storeNames.includes(mount.name)
(mount) => !configMapVolumeNames.includes(mount.name) && !storeNames.includes(mount.name)
);
};
const getFilteredVolumes = () => {
return (
appDeploy?.spec?.template?.spec?.volumes?.filter((volume) => {
if (!deployKindsMap.ConfigMap) return true;
const configMapName = deployKindsMap.ConfigMap.metadata?.name;
return !(volume.configMap?.name === configMapName);
}) || []
);
};
@@ -276,7 +361,7 @@ export const adaptAppDetail = (configs: DeployKindsType[], envs: EnvResponse): A
labels: appDeploy?.metadata?.labels || {},
crYamlList: configs,
id: appDeploy.metadata?.uid || ``,
appName: appDeploy.metadata?.name || 'app Name',
appName: appName,
createTime: dayjs(appDeploy.metadata?.creationTimestamp).format('YYYY-MM-DD HH:mm'),
status: appStatusMap.waiting,
isPause: !!appDeploy?.metadata?.annotations?.[pauseKey],
@@ -356,7 +441,7 @@ export const adaptAppDetail = (configs: DeployKindsType[], envs: EnvResponse): A
customDomain: isCustomDomain ? domain : '',
domain: isCustomDomain
? envs.SEALOS_DOMAIN
: item?.nodePort // 如果有 nodePort,则使用域名
: item?.nodePort
? domain
: domain.split('.').slice(1).join('.') || envs.SEALOS_DOMAIN
};
@@ -383,15 +468,7 @@ export const adaptAppDetail = (configs: DeployKindsType[], envs: EnvResponse): A
maxReplicas: deployKindsMap.HorizontalPodAutoscaler.spec.maxReplicas || 10
}
: defaultEditVal.hpa,
configMapList: deployKindsMap.ConfigMap?.data
? Object.entries(deployKindsMap.ConfigMap.data).map(([key, value], i) => ({
mountPath:
appDeploy?.spec?.template.spec?.containers[0].volumeMounts?.find(
(item) => item.name === key
)?.mountPath || key,
value
}))
: [],
configMapList: getConfigMapList(),
secret: atobSecretYaml(deployKindsMap?.Secret?.data?.['.dockerconfigjson']),
storeList: deployKindsMap.StatefulSet?.spec?.volumeClaimTemplates
? deployKindsMap.StatefulSet?.spec?.volumeClaimTemplates.map((item) => ({
@@ -401,8 +478,7 @@ export const adaptAppDetail = (configs: DeployKindsType[], envs: EnvResponse): A
}))
: [],
volumeMounts: getFilteredVolumeMounts(),
// keep original non-configMap type volumes
volumes: appDeploy?.spec?.template?.spec?.volumes || [],
volumes: getFilteredVolumes(),
kind: appDeploy?.kind?.toLowerCase() as 'deployment' | 'statefulset',
source: getAppSource(appDeploy)
};
@@ -9,7 +9,7 @@ import {
} from '@/constants/app';
import { SEALOS_USER_DOMAINS } from '@/store/static';
import type { AppEditType } from '@/types/app';
import { pathFormat, pathToNameFormat, str2Num, strToBase64 } from '@/utils/tools';
import { pathFormat, mountPathToConfigMapKey, str2Num, strToBase64 } from '@/utils/tools';
import dayjs from 'dayjs';
import yaml from 'js-yaml';
@@ -97,44 +97,31 @@ export const json2DeployCr = (data: AppEditType, type: 'deployment' | 'statefuls
})),
imagePullPolicy: 'Always'
};
const configMapVolumeMounts = data.configMapList.map((item) => ({
name: pathToNameFormat(item.mountPath),
name: item.volumeName,
mountPath: item.mountPath,
subPath: pathFormat(item.mountPath)
}));
const configMapVolumes = data.configMapList.map((item) => ({
name: pathToNameFormat(item.mountPath), // name === [development.***.volumeMounts[*].name]
configMap: {
name: data.appName, // name === configMap.yaml.meta.name
items: [
{
key: pathToNameFormat(item.mountPath),
path: pathFormat(item.mountPath) // path ===[development.***.volumeMounts[*].subPath]
}
]
}
subPath: item.key
}));
const deduplicateVolumes = () => {
const existingVolumes = data.volumes || [];
const formVolumeNames = new Set(
data.configMapList.map((item) => pathToNameFormat(item.mountPath))
);
const preservedVolumes = existingVolumes.filter(
(volume) => !(volume.configMap && formVolumeNames.has(volume.name))
);
return [...preservedVolumes, ...configMapVolumes];
const generateConfigMapVolumes = () => {
if (data.configMapList.length === 0) return [];
const uniqueVolumeNames = [...new Set(data.configMapList.map((item) => item.volumeName))];
return uniqueVolumeNames.map((volumeName) => ({
name: volumeName,
configMap: {
name: data.appName
}
}));
};
const deduplicateVolumeMounts = () => {
const existingMounts = data.volumeMounts || [];
const formMountNames = new Set(configMapVolumeMounts.map((m) => m.name));
const preservedMounts = existingMounts.filter((mount) => !formMountNames.has(mount.name));
return [...preservedMounts, ...configMapVolumeMounts];
};
const configMapVolumes = generateConfigMapVolumes();
const finalVolumes = deduplicateVolumes();
const finalVolumeMounts = deduplicateVolumeMounts();
const finalVolumes = configMapVolumes
? [...(data.volumes || []), ...configMapVolumes]
: data.volumes || [];
const finalVolumeMounts = [...(data.volumeMounts || []), ...configMapVolumeMounts];
// pvc settings
const storageTemplates = data.storeList.map((store) => ({
@@ -452,7 +439,7 @@ export const json2ConfigMap = (data: AppEditType) => {
const configFile: { [key: string]: string } = {};
data.configMapList.forEach((item) => {
configFile[pathToNameFormat(item.mountPath)] = item.value; // key === [development.***.volumes[*].configMap.items[0].key]
configFile[item.key] = item.value;
});
const template = {
@@ -69,12 +69,18 @@ export const pathFormat = (str: string) => {
if (str.startsWith('/')) return `.${str}`;
return `./${str}`;
};
export const pathToNameFormat = (str: string) => {
export const mountPathToConfigMapKey = (str: string) => {
const endsWithSlash = str.endsWith('/');
const withoutTrailingSlash = endsWithSlash ? str.slice(0, -1) : str;
const replacedStr = withoutTrailingSlash.replace(/_/g, '-').replace(/[\/.]/g, 'vn-');
const result = replacedStr.toLowerCase();
return replacedStr.toLowerCase();
if (result.length > 63) {
return result.slice(-63);
}
return result;
};
/**
@@ -274,10 +280,16 @@ export const patchYamlList = ({
// find create and patch
newFormJsonList.forEach((newYamlJson) => {
const oldFormJson = oldFormJsonList.find(
(item) =>
item.kind === newYamlJson.kind && item?.metadata?.name === newYamlJson?.metadata?.name
);
const oldFormJson =
newYamlJson.kind === 'ConfigMap'
? originalYamlList.find(
(item) =>
item.kind === newYamlJson.kind && item?.metadata?.name === newYamlJson?.metadata?.name
)
: oldFormJsonList.find(
(item) =>
item.kind === newYamlJson.kind && item?.metadata?.name === newYamlJson?.metadata?.name
);
if (oldFormJson) {
const patchRes = jsonpatch.compare(oldFormJson, newYamlJson);
@@ -299,7 +311,7 @@ export const patchYamlList = ({
if (!crOldYamlJson) return newYamlJson;
/* Fill in volumn */
/* Fill in volume - Handle Deployment/StatefulSet */
if (
oldFormJson.kind === YamlKindEnum.Deployment ||
oldFormJson.kind === YamlKindEnum.StatefulSet
@@ -380,8 +392,6 @@ export const patchYamlList = ({
}
}
console.log('patch result:', oldFormJson.metadata?.name, oldFormJson.kind, actionsJson);
actions.push({
type: 'patch',
kind: newYamlJson.kind as `${YamlKindEnum}`,
@@ -299,8 +299,6 @@ export async function getServerSideProps(content: any) {
const forcedLanguage = process.env.FORCED_LANGUAGE;
const brandName = process.env.NEXT_PUBLIC_BRAND_NAME || 'Sealos';
console.log('content', content);
const local: string =
forcedLanguage ||
content?.req?.cookies?.NEXT_LOCALE ||
@@ -325,8 +323,6 @@ export async function getServerSideProps(content: any) {
}
};
console.log('local', local);
console.log('seoData', seoData);
return {
props: {
...(await serviceSideProps(content)),