diff --git a/frontend/providers/applaunchpad/public/locales/en/common.json b/frontend/providers/applaunchpad/public/locales/en/common.json index 9bc6b27737b..2097d7e465e 100644 --- a/frontend/providers/applaunchpad/public/locales/en/common.json +++ b/frontend/providers/applaunchpad/public/locales/en/common.json @@ -178,5 +178,9 @@ "let me think again": "let me think again", "Configurable number of instances or automatic horizontal scaling": "Configurable number of instances or automatic horizontal scaling", "gift time tip": "Within {{time}} hours ", - "gift amount tip": "Recharge {{amount}} and get {{gift}} free" -} \ No newline at end of file + "gift amount tip": "Recharge {{amount}} and get {{gift}} free", + "Add Port": "Add Port", + "Age": "Age", + "Amount": "Amount", + "ConfigMap Tip": "ConfigMap" +} diff --git a/frontend/providers/applaunchpad/public/locales/zh/common.json b/frontend/providers/applaunchpad/public/locales/zh/common.json index e17909b2f67..a1596913eaa 100644 --- a/frontend/providers/applaunchpad/public/locales/zh/common.json +++ b/frontend/providers/applaunchpad/public/locales/zh/common.json @@ -220,5 +220,6 @@ "let me think again": "我再想想", "Configurable number of instances or automatic horizontal scaling": "可配置实例数或自动横向伸缩", "gift time tip": "{{time}} 小时内", - "gift amount tip": "充值 {{amount}} 赠送 {{gift}} " -} \ No newline at end of file + "gift amount tip": "充值 {{amount}} 赠送 {{gift}} ", + "ConfigMap Tip": "配置文件" +} diff --git a/frontend/providers/applaunchpad/src/pages/api/delApp.ts b/frontend/providers/applaunchpad/src/pages/api/delApp.ts index 7d1b9539e70..f2ecf7a6ed2 100644 --- a/frontend/providers/applaunchpad/src/pages/api/delApp.ts +++ b/frontend/providers/applaunchpad/src/pages/api/delApp.ts @@ -33,11 +33,65 @@ export async function DeleteAppByName({ name, req }: DeleteAppParams & { req: Ne kubeconfig: await authSession(req.headers) }); + // delete Certificate + const certificatesList = (await k8sCustomObjects.listNamespacedCustomObject( + 'cert-manager.io', + 'v1', + namespace, + 'certificates', + undefined, + undefined, + undefined, + undefined, + `${appDeployKey}=${name}` + )) as { body: { items: any[] } }; + const delCertList = certificatesList.body.items.map((item) => + k8sCustomObjects.deleteNamespacedCustomObject( + 'cert-manager.io', + 'v1', + namespace, + 'certificates', + item.metadata.name + ) + ); + + // delete Issuer + const issuersList = (await k8sCustomObjects.listNamespacedCustomObject( + 'cert-manager.io', + 'v1', + namespace, + 'issuers', + undefined, + undefined, + undefined, + undefined, + `${appDeployKey}=${name}` + )) as { body: { items: any[] } }; + const delIssuerList = issuersList.body.items.map(async (item) => + k8sCustomObjects.deleteNamespacedCustomObject( + 'cert-manager.io', + 'v1', + namespace, + 'issuers', + item.metadata.name + ) + ); + + const delIssuerAndCert = await Promise.allSettled([...delCertList, ...delIssuerList]); + /* find not 404 error */ + delIssuerAndCert.forEach((item) => { + console.log(item, 'delIssuerAndCert err'); + if (item.status === 'rejected' && +item?.reason?.body?.code !== 404) { + throw new Error(item?.reason?.body?.message || item?.reason?.body?.reason || '删除 App 异常'); + } + }); + /* delete all sources */ const delDependent = await Promise.allSettled([ k8sCore.deleteNamespacedService(name, namespace), // delete service k8sCore.deleteNamespacedConfigMap(name, namespace), // delete configMap k8sCore.deleteNamespacedSecret(name, namespace), // delete secret + // delete Ingress k8sNetworkingApp.deleteCollectionNamespacedIngress( namespace, undefined, @@ -46,25 +100,9 @@ export async function DeleteAppByName({ name, req }: DeleteAppParams & { req: Ne undefined, undefined, `${appDeployKey}=${name}` - ), // delete Ingress - k8sCustomObjects.deleteNamespacedCustomObject( - // delete Issuer - 'cert-manager.io', - 'v1', - namespace, - 'issuers', - name - ), - k8sCustomObjects.deleteNamespacedCustomObject( - // delete Certificate - 'cert-manager.io', - 'v1', - namespace, - 'certificates', - name ), + // delete pvc k8sCore.deleteCollectionNamespacedPersistentVolumeClaim( - // delete pvc namespace, undefined, undefined, @@ -92,7 +130,7 @@ export async function DeleteAppByName({ name, req }: DeleteAppParams & { req: Ne /* find not 404 error */ delApp.forEach((item) => { - console.log(item, 'delApp err'); + console.log(item, 'delApp Deployment StatefulSet err'); if (item.status === 'rejected' && +item?.reason?.body?.code !== 404) { throw new Error(item?.reason?.body?.reason || item?.reason?.body?.message || '删除 App 异常'); } diff --git a/frontend/providers/applaunchpad/src/pages/api/updateApp.ts b/frontend/providers/applaunchpad/src/pages/api/updateApp.ts index 2f68d202612..0fcbf1188b0 100644 --- a/frontend/providers/applaunchpad/src/pages/api/updateApp.ts +++ b/frontend/providers/applaunchpad/src/pages/api/updateApp.ts @@ -110,19 +110,22 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< delete: (name) => k8sNetworkingApp.deleteNamespacedIngress(name, namespace) }, [YamlKindEnum.Issuer]: { - patch: (jsonPatch: Object) => - k8sCustomObjects.patchNamespacedCustomObject( + patch: (jsonPatch: Object) => { + // @ts-ignore + const name = jsonPatch?.metadata?.name; + return k8sCustomObjects.patchNamespacedCustomObject( 'cert-manager.io', 'v1', namespace, 'issuers', - appName, + name, jsonPatch, undefined, undefined, undefined, { headers: { 'Content-type': PatchUtils.PATCH_FORMAT_JSON_MERGE_PATCH } } - ), + ); + }, delete: (name) => k8sCustomObjects.deleteNamespacedCustomObject( 'cert-manager.io', @@ -133,19 +136,22 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< ) }, [YamlKindEnum.Certificate]: { - patch: (jsonPatch: Object) => - k8sCustomObjects.patchNamespacedCustomObject( + patch: (jsonPatch: Object) => { + // @ts-ignore + const name = jsonPatch?.metadata?.name; + return k8sCustomObjects.patchNamespacedCustomObject( 'cert-manager.io', 'v1', namespace, 'certificates', - appName, + name, jsonPatch, undefined, undefined, undefined, { headers: { 'Content-type': PatchUtils.PATCH_FORMAT_JSON_MERGE_PATCH } } - ), + ); + }, delete: (name) => k8sCustomObjects.deleteNamespacedCustomObject( 'cert-manager.io', diff --git a/frontend/providers/applaunchpad/src/pages/app/edit/components/ConfigmapModal.tsx b/frontend/providers/applaunchpad/src/pages/app/edit/components/ConfigmapModal.tsx index 9e33ba38415..afbed15d6df 100644 --- a/frontend/providers/applaunchpad/src/pages/app/edit/components/ConfigmapModal.tsx +++ b/frontend/providers/applaunchpad/src/pages/app/edit/components/ConfigmapModal.tsx @@ -60,7 +60,10 @@ const ConfigmapModal = ({ - {t(textMap[type].title)} ConfigMap + + {t(textMap[type].title)} + {t('ConfigMap Tip')} + diff --git a/frontend/providers/applaunchpad/src/pages/app/edit/index.tsx b/frontend/providers/applaunchpad/src/pages/app/edit/index.tsx index ee75498bf68..5581c00dc14 100644 --- a/frontend/providers/applaunchpad/src/pages/app/edit/index.tsx +++ b/frontend/providers/applaunchpad/src/pages/app/edit/index.tsx @@ -36,9 +36,9 @@ import Yaml from './components/Yaml'; const ErrorModal = dynamic(() => import('./components/ErrorModal')); export const formData2Yamls = ( - data: AppEditType, - handleType: 'edit' | 'create' = 'create', - crYamlList?: DeployKindsType[] + data: AppEditType + // handleType: 'edit' | 'create' = 'create', + // crYamlList?: DeployKindsType[] ) => [ { filename: 'service.yaml', @@ -47,11 +47,11 @@ export const formData2Yamls = ( !!data.storeList?.length ? { filename: 'statefulSet.yaml', - value: json2DeployCr(data, 'statefulset', handleType, crYamlList) + value: json2DeployCr(data, 'statefulset') } : { filename: 'deployment.yaml', - value: json2DeployCr(data, 'deployment', handleType, crYamlList) + value: json2DeployCr(data, 'deployment') }, ...(data.configMapList.length > 0 ? [ @@ -257,12 +257,9 @@ const EditApp = ({ appName, tabType }: { appName?: string; tabType: string }) => { onSuccess(res) { if (!res) return; + console.log(res, 'init res'); oldAppEditData.current = res; - formOldYamls.current = formData2Yamls( - res, - appName !== '' ? 'edit' : 'create', - res.crYamlList - ); + formOldYamls.current = formData2Yamls(res); crOldYamls.current = res.crYamlList; setDefaultStorePathList(res.storeList.map((item) => item.path)); @@ -285,16 +282,10 @@ const EditApp = ({ appName, tabType }: { appName?: string; tabType: string }) => useEffect(() => { if (tabType === 'yaml') { try { - setYamlList( - formData2Yamls( - realTimeForm.current, - appName !== '' ? 'edit' : 'create', - crOldYamls.current - ) - ); + setYamlList(formData2Yamls(realTimeForm.current)); } catch (error) {} } - }, [appName, router.query.name, tabType]); + }, [router.query.name, tabType]); return ( <> @@ -313,11 +304,7 @@ const EditApp = ({ appName, tabType }: { appName?: string; tabType: string }) => applyCb={() => { closeGuide(); formHook.handleSubmit((data) => { - const parseYamls = formData2Yamls( - data, - appName !== '' ? 'edit' : 'create', - crOldYamls.current - ); + const parseYamls = formData2Yamls(data); setYamlList(parseYamls); // balance check if (balance <= 0) { diff --git a/frontend/providers/applaunchpad/src/types/app.d.ts b/frontend/providers/applaunchpad/src/types/app.d.ts index 0de0b07b7cd..33e4d2e6571 100644 --- a/frontend/providers/applaunchpad/src/types/app.d.ts +++ b/frontend/providers/applaunchpad/src/types/app.d.ts @@ -71,8 +71,8 @@ export interface AppEditType { port: number; protocol: 'HTTP' | 'GRPC' | 'WS'; openPublicDomain: boolean; - publicDomain: string; - customDomain: string; + publicDomain: string; // default domain + customDomain: string; // custom domain }[]; envs: { key: string; diff --git a/frontend/providers/applaunchpad/src/utils/deployYaml2Json.ts b/frontend/providers/applaunchpad/src/utils/deployYaml2Json.ts index b6414ee140c..2df50b284ca 100644 --- a/frontend/providers/applaunchpad/src/utils/deployYaml2Json.ts +++ b/frontend/providers/applaunchpad/src/utils/deployYaml2Json.ts @@ -14,12 +14,7 @@ import { import dayjs from 'dayjs'; import jsonpatch, { Operation } from 'fast-json-patch'; -export const json2DeployCr = ( - data: AppEditType, - type: 'deployment' | 'statefulset', - handleType: 'edit' | 'create' = 'create', - crYamlList?: DeployKindsType[] -) => { +export const json2DeployCr = (data: AppEditType, type: 'deployment' | 'statefulset') => { const totalStorage = data.storeList.reduce((acc, item) => acc + item.value, 0); const metadata = { @@ -220,91 +215,6 @@ export const json2DeployCr = ( } }; - const differentJsonPatch = jsonpatch.compare(template['deployment'], template['statefulset']); - - // JSON Patch - const patch: Operation[] = [ - { op: 'add', path: '/metadata', value: metadata }, - { op: 'replace', path: '/spec/replicas', value: commonSpec.replicas }, - { op: 'replace', path: '/spec/revisionHistoryLimit', value: commonSpec.revisionHistoryLimit }, - { op: 'replace', path: '/spec/selector', value: commonSpec.selector }, - { op: 'replace', path: '/spec/template/metadata/labels', value: templateMetadata.labels }, - { - op: 'replace', - path: '/spec/template/spec/containers/0', - value: { - ...commonContainer, - volumeMounts: [...configMapVolumeMounts] - } - }, - { - op: 'replace', - path: '/spec/template/spec/volumes', - value: [...configMapVolumes] - }, - // gpu - { - op: 'replace', - path: '/spec/template/spec/restartPolicy', - value: gpuMap.restartPolicy - }, - { - op: 'replace', - path: '/spec/template/spec/runtimeClassName', - value: gpuMap.runtimeClassName - }, - { - op: 'replace', - path: '/spec/template/spec/nodeSelector', - value: gpuMap.nodeSelector - }, - // status - { - op: 'remove', - path: '/status' - } - ]; - - const statefulsetPatch: Operation[] = [ - { - op: 'replace', - path: '/spec/template/spec/containers/0', - value: { - ...commonContainer, - volumeMounts: [ - ...configMapVolumeMounts, - ...data.storeList.map((item) => ({ - name: item.name, - mountPath: item.path - })) - ] - } - } - ]; - - if (handleType === 'edit' && crYamlList) { - if (type === 'deployment') { - const originYaml = crYamlList.find((i) => i.kind === 'Deployment'); - const cloneYaml = jsonpatch.deepClone(originYaml); - const updated = jsonpatch.applyPatch(cloneYaml, patch); - return yaml.dump(updated.newDocument); - } else { - let originStatefulSetYaml = crYamlList.find((i) => i.kind === 'StatefulSet'); - const deploymentYaml = crYamlList.find((i) => i.kind === 'Deployment'); - - // handle deployment to StatefulSet - if (!originStatefulSetYaml && deploymentYaml?.metadata?.name) { - originStatefulSetYaml = jsonpatch.applyPatch( - jsonpatch.deepClone(deploymentYaml), - differentJsonPatch - ).newDocument; - } - - const cloneYaml = jsonpatch.deepClone(originStatefulSetYaml); - const updated = jsonpatch.applyPatch(cloneYaml, patch.concat(statefulsetPatch)); - return yaml.dump(updated.newDocument); - } - } return yaml.dump(template[type]); }; @@ -363,7 +273,7 @@ export const json2Ingress = (data: AppEditType) => { ? network.customDomain : `${network.publicDomain}.${SEALOS_DOMAIN}`; - const secretName = network.customDomain ? data.appName : INGRESS_SECRET; + const secretName = network.customDomain ? network.networkName : INGRESS_SECRET; const ingress = { apiVersion: 'networking.k8s.io/v1', diff --git a/frontend/providers/applaunchpad/src/utils/tools.ts b/frontend/providers/applaunchpad/src/utils/tools.ts index 30b05158026..88fff9bb043 100644 --- a/frontend/providers/applaunchpad/src/utils/tools.ts +++ b/frontend/providers/applaunchpad/src/utils/tools.ts @@ -233,6 +233,8 @@ export const patchYamlList = ({ newYamlList: string[]; crYamlList: DeployKindsType[]; }) => { + console.log(formOldYamlList, newYamlList, crYamlList, '======='); + const oldFormJsonList = formOldYamlList .map((item) => yaml.loadAll(item)) .flat() as DeployKindsType[]; @@ -355,7 +357,7 @@ export const patchYamlList = ({ }); } }); - + console.log(actions, 'actions'); return actions; }; diff --git a/frontend/providers/dbprovider/src/api/migrate.ts b/frontend/providers/dbprovider/src/api/migrate.ts index 71439376f91..1a1772eb9fe 100644 --- a/frontend/providers/dbprovider/src/api/migrate.ts +++ b/frontend/providers/dbprovider/src/api/migrate.ts @@ -1,5 +1,5 @@ import { DELETE, GET, POST } from '@/services/request'; -import { InternetMigrationCR } from '@/types/migrate'; +import { DumpForm, InternetMigrationCR } from '@/types/migrate'; import { adaptMigrateList } from '@/utils/adapt'; import { V1Pod } from '@kubernetes/client-node'; @@ -39,3 +39,6 @@ export const getPodStatusByName = (podName: string) => export const deleteMigrateJobByName = (name: string) => DELETE(`/api/migrate/delJobByName?name=${name}`); + +export const applyDumpCR = (data: DumpForm) => + POST<{ name: string }>('/api/migrate/createDump', data); diff --git a/frontend/providers/dbprovider/src/pages/api/migrate/createDump.ts b/frontend/providers/dbprovider/src/pages/api/migrate/createDump.ts new file mode 100644 index 00000000000..74eacaad980 --- /dev/null +++ b/frontend/providers/dbprovider/src/pages/api/migrate/createDump.ts @@ -0,0 +1,125 @@ +import { CloudMigraionLabel } from '@/constants/db'; +import { authSession } from '@/services/backend/auth'; +import { getK8s } from '@/services/backend/kubernetes'; +import { jsonRes } from '@/services/backend/response'; +import { ApiResp } from '@/services/kubernet'; +import { DumpForm } from '@/types/migrate'; +import { formatTime } from '@/utils/tools'; +import yaml from 'js-yaml'; +import type { NextApiRequest, NextApiResponse } from 'next'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + const data = req.body as DumpForm; + + const { namespace, applyYamlList } = await getK8s({ + kubeconfig: await authSession(req) + }); + + const { yamlStr, yamlObj } = await json2DumpCR({ + ...data, + namespace + }); + + console.log(yamlStr); + + await applyYamlList([yamlStr], 'create'); + + return jsonRes(res, { + data: { + name: yamlObj.metadata.name + } + }); + } catch (err: any) { + jsonRes(res, { + code: 500, + error: err + }); + } +} + +export const json2DumpCR = async (data: { namespace: string } & DumpForm) => { + const userNS = data.namespace; + const time = formatTime(new Date(), 'YYYYMMDDHHmmss'); + + const MINIO_ACCESS_KEY = process.env.MINIO_ACCESS_KEY; + const MINIO_SECRET_KEY = process.env.MINIO_SECRET_KEY; + const MIGRATE_FILE_IMAGE = process.env.MIGRATE_FILE_IMAGE; + const MINIO_URL = process.env.MINIO_URL; + + const template = { + apiVersion: 'batch/v1', + kind: 'Job', + metadata: { + name: `${userNS}-${time}-${data.dbName}-file`, + labels: { + [CloudMigraionLabel]: data.dbName + } + }, + spec: { + backoffLimit: 3, + template: { + metadata: { + name: `${userNS}-${time}-${data.dbName}-file`, + labels: { + [CloudMigraionLabel]: data.dbName + } + }, + spec: { + restartPolicy: 'Never', + containers: [ + { + name: `${userNS}-${time}-${data.dbName}-file`, + image: MIGRATE_FILE_IMAGE, + env: [ + { + name: 'MINIO_ACCESS_KEY', + value: MINIO_ACCESS_KEY + }, + { + name: 'MINIO_SECRET_KEY', + value: MINIO_SECRET_KEY + }, + { + name: 'MINIO_URL', + value: `https://${MINIO_URL}` + }, + { + name: 'DATABASE_USER', + value: data.databaseUser + }, + { + name: 'DATABASE_PASSWORD', + value: data.databasePassword + }, + { + name: 'DATABASE_HOST', + value: data.databaseHost + }, + { + name: 'FILE_NAME', + value: data.fileName + }, + { + name: 'DATABASE_TYPE', + value: data.databaseType + }, + { + name: 'DATABASE_NAME', + value: data.databaseName + }, + { + name: 'COLLECTION_NAME', + value: data.collectionName + }, + { name: 'TABLES_NAME', value: '' } + ] + } + ] + } + } + } + }; + + return { yamlStr: yaml.dump(template), yamlObj: template }; +}; diff --git a/frontend/providers/dbprovider/src/pages/db/detail/components/DumpImport/index.tsx b/frontend/providers/dbprovider/src/pages/db/detail/components/DumpImport/index.tsx index cd2f60f229b..b9aa2d9b65d 100644 --- a/frontend/providers/dbprovider/src/pages/db/detail/components/DumpImport/index.tsx +++ b/frontend/providers/dbprovider/src/pages/db/detail/components/DumpImport/index.tsx @@ -1,5 +1,6 @@ import { applyYamlList, getDBSecret } from '@/api/db'; import { + applyDumpCR, deleteMigrateJobByName, getLogByNameAndContainerName, getMigratePodList @@ -10,7 +11,6 @@ import MyIcon from '@/components/Icon'; import { useToast } from '@/hooks/useToast'; import { DBDetailType } from '@/types/db'; import { DumpForm } from '@/types/migrate'; -import { json2DumpCR } from '@/utils/json2Yaml'; import { Box, Button, @@ -89,10 +89,8 @@ export default function DumpImport({ db }: { db?: DBDetailType }) { }); } formHook.setValue('fileName', result[0]); - const { yamlStr, yamlObj } = await json2DumpCR({ ...data, fileName: result[0] }); - setMigrateName(yamlObj.metadata.name); - console.log(yamlStr, yamlObj.metadata.name); - await applyYamlList([yamlStr], 'create'); + const res = await applyDumpCR({ ...data, fileName: result[0] }); + setMigrateName(res.name); } catch (error: any) { toast({ title: String(error), diff --git a/frontend/providers/dbprovider/src/utils/json2Yaml.ts b/frontend/providers/dbprovider/src/utils/json2Yaml.ts index 25207389b7d..2519ebe0d42 100644 --- a/frontend/providers/dbprovider/src/utils/json2Yaml.ts +++ b/frontend/providers/dbprovider/src/utils/json2Yaml.ts @@ -917,80 +917,6 @@ export const json2MigrateCR = (data: MigrateForm) => { return yaml.dump(template); }; -export const json2DumpCR = async (data: DumpForm) => { - const userNS = getUserNamespace(); - const time = formatTime(new Date(), 'YYYYMMDDHHmmss'); - const systemEnvs = await getAppEnv(); - - const template = { - apiVersion: 'batch/v1', - kind: 'Job', - metadata: { - name: `${userNS}-${time}-${data.dbName}-file`, - labels: { - [CloudMigraionLabel]: data.dbName - } - }, - spec: { - backoffLimit: 3, - template: { - metadata: { - name: `${userNS}-${time}-${data.dbName}-file`, - labels: { - [CloudMigraionLabel]: data.dbName - } - }, - spec: { - restartPolicy: 'Never', - containers: [ - { - name: `${userNS}-${time}-${data.dbName}-file`, - image: systemEnvs?.migrate_file_image, - env: [ - { - name: 'MINIO_URL', - value: `https://${systemEnvs?.minio_url}` - }, - { - name: 'DATABASE_USER', - value: data.databaseUser - }, - { - name: 'DATABASE_PASSWORD', - value: data.databasePassword - }, - { - name: 'DATABASE_HOST', - value: data.databaseHost - }, - { - name: 'FILE_NAME', - value: data.fileName - }, - { - name: 'DATABASE_TYPE', - value: data.databaseType - }, - { - name: 'DATABASE_NAME', - value: data.databaseName - }, - { - name: 'COLLECTION_NAME', - value: data.collectionName - }, - { name: 'TABLES_NAME', value: '' } - ] - } - ] - } - } - } - }; - - return { yamlStr: yaml.dump(template), yamlObj: template }; -}; - export const json2NetworkService = ({ dbDetail, dbStatefulSet