diff --git a/packages/server/src/services/registry.ts b/packages/server/src/services/registry.ts index f9d3977b9..ad18e0b66 100644 --- a/packages/server/src/services/registry.ts +++ b/packages/server/src/services/registry.ts @@ -16,7 +16,7 @@ function shEscape(s: string | undefined): string { return `'${s.replace(/'/g, `'\\''`)}'`; } -function safeDockerLoginCommand( +export function safeDockerLoginCommand( registry: string | undefined, user: string | undefined, pass: string | undefined, diff --git a/packages/server/src/services/rollbacks.ts b/packages/server/src/services/rollbacks.ts index 00c60ebc8..51d978572 100644 --- a/packages/server/src/services/rollbacks.ts +++ b/packages/server/src/services/rollbacks.ts @@ -23,7 +23,7 @@ import { findDeploymentById } from "./deployment"; import type { Mount } from "./mount"; import type { Port } from "./port"; import type { Project } from "./project"; -import type { Registry } from "./registry"; +import { type Registry, safeDockerLoginCommand } from "./registry"; export const createRollback = async ( input: z.infer, @@ -111,7 +111,7 @@ const deleteRollbackImage = async (image: string, serverId?: string | null) => { const command = `docker image rm ${image} --force`; if (serverId) { - await execAsyncRemote(command, serverId); + await execAsyncRemote(serverId, command); } else { await execAsync(command); } @@ -171,6 +171,23 @@ export const rollback = async (rollbackId: string) => { ); }; +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, @@ -188,6 +205,14 @@ const rollbackApplication = async ( 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