243 lines
6.4 KiB
JavaScript
243 lines
6.4 KiB
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
|
|
};
|