iop
Version:
Ship Docker Anywhere
320 lines • 14 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.performBlueGreenDeployment = performBlueGreenDeployment;
const docker_1 = require("../docker");
const image_utils_1 = require("../utils/image-utils");
const utils_1 = require("../utils");
/**
* Generates blue-green container names based on project name, service name, color, and replica count
*/
function generateContainerNames(projectName, serviceName, color, replicas) {
if (replicas === 1) {
return [`${projectName}-${serviceName}-${color}`];
}
const names = [];
for (let i = 1; i <= replicas; i++) {
names.push(`${projectName}-${serviceName}-${color}-${i}`);
}
return names;
}
/**
* Creates container options for blue-green deployment
*/
function createBlueGreenContainerOptions(serviceEntry, releaseId, secrets, projectName, containerName, fingerprint) {
const imageNameWithRelease = (0, image_utils_1.buildServiceImageName)(serviceEntry, releaseId);
const envVars = resolveEnvironmentVariables(serviceEntry, secrets);
// Dual alias approach: generic name for internal communication + project-specific for proxy routing
const projectSpecificAlias = `${projectName}-${serviceEntry.name}`;
// Add logging configuration for user services
const loggingConfig = (0, docker_1.createServiceLoggingConfig)();
return {
name: containerName,
image: imageNameWithRelease,
ports: serviceEntry.ports,
volumes: (0, utils_1.processVolumes)(serviceEntry.volumes, projectName),
envVars: envVars,
network: `${projectName}-network`,
networkAliases: [
serviceEntry.name, // "web" - for internal project communication
projectSpecificAlias, // "gmail-web" - for proxy routing
],
restart: "unless-stopped",
command: serviceEntry.command,
logDriver: loggingConfig.logDriver,
logOpts: loggingConfig.logOpts,
labels: {
"iop.managed": "true",
"iop.project": projectName,
"iop.type": "service",
"iop.app": serviceEntry.name,
// Add fingerprint labels if provided
...(fingerprint ? {
"iop.fingerprint-type": fingerprint.type,
"iop.runtime-hash": fingerprint.runtimeHash,
...(fingerprint.type === 'external' && fingerprint.imageReference && {
"iop.image-reference": fingerprint.imageReference
}),
} : {}),
},
};
}
/**
* Resolves environment variables for a container from plain and secret sources
*/
function resolveEnvironmentVariables(entry, secrets) {
const envVars = {};
if (entry.environment?.plain) {
for (const envVar of entry.environment.plain) {
const [key, ...valueParts] = envVar.split("=");
if (key && valueParts.length > 0) {
envVars[key] = valueParts.join("=");
}
}
}
if (entry.environment?.secret) {
for (const secretKey of entry.environment.secret) {
if (secrets[secretKey] !== undefined) {
envVars[secretKey] = secrets[secretKey];
}
else {
console.warn(`Secret key "${secretKey}" for entry "${entry.name}" not found in loaded secrets.`);
}
}
}
return envVars;
}
/**
* Performs health checks on all new containers
*/
async function performBlueGreenHealthChecks(containerNames, serviceEntry, dockerClient, serverHostname, projectName, verbose) {
if (verbose) {
console.log(` [${serverHostname}] Running health checks for ${containerNames.length} containers...`);
}
const servicePort = serviceEntry.proxy?.app_port || 3000;
const healthCheckPath = serviceEntry.health_check?.path || "/up";
const healthPromises = containerNames.map(async (containerName) => {
try {
const healthCheckPassed = await dockerClient.checkHealthWithIopProxy("iop-proxy", serviceEntry.name, containerName, projectName, servicePort, healthCheckPath);
if (healthCheckPassed) {
if (verbose) {
console.log(` [${serverHostname}] Health check passed for ${containerName}`);
}
return true;
}
else {
if (verbose) {
console.error(` [${serverHostname}] Health check failed for ${containerName}`);
}
return false;
}
}
catch (error) {
if (verbose) {
console.error(` [${serverHostname}] Health check error for ${containerName}:`, error);
}
return false;
}
});
const healthResults = await Promise.all(healthPromises);
const allHealthy = healthResults.every((result) => result === true);
if (allHealthy) {
if (verbose) {
console.log(` [${serverHostname}] All ${containerNames.length} containers passed health checks ✓`);
}
}
else {
if (verbose) {
console.error(` [${serverHostname}] Health checks failed for some containers`);
}
}
return allHealthy;
}
/**
* Cleans up failed deployment containers
*/
async function cleanupFailedDeployment(containerNames, dockerClient, serverHostname, verbose) {
if (verbose) {
console.log(` [${serverHostname}] Cleaning up failed deployment containers...`);
}
for (const containerName of containerNames) {
try {
await dockerClient.stopContainer(containerName);
await dockerClient.removeContainer(containerName);
if (verbose) {
console.log(` [${serverHostname}] Removed failed container ${containerName}`);
}
}
catch (error) {
if (verbose) {
console.warn(` [${serverHostname}] Could not remove container ${containerName}:`, error);
}
}
}
}
/**
* Main zero-downtime deployment function
*/
async function performBlueGreenDeployment(options) {
const { serviceEntry, // Now directly using serviceEntry from interface
releaseId, secrets, projectName, networkName, dockerClient, serverHostname, verbose = false, fingerprint, } = options;
if (verbose) {
console.log(` [${serverHostname}] Starting zero-downtime deployment for ${serviceEntry.name}...`);
}
try {
// Step 1: Determine deployment color
const currentActiveColor = await dockerClient.getCurrentActiveColorForProject(serviceEntry.name, projectName);
const newColor = currentActiveColor === "blue" ? "green" : "blue";
if (verbose) {
console.log(` [${serverHostname}] Deploying new version...`);
}
// Step 2: Generate container names for new deployment
const replicas = serviceEntry.replicas || 1;
const newContainerNames = generateContainerNames(projectName, serviceEntry.name, newColor, replicas);
if (verbose) {
console.log(` [${serverHostname}] Creating ${replicas} container(s): ${newContainerNames.join(", ")}`);
}
// Step 2.5: Clean up any existing containers with the target color names
// This handles cases where previous deployments failed and left containers behind
if (verbose) {
console.log(` [${serverHostname}] Cleaning up existing ${newColor} containers...`);
}
for (const containerName of newContainerNames) {
try {
const exists = await dockerClient.containerExists(containerName);
if (exists) {
if (verbose) {
console.log(` [${serverHostname}] Removing existing container ${containerName}...`);
}
await dockerClient.stopContainer(containerName);
await dockerClient.removeContainer(containerName);
}
}
catch (error) {
if (verbose) {
console.warn(` [${serverHostname}] Could not remove existing container ${containerName}: ${error}`);
}
// Continue despite cleanup errors - createContainer will give a more specific error if needed
}
}
// Step 3: Create new containers
const deployedContainers = [];
for (let i = 0; i < newContainerNames.length; i++) {
const containerName = newContainerNames[i];
const replicaIndex = i + 1;
const containerOptions = createBlueGreenContainerOptions(serviceEntry, releaseId, secrets, projectName, containerName, options.fingerprint);
if (verbose) {
console.log(` [${serverHostname}] Creating container ${containerName}...`);
}
const success = await dockerClient.createContainerWithLabels(containerOptions, serviceEntry.name, newColor, replicaIndex, false // Not active yet
);
if (!success) {
// Cleanup and abort
await cleanupFailedDeployment(deployedContainers, dockerClient, serverHostname, verbose);
return {
success: false,
newColor,
deployedContainers: [],
error: `Failed to create container ${containerName}`,
};
}
deployedContainers.push(containerName);
}
// Step 4: Health check all new containers (only if ports are exposed)
let allHealthy = true;
if (serviceEntry.ports && serviceEntry.ports.length > 0) {
if (verbose) {
console.log(` [${serverHostname}] Service exposes ports, performing health checks...`);
}
allHealthy = await performBlueGreenHealthChecks(newContainerNames, serviceEntry, dockerClient, serverHostname, projectName, verbose);
if (!allHealthy) {
await cleanupFailedDeployment(deployedContainers, dockerClient, serverHostname, verbose);
return {
success: false,
newColor,
deployedContainers: [],
error: "Health checks failed for new containers",
};
}
}
else {
if (verbose) {
console.log(` [${serverHostname}] Service has no exposed ports, skipping health checks (assuming healthy if running)`);
}
}
// Step 5: Switch network alias (zero-downtime transition)
// Only needed if there are existing containers to switch from
if (currentActiveColor !== null) {
if (verbose) {
console.log(` [${serverHostname}] Switching traffic to new version (zero downtime)...`);
}
const aliasSwitch = await dockerClient.switchNetworkAliasForProject(serviceEntry.name, newColor, networkName, projectName);
if (!aliasSwitch) {
await cleanupFailedDeployment(deployedContainers, dockerClient, serverHostname, verbose);
return {
success: false,
newColor,
deployedContainers: [],
error: "Failed to switch network alias",
};
}
}
else {
if (verbose) {
console.log(` [${serverHostname}] First deployment - network aliases already configured during container creation`);
}
}
// Step 6: Update labels to mark new containers as active
await dockerClient.updateActiveLabels(serviceEntry.name, newColor);
// Step 7: Graceful shutdown of old containers
if (currentActiveColor) {
const oldContainers = await dockerClient.findContainersByLabelAndProject(`iop.app=${serviceEntry.name}`, projectName);
const oldActiveContainers = [];
for (const containerName of oldContainers) {
const labels = await dockerClient.getContainerLabels(containerName);
if (labels["iop.color"] === currentActiveColor) {
oldActiveContainers.push(containerName);
}
}
if (oldActiveContainers.length > 0) {
if (verbose) {
console.log(` [${serverHostname}] Gracefully shutting down ${oldActiveContainers.length} old containers...`);
}
await dockerClient.gracefulShutdown(oldActiveContainers, 30);
// Remove old containers
for (const containerName of oldActiveContainers) {
try {
await dockerClient.removeContainer(containerName);
if (verbose) {
console.log(` [${serverHostname}] Removed old container ${containerName}`);
}
}
catch (error) {
if (verbose) {
console.warn(` [${serverHostname}] Could not remove old container ${containerName}:`, error);
}
}
}
}
}
if (verbose) {
console.log(` [${serverHostname}] Zero-downtime deployment completed successfully ✅`);
}
return {
success: true,
newColor,
deployedContainers,
};
}
catch (error) {
if (verbose) {
console.error(` [${serverHostname}] Deployment failed:`, error);
}
return {
success: false,
newColor: "blue",
deployedContainers: [],
error: error instanceof Error ? error.message : String(error),
};
}
}
//# sourceMappingURL=blue-green.js.map