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 type { ApplicationNested } from "../utils/builders";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 type { Registry } 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); await tx .update(rollbacks) .set({ image: tagImage, fullContext: rest, }) .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(command, serverId); } 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"); } // Use the full context for rollback await rollbackApplication( application.appName, result.image || "", application.serverId, result.fullContext, );}; const rollbackApplication = async ( appName: string, image: string, serverId?: string | null, fullContext?: Application & { environment: { project: Project; }; mounts: Mount[]; ports: Port[]; rollbackRegistry?: Registry; },) => { if (!fullContext) { throw new Error("Full context is required for rollback"); } const docker = await getRemoteDocker(serverId); // Use the same configuration as mechanizeDockerContainer 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, } = generateConfigContainer(fullContext as ApplicationNested); const bindsMount = generateBindMounts(mounts); const envVariables = prepareEnvironmentVariables( env, fullContext.environment.project.env, ); // Build the full registry image path if rollbackRegistry is available // e.g., "appName:v5" -> "siumauricio/appName:v5" or "registry.com/prefix/appName:v5" let rollbackImage = image; if (fullContext.rollbackRegistry) { rollbackImage = getRegistryTag(fullContext.rollbackRegistry, image); } const settings: CreateServiceOptions = { authconfig: { password: fullContext.rollbackRegistry?.password || "", username: fullContext.rollbackRegistry?.username || "", serveraddress: fullContext.rollbackRegistry?.registryUrl || "", }, Name: appName, TaskTemplate: { ContainerSpec: { HealthCheck, Image: rollbackImage, Env: envVariables, Mounts: [...volumesMount, ...bindsMount], ...(command ? { Command: ["/bin/sh"], Args: ["-c", command], } : {}), 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); }};
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 type { ApplicationNested } from "../utils/builders";
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 type { Registry } 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);
await tx
.update(rollbacks)
.set({
image: tagImage,
fullContext: rest,
})
.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(command, serverId);
} 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");
}
// Use the full context for rollback
await rollbackApplication(
application.appName,
result.image || "",
application.serverId,
result.fullContext,
);
};
const rollbackApplication = async (
appName: string,
image: string,
serverId?: string | null,
fullContext?: Application & {
environment: {
project: Project;
};
mounts: Mount[];
ports: Port[];
rollbackRegistry?: Registry;
},
) => {
if (!fullContext) {
throw new Error("Full context is required for rollback");
}
const docker = await getRemoteDocker(serverId);
// Use the same configuration as mechanizeDockerContainer
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,
} = generateConfigContainer(fullContext as ApplicationNested);
const bindsMount = generateBindMounts(mounts);
const envVariables = prepareEnvironmentVariables(
env,
fullContext.environment.project.env,
);
// Build the full registry image path if rollbackRegistry is available
// e.g., "appName:v5" -> "siumauricio/appName:v5" or "registry.com/prefix/appName:v5"
let rollbackImage = image;
if (fullContext.rollbackRegistry) {
rollbackImage = getRegistryTag(fullContext.rollbackRegistry, image);
}
const settings: CreateServiceOptions = {
authconfig: {
password: fullContext.rollbackRegistry?.password || "",
username: fullContext.rollbackRegistry?.username || "",
serveraddress: fullContext.rollbackRegistry?.registryUrl || "",
},
Name: appName,
TaskTemplate: {
ContainerSpec: {
HealthCheck,
Image: rollbackImage,
Env: envVariables,
Mounts: [...volumesMount, ...bindsMount],
...(command
? {
Command: ["/bin/sh"],
Args: ["-c", command],
}
: {}),
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);
}
};