Skip to content

Commit b5dd7b9

Browse files
committed
add ownerReferences
1 parent 91ce295 commit b5dd7b9

File tree

6 files changed

+241
-20
lines changed

6 files changed

+241
-20
lines changed

frontend/providers/applaunchpad/src/constants/app.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ export const gpuNodeSelectorKey = 'nvidia.com/gpu.product';
9797
export const gpuResourceKey = 'nvidia.com/gpu';
9898
export const templateDeployKey = 'cloud.sealos.io/deploy-on-sealos';
9999
export const sealafDeployKey = 'sealaf-app';
100+
export const ownerReferencesKey = 'cloud.sealos.io/owner-references';
101+
export const ownerReferencesReadyValue = 'ready';
100102

101103
export enum Coin {
102104
cny = 'cny',

frontend/providers/applaunchpad/src/pages/api/applyApp.ts

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { ApiResp } from '@/services/kubernet';
33
import { authSession } from '@/services/backend/auth';
44
import { getK8s } from '@/services/backend/kubernetes';
55
import { handleK8sError, jsonRes } from '@/services/backend/response';
6+
import yaml from 'js-yaml';
7+
import { generateOwnerReference, shouldHaveOwnerReference } from '@/utils/deployYaml2Json';
68

79
export default async function handler(req: NextApiRequest, res: NextApiResponse<ApiResp>) {
810
const { yamlList, mode = 'create' }: { yamlList: string[]; mode?: 'create' | 'replace' } =
@@ -15,13 +17,87 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
1517
return;
1618
}
1719
try {
18-
const { applyYamlList } = await getK8s({
20+
const { k8sApp, applyYamlList, namespace } = await getK8s({
1921
kubeconfig: await authSession(req.headers)
2022
});
2123

22-
const applyRes = await applyYamlList(yamlList, mode);
24+
// Parse all resources from YAML list
25+
const allResources: any[] = [];
26+
yamlList.forEach((yamlStr) => {
27+
const resources = yaml.loadAll(yamlStr).filter((item) => item);
28+
allResources.push(...resources);
29+
});
30+
31+
console.log(allResources, 'allResources');
32+
// Separate main workload (Deployment or StatefulSet) from dependent resources
33+
const mainWorkloadIndex = allResources.findIndex(
34+
(resource) => resource.kind === 'Deployment' || resource.kind === 'StatefulSet'
35+
);
36+
37+
if (mainWorkloadIndex === -1) {
38+
// No main workload found, use legacy flow
39+
const applyRes = await applyYamlList(yamlList, mode);
40+
jsonRes(res, { data: applyRes.map((item) => item.kind) });
41+
return;
42+
}
43+
44+
const mainWorkload = allResources[mainWorkloadIndex];
45+
const dependentResources = allResources.filter((_, index) => index !== mainWorkloadIndex);
46+
47+
// Phase 1: Create main workload first
48+
const mainWorkloadYaml = yaml.dump(mainWorkload);
49+
50+
await applyYamlList([mainWorkloadYaml], mode);
51+
52+
// Phase 2: Get UID from created workload
53+
let workloadUid: string;
54+
try {
55+
if (mainWorkload.kind === 'Deployment') {
56+
const deployment = await k8sApp.readNamespacedDeployment(
57+
mainWorkload.metadata.name,
58+
namespace
59+
);
60+
workloadUid = deployment.body.metadata?.uid || '';
61+
} else {
62+
const statefulSet = await k8sApp.readNamespacedStatefulSet(
63+
mainWorkload.metadata.name,
64+
namespace
65+
);
66+
workloadUid = statefulSet.body.metadata?.uid || '';
67+
}
68+
} catch (err) {
69+
console.error('Failed to get workload UID:', err);
70+
throw new Error('Failed to get workload UID after creation');
71+
}
72+
73+
if (!workloadUid) {
74+
throw new Error('Workload UID is empty');
75+
}
76+
77+
// Phase 3: Add ownerReferences to dependent resources
78+
const ownerReferences = generateOwnerReference(
79+
mainWorkload.metadata.name,
80+
mainWorkload.kind,
81+
workloadUid
82+
);
83+
84+
dependentResources.forEach((resource) => {
85+
if (shouldHaveOwnerReference(resource.kind)) {
86+
if (!resource.metadata) {
87+
resource.metadata = {};
88+
}
89+
resource.metadata.ownerReferences = ownerReferences;
90+
}
91+
});
92+
93+
// Phase 4: Create dependent resources
94+
if (dependentResources.length > 0) {
95+
const dependentYamlList = dependentResources.map((resource) => yaml.dump(resource));
96+
await applyYamlList(dependentYamlList, mode);
97+
}
2398

24-
jsonRes(res, { data: applyRes.map((item) => item.kind) });
99+
const allKinds = [mainWorkload, ...dependentResources].map((item) => item.kind);
100+
jsonRes(res, { data: allKinds });
25101
} catch (err: any) {
26102
console.log(err);
27103
jsonRes(res, handleK8sError(err));

frontend/providers/applaunchpad/src/pages/api/delApp.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { ApiResp } from '@/services/kubernet';
33
import { authSession } from '@/services/backend/auth';
44
import { getK8s } from '@/services/backend/kubernetes';
55
import { jsonRes } from '@/services/backend/response';
6-
import { appDeployKey } from '@/constants/app';
6+
import { appDeployKey, ownerReferencesKey, ownerReferencesReadyValue } from '@/constants/app';
77

88
export type DeleteAppParams = { name: string };
99

@@ -33,6 +33,44 @@ export async function DeleteAppByName({ name, req }: DeleteAppParams & { req: Ne
3333
kubeconfig: await authSession(req.headers)
3434
});
3535

36+
// Check if app has ownerReferences annotation (new apps)
37+
let hasOwnerReferences = false;
38+
let workloadKind: 'Deployment' | 'StatefulSet' | null = null;
39+
40+
try {
41+
// Try to read Deployment first
42+
const deployment = await k8sApp.readNamespacedDeployment(name, namespace);
43+
const annotation = deployment.body.metadata?.annotations?.[ownerReferencesKey];
44+
hasOwnerReferences = annotation === ownerReferencesReadyValue;
45+
workloadKind = 'Deployment';
46+
} catch (err: any) {
47+
if (err?.body?.code !== 404) {
48+
console.error('Error reading Deployment:', err);
49+
}
50+
// Try to read StatefulSet
51+
try {
52+
const statefulSet = await k8sApp.readNamespacedStatefulSet(name, namespace);
53+
const annotation = statefulSet.body.metadata?.annotations?.[ownerReferencesKey];
54+
hasOwnerReferences = annotation === ownerReferencesReadyValue;
55+
workloadKind = 'StatefulSet';
56+
} catch (err2: any) {
57+
if (err2?.body?.code !== 404) {
58+
console.error('Error reading StatefulSet:', err2);
59+
}
60+
}
61+
}
62+
63+
// If app has ownerReferences, only delete main workload (cascade delete handles rest)
64+
if (hasOwnerReferences && workloadKind) {
65+
if (workloadKind === 'Deployment') {
66+
await k8sApp.deleteNamespacedDeployment(name, namespace);
67+
} else {
68+
await k8sApp.deleteNamespacedStatefulSet(name, namespace);
69+
}
70+
return;
71+
}
72+
73+
// Legacy delete flow for apps without ownerReferences
3674
// delete Certificate
3775
const certificatesList = (await k8sCustomObjects.listNamespacedCustomObject(
3876
'cert-manager.io',

frontend/providers/applaunchpad/src/pages/api/updateApp.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type { AppPatchPropsType } from '@/types/app';
99
import { initK8s } from 'sealos-desktop-sdk/service';
1010
import { errLog, infoLog, warnLog } from 'sealos-desktop-sdk';
1111
import type { V1Service } from '@kubernetes/client-node';
12+
import { generateOwnerReference, shouldHaveOwnerReference } from '@/utils/deployYaml2Json';
1213

1314
export type Props = {
1415
patch: AppPatchPropsType;
@@ -364,7 +365,53 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
364365
return item.value;
365366
})
366367
.filter((item) => item);
367-
await applyYamlList(createYamlList as string[], 'create');
368+
369+
// Add ownerReferences to newly created resources
370+
if (createYamlList.length > 0) {
371+
// Get workload UID
372+
let workloadUid: string | undefined;
373+
let workloadKind: 'Deployment' | 'StatefulSet' | undefined;
374+
375+
try {
376+
// Try to read Deployment first
377+
const deployment = await k8sApp.readNamespacedDeployment(appName, namespace);
378+
workloadUid = deployment.body.metadata?.uid;
379+
workloadKind = 'Deployment';
380+
} catch (err: any) {
381+
if (err?.body?.code === 404) {
382+
// Try StatefulSet
383+
try {
384+
const statefulSet = await k8sApp.readNamespacedStatefulSet(appName, namespace);
385+
workloadUid = statefulSet.body.metadata?.uid;
386+
workloadKind = 'StatefulSet';
387+
} catch (err2) {
388+
warnLog('Could not find workload for ownerReferences', { appName });
389+
}
390+
}
391+
}
392+
393+
// Add ownerReferences to new resources
394+
if (workloadUid && workloadKind) {
395+
const ownerReferences = generateOwnerReference(appName, workloadKind, workloadUid);
396+
const updatedCreateYamlList = createYamlList.map((yamlStr) => {
397+
const resource = yaml.load(yamlStr as string) as any;
398+
if (shouldHaveOwnerReference(resource.kind)) {
399+
if (!resource.metadata) {
400+
resource.metadata = {};
401+
}
402+
resource.metadata.ownerReferences = ownerReferences;
403+
infoLog('Added ownerReferences to new resource', {
404+
kind: resource.kind,
405+
name: resource.metadata.name
406+
});
407+
}
408+
return yaml.dump(resource);
409+
});
410+
await applyYamlList(updatedCreateYamlList, 'create');
411+
} else {
412+
await applyYamlList(createYamlList as string[], 'create');
413+
}
414+
}
368415

369416
// delete
370417
await Promise.all(

frontend/providers/applaunchpad/src/pages/app/edit/index.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type { YamlItemType } from '@/types';
1111
import type { AppEditSyncedFields, AppEditType, DeployKindsType } from '@/types/app';
1212
import { adaptEditAppData } from '@/utils/adapt';
1313
import {
14+
generateOwnerReference,
1415
json2ConfigMap,
1516
json2DeployCr,
1617
json2HPA,
@@ -268,7 +269,22 @@ const EditApp = ({ appName, tabType }: { appName?: string; tabType: string }) =>
268269
if (data.networks?.[index]) {
269270
data.networks[index].customDomain = customDomain;
270271
}
271-
const ingressYaml = json2Ingress(data);
272+
273+
// Get ownerReferences from existing workload
274+
let ownerReferences: any[] | undefined;
275+
const workload = crOldYamls.current.find(
276+
(item) => item.kind === YamlKindEnum.Deployment || item.kind === YamlKindEnum.StatefulSet
277+
);
278+
if (workload) {
279+
const workloadObj = yaml.load(workload.value) as any;
280+
const workloadUid = workloadObj?.metadata?.uid;
281+
const workloadKind = workload.kind as 'Deployment' | 'StatefulSet';
282+
if (workloadUid && workloadKind) {
283+
ownerReferences = generateOwnerReference(data.appName, workloadKind, workloadUid);
284+
}
285+
}
286+
287+
const ingressYaml = json2Ingress(data, ownerReferences);
272288
setIsLoading(true);
273289
postDeployApp([ingressYaml], 'replace')
274290
.then(() => {
@@ -281,7 +297,7 @@ const EditApp = ({ appName, tabType }: { appName?: string; tabType: string }) =>
281297
.finally(() => setIsLoading(false));
282298
} catch (error) {}
283299
},
284-
[formHook, setIsLoading, toast, t]
300+
[formHook, setIsLoading, toast, t, appName]
285301
);
286302

287303
useQuery(

0 commit comments

Comments
 (0)