mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-15 20:25:23 +02:00
323 lines
7.5 KiB
TypeScript
323 lines
7.5 KiB
TypeScript
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, 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);
|
|
|
|
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(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");
|
|
}
|
|
// Use the full context for rollback
|
|
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;
|
|
},
|
|
) => {
|
|
if (!fullContext) {
|
|
throw new Error("Full context is required for rollback");
|
|
}
|
|
|
|
// 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 (fullContext.rollbackRegistry) {
|
|
await dockerLoginForRegistry(fullContext.rollbackRegistry, serverId);
|
|
}
|
|
|
|
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,
|
|
Ulimits,
|
|
} = 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],
|
|
}
|
|
: {}),
|
|
...(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);
|
|
}
|
|
};
|