init project
This commit is contained in:
+242
@@ -0,0 +1,242 @@
|
||||
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
|
||||
};
|
||||
Reference in New Issue
Block a user