dockerode
Version:
Docker Remote API module.
243 lines (211 loc) • 6.54 kB
JavaScript
var protobuf = require("protobufjs");
var path = require("path");
// Constants
var BUILDKIT_TRACE_ID = "moby.buildkit.trace";
var BUILDKIT_IMAGE_ID = "moby.image.id";
var PROTO_TYPE = "moby.buildkit.v1.StatusResponse";
var ENCODING_UTF8 = "utf8";
var ENCODING_BASE64 = "base64";
var StatusResponse;
// Load the protobuf schema
function loadProto() {
if (StatusResponse) return StatusResponse;
var root = protobuf.loadSync(
path.resolve(__dirname, "proto", "buildkit_status.proto")
);
StatusResponse = root.lookupType(PROTO_TYPE);
return StatusResponse;
}
/**
* Decodes a BuildKit trace message
* @param {string} base64Data - Base64-encoded protobuf data from aux field
* @returns {Object} Decoded status response with vertexes, logs, etc.
*/
function decodeBuildKitStatus(base64Data) {
var StatusResponse = loadProto();
// Handle empty messages
if (!base64Data || base64Data.length === 0) {
return {
vertexes: [],
statuses: [],
logs: [],
warnings: []
};
}
var buffer = Buffer.from(base64Data, ENCODING_BASE64);
var message = StatusResponse.decode(buffer);
return StatusResponse.toObject(message, {
longs: String,
enums: String,
bytes: String,
defaults: true
});
}
/**
* Formats BuildKit status into human-readable text
* @param {Object} status - Decoded status response
* @returns {string[]} Array of human-readable log lines
*/
function formatBuildKitStatus(status) {
var lines = [];
// Process vertexes (build steps)
if (status.vertexes && status.vertexes.length > 0) {
status.vertexes.forEach(function(vertex) {
if (vertex.name && vertex.started && !vertex.completed) {
lines.push("[" + vertex.digest.substring(0, 12) + "] " + vertex.name);
}
if (vertex.error) {
lines.push("ERROR: " + vertex.error);
}
if (vertex.completed && vertex.cached) {
lines.push("CACHED: " + vertex.name);
}
});
}
// Process logs (command output)
if (status.logs && status.logs.length > 0) {
status.logs.forEach(function(log) {
var msg = Buffer.from(log.msg).toString(ENCODING_UTF8);
if (msg.trim()) {
lines.push(msg.trimEnd());
}
});
}
// Process status updates (progress)
if (status.statuses && status.statuses.length > 0) {
status.statuses.forEach(function(s) {
if (s.name && s.total > 0) {
var percent = Math.floor((s.current / s.total) * 100);
lines.push(s.name + ": " + percent + "% (" + s.current + "/" + s.total + ")");
}
});
}
// Process warnings
if (status.warnings && status.warnings.length > 0) {
status.warnings.forEach(function(warning) {
var msg = Buffer.from(warning.short).toString(ENCODING_UTF8);
lines.push("WARNING: " + msg);
});
}
return lines;
}
/**
* Parse a BuildKit stream line and extract human-readable logs
* @param {string} line - JSON line from build stream
* @returns {Object} { isBuildKit: boolean, logs: string[], raw: Object }
*/
function parseBuildKitLine(line) {
try {
var json = JSON.parse(line);
// Check if it's a BuildKit trace message
if (json.id === BUILDKIT_TRACE_ID && json.aux !== undefined) {
var status = decodeBuildKitStatus(json.aux);
var logs = formatBuildKitStatus(status);
return {
isBuildKit: true,
logs: logs,
raw: status
};
}
// Check if it's the final image ID
if (json.id === BUILDKIT_IMAGE_ID && json.aux && json.aux.ID) {
return {
isBuildKit: true,
logs: ["Built image: " + json.aux.ID],
raw: json.aux
};
}
// Not a BuildKit message
return {
isBuildKit: false,
logs: [],
raw: json
};
} catch (e) {
return {
isBuildKit: false,
logs: [],
raw: null,
error: e.message
};
}
}
/**
* Follow progress of a stream, automatically handling both BuildKit and regular output.
* This provides the same ergonomics as modem.followProgress but decodes BuildKit logs.
*
* @param {Stream} stream - Stream from buildImage(), pull(), push(), etc.
* @param {Function} onFinished - Called when stream ends: (err, output) => void
* @param {Function} onProgress - Called for each log event: (event) => void
* @returns {void}
*/
function followProgress(stream, onFinished, onProgress) {
var buffer = '';
var output = [];
var finished = false;
stream.on('data', onStreamEvent);
stream.on('error', onStreamError);
stream.on('end', onStreamEnd);
stream.on('close', onStreamEnd);
function onStreamEvent(data) {
buffer += data.toString();
// Process complete lines
var lines = buffer.split('\n');
buffer = lines.pop(); // Save incomplete line
lines.forEach(function(line) {
if (!line.trim()) return;
processLine(line);
});
}
function processLine(line) {
try {
// Try to parse as BuildKit or regular Docker output
var result = parseBuildKitLine(line);
if (result.isBuildKit) {
// BuildKit message - create events from decoded logs
result.logs.forEach(function(log) {
var event = { stream: log + '\n' };
output.push(event);
if (onProgress) onProgress(event);
});
} else if (result.raw) {
// Regular Docker message
output.push(result.raw);
if (onProgress) onProgress(result.raw);
}
} catch (e) {
// If parsing fails, try plain JSON
try {
var json = JSON.parse(line);
output.push(json);
if (onProgress) onProgress(json);
} catch (e2) {
// Ignore parse errors
}
}
}
function onStreamError(err) {
finished = true;
stream.removeListener('data', onStreamEvent);
stream.removeListener('error', onStreamError);
stream.removeListener('end', onStreamEnd);
stream.removeListener('close', onStreamEnd);
if (onFinished) onFinished(err, output);
}
function onStreamEnd() {
if (finished) return;
finished = true;
// Process any remaining data in buffer
if (buffer.trim()) {
processLine(buffer);
}
stream.removeListener('data', onStreamEvent);
stream.removeListener('error', onStreamError);
stream.removeListener('end', onStreamEnd);
stream.removeListener('close', onStreamEnd);
if (onFinished) onFinished(null, output);
}
}
module.exports = {
followProgress: followProgress
};