262 lines
8.6 KiB
JavaScript
262 lines
8.6 KiB
JavaScript
#!/usr/bin/env node
|
|
const { execFileSync } = require("child_process");
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
|
|
function exitError(message, code = 1) {
|
|
console.error("ERROR: " + message);
|
|
process.exit(code);
|
|
}
|
|
|
|
function run(command, args) {
|
|
return execFileSync(command, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
}
|
|
|
|
function ensureDocker() {
|
|
try {
|
|
run("docker", ["version", "--format", "{{.Server.Version}}"]);
|
|
} catch (e) {
|
|
exitError("docker command is not executable, or Docker daemon is unavailable");
|
|
}
|
|
}
|
|
|
|
function inspectContainer(container) {
|
|
try {
|
|
const text = run("docker", ["inspect", container]);
|
|
const data = JSON.parse(text);
|
|
if (!Array.isArray(data) || data.length === 0) {
|
|
exitError("container not found: " + container);
|
|
}
|
|
return data[0];
|
|
} catch (e) {
|
|
if (e instanceof SyntaxError) {
|
|
exitError("docker inspect output is not valid JSON");
|
|
}
|
|
exitError("docker inspect failed: " + container);
|
|
}
|
|
}
|
|
|
|
function quoteSh(value) {
|
|
if (value === undefined || value === null) {
|
|
return "''";
|
|
}
|
|
const text = String(value);
|
|
if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(text)) {
|
|
return text;
|
|
}
|
|
return "'" + text.replace(/'/g, "'\\''") + "'";
|
|
}
|
|
|
|
function quoteMountSpec(value) {
|
|
const text = String(value);
|
|
if (text.startsWith("$(pwd)")) {
|
|
const rest = text.slice("$(pwd)".length).replace(/(["\\`$])/g, "\\$1");
|
|
return '"$(pwd)' + rest + '"';
|
|
}
|
|
return quoteSh(text);
|
|
}
|
|
|
|
function normalizeContainerName(name) {
|
|
return (name || "container").replace(/^\/+/, "") || "container";
|
|
}
|
|
|
|
function isLikelyProjectPath(source) {
|
|
if (!source) return false;
|
|
const normalized = source.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
if (normalized === "/") return false;
|
|
if (/^[A-Za-z]:$/i.test(normalized)) return false;
|
|
|
|
const specialPrefixes = [
|
|
"/bin", "/boot", "/dev", "/etc", "/lib", "/lib64", "/proc", "/run", "/sbin", "/sys", "/tmp", "/usr", "/var",
|
|
"/opt/docker", "/var/lib/docker", "/var/run/docker.sock",
|
|
"C:/Program Files", "C:/Program Files (x86)", "C:/ProgramData", "C:/Windows"
|
|
];
|
|
return !specialPrefixes.some(prefix => normalized === prefix || normalized.startsWith(prefix + "/"));
|
|
}
|
|
|
|
function relativeToPwdIfProject(source) {
|
|
if (!isLikelyProjectPath(source)) {
|
|
return source;
|
|
}
|
|
|
|
const normalized = source.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
const base = path.posix.basename(normalized);
|
|
if (!base || base === "." || base === "/") {
|
|
return source;
|
|
}
|
|
return "$(pwd)/" + base;
|
|
}
|
|
|
|
function collectEnv(config) {
|
|
const env = Array.isArray(config.Env) ? config.Env : [];
|
|
return env
|
|
.filter(item => typeof item === "string" && item.length > 0)
|
|
.map(item => " -e " + quoteSh(item) + " \\");
|
|
}
|
|
|
|
function collectPorts(networkSettings, hostConfig) {
|
|
const lines = [];
|
|
const seen = new Set();
|
|
const ports = Object.assign({}, networkSettings && networkSettings.Ports ? networkSettings.Ports : {});
|
|
const bindings = hostConfig && hostConfig.PortBindings ? hostConfig.PortBindings : {};
|
|
for (const containerPort of Object.keys(bindings)) {
|
|
if (!ports[containerPort]) ports[containerPort] = bindings[containerPort];
|
|
}
|
|
|
|
for (const containerPort of Object.keys(ports).sort()) {
|
|
const entries = ports[containerPort];
|
|
if (!Array.isArray(entries)) continue;
|
|
for (const entry of entries) {
|
|
if (!entry || !entry.HostPort) continue;
|
|
const hostIp = entry.HostIp && entry.HostIp !== "0.0.0.0" && entry.HostIp !== "::" ? entry.HostIp + ":" : "";
|
|
const spec = hostIp + entry.HostPort + ":" + containerPort;
|
|
if (seen.has(spec)) continue;
|
|
seen.add(spec);
|
|
lines.push(" -p " + quoteSh(spec) + " \\");
|
|
}
|
|
}
|
|
return lines;
|
|
}
|
|
|
|
function collectMounts(info) {
|
|
const mounts = Array.isArray(info.Mounts) ? info.Mounts : [];
|
|
const lines = [];
|
|
for (const mount of mounts) {
|
|
if (!mount || !mount.Destination) continue;
|
|
if (mount.Type === "volume" && mount.Name) {
|
|
let spec = mount.Name + ":" + mount.Destination;
|
|
if (mount.RW === false) spec += ":ro";
|
|
lines.push(" -v " + quoteMountSpec(spec) + " \\");
|
|
continue;
|
|
}
|
|
if (mount.Source) {
|
|
let spec = relativeToPwdIfProject(mount.Source) + ":" + mount.Destination;
|
|
if (mount.RW === false) spec += ":ro";
|
|
lines.push(" -v " + quoteMountSpec(spec) + " \\");
|
|
}
|
|
}
|
|
return lines;
|
|
}
|
|
|
|
function collectNetworks(info) {
|
|
const hostConfig = info.HostConfig || {};
|
|
const networkMode = hostConfig.NetworkMode;
|
|
const lines = [];
|
|
if (networkMode && networkMode !== "default" && networkMode !== "bridge") {
|
|
lines.push(" --network " + quoteSh(networkMode) + " \\");
|
|
return lines;
|
|
}
|
|
|
|
const networkSettings = info.NetworkSettings || {};
|
|
const networks = networkSettings.Networks ? Object.keys(networkSettings.Networks) : [];
|
|
if (networks.length === 1 && networks[0] !== "bridge") {
|
|
lines.push(" --network " + quoteSh(networks[0]) + " \\");
|
|
}
|
|
return lines;
|
|
}
|
|
|
|
function collectSimpleOptions(info) {
|
|
const config = info.Config || {};
|
|
const hostConfig = info.HostConfig || {};
|
|
const lines = [];
|
|
|
|
if (hostConfig.RestartPolicy && hostConfig.RestartPolicy.Name && hostConfig.RestartPolicy.Name !== "no") {
|
|
let restart = hostConfig.RestartPolicy.Name;
|
|
if (restart === "on-failure" && hostConfig.RestartPolicy.MaximumRetryCount) {
|
|
restart += ":" + hostConfig.RestartPolicy.MaximumRetryCount;
|
|
}
|
|
lines.push(" --restart " + quoteSh(restart) + " \\");
|
|
}
|
|
if (config.WorkingDir) lines.push(" -w " + quoteSh(config.WorkingDir) + " \\");
|
|
if (config.User) lines.push(" -u " + quoteSh(config.User) + " \\");
|
|
if (hostConfig.Privileged) lines.push(" --privileged \\");
|
|
if (config.Tty) lines.push(" -t \\");
|
|
if (config.OpenStdin) lines.push(" -i \\");
|
|
|
|
return lines;
|
|
}
|
|
|
|
function commandTail(config) {
|
|
let entrypoint = [];
|
|
if (Array.isArray(config.Entrypoint)) {
|
|
entrypoint = config.Entrypoint.filter(item => item !== null && item !== undefined).map(String);
|
|
} else if (typeof config.Entrypoint === "string" && config.Entrypoint) {
|
|
entrypoint = [config.Entrypoint];
|
|
}
|
|
|
|
const cmd = Array.isArray(config.Cmd) ? config.Cmd : (typeof config.Cmd === "string" && config.Cmd ? [config.Cmd] : []);
|
|
return {
|
|
entrypoint: entrypoint.length > 0 ? entrypoint[0] : null,
|
|
args: entrypoint.slice(1).concat(cmd)
|
|
};
|
|
}
|
|
|
|
function buildScript(info) {
|
|
const config = info.Config || {};
|
|
const hostConfig = info.HostConfig || {};
|
|
const name = normalizeContainerName(info.Name);
|
|
const image = config.Image || info.Image || "";
|
|
const tail = commandTail(config);
|
|
const lines = [
|
|
"#!/usr/bin/env bash",
|
|
"set -euo pipefail",
|
|
"",
|
|
"name=" + quoteSh(name),
|
|
"img=" + quoteSh(image),
|
|
"",
|
|
"docker run -d \\",
|
|
" --name \"$name\" \\",
|
|
];
|
|
|
|
lines.push(...collectSimpleOptions(info));
|
|
lines.push(...collectEnv(config));
|
|
lines.push(...collectPorts(info.NetworkSettings || {}, hostConfig));
|
|
lines.push(...collectMounts(info));
|
|
lines.push(...collectNetworks(info));
|
|
|
|
if (tail.entrypoint) {
|
|
lines.push(" --entrypoint " + quoteSh(tail.entrypoint) + " \\");
|
|
}
|
|
|
|
const cmd = tail.args.map(quoteSh).join(" ");
|
|
if (cmd) {
|
|
lines.push(" \"$img\" " + cmd);
|
|
} else {
|
|
lines.push(" \"$img\"");
|
|
}
|
|
|
|
lines.push("");
|
|
return lines.join("\n");
|
|
}
|
|
|
|
function outputFile(container, explicitOutput) {
|
|
if (explicitOutput) return explicitOutput;
|
|
const safe = container.replace(/[^A-Za-z0-9_.-]+/g, "_").replace(/^_+|_+$/g, "") || "container";
|
|
return path.resolve(process.cwd(), "docker-run-" + safe + ".sh");
|
|
}
|
|
|
|
function main() {
|
|
const args = process.argv.slice(2);
|
|
if (args.length < 1 || args[0] === "-h" || args[0] === "--help") {
|
|
console.log("Usage: node restore-run.js <container-id-or-name> [output-file]");
|
|
process.exit(args.length < 1 ? 1 : 0);
|
|
}
|
|
|
|
ensureDocker();
|
|
const container = args[0];
|
|
const info = inspectContainer(container);
|
|
const script = buildScript(info);
|
|
const file = outputFile(container, args[1]);
|
|
fs.writeFileSync(file, script, { mode: 0o755 });
|
|
console.log(file);
|
|
}
|
|
|
|
if (require.main === module) {
|
|
main();
|
|
}
|
|
|
|
module.exports = {
|
|
buildScript,
|
|
main
|
|
};
|