mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-19 06:05:25 +02:00
* fix: strip credentials from service-level API responses
Registry passwords and S3 destination credentials were being returned
in service `.one` tRPC endpoints to any user with service-level read
access. Reported by Nihon Kohden Corporation security team.
- Strip registry `password` from `findApplicationById` via Drizzle `columns: { password: false }`
- Strip destination `accessKey`/`secretAccessKey` from all DB service finders (postgres, mysql, mariadb, mongo, libsql, compose, backup, volume-backups)
- Add `findRegistryByIdWithCredentials` for internal use only
- Builders and upload utils now load registry credentials by ID at execution time
- `createRollback` enriches `fullContext` with registry credentials before persisting to DB so rollback execution has what it needs
- Remove `findApplicationByIdWithCredentials` and `ApplicationNestedWithCredentials` — no longer needed
- Backup execution utils load full destination via `findDestinationById` at runtime instead of reading from the joined relation
* [autofix.ci] apply automated fixes
---------
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
344 lines
7.8 KiB
TypeScript
344 lines
7.8 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 { 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);
|
|
}
|
|
};
|