diff --git a/packages/server/src/services/project-export-import/import-job.ts b/packages/server/src/services/project-export-import/import-job.ts index 97e7d970b..00b439c90 100644 --- a/packages/server/src/services/project-export-import/import-job.ts +++ b/packages/server/src/services/project-export-import/import-job.ts @@ -91,6 +91,7 @@ const executeImport = async ( ...postApplyErrors, ...(await restoreDumps(ctx.db, snapshot, result, workDir, { emit: (message) => updateImportJob(importId, { message }), + skipServiceIds: new Set(postApplyErrors.map((error) => error.serviceId)), })), ]; updateImportJob(importId, { restoreErrors, deployCandidates }); diff --git a/packages/server/src/services/project-export-import/plan.ts b/packages/server/src/services/project-export-import/plan.ts index 83303b71e..27ae7359b 100644 --- a/packages/server/src/services/project-export-import/plan.ts +++ b/packages/server/src/services/project-export-import/plan.ts @@ -885,7 +885,12 @@ export const validatePlan = async ( if (!row || row.organizationId !== organizationId) { throw new Error("Not found in this organization"); } - if ((row.serverId ?? null) !== probeServerIdForRequirement(key)) { + const requirement = requirementByKey.get(key); + if ( + requirement && + (row.serverId ?? null) !== + networkDestServerId(requirement, serviceServerMap, plan.serverMap) + ) { throw new Error( `Network ${row.name} is on a different server than the services that use it`, ); diff --git a/packages/server/src/services/project-export-import/post-apply.ts b/packages/server/src/services/project-export-import/post-apply.ts index bbc397e5c..ffac92d0e 100644 --- a/packages/server/src/services/project-export-import/post-apply.ts +++ b/packages/server/src/services/project-export-import/post-apply.ts @@ -141,20 +141,29 @@ const writeCertificateFiles = async ( plan: Plan, result: TransferResult, emit: Emit, -): Promise => { +): Promise => { + const errors: RestoreError[] = []; for (const requirement of snapshot.requirements) { if (requirement.kind !== "certificate" || !requirement.originalId) continue; const resolution = plan.resolutions[requirementKey(requirement)]; if (resolution?.action !== "create") continue; const newCertificateId = result.idMap.certificate?.[requirement.originalId]; if (!newCertificateId) continue; - const row = await db.query.certificates.findFirst({ - where: (fields, { eq }) => eq(fields.certificateId, newCertificateId), - }); - if (!row) continue; - await createCertificateFiles(row as Certificate); - emit(`Wrote certificate files for ${row.name}`); + try { + const row = await db.query.certificates.findFirst({ + where: (fields, { eq }) => eq(fields.certificateId, newCertificateId), + }); + if (!row) continue; + await createCertificateFiles(row as Certificate); + emit(`Wrote certificate files for ${row.name}`); + } catch (error) { + errors.push({ + serviceId: newCertificateId, + error: safeErrorMessage(error), + }); + } } + return errors; }; const writeRawTraefikConfig = async ( @@ -235,7 +244,8 @@ const writeFileMounts = async ( snapshot: Snapshot, result: TransferResult, emit: Emit, -): Promise => { +): Promise => { + const errors: RestoreError[] = []; for (const service of snapshot.resources.services) { const kind = service.kind as ServiceType; const oldId = service[SERVICE_ID_FIELD[kind]] as string | undefined; @@ -246,15 +256,24 @@ const writeFileMounts = async ( const ownerField = SERVICE_ID_FIELD[ kind ] as keyof typeof mounts.$inferSelect; - const fileMounts = await db.query.mounts.findMany({ - where: (fields, { eq, and }) => - and(eq(fields[ownerField], newId), eq(fields.type, "file")), - }); - for (const mount of fileMounts) { - await createFileMount(mount.mountId); - emit(`Wrote mount file ${mount.filePath} for ${newId}`); + try { + const fileMounts = await db.query.mounts.findMany({ + where: (fields, { eq, and }) => + and(eq(fields[ownerField], newId), eq(fields.type, "file")), + }); + for (const mount of fileMounts) { + await createFileMount(mount.mountId); + emit(`Wrote mount file ${mount.filePath} for ${newId}`); + } + } catch (error) { + errors.push({ + serviceId: newId, + serviceName: (service.name as string | undefined) ?? newId, + error: safeErrorMessage(error), + }); } } + return errors; }; export const postApply = async ( @@ -337,8 +356,10 @@ export const postApply = async ( postApplyErrors.push( ...(await createImportedNetworks(snapshot, plan, result, emit)), ); - await writeCertificateFiles(db, snapshot, plan, result, emit); - await writeFileMounts(db, snapshot, result, emit); + postApplyErrors.push( + ...(await writeCertificateFiles(db, snapshot, plan, result, emit)), + ...(await writeFileMounts(db, snapshot, result, emit)), + ); return { deployCandidates, postApplyErrors }; }; diff --git a/packages/server/src/services/project-export-import/restore.ts b/packages/server/src/services/project-export-import/restore.ts index 93d4c34e9..087289ca6 100644 --- a/packages/server/src/services/project-export-import/restore.ts +++ b/packages/server/src/services/project-export-import/restore.ts @@ -180,6 +180,7 @@ export type RestoreDumpsOptions = { emit?: Emit; deploy?: Partial>; readiness?: ReadinessOptions; + skipServiceIds?: Set; }; export const restoreDumps = async ( @@ -228,6 +229,15 @@ export const restoreDumps = async ( }); continue; } + if (opts.skipServiceIds?.has(newServiceId)) { + restoreErrors.push({ + serviceId: newServiceId, + serviceName, + error: + "Dump not restored because the service's files could not be written", + }); + continue; + } try { const row = await (