343 lines · 7.8 KB · TypeScript
RawDownload
import type { CreateServiceOptions } from "dockerode";import { eq } from "drizzle-orm";import type { z } from "zod";import { db } from "../db";import {	type createRollbackSchema,	deployments as deploymentsSchema,	rollbacks,} from "../db/schema";import { getRegistryTag } from "../utils/cluster/upload";import {	calculateResources,	generateBindMounts,	generateConfigContainer,	generateVolumeMounts,	prepareEnvironmentVariables,} from "../utils/docker/utils";import { execAsync, execAsyncRemote } from "../utils/process/execAsync";import { getRemoteDocker } from "../utils/servers/remote-docker";import { type Application, findApplicationById } from "./application";import { findDeploymentById } from "./deployment";import type { Mount } from "./mount";import type { Port } from "./port";import type { Project } from "./project";import {	findRegistryByIdWithCredentials,	type Registry,	safeDockerLoginCommand,} from "./registry"; export const createRollback = async (	input: z.infer<typeof createRollbackSchema>,) => {	return await db.transaction(async (tx) => {		const { fullContext, ...other } = input;		const rollback = await tx			.insert(rollbacks)			.values(other)			.returning()			.then((res) => res[0]); 		if (!rollback) {			throw new Error("Failed to create rollback");		} 		const tagImage = `${input.appName}:v${rollback.version}`;		const deployment = await findDeploymentById(rollback.deploymentId); 		if (!deployment?.applicationId) {			throw new Error("Deployment not found");		} 		const {			deployments: _,			bitbucket,			github,			gitlab,			gitea,			...rest		} = await findApplicationById(deployment.applicationId); 		const registry = rest.registryId			? await findRegistryByIdWithCredentials(rest.registryId)			: rest.registry;		const buildRegistry = rest.buildRegistryId			? await findRegistryByIdWithCredentials(rest.buildRegistryId)			: rest.buildRegistry;		const rollbackRegistry = rest.rollbackRegistryId			? await findRegistryByIdWithCredentials(rest.rollbackRegistryId)			: rest.rollbackRegistry; 		const fullContextWithCredentials = {			...rest,			registry,			buildRegistry,			rollbackRegistry,		}; 		await tx			.update(rollbacks)			.set({				image: tagImage,				// eslint-disable-next-line @typescript-eslint/no-explicit-any				fullContext: fullContextWithCredentials as any,			})			.where(eq(rollbacks.rollbackId, rollback.rollbackId)); 		// Update the deployment to reference this rollback		await tx			.update(deploymentsSchema)			.set({				rollbackId: rollback.rollbackId,			})			.where(eq(deploymentsSchema.deploymentId, rollback.deploymentId)); 		const updatedRollback = await tx.query.rollbacks.findFirst({			where: eq(rollbacks.rollbackId, rollback.rollbackId),		}); 		return updatedRollback;	});}; export const findRollbackById = async (rollbackId: string) => {	const result = await db.query.rollbacks.findFirst({		where: eq(rollbacks.rollbackId, rollbackId),		with: {			deployment: {				with: {					application: {						with: {							environment: {								with: {									project: true,								},							},						},					},				},			},		},	}); 	if (!result) {		throw new Error("Rollback not found");	} 	return result;}; const deleteRollbackImage = async (image: string, serverId?: string | null) => {	const command = `docker image rm ${image} --force`; 	if (serverId) {		await execAsyncRemote(serverId, command);	} else {		await execAsync(command);	}}; export const removeRollbackById = async (rollbackId: string) => {	const rollback = await findRollbackById(rollbackId); 	if (!rollback) {		throw new Error("Rollback not found");	} 	if (rollback?.image) {		try {			const deployment = await findDeploymentById(rollback.deploymentId); 			if (!deployment?.applicationId) {				throw new Error("Deployment not found");			} 			const application = await findApplicationById(deployment.applicationId);			await deleteRollbackImage(rollback.image, application.serverId); 			await db				.delete(rollbacks)				.where(eq(rollbacks.rollbackId, rollbackId))				.returning()				.then((res) => res[0]);		} catch (error) {			console.error(error);		}	} 	return rollback;}; export const rollback = async (rollbackId: string) => {	const result = await findRollbackById(rollbackId); 	const deployment = await findDeploymentById(result.deploymentId); 	if (!deployment?.applicationId) {		throw new Error("Deployment not found");	} 	const application = await findApplicationById(deployment.applicationId); 	if (!result.fullContext) {		throw new Error("Rollback context not found");	}	await rollbackApplication(		application.appName,		result.image || "",		application.serverId,		result.fullContext,	);}; const dockerLoginForRegistry = async (	registry: Registry,	serverId?: string | null,) => {	const loginCommand = safeDockerLoginCommand(		registry.registryUrl,		registry.username,		registry.password,	); 	if (serverId) {		await execAsyncRemote(serverId, loginCommand);	} else {		await execAsync(loginCommand);	}}; const rollbackApplication = async (	appName: string,	image: string,	serverId?: string | null,	fullContext?: Application & {		environment: {			project: Project;		};		mounts: Mount[];		ports: Port[];		rollbackRegistry?: Registry | null;	},) => {	if (!fullContext) {		throw new Error("Full context is required for rollback");	} 	const rollbackRegistry = fullContext.rollbackRegistry ?? undefined; 	// Ensure Docker daemon is authenticated with the rollback registry	// before updating the swarm service. The authconfig in CreateServiceOptions	// alone is not sufficient — Docker Swarm also relies on the daemon's	// cached credentials (~/.docker/config.json) to distribute auth to nodes.	if (rollbackRegistry) {		await dockerLoginForRegistry(rollbackRegistry, serverId);	} 	const docker = await getRemoteDocker(serverId); 	const {		env,		mounts,		cpuLimit,		memoryLimit,		memoryReservation,		cpuReservation,		command,		ports,	} = fullContext; 	const resources = calculateResources({		memoryLimit,		memoryReservation,		cpuLimit,		cpuReservation,	}); 	const volumesMount = generateVolumeMounts(mounts); 	const {		HealthCheck,		RestartPolicy,		Placement,		Labels,		Mode,		RollbackConfig,		UpdateConfig,		Networks,		Ulimits,	} = generateConfigContainer(		fullContext as Parameters<typeof generateConfigContainer>[0],	); 	const bindsMount = generateBindMounts(mounts);	const envVariables = prepareEnvironmentVariables(		env,		fullContext.environment.project.env,	); 	let rollbackImage = image;	if (rollbackRegistry) {		rollbackImage = getRegistryTag(rollbackRegistry, image);	} 	const settings: CreateServiceOptions = {		authconfig: {			password: rollbackRegistry?.password || "",			username: rollbackRegistry?.username || "",			serveraddress: rollbackRegistry?.registryUrl || "",		},		Name: appName,		TaskTemplate: {			ContainerSpec: {				HealthCheck,				Image: rollbackImage,				Env: envVariables,				Mounts: [...volumesMount, ...bindsMount],				...(command					? {							Command: ["/bin/sh"],							Args: ["-c", command],						}					: {}),				...(Ulimits && { Ulimits }),				Labels,			},			Networks,			RestartPolicy,			Placement,			Resources: {				...resources,			},		},		Mode,		RollbackConfig,		EndpointSpec: {			Ports: ports.map((port) => ({				PublishMode: port.publishMode,				Protocol: port.protocol,				TargetPort: port.targetPort,				PublishedPort: port.publishedPort,			})),		},		UpdateConfig,	}; 	try {		const service = docker.getService(appName);		const inspect = await service.inspect(); 		await service.update({			version: Number.parseInt(inspect.Version.Index),			...settings,			TaskTemplate: {				...settings.TaskTemplate,				ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1,			},		});	} catch (error) {		console.error(error);		await docker.createService(settings);	}};