thinkncollab-shell 2.0.0-beta.2 → 2.0.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Readme.md +43 -10
- package/dist/native/brand/favicon-64.png +0 -0
- package/dist/native/brand/tnc.ico +0 -0
- package/dist/native/tnc-tray/build-macos.sh +59 -0
- package/dist/native/tnc-tray/main.swift +204 -0
- package/dist/native/tnc-tray/tray.ps1 +137 -0
- package/dist/src/commands/dock/containers.js +1 -1
- package/dist/src/commands/dock/dock.js +1 -1
- package/dist/src/dock/api/routes.js +1 -1
- package/dist/src/dock/api/server.js +1 -1
- package/dist/src/dock/api/websocket.js +7 -0
- package/dist/src/dock/api/wire.js +1 -1
- package/dist/src/dock/build/builder.js +1 -1
- package/dist/src/dock/build/buildkit.js +1 -1
- package/dist/src/dock/build/cache.js +7 -0
- package/dist/src/dock/build/sshagent.js +7 -0
- package/dist/src/dock/cli/api.js +1 -1
- package/dist/src/dock/cli/commands.js +1 -1
- package/dist/src/dock/cli/compose.js +1 -1
- package/dist/src/dock/cli/trust.js +7 -0
- package/dist/src/dock/compat/cases.js +1 -1
- package/dist/src/dock/config.js +1 -1
- package/dist/src/dock/daemon-main.js +1 -1
- package/dist/src/dock/daemon.js +1 -1
- package/dist/src/dock/engine.js +1 -1
- package/dist/src/dock/image/auth.js +1 -1
- package/dist/src/dock/image/credhelpers.js +7 -0
- package/dist/src/dock/image/keychain.js +7 -0
- package/dist/src/dock/image/registry.js +1 -1
- package/dist/src/dock/image/scan.js +7 -0
- package/dist/src/dock/image/sign.js +7 -0
- package/dist/src/dock/linux/host.js +1 -1
- package/dist/src/dock/paths.js +1 -1
- package/dist/src/dock/runtime/cdi.js +7 -0
- package/dist/src/dock/runtime/fsops.js +1 -1
- package/dist/src/dock/runtime/network.js +1 -1
- package/dist/src/dock/runtime/overlaydns.js +7 -0
- package/dist/src/dock/runtime/ports.js +1 -1
- package/dist/src/dock/runtime/rootfs.js +1 -1
- package/dist/src/dock/runtime/spec.js +1 -1
- package/dist/src/dock/runtime/stats.js +1 -1
- package/dist/src/dock/runtime/userns.js +7 -0
- package/dist/src/dock/runtime/volumes.js +1 -1
- package/dist/src/dock/setup/autostart.js +1 -1
- package/dist/src/dock/setup/diagnose.js +7 -0
- package/dist/src/dock/setup/index.js +1 -1
- package/dist/src/dock/tray/agent.js +7 -0
- package/dist/src/dock/tray/install.js +7 -0
- package/dist/src/dock/ui/page.js +232 -16
- package/dist/src/dock/ui/server.js +1 -1
- package/package.json +1 -1
- package/dist/src/dock/setup/app.js +0 -7
|
@@ -4,4 +4,4 @@
|
|
|
4
4
|
* PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
|
|
5
5
|
* UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
|
|
6
6
|
*/
|
|
7
|
-
"use strict";const fs=require("fs"),path=require("path"),chalk=require("chalk"),{spawn}=require("child_process"),{dockPaths}=require("../../dock/paths"),{request,ping}=require("../../dock/client"),{readPid,pidAlive}=require("../../dock/daemon"),{PHASE}=require("../../dock/version"),sleep=e=>new Promise(o=>setTimeout(o,e));async function setupCmd(e){const{setup:o}=require("../../dock/setup"),s=o=>{const s=e.indexOf(o);return-1===s?void 0:e[s+1]};try{const t=await o({from:s("--from"),baseUrl:s("--url"),force:e.includes("--force"),log:e=>console.log(chalk.dim(` ${e}`))});return console.log(chalk.green(`✓ dock runtime ${t} installed for ${process.platform}/${process.arch}`)),console.log(chalk.dim(" next: thinknsh dock start")),!0}catch(e){return console.log(chalk.red(`✗ setup failed: ${e.message}`)),!1}}async function start(e){if(await ping(e.socket))return console.log(chalk.green(`dock is already running (pid ${readPid(e.pidFile)||"?"})`)),!0;fs.mkdirSync(e.home,{recursive:!0,mode:448});const o=fs.openSync(e.logFile,"a"),s=[];try{require("node:sqlite")}catch(e){s.push("--experimental-sqlite")}const t=spawn(process.execPath,[...s,path.join(__dirname,"..","..","dock","daemon-main.js")],{detached:!0,stdio:["ignore",o,o],env:process.env});t.unref(),fs.closeSync(o);for(let o=0;o<50;o++){if(await ping(e.socket))return console.log(chalk.green(`✓ dock started (pid ${t.pid})`)),console.log(chalk.dim(` socket: ${e.socket}`)),console.log(chalk.dim(" try: thinknsh run --rm alpine echo hello")),!0;if(null!==t.exitCode)break;await sleep(100)}console.log(chalk.red(`✗ dock failed to start. Last log lines (${e.logFile}):`));try{console.log(chalk.dim(fs.readFileSync(e.logFile,"utf8").trim().split("\n").slice(-5).join("\n")))}catch(e){}return!1}async function stop(e){const o=readPid(e.pidFile);if(!o||!pidAlive(o))return console.log(chalk.dim("dock is not running")),!0;process.kill(o,"SIGTERM");for(let e=0;e<50;e++){if(!pidAlive(o))return console.log(chalk.green(`✓ dock stopped (pid ${o})`)),!0;await sleep(100)}return console.log(chalk.red(`✗ dock (pid ${o}) did not stop within 5s`)),!1}async function status(e){if(!await ping(e.socket))return console.log(`${chalk.red("●")} dock is not running ${chalk.dim("start it with: thinknsh dock start")}`),!1;const[o,s]=await Promise.all([request(e.socket,"GET","/version"),request(e.socket,"GET","/info")]);if(console.log(`${chalk.green("●")} dock ${o.body.Version} running (pid ${readPid(e.pidFile)||"?"})`),console.log(` socket ${e.socket}`),console.log(` API version ${o.body.ApiVersion} (min ${o.body.MinAPIVersion})`),console.log(` phase ${PHASE}`),console.log(` containers ${s.body.Containers} (${s.body.ContainersRunning} running)`),console.log(` images ${s.body.Images}`),"darwin"===process.platform){const e=require("../../dock/setup/autostart").installed();console.log(` autostart ${e?"on (starts when you log in)":chalk.dim("off (turn on: thinknsh dock autostart)")}`)}for(const e of s.body.Warnings||[])console.log(chalk.yellow(` ${e}`));return!0}async function compat(e,o){const{runCompat:s,toMarkdown:t}=require("../../dock/compat/runner");let n=e.socket,r=null;if(!await ping(n)){const e=require("os"),{DockDaemon:o}=require("../../dock/daemon"),s=fs.mkdtempSync(path.join(e.tmpdir(),"tnc-dock-check-"));r={daemon:new o({home:s}),home:s},await r.daemon.start(),n=r.daemon.paths.socket}try{const a=await s({socketPath:n,home:r?r.home:e.home}),c={pass:chalk.green,fail:chalk.red,pending:chalk.dim,early:chalk.cyan};console.log(chalk.bold(`\ndock check — phase ${a.phase}${r?chalk.dim(" (temporary daemon)"):""}\n`));for(const e of a.results){const o=c[e.result](e.result.padEnd(8));console.log(` ${o} ${e.phase} ${e.id.padEnd(20)} ${chalk.dim(e.detail)}`)}const i=a.tally;console.log(`\n ${chalk.green(`${i.pass||0} pass`)} ${chalk.red(`${i.fail||0} fail`)} ${i.pending||0} pending ${i.early||0} early`);const l=o.indexOf("--md");return-1!==l&&o[l+1]&&(fs.writeFileSync(o[l+1],t(a)),console.log(chalk.dim(` report written to ${o[l+1]}`))),a.ok}finally{r&&(await r.daemon.stop(),fs.rmSync(r.home,{recursive:!0,force:!0}))}}async function selftest(e){if(!await ping(e.socket)&&!await start(e))return!1;console.log(chalk.bold(`\ndock selftest — ${process.platform}/${process.arch}\n`));const{runSelftest:o}=require("../../dock/selftest"),{report:s,file:t}=await o({log:e=>console.log(` ${e.startsWith("✓")?chalk.green(e):chalk.red(e)}`)}),n=s.passed===s.total?chalk.green:chalk.red;return console.log(n(`\n ${s.passed}/${s.total} checks passed`)),console.log(chalk.dim(` report: ${t} (share this file)\n`)),s.passed===s.total}function parseSize(e){const o=/^(\d+(?:\.\d+)?)\s*([mg]?)b?$/i.exec(String(e||"").trim());if(!o)throw new Error(`invalid size "${e}" (examples: 4g, 6144m)`);return Math.round(parseFloat(o[1])*("m"===o[2].toLowerCase()?1:1024))}async function configCmd(e){const{readConfig:o,writeConfig:s,effective:t,configFile:n}=require("../../dock/config"),r=o();let a=!1;for(let o=0;o<e.length;o++){const s=e[o],t=()=>{if(o+1>=e.length)throw new Error(`${s} needs a value`);return e[++o]};if("--cpus"===s)r.cpus=Math.max(1,parseInt(t(),10)),a=!0;else if("--memory"===s||"-m"===s)r.memoryMB=parseSize(t()),a=!0;else if("--disk-size"===s)r.diskGB=parseInt(t(),10),a=!0;else if("--share"===s){const e=path.resolve(t().replace(/^~(?=\/|$)/,require("os").homedir()));if(!fs.existsSync(e)||!fs.statSync(e).isDirectory())throw new Error(`${e} is not a folder`);r.shares=[...new Set([...r.shares||[],e])],a=!0}else if("--unshare"===s){const e=path.resolve(t());r.shares=(r.shares||[]).filter(o=>o!==e),a=!0}else if("--publish-address"===s){const e=t();if(!require("net").isIP(e))throw new Error(`--publish-address needs an IP address (127.0.0.1, 0.0.0.0 …), not "${e}"`);r.publishAddress=e,a=!0}else{if("--reset"!==s)throw new Error(`unknown option ${s} (--cpus N, --memory 8g, --disk-size GB, --share PATH, --unshare PATH, --publish-address IP, --reset)`);for(const e of Object.keys(r))delete r[e];a=!0}}a&&s(r);const c=t(),i=(e,s)=>void 0!==o()[e]?s:`${s} ${chalk.dim("(default)")}`;console.log(` cpus ${i("cpus",c.cpus)}`),console.log(` memory ${i("memoryMB",`${c.memoryMB} MB`)} ${chalk.dim("a ceiling: macOS backs only what containers use")}`),console.log(` disk size ${i("diskGB",`${c.diskGB} GB`)} ${chalk.dim("used when the data disk is created")}`),console.log(` shared ${[require("os").homedir(),"/private/tmp","/private/var/folders","/Volumes",...c.shares].join(", ")}`),console.log(` ports ${i("publishAddress",c.publishAddress)} ${chalk.dim("127.0.0.1"===c.publishAddress?"-p without an address: this machine only":"-p without an address: open to the network")}`),console.log(chalk.dim(` file: ${n()}`));const l=e.filter(e=>e.startsWith("-")&&"--publish-address"!==e);return a&&l.length?console.log(chalk.yellow(" applies when the Linux VM next boots: thinknsh dock vm-restart (stops running containers)")):a&&console.log(chalk.dim(" ports: applies to containers started from now on")),!0}async function migrateCmd(e,o){const{migrate:s}=require("../../dock/setup/migrate"),t=e=>{const s=o.indexOf(e);return-1===s?void 0:o[s+1]};if(!await ping(e.socket))return console.log(chalk.red("✗ dock is not running. Start it with: thinknsh dock start")),!1;try{const n=await s({from:t("--from"),to:`unix://${e.socket}`,images:o.includes("--images"),volumes:o.includes("--volumes"),dryRun:o.includes("--dry-run"),overwrite:o.includes("--overwrite"),log:e=>console.log(chalk.dim(` ${e}`))}),r=(e,o)=>o?`${e}: ${o.moved} copied, ${o.skipped} already here`:null;return console.log(chalk.green(`✓ ${[r("images",n.images),r("volumes",n.volumes)].filter(Boolean).join("; ")}`)),console.log(chalk.dim(" containers are not moved: recreate them here (thinknsh compose up / thinknsh run)")),!0}catch(e){return console.log(chalk.red(`✗ migrate failed: ${e.message}`)),!1}}async function uiCmd(e,o){const{startUi:s}=require("../../dock/ui/server");if(!await ping(e.socket))return console.log(chalk.red("✗ dock is not running. Start it with: thinknsh dock start")),!1;const t=await s({socketPath:e.socket,port:Number((()=>{const e=o.indexOf("--port");return-1===e?void 0:o[e+1]})())||0});if(console.log(chalk.green("✓ dock dashboard")),console.log(` ${t.url}`),console.log(chalk.dim(" the link carries this session's token; Ctrl-C stops the dashboard")),!o.includes("--no-open")){const e="darwin"===process.platform?["open",[t.url]]:"win32"===process.platform?["cmd",["/c","start","",t.url]]:["xdg-open",[t.url]];try{spawn(e[0],e[1],{stdio:"ignore",detached:!0}).on("error",()=>{}).unref()}catch(e){}}return await new Promise(e=>{const o=()=>{t.close().then(e)};process.once("SIGINT",o),process.once("SIGTERM",o)}),!0}async function useCmd(e,o){const{use:s}=require("../../dock/setup/integrate");try{for(const t of s(e.socket,{off:o.includes("--off"),system:o.includes("--system")}))console.log(` ${t}`);return o.includes("--off")||console.log(chalk.green("✓ tools built for other engines now use dock")+chalk.dim(" (undo: thinknsh dock use --off)")),!0}catch(e){return console.log(chalk.red(`✗ ${e.message}`)),!1}}async function vmRestart(e,o){if(await ping(e.socket)){const s=((await request(e.socket,"GET","/containers/json")).body||[]).map(e=>e.Names[0].slice(1));if(s.length&&!o.includes("--force"))return console.log(chalk.yellow(` running containers would stop: ${s.join(", ")}`)),console.log(chalk.dim(" stop them first, or pass --force")),!1}if(await stop(e),"darwin"===process.platform){const{spawnSync:e}=require("child_process");e(path.join(require("os").homedir(),".tnc","bin","tnc-vz"),["stop"],{stdio:"ignore"})}else if("win32"===process.platform){const{spawnSync:e}=require("child_process");e("wsl.exe",["--terminate",require("../../dock/setup").WSL_DISTRO],{stdio:"ignore"})}const s=await start(e);return s&&console.log(chalk.dim(" the Linux VM boots with the new settings on the next command that needs it")),s}function appCmd(e,o){try{const s=require("../../dock/setup/app").app({off:o.includes("--off"),home:e.home,socket:e.socket,cli:process.argv[1],version:require("../../dock/setup").BUNDLE_VERSION,log:e=>console.log(chalk.dim(` ${e}`))});for(const e of s)console.log(` ${e}`);return!0}catch(e){return console.log(chalk.red(`✗ ${e.message}`)),!1}}function autostartCmd(e,o){try{const s=require("../../dock/setup/autostart").autostart({off:o.includes("--off"),cli:process.argv[1],logFile:e.logFile});for(const e of s)console.log(` ${e}`);return!0}catch(e){return console.log(chalk.red(`✗ ${e.message}`)),!1}}async function foreground(){require("../../dock/daemon-main"),await new Promise(()=>{})}module.exports={name:"dock",description:"dock: ThinkNCollab container engine daemon (TNC Engine API)",usage:"thinknsh dock <setup|start|stop|status|check|selftest|config|use|env|autostart|app|vm-restart|migrate|ui|daemon>",requiresAuth:!1,examples:["thinknsh dock setup","thinknsh dock start","thinknsh dock selftest","thinknsh dock check --md check.md","thinknsh dock config --memory 8g --cpus 6","thinknsh dock use","thinknsh dock autostart","thinknsh dock app"],async execute(e=[]){const o=(e[0]||"status").toLowerCase(),s=dockPaths();let t=!0;switch(o){case"setup":t=await setupCmd(e.slice(1));break;case"start":t=await start(s);break;case"stop":t=await stop(s);break;case"status":t=await status(s);break;case"check":t=await compat(s,e.slice(1));break;case"selftest":t=await selftest(s);break;case"daemon":await foreground();break;case"config":try{t=await configCmd(e.slice(1))}catch(e){console.log(chalk.red(`✗ ${e.message}`)),t=!1}break;case"use":t=await useCmd(s,e.slice(1));break;case"env":console.log(require("../../dock/setup/integrate").env(s.socket));break;case"vm-restart":t=await vmRestart(s,e.slice(1));break;case"migrate":t=await migrateCmd(s,e.slice(1));break;case"ui":t=await uiCmd(s,e.slice(1));break;case"autostart":t=autostartCmd(s,e.slice(1));break;case"app":t=appCmd(s,e.slice(1));break;default:console.log(chalk.yellow(`unknown subcommand "${o}". Usage: ${module.exports.usage}`)),t=!1}t||(process.exitCode=1)}};
|
|
7
|
+
"use strict";const fs=require("fs"),path=require("path"),chalk=require("chalk"),{spawn}=require("child_process"),{dockPaths}=require("../../dock/paths"),{request,ping}=require("../../dock/client"),{readPid,pidAlive}=require("../../dock/daemon"),{PHASE}=require("../../dock/version"),sleep=e=>new Promise(o=>setTimeout(o,e));async function setupCmd(e){const{setup:o}=require("../../dock/setup"),s=o=>{const s=e.indexOf(o);return-1===s?void 0:e[s+1]};try{const t=await o({from:s("--from"),baseUrl:s("--url"),force:e.includes("--force"),log:e=>console.log(chalk.dim(` ${e}`))});return console.log(chalk.green(`✓ dock runtime ${t} installed for ${process.platform}/${process.arch}`)),console.log(chalk.dim(" next: thinknsh dock start")),!0}catch(e){return console.log(chalk.red(`✗ setup failed: ${e.message}`)),!1}}async function start(e){if(await ping(e.socket))return console.log(chalk.green(`dock is already running (pid ${readPid(e.pidFile)||"?"})`)),!0;fs.mkdirSync(e.home,{recursive:!0,mode:448});let o={pid:null,exitCode:null};if(!require("../../dock/setup/autostart").kickstart()){const s=fs.openSync(e.logFile,"a"),t=[];try{require("node:sqlite")}catch(e){t.push("--experimental-sqlite")}o=spawn(process.execPath,[...t,path.join(__dirname,"..","..","dock","daemon-main.js")],{detached:!0,stdio:["ignore",s,s],env:process.env,windowsHide:!0}),o.unref(),fs.closeSync(s)}for(let s=0;s<50;s++){if(await ping(e.socket))return console.log(chalk.green(`✓ dock started (pid ${readPid(e.pidFile)||o.pid})`)),console.log(chalk.dim(` socket: ${e.socket}`)),console.log(chalk.dim(" try: thinknsh run --rm alpine echo hello")),!0;if(null!==o.exitCode)break;await sleep(100)}console.log(chalk.red(`✗ dock failed to start. Last log lines (${e.logFile}):`));try{console.log(chalk.dim(fs.readFileSync(e.logFile,"utf8").trim().split("\n").slice(-5).join("\n")))}catch(e){}return!1}async function stop(e){const o=readPid(e.pidFile);if(!o||!pidAlive(o))return console.log(chalk.dim("dock is not running")),!0;process.kill(o,"SIGTERM");for(let e=0;e<50;e++){if(!pidAlive(o))return console.log(chalk.green(`✓ dock stopped (pid ${o})`)),!0;await sleep(100)}return console.log(chalk.red(`✗ dock (pid ${o}) did not stop within 5s`)),!1}async function status(e){if(!await ping(e.socket))return console.log(`${chalk.red("●")} dock is not running ${chalk.dim("start it with: thinknsh dock start")}`),!1;const[o,s]=await Promise.all([request(e.socket,"GET","/version"),request(e.socket,"GET","/info")]);console.log(`${chalk.green("●")} dock ${o.body.Version} running (pid ${readPid(e.pidFile)||"?"})`),console.log(` socket ${e.socket}`),console.log(` API version ${o.body.ApiVersion} (min ${o.body.MinAPIVersion})`),console.log(` phase ${PHASE}`),s.body.SecurityOptions?.includes("name=rootless")&&console.log(" mode rootless (no root)"),console.log(` containers ${s.body.Containers} (${s.body.ContainersRunning} running)`),console.log(` images ${s.body.Images}`);const t=require("../../dock/setup/autostart").status().enabled;console.log(` autostart ${t?"on (starts when you log in)":chalk.dim("off (turn on: thinknsh dock autostart)")}`);for(const e of s.body.Warnings||[])console.log(chalk.yellow(` ${e}`));return!0}async function compat(e,o){const{runCompat:s,toMarkdown:t}=require("../../dock/compat/runner");let r=e.socket,n=null;if(!await ping(r)){const e=require("os"),{DockDaemon:o}=require("../../dock/daemon"),s=fs.mkdtempSync(path.join(e.tmpdir(),"tnc-dock-check-"));n={daemon:new o({home:s}),home:s},await n.daemon.start(),r=n.daemon.paths.socket}try{const i=await s({socketPath:r,home:n?n.home:e.home}),a={pass:chalk.green,fail:chalk.red,pending:chalk.dim,early:chalk.cyan};console.log(chalk.bold(`\ndock check — phase ${i.phase}${n?chalk.dim(" (temporary daemon)"):""}\n`));for(const e of i.results){const o=a[e.result](e.result.padEnd(8));console.log(` ${o} ${e.phase} ${e.id.padEnd(20)} ${chalk.dim(e.detail)}`)}const c=i.tally;console.log(`\n ${chalk.green(`${c.pass||0} pass`)} ${chalk.red(`${c.fail||0} fail`)} ${c.pending||0} pending ${c.early||0} early`);const l=o.indexOf("--md");return-1!==l&&o[l+1]&&(fs.writeFileSync(o[l+1],t(i)),console.log(chalk.dim(` report written to ${o[l+1]}`))),i.ok}finally{n&&(await n.daemon.stop(),fs.rmSync(n.home,{recursive:!0,force:!0}))}}async function selftest(e){if(!await ping(e.socket)&&!await start(e))return!1;console.log(chalk.bold(`\ndock selftest — ${process.platform}/${process.arch}\n`));const{runSelftest:o}=require("../../dock/selftest"),{report:s,file:t}=await o({log:e=>console.log(` ${e.startsWith("✓")?chalk.green(e):chalk.red(e)}`)}),r=s.passed===s.total?chalk.green:chalk.red;return console.log(r(`\n ${s.passed}/${s.total} checks passed`)),console.log(chalk.dim(` report: ${t} (share this file)\n`)),s.passed===s.total}function parseSize(e){const o=/^(\d+(?:\.\d+)?)\s*([mg]?)b?$/i.exec(String(e||"").trim());if(!o)throw new Error(`invalid size "${e}" (examples: 4g, 6144m)`);return Math.round(parseFloat(o[1])*("m"===o[2].toLowerCase()?1:1024))}async function configCmd(e){const{readConfig:o,writeConfig:s,effective:t,configFile:r}=require("../../dock/config"),n=o();let i=!1;for(let o=0;o<e.length;o++){const s=e[o],t=()=>{if(o+1>=e.length)throw new Error(`${s} needs a value`);return e[++o]};if("--cpus"===s)n.cpus=Math.max(1,parseInt(t(),10)),i=!0;else if("--memory"===s||"-m"===s)n.memoryMB=parseSize(t()),i=!0;else if("--disk-size"===s)n.diskGB=parseInt(t(),10),i=!0;else if("--share"===s){const e=path.resolve(t().replace(/^~(?=\/|$)/,require("os").homedir()));if(!fs.existsSync(e)||!fs.statSync(e).isDirectory())throw new Error(`${e} is not a folder`);n.shares=[...new Set([...n.shares||[],e])],i=!0}else if("--unshare"===s){const e=path.resolve(t());n.shares=(n.shares||[]).filter(o=>o!==e),i=!0}else if("--http-proxy"===s||"--https-proxy"===s||"--no-proxy"===s){const e={"--http-proxy":"http","--https-proxy":"https","--no-proxy":"noProxy"}[s],o=t();if(n.proxy={...n.proxy||{}},o){if("noProxy"!==e&&!/^https?:\/\/\S+$/.test(o))throw new Error(`${s} takes a URL like http://proxy.corp:3128`);n.proxy[e]=o}else delete n.proxy[e];i=!0}else if("--proxy-containers"===s){const e=t();if(!["on","off"].includes(e))throw new Error("--proxy-containers takes on or off");n.proxy={...n.proxy||{},containers:"on"===e},i=!0}else if("--ca-cert"===s){const e=path.resolve(t().replace(/^~(?=\/|$)/,require("os").homedir()));if(!fs.existsSync(e)||!/-----BEGIN CERTIFICATE-----/.test(fs.readFileSync(e,"utf8")))throw new Error(`${e} is not a PEM certificate file`);n.caCerts=[...new Set([...n.caCerts||[],e])],i=!0}else if("--no-ca-cert"===s){const e=path.resolve(t());n.caCerts=(n.caCerts||[]).filter(o=>o!==e),i=!0}else if("--mirror"===s){const e=t();if(!/^https?:\/\/[^\s/]+/.test(e))throw new Error("--mirror takes a URL like https://mirror.gcr.io");n.registryMirrors=[...new Set([...n.registryMirrors||[],e.replace(/\/+$/,"")])],i=!0}else if("--no-mirror"===s){const e=t().replace(/\/+$/,"");n.registryMirrors=(n.registryMirrors||[]).filter(o=>o!==e),i=!0}else if("--insecure-registry"===s){const e=t().toLowerCase();if(!/^[a-z0-9.-]+(:\d+)?$|^\[[0-9a-f:]+\](:\d+)?$/.test(e))throw new Error("--insecure-registry takes host or host:port");n.insecureRegistries=[...new Set([...n.insecureRegistries||[],e])],i=!0}else if("--no-insecure-registry"===s){const e=t().toLowerCase();n.insecureRegistries=(n.insecureRegistries||[]).filter(o=>o!==e),i=!0}else if("--credential-store"===s){const e=t();if(!["auto","file"].includes(e))throw new Error("--credential-store takes auto (system keychain) or file");n.credentialStore=e,i=!0}else if("--credential-helper"===s){const e=t(),o=/^([a-z0-9.-]+(?::\d+)?)=([A-Za-z0-9._-]*)$/i.exec(e);if(!o)throw new Error("--credential-helper takes HOST=HELPER, e.g. 123456789.dkr.ecr.eu-west-1.amazonaws.com=ecr-login (empty HELPER removes it)");n.credentialHelpers={...n.credentialHelpers||{}},o[2]?n.credentialHelpers[o[1].toLowerCase()]=o[2]:delete n.credentialHelpers[o[1].toLowerCase()],i=!0}else if("--publish-address"===s){const e=t();if(!require("net").isIP(e))throw new Error(`--publish-address needs an IP address (127.0.0.1, 0.0.0.0 …), not "${e}"`);n.publishAddress=e,i=!0}else if("--trust-key"===s){const e=t(),o=/^([^=]+)=(.+)$/.exec(e);if(!o)throw new Error("--trust-key takes SCOPE=PUBLIC_KEY, e.g. ghcr.io/me/*=~/keys/tnc-sign.pub");const s=path.resolve(o[2].replace(/^~(?=\/|$)/,require("os").homedir()));if(!fs.existsSync(s)||!/PUBLIC KEY/.test(fs.readFileSync(s,"utf8")))throw new Error(`${s} is not a PEM public key`);n.trustPolicy=[...(n.trustPolicy||[]).filter(e=>e.scope!==o[1]),{scope:o[1],key:s}],i=!0}else if("--no-trust-key"===s){const e=t();n.trustPolicy=(n.trustPolicy||[]).filter(o=>o.scope!==e),i=!0}else if("--userns-remap"===s){const e=t();if("off"!==e&&"default"!==e&&!/^[a-z_][a-z0-9_-]*(:[a-z_][a-z0-9_-]*)?$/i.test(e))throw new Error("--userns-remap takes default, a user name, user:group, or off");"off"===e?delete n.usernsRemap:n.usernsRemap=e,i=!0}else{if("--reset"!==s)throw new Error(`unknown option ${s} (--cpus N, --memory 8g, --disk-size GB, --share PATH, --unshare PATH, --http-proxy URL, --https-proxy URL, --no-proxy LIST, --proxy-containers on|off, --ca-cert FILE, --mirror URL, --insecure-registry HOST, --credential-store auto|file, --credential-helper HOST=NAME, --publish-address IP, --userns-remap default|USER|off, --trust-key SCOPE=KEY.pub, --no-trust-key SCOPE, --reset)`);for(const e of Object.keys(n))delete n[e];i=!0}}i&&s(n);const a=t(),c=(e,s)=>void 0!==o()[e]?s:`${s} ${chalk.dim("(default)")}`;console.log(` cpus ${c("cpus",a.cpus)}`),console.log(` memory ${c("memoryMB",`${a.memoryMB} MB`)} ${chalk.dim("a ceiling: macOS backs only what containers use")}`),console.log(` disk size ${c("diskGB",`${a.diskGB} GB`)} ${chalk.dim("used when the data disk is created")}`),console.log(` shared ${[require("os").homedir(),"/private/tmp","/private/var/folders","/Volumes",...a.shares].join(", ")}`),console.log(` ports ${c("publishAddress",a.publishAddress)} ${chalk.dim("127.0.0.1"===a.publishAddress?"-p without an address: this machine only":"-p without an address: open to the network")}`);const l=a.proxy||{},{redact:d}=require("../../dock/setup/diagnose"),u=e=>e||chalk.dim("none");console.log(` http proxy ${u(l.http&&d(l.http))}`),console.log(` https proxy ${u(l.https&&d(l.https))}${l.noProxy?` no proxy: ${l.noProxy}`:""}${(l.http||l.https)&&!1===l.containers?chalk.dim(" (not handed to containers)"):""}`),console.log(` CA certs ${u(a.caCerts.join(", "))}`),console.log(` mirrors ${u(a.registryMirrors.join(", "))}`),console.log(` insecure ${u(a.insecureRegistries.join(", "))}`),"linux"===process.platform&&console.log(` userns remap ${u(a.usernsRemap)}${a.usernsRemap?chalk.dim(" containers created from now: root is an unprivileged id range"):""}`);const h=a.trustPolicy||[];console.log(` signed only ${u(h.map(e=>`${e.scope} (${path.basename(e.key)})`).join(", "))}`),console.log(` logins in ${"file"===a.credentialStore?"auth.json":require("../../dock/image/keychain").describe()}`);const p=Object.entries(a.credentialHelpers||{});if(p.length&&console.log(` helpers ${p.map(([e,o])=>`${e} → ${o}`).join(", ")}`),console.log(chalk.dim(` file: ${r()}`)),i){const o=e.filter(e=>e.startsWith("-"));o.some(e=>["--cpus","--memory","-m","--disk-size","--share","--unshare","--reset"].includes(e))&&console.log(chalk.yellow(" VM settings apply when the Linux VM next boots: thinknsh dock vm-restart (stops running containers)")),o.some(e=>/proxy|ca-cert|--reset/.test(e))&&console.log(chalk.yellow(" proxy and CA settings apply when dock next starts: thinknsh dock stop && thinknsh dock start")),o.includes("--userns-remap")&&console.log(chalk.yellow(" userns-remap applies to containers created after dock next starts: thinknsh dock stop && thinknsh dock start")),o.includes("--publish-address")&&console.log(chalk.dim(" ports: applies to containers started from now on")),!l.http&&!l.https||require("../../dock/config").nodeHasEnvProxy()||console.log(chalk.red(` node ${process.version} ignores proxy settings: install Node 22.21+ or 24.5+`))}return!0}async function autostartCmd(e){const o=require("../../dock/setup/autostart"),s=e.includes("--off")?"off":e.find(e=>!e.startsWith("-"))||"on";try{if("on"===s){for(const s of o.enable({app:!e.includes("--no-app")}))console.log(` ${s}`);console.log(chalk.green("✓ dock starts at login")+chalk.dim(" (undo: thinknsh dock autostart off)"))}else if("off"===s){for(const e of o.disable())console.log(` ${e}`);console.log(chalk.green("✓ dock no longer starts at login")+chalk.dim(" (it keeps running now; thinknsh dock stop stops it)"))}else{if("status"!==s)throw new Error("usage: thinknsh dock autostart [on] [--no-app] | off | status");{const e=o.status();console.log(e.enabled?`${chalk.green("●")} starts at login ${chalk.dim(e.file)}${e.app?chalk.dim(" (with the menu-bar app)"):""}`:`${chalk.dim("○")} does not start at login ${chalk.dim("turn on: thinknsh dock autostart on")}`)}}return!0}catch(e){return console.log(chalk.red(`✗ ${e.message}`)),!1}}async function appCmd(e){const o=require("../../dock/tray/install"),s=e.includes("--off")?"uninstall":e.find(e=>!e.startsWith("-"))||(o.installedPath()?"open":"install");try{if("install"===s){for(const e of o.install({log:e=>console.log(chalk.dim(` ${e}`))}))console.log(` ${e}`);console.log(chalk.green("✓ tray app installed")+chalk.dim(" (it opens at login when dock autostart is on)"))}else if("uninstall"===s)for(const e of o.uninstall())console.log(` ${e}`);else{if("open"!==s)throw new Error("usage: thinknsh dock app install | uninstall | open");o.open()}return!0}catch(e){return console.log(chalk.red(`✗ ${e.message}`)),!1}}async function trayCmd(){const{TrayAgent:e}=require("../../dock/tray/agent");return await(new e).run(),!0}async function diagnoseCmd(e){const{diagnose:o}=require("../../dock/setup/diagnose"),s=await o({paths:e});return console.log(chalk.green(`✓ diagnostics bundle: ${s}`)),console.log(chalk.dim(" attach it to a bug report; it has no registry logins, env values or proxy passwords")),!0}async function resetCmd(e,o){const{reset:s}=require("../../dock/setup/diagnose"),t=o.includes("--all"),r=require("../../dock/paths").rootlessMode();if("linux"===process.platform&&0!==process.getuid?.()&&!r)throw new Error("dock runs as root on Linux: sudo thinknsh dock reset");if(console.log(chalk.yellow(" this deletes every container, image, volume and network, the build cache and the Linux VM disk"+(t?", plus settings and registry logins":""))),!o.includes("--yes")){if(!process.stdin.isTTY)throw new Error("refusing to reset without confirmation; pass --yes");const e=require("readline").createInterface({input:process.stdin,output:process.stderr}),o=await new Promise(o=>e.question("Reset dock to factory defaults? [y/N] ",o));if(e.close(),!/^y(es)?$/i.test(o.trim()))return!0}await stop(e);const{spawnSync:n}=require("child_process");if("darwin"===process.platform)n(path.join(require("os").homedir(),".tnc","bin","tnc-vz"),["stop"],{stdio:"ignore"});else if("win32"===process.platform){const e=require("../../dock/setup").WSL_DISTRO;n("wsl.exe",["-d",e,"-u","root","--","sh","-c","rm -rf /var/lib/tnc-dock/*"],{stdio:"ignore"}),n("wsl.exe",["--terminate",e],{stdio:"ignore"})}else r?((new(require("../../dock/linux/host").RootlessLinuxHost)).stop(),n("unshare",["--user","--map-root-user","--map-auto","--","rm","-rf",path.join(e.home,"rootless")],{stdio:"ignore"})):"linux"===process.platform&&n("sh",["-c",'for m in $(grep " /var/lib/tnc-dock" /proc/mounts | cut -d" " -f2 | sort -r); do umount -l "$m"; done; rm -rf /var/lib/tnc-dock'],{stdio:"ignore"});const i=s({paths:e,all:t});return console.log(chalk.green(`✓ dock reset (${i.length} items removed)`)),console.log(chalk.dim(" start again with: thinknsh dock start")),!0}async function migrateCmd(e,o){const{migrate:s}=require("../../dock/setup/migrate"),t=e=>{const s=o.indexOf(e);return-1===s?void 0:o[s+1]};if(!await ping(e.socket))return console.log(chalk.red("✗ dock is not running. Start it with: thinknsh dock start")),!1;try{const r=await s({from:t("--from"),to:`unix://${e.socket}`,images:o.includes("--images"),volumes:o.includes("--volumes"),dryRun:o.includes("--dry-run"),overwrite:o.includes("--overwrite"),log:e=>console.log(chalk.dim(` ${e}`))}),n=(e,o)=>o?`${e}: ${o.moved} copied, ${o.skipped} already here`:null;return console.log(chalk.green(`✓ ${[n("images",r.images),n("volumes",r.volumes)].filter(Boolean).join("; ")}`)),console.log(chalk.dim(" containers are not moved: recreate them here (thinknsh compose up / thinknsh run)")),!0}catch(e){return console.log(chalk.red(`✗ migrate failed: ${e.message}`)),!1}}async function uiCmd(e,o){const{startUi:s}=require("../../dock/ui/server");if(!await ping(e.socket))return console.log(chalk.red("✗ dock is not running. Start it with: thinknsh dock start")),!1;const t=await s({socketPath:e.socket,port:Number((()=>{const e=o.indexOf("--port");return-1===e?void 0:o[e+1]})())||0});return console.log(chalk.green("✓ dock dashboard")),console.log(` ${t.url}`),console.log(chalk.dim(" the link carries this session's token; Ctrl-C stops the dashboard")),o.includes("--no-open")||require("../../dock/tray/agent").openTarget(t.url),await new Promise(e=>{const o=()=>{t.close().then(e)};process.once("SIGINT",o),process.once("SIGTERM",o)}),!0}async function useCmd(e,o){const{use:s}=require("../../dock/setup/integrate");try{for(const t of s(e.socket,{off:o.includes("--off"),system:o.includes("--system")}))console.log(` ${t}`);return o.includes("--off")||console.log(chalk.green("✓ tools built for other engines now use dock")+chalk.dim(" (undo: thinknsh dock use --off)")),!0}catch(e){return console.log(chalk.red(`✗ ${e.message}`)),!1}}async function vmRestart(e,o){if(await ping(e.socket)){const s=((await request(e.socket,"GET","/containers/json")).body||[]).map(e=>e.Names[0].slice(1));if(s.length&&!o.includes("--force"))return console.log(chalk.yellow(` running containers would stop: ${s.join(", ")}`)),console.log(chalk.dim(" stop them first, or pass --force")),!1}if(await stop(e),"darwin"===process.platform){const{spawnSync:e}=require("child_process");e(path.join(require("os").homedir(),".tnc","bin","tnc-vz"),["stop"],{stdio:"ignore"})}else if("win32"===process.platform){const{spawnSync:e}=require("child_process");e("wsl.exe",["--terminate",require("../../dock/setup").WSL_DISTRO],{stdio:"ignore"})}else require("../../dock/paths").rootlessMode()&&(new(require("../../dock/linux/host").RootlessLinuxHost)).stop();const s=await start(e);return s&&console.log(chalk.dim(" the Linux VM boots with the new settings on the next command that needs it")),s}async function foreground(){require("../../dock/daemon-main"),await new Promise(()=>{})}module.exports={name:"dock",description:"dock: ThinkNCollab container engine daemon (TNC Engine API)",usage:"thinknsh dock <setup|start|stop|status|check|selftest|config|use|env|vm-restart|migrate|ui|autostart|app|tray|diagnose|reset|daemon>",requiresAuth:!1,examples:["thinknsh dock setup","thinknsh dock start","thinknsh dock selftest","thinknsh dock check --md check.md","thinknsh dock config --memory 8g --cpus 6","thinknsh dock use","thinknsh dock autostart","thinknsh dock app"],async execute(e=[]){const o=(e[0]||"status").toLowerCase(),s=dockPaths();let t=!0;switch(o){case"setup":t=await setupCmd(e.slice(1));break;case"start":t=await start(s);break;case"stop":t=await stop(s);break;case"status":t=await status(s);break;case"check":t=await compat(s,e.slice(1));break;case"selftest":t=await selftest(s);break;case"daemon":await foreground();break;case"config":try{t=await configCmd(e.slice(1))}catch(e){console.log(chalk.red(`✗ ${e.message}`)),t=!1}break;case"use":t=await useCmd(s,e.slice(1));break;case"env":console.log(require("../../dock/setup/integrate").env(s.socket));break;case"vm-restart":t=await vmRestart(s,e.slice(1));break;case"migrate":t=await migrateCmd(s,e.slice(1));break;case"ui":t=await uiCmd(s,e.slice(1));break;case"autostart":t=await autostartCmd(e.slice(1));break;case"app":t=await appCmd(e.slice(1));break;case"tray":t=await trayCmd();break;case"diagnose":t=await diagnoseCmd(s);break;case"reset":try{t=await resetCmd(s,e.slice(1))}catch(e){console.log(chalk.red(`✗ ${e.message}`)),t=!1}break;default:console.log(chalk.yellow(`unknown subcommand "${o}". Usage: ${module.exports.usage}`)),t=!1}t||(process.exitCode=1)}};
|
|
@@ -4,4 +4,4 @@
|
|
|
4
4
|
* PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
|
|
5
5
|
* UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
|
|
6
6
|
*/
|
|
7
|
-
"use strict";const os=require("os"),{DOCK_VERSION,API_VERSION,MIN_API_VERSION}=require("../version"),{DEFAULT_REGISTRY,RAW_STREAM,MUX_STREAM,ROOT_DIR_FIELD,REGISTRY_AUTH_HEADER,PATH_STAT_HEADER,BUILD_FILE,BUILD_FILE_PARAM}=require("./wire"),{muxFrame,readEntries,follow,formatEntry}=require("../runtime/logs"),{labelMatch}=require("../engine"),GO_ARCH={x64:"amd64",arm64:"arm64",arm:"arm",ia32:"386"},UNAME_ARCH={x64:"x86_64",arm64:"aarch64",arm:"armv7l",ia32:"i386"};function httpError(e,t){const r=new Error(t);return r.statusCode=e,r}function parseFilters(e){const t=e.query.get("filters");if(!t)return{};try{const e=JSON.parse(t);return Object.fromEntries(Object.entries(e).map(([e,t])=>[e,Array.isArray(t)?t:Object.keys(t)]))}catch(e){throw httpError(400,"invalid filters")}}function parseTime(e,t){if(null==e||""===e)return t;if(/^\d+(\.\d+)?$/.test(e))return Math.floor(1e3*parseFloat(e));const r=Date.parse(e);if(Number.isNaN(r))throw httpError(400,`invalid timestamp: ${e}`);return r}function requestCredentials(e){const t=e.req.headers[REGISTRY_AUTH_HEADER];if(!t)return null;try{const e=JSON.parse(Buffer.from(String(t).replace(/-/g,"+").replace(/_/g,"/"),"base64").toString("utf8"));return e&&e.username&&"string"==typeof e.password?{username:e.username,password:e.password}:null}catch(e){return null}}async function progressStream(e,t){let r=null;const a=t=>{r||(r=e.stream("application/json")),r.write(JSON.stringify(t)+"\r\n")};try{await t(a),r||(r=e.stream("application/json")),r.end()}catch(e){if(!r)throw e;a({errorDetail:{message:e.message},error:e.message}),r.end()}}function toApiEvent(e,t){const r={...e.attributes},a={Type:e.type,Action:e.action,Actor:{ID:e.actorId,Attributes:r},scope:"local",time:Math.floor(e.time/1e3),timeNano:1e6*e.time};return"container"!==e.type&&"image"!==e.type||(a.status=e.action,a.id=e.actorId,r.image&&(a.from=r.image)),a}function eventMatches(e,t){const r=(e,r)=>!t[e]?.length||t[e].some(r);return r("type",t=>t===e.type)&&r("event",t=>e.action===t||e.action.startsWith(t+":"))&&r("container",t=>"container"===e.type&&(e.actorId.startsWith(t)||e.attributes.name===t))&&r("image",t=>e.attributes.image===t||e.actorId===t)&&r("volume",t=>"volume"===e.type&&e.actorId===t)&&(t.label||[]).every(t=>labelMatch(e.attributes,t))}function buildRoutes({store:e,engine:t,home:r}){const a=[],n=(e,t,r,n,s={})=>{const o=t instanceof RegExp?t:new RegExp("^"+t.replace(/:\w+/g,"([^/]+)")+"$");a.push({method:e,re:o,name:r,handler:n,...s})},s=e=>e.text(200,"OK","text/plain; charset=utf-8",{"Cache-Control":"no-cache, no-store, must-revalidate",Pragma:"no-cache","Builder-Version":"0"===process.env.TNC_DOCK_BUILDKIT?"1":"2"});n("GET","/_ping","ping",s),n("HEAD","/_ping","ping",s),n("GET","/version","version",e=>{const r={ApiVersion:API_VERSION,MinAPIVersion:MIN_API_VERSION,Os:"linux",Arch:GO_ARCH[process.arch]||process.arch};e.json(200,{Platform:{Name:"ThinkNCollab dock"},Components:[{Name:"Engine",Version:DOCK_VERSION,Details:r}],Version:DOCK_VERSION,ApiVersion:API_VERSION,MinAPIVersion:MIN_API_VERSION,GitCommit:"",GoVersion:`node ${process.version}`,Os:"linux",Arch:r.Arch,KernelVersion:t.kernelVersion||"",BuildTime:""})}),n("GET","/info","info",a=>{const n=e.counts();a.json(200,{ID:"",Name:os.hostname(),ServerVersion:DOCK_VERSION,Containers:n.containers,ContainersRunning:n.running,ContainersPaused:n.paused,ContainersStopped:n.stopped,Images:n.images,Driver:"overlayfs",DriverStatus:[],OperatingSystem:"ThinkNCollab dock",OSType:"linux",OSVersion:"",KernelVersion:t.kernelVersion||"",Architecture:UNAME_ARCH[process.arch]||process.arch,NCPU:os.cpus().length,MemTotal:os.totalmem(),[ROOT_DIR_FIELD]:r,IndexServerAddress:DEFAULT_REGISTRY,RegistryConfig:{IndexConfigs:{},InsecureRegistryCIDRs:[],Mirrors:[]},Plugins:{Volume:["local"],Network:["bridge","host","null"],Authorization:null,Log:["json-file"]},LoggingDriver:"json-file",CgroupDriver:"cgroupfs",CgroupVersion:"2",Runtimes:{crun:{path:"crun"}},DefaultRuntime:"crun",SecurityOptions:[],Labels:[],ExperimentalBuild:!1,Swarm:{LocalNodeState:"inactive",ControlAvailable:!1,NodeID:"",NodeAddr:"",RemoteManagers:null},Warnings:[]})}),n("GET","/events","events",async r=>{const a=parseFilters(r),n=Date.now(),s=parseTime(r.query.get("since"),null),o=parseTime(r.query.get("until"),null),i=r.stream("application/json"),c=t=>{eventMatches(t,a)&&i.write(JSON.stringify(toApiEvent(t,e))+"\n")};if(null!==s||null!==o)for(const t of e.eventsBetween(s??0,Math.min(o??n,n)))c(t);if(null!==o&&o<=n)return i.end();let l=null;const m=e=>{null!==o&&e.time>o||c(e)};t.on("event",m);const u=()=>{t.off("event",m),l&&clearTimeout(l)};i.onClose(u),null!==o&&(l=setTimeout(()=>{u(),i.end()},o-n))}),n("POST","/auth","auth",async e=>{const{username:r,password:a,serveraddress:n}=e.body||{},s=await t.login({username:r,password:a,serveraddress:n});e.json(200,{Status:"Login Succeeded",IdentityToken:"",Registry:s})}),n("DELETE","/auth","auth.remove",e=>{const{host:r,removed:a}=t.logout(e.query.get("serveraddress")||"");e.json(200,{Registry:r,Removed:a})});const o=e=>{let t=0;const r=e=>{let a;try{a=require("fs").lstatSync(e)}catch(e){return}if(!a.isDirectory())return void(t+=a.size);let n=[];try{n=require("fs").readdirSync(e)}catch(e){}for(const t of n)r(require("path").join(e,t))};return r(e),t};n("GET","/system/df","system.df",r=>{const a=e.listImages().map(e=>t.imageSummary(e)),n=new Map;for(const t of e.listImages())for(const e of t.layers||[])n.set(e.digest,e.size);r.json(200,{LayersSize:[...n.values()].reduce((e,t)=>e+t,0),Images:a,Containers:e.listContainers().map(e=>({...t.containerSummary(e),SizeRw:0,SizeRootFs:0})),Volumes:e.listVolumes().map(e=>({...t.volumes.toApi(e),UsageData:{Size:o(e.mountpoint),RefCount:t.volumes.usedBy(e.name).length}})),BuildCache:[]})});const i=(e,t,r)=>!(r.label||[]).some(t=>!labelMatch(e,t))&&!(r["label!"]||[]).some(t=>labelMatch(e,t))&&(!r.until?.length||t<parseTime(r.until[0],1/0));n("POST","/containers/prune","containers.prune",async r=>{const a=parseFilters(r),n=[];for(const r of e.listContainers())"running"!==r.state&&"removing"!==r.state&&i(r.config.Config.Labels||{},r.createdAt,a)&&(await t.remove(r.id,{}),n.push(r.id));r.json(200,{ContainersDeleted:n,SpaceReclaimed:0})}),n("POST","/images/prune","images.prune",async r=>{const a=parseFilters(r),n="false"===a.dangling?.[0]||"0"===a.dangling?.[0],s=new Set(e.listContainers().map(e=>e.image)),o=[];let i=0;for(const r of e.listImages())s.has(r.id)||!n&&r.repoTags.length||(o.push(...await t.removeImage(r.id,{force:!0})),i+=r.size);r.json(200,{ImagesDeleted:o,SpaceReclaimed:i})}),n("POST","/volumes/prune","volumes.prune",r=>{const a=parseFilters(r),n="true"===a.all?.[0]||"1"===a.all?.[0],s=[];let c=0;for(const r of e.listVolumes())!n&&!r.anonymous||t.volumes.usedBy(r.name).length||i(r.labels||{},r.createdAt,a)&&(c+=o(r.mountpoint),t.volumes.remove(r.name),s.push(r.name));r.json(200,{VolumesDeleted:s,SpaceReclaimed:c})}),n("POST","/build/prune","build.prune",t=>{const r=e.db.prepare("SELECT COUNT(*) AS n FROM build_cache").get().n;e.db.exec("DELETE FROM build_cache"),t.json(200,{CachesDeleted:Array.from({length:r},(e,t)=>String(t)),SpaceReclaimed:0})}),n("GET","/images/json","images.list",r=>{const a=parseFilters(r);let n=e.listImages();a.reference?.length&&(n=n.filter(e=>e.repoTags.some(e=>a.reference.some(t=>e===t||e.startsWith(t+":"))))),a.dangling?.length&&(n=n.filter(e=>0===e.repoTags.length==("true"===a.dangling[0]))),r.json(200,n.map(e=>t.imageSummary(e)))}),n("POST","/images/create","images.pull",async e=>{const r=e.query.get("fromSrc");if(r){if("-"!==r)throw httpError(501,"importing from a URL is not supported; send the tar as the request body (fromSrc=-)");return void await progressStream(e,async r=>{r({status:(await t.importImage(e.req,{repo:e.query.get("repo")||"",tag:e.query.get("tag")||"",changes:e.query.getAll("changes"),message:e.query.get("message")||"",platform:e.query.get("platform")||""})).id})})}e.req.resume();const a=e.query.get("fromImage");if(!a)throw httpError(400,"fromImage is required");const n=e.query.get("tag"),s=n?n.startsWith("sha256:")?`${a}@${n}`:`${a}:${n}`:a;await progressStream(e,r=>t.pull(s,r,{credentials:requestCredentials(e),platform:e.query.get("platform")||void 0}))},{rawBody:!0});const c=async(e,r)=>{const a=r.flatMap(e=>e.split(",")).map(e=>e.trim()).filter(Boolean);t.saveSelection(a);const n=e.stream("application/x-tar");try{await t.saveImages(a,n)}finally{n.end()}};n("GET","/images/get","images.save",e=>c(e,e.query.getAll("names"))),n("GET",/^\/images\/(.+)\/get$/,"images.save",e=>c(e,[e.params[0]])),n("POST","/images/load","images.load",async e=>{const r=e.flag("quiet");await progressStream(e,a=>t.loadImages(e.req,e=>{r&&!e.stream?.startsWith("Loaded")||a(e)}))},{rawBody:!0}),n("GET",/^\/images\/(.+)\/history$/,"images.history",e=>{e.json(200,t.imageHistory(e.params[0]))}),n("POST","/commit","commit",async e=>{const r=e.query.get("container");if(!r)throw httpError(400,"container is required");e.json(201,await t.commit(r,{repo:e.query.get("repo")||"",tag:e.query.get("tag")||"",comment:e.query.get("comment")||"",author:e.query.get("author")||"",pause:e.flag("pause",!0),changes:e.query.getAll("changes"),config:e.body||{}}))}),n("GET",/^\/images\/(.+)\/json$/,"images.inspect",e=>{e.json(200,t.imageInspect(t.requireImage(e.params[0])))}),n("POST",/^\/images\/(.+)\/tag$/,"images.tag",e=>{const r=e.query.get("repo");if(!r)throw httpError(400,"repo is required");t.tagImage(e.params[0],r,e.query.get("tag")||"latest"),e.empty(201)}),n("POST",/^\/images\/(.+)\/push$/,"images.push",async e=>{const r=e.params[0],a=e.query.get("tag")||"";await progressStream(e,n=>t.push(r,a,n,{credentials:requestCredentials(e)}))}),n("DELETE",/^\/images\/(.+)$/,"images.remove",async e=>{e.json(200,await t.removeImage(e.params[0],{force:e.flag("force")}))}),n("POST","/build","build",async e=>{if(e.query.get("remote"))throw httpError(501,"building from a remote URL is not supported yet; send a build context");const a=(t,r)=>{try{return JSON.parse(e.query.get(t)||"null")??r}catch(e){throw httpError(400,`invalid ${t}`)}},n=Object.fromEntries(Object.entries(a("buildargs",{})).filter(([,e])=>null!==e)),s=require("fs"),o=require("path").join(r,`build-context-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}.tar`);await new Promise((t,r)=>{const a=s.createWriteStream(o);e.req.pipe(a),a.on("finish",t),a.on("error",r),e.req.on("error",r)});const i=e.stream("application/json"),c=e=>i.write(JSON.stringify(e)+"\r\n");try{await t.build(o,{buildFile:e.query.get(BUILD_FILE_PARAM)||BUILD_FILE,tags:e.query.getAll("t"),buildArgs:n,labels:a("labels",{}),target:e.query.get("target")||"",noCache:e.flag("nocache"),pull:e.flag("pull"),quiet:e.flag("q"),networkMode:["","default",null].includes(e.query.get("networkmode"))?"bridge":e.query.get("networkmode"),platform:e.query.get("platform")||""},c)}catch(e){c({errorDetail:{code:e.code||1,message:e.message},error:e.message})}finally{s.rmSync(o,{force:!0}),i.end()}},{rawBody:!0}),n("POST","/grpc","buildkit.grpc",async e=>{if("0"===process.env.TNC_DOCK_BUILDKIT)throw httpError(501,"BuildKit is turned off (TNC_DOCK_BUILDKIT=0)");await t.buildkit.ready();const r=e.upgrade("h2c");r&&await t.track(()=>t.buildkit.grpc(r))}),n("POST","/session","buildkit.session",async e=>{if("0"===process.env.TNC_DOCK_BUILDKIT)throw httpError(501,"BuildKit is turned off (TNC_DOCK_BUILDKIT=0)");await t.buildkit.ready();const r=e.upgradeStream(e.req.headers.upgrade||"h2c");r&&await t.track(()=>t.buildkit.session(r,e.req.rawHeaders))}),n("GET","/containers/json","containers.list",e=>{let r=t.listContainers({all:e.flag("all"),filters:parseFilters(e)});const a=parseInt(e.query.get("limit")||"-1",10);a>0&&(r=r.slice(0,a)),e.json(200,r)}),n("POST","/containers/create","containers.create",e=>{e.json(201,t.create(e.query.get("name"),e.body,{platform:e.query.get("platform")||e.body.Platform||""}))});const l=e=>Buffer.from(JSON.stringify(e)).toString("base64");return n("HEAD","/containers/:id/archive","containers.archive.stat",async e=>{const r=await t.fs.stat(e.params[0],e.query.get("path"));e.head(200,{[PATH_STAT_HEADER]:l(r),"Content-Type":"application/json"})}),n("GET","/containers/:id/archive","containers.archive.get",async e=>{let r=null;await t.fs.getArchive(e.params[0],e.query.get("path"),t=>{r=e.stream("application/x-tar",{[PATH_STAT_HEADER]:l(t)})},{write:e=>r.write(e)}),r?.end()}),n("PUT","/containers/:id/archive","containers.archive.put",async e=>{await t.fs.putArchive(e.params[0],e.query.get("path"),e.req,{copyUIDGID:e.flag("copyUIDGID"),noOverwriteDirNonDir:e.flag("noOverwriteDirNonDir")}),e.empty(200)},{rawBody:!0}),n("GET","/containers/:id/export","containers.export",async e=>{t.findContainer(e.params[0]);const r=e.stream("application/x-tar");try{await t.fs.exportTar(e.params[0],r)}finally{r.end()}}),n("GET","/containers/:id/changes","containers.changes",async e=>{const r=await t.fs.changes(e.params[0]);e.json(200,r.length?r:null)}),n("GET","/containers/:id/stats","containers.stats",async e=>{t.findContainer(e.params[0]);const r=e.flag("stream",!0),a=e.stream("application/json");let n=!1;a.onClose(()=>{n=!0});try{await t.stats.run(e.params[0],{stream:r,oneShot:e.flag("one-shot")},e=>a.write(JSON.stringify(e)+"\n"),()=>n)}finally{a.end()}}),n("GET","/containers/:id/top","containers.top",async e=>e.json(200,await t.stats.top(e.params[0]))),n("POST","/containers/:id/pause","containers.pause",async e=>{await t.pause(e.params[0]),e.empty(204)}),n("POST","/containers/:id/unpause","containers.unpause",async e=>{await t.unpause(e.params[0]),e.empty(204)}),n("POST","/containers/:id/rename","containers.rename",e=>{t.rename(e.params[0],e.query.get("name")),e.empty(204)}),n("POST","/containers/:id/update","containers.update",async e=>{e.json(200,await t.update(e.params[0],e.body||{}))}),n("GET","/containers/:id/json","containers.inspect",e=>e.json(200,t.inspect(e.params[0]))),n("POST","/containers/:id/start","containers.start",async e=>{const r=await t.start(e.params[0]);e.empty("notModified"===r?304:204)}),n("POST","/containers/:id/stop","containers.stop",async e=>{const r=e.query.get("t")??e.query.get("timeout"),a=await t.stop(e.params[0],null===r?void 0:parseInt(r,10));e.empty("notModified"===a?304:204)}),n("POST","/containers/:id/restart","containers.restart",async e=>{const r=e.query.get("t");await t.restart(e.params[0],null===r?void 0:parseInt(r,10)),e.empty(204)}),n("POST","/containers/:id/kill","containers.kill",async e=>{await t.kill(e.params[0],e.query.get("signal")||"SIGKILL"),e.empty(204)}),n("POST","/containers/:id/wait","containers.wait",async e=>{const r=t.wait(e.params[0],e.query.get("condition")||"not-running"),a=e.stream("application/json"),n=await r;a.write(JSON.stringify(n)+"\n"),a.end()}),n("GET","/containers/:id/probe","containers.probe",async e=>{e.json(200,{ok:await t.probe(e.params[0],parseInt(e.query.get("port"),10))})}),n("POST","/containers/:id/resize","containers.resize",async e=>{await t.resize(e.params[0],parseInt(e.query.get("h"),10),parseInt(e.query.get("w"),10)),e.empty(200)}),n("DELETE","/containers/:id","containers.remove",async e=>{await t.remove(e.params[0],{force:e.flag("force"),volumes:e.flag("v")}),e.empty(204)}),n("GET","/containers/:id/logs","containers.logs",async r=>{const a=t.findContainer(r.params[0]),n={stdout:r.flag("stdout"),stderr:r.flag("stderr")};if(!n.stdout&&!n.stderr)throw httpError(400,"Bad parameters: you must choose at least one stream");const s={since:parseTime(r.query.get("since"),0),until:parseTime(r.query.get("until"),1/0),tail:r.query.get("tail")||"all"},o=r.flag("timestamps"),i=!!a.config.Config.Tty,c=r.stream(i?RAW_STREAM:MUX_STREAM),l=e=>{if(!n[e.stream])return;const t=formatEntry(e,{timestamps:o});c.write(i?t:muxFrame(e.stream,t))},m=t.logFile(a.id),{entries:u,offset:p}=readEntries(m,s);if(u.forEach(l),!r.flag("follow")||"running"!==a.state)return c.end();const d=follow(m,p,l,()=>{const t=e.getContainer(a.id);return!t||"running"!==t.state}),g=e=>{e.actorId!==a.id||"die"!==e.action&&"destroy"!==e.action||d.pin()};t.on("event",g),c.onClose(d.cancel);try{await d.done}finally{t.off("event",g)}c.end()}),n("POST","/containers/:id/attach","containers.attach",async r=>{const a=t.findContainer(r.params[0]);if("exited"===a.state)throw httpError(409,"You cannot attach to a stopped container, start it first");const n={stdout:r.flag("stdout"),stderr:r.flag("stderr")},s=!!a.config.Config.Tty,o=r.hijack(s?RAW_STREAM:MUX_STREAM),i=t.logFile(a.id),c=e=>{n[e.stream]&&o.write(s?formatEntry(e):muxFrame(e.stream,formatEntry(e)))};let l=0;if(r.flag("logs")){const e=readEntries(i);e.entries.forEach(c),l=e.offset}else try{l=require("fs").statSync(i).size}catch(e){l=0}if(!r.flag("stream"))return o.end();let m=!1,u=null;const p=e=>{e.actorId!==a.id||"die"!==e.action&&"destroy"!==e.action||(m=!0,u?.pin())};if(t.on("event",p),r.flag("stdin")&&a.config.Config.OpenStdin){let r=null;const n=[],s=(async()=>{for(let t=0;t<200&&!m;t++){const t=e.getContainer(a.id);if("running"===t?.state)break;await new Promise(e=>setTimeout(e,50))}r=t.openStdin(a);for(const e of n.splice(0))r.write(e)})();o.onData(e=>{r?r.write(e):n.push(e)}),o.onEnd(async()=>{await s,await r.end()})}else o.onData(()=>{});u=follow(i,l,c,()=>m,s?25:100),o.onClose(()=>u.cancel());try{await u.done}finally{t.off("event",p)}o.end()}),n("POST","/containers/:id/exec","exec.create",e=>{e.json(201,t.createExec(e.params[0],e.body))}),n("POST","/exec/:id/start","exec.start",async e=>{const r=t.getExec(e.params[0]),a=await t.startExec(e.params[0]);if(e.body.Detach)return a.end(),a.stdout.resume(),a.stderr.resume(),e.empty(200);const n=!!r.config.Tty,s=e.hijack(n?RAW_STREAM:MUX_STREAM);a.stdout.on("data",e=>{!1!==r.config.AttachStdout&&s.write(n?e:muxFrame("stdout",e))}),a.stderr.on("data",e=>{!1!==r.config.AttachStderr&&s.write(n?e:muxFrame("stderr",e))}),r.config.AttachStdin?(s.onData(e=>a.write(e)),s.onEnd(()=>a.end())):a.end(),s.onClose(()=>{r.Running&&a.kill("SIGKILL")}),await a.exited,await new Promise(e=>setImmediate(e)),s.end()}),n("GET","/exec/:id/json","exec.inspect",e=>e.json(200,t.execInspect(e.params[0]))),n("POST","/exec/:id/resize","exec.resize",async e=>{await t.resizeExec(e.params[0],parseInt(e.query.get("h"),10),parseInt(e.query.get("w"),10)),e.empty(200)}),n("GET","/volumes","volumes.list",r=>{const a=parseFilters(r);let n=e.listVolumes();if(a.name?.length&&(n=n.filter(e=>a.name.some(t=>e.name.includes(t)))),a.label?.length&&(n=n.filter(e=>a.label.every(t=>labelMatch(e.labels,t)))),a.dangling?.length){const e="true"===a.dangling[0]||"1"===a.dangling[0];n=n.filter(r=>0===t.volumes.usedBy(r.name).length===e)}r.json(200,{Volumes:n.map(e=>t.volumes.toApi(e)),Warnings:[]})}),n("POST","/volumes/create","volumes.create",e=>{if(e.body.Driver&&"local"!==e.body.Driver)throw httpError(400,`volume driver "${e.body.Driver}" is not supported; only local`);e.json(201,t.volumes.toApi(t.volumes.create(e.body.Name||null,e.body.Labels||{})))}),n("GET","/volumes/:name","volumes.inspect",r=>{const a=e.getVolume(r.params[0]);if(!a)throw httpError(404,`get ${r.params[0]}: no such volume`);r.json(200,t.volumes.toApi(a))}),n("DELETE","/volumes/:name","volumes.remove",e=>{t.volumes.remove(e.params[0],e.flag("force")),e.empty(204)}),n("GET","/networks","networks.list",e=>{t.host;const r=parseFilters(e);e.json(200,t.network.list().filter(e=>((e,t)=>{const r=(e,r)=>!t[e]?.length||t[e].some(r);return r("name",t=>e.name.includes(t))&&r("id",t=>e.id.startsWith(t))&&r("driver",t=>e.driver===t)&&r("type",t=>"builtin"===t==!!e.builtin)&&(t.label||[]).every(t=>labelMatch(e.labels,t))})(e,r)).map(e=>t.networkInspect(e)))}),n("POST","/networks/create","networks.create",e=>{t.host;const r=t.network.create(e.body);t.emitEvent("network","create",r.id,{name:r.name,type:r.driver}),e.json(201,{Id:r.id,Warning:""})}),n("POST","/networks/prune","networks.prune",async r=>{t.host;const a=[];for(const r of e.listNetworks())e.listContainers().some(e=>(e.config.networks||[]).some(e=>e.name===r.name))||(await t.removeNetwork(r.id),a.push(r.name));r.json(200,{NetworksDeleted:a})}),n("GET","/networks/:id","networks.inspect",e=>{t.host,e.json(200,t.networkInspect(t.network.resolve(e.params[0])))}),n("DELETE","/networks/:id","networks.remove",async e=>{await t.removeNetwork(e.params[0]),e.empty(204)}),n("POST","/networks/:id/connect","networks.connect",async e=>{await t.connectNetwork(e.params[0],e.body.Container,e.body.EndpointConfig||{}),e.empty(200)}),n("POST","/networks/:id/disconnect","networks.disconnect",async e=>{await t.disconnectNetwork(e.params[0],e.body.Container),e.empty(200)}),a}module.exports={buildRoutes,toApiEvent,parseTime};
|
|
7
|
+
"use strict";const os=require("os"),{DOCK_VERSION,API_VERSION,MIN_API_VERSION}=require("../version"),{MULTI_PLATFORM_STORE_STATUS,DEFAULT_REGISTRY,RAW_STREAM,MUX_STREAM,ROOT_DIR_FIELD,REGISTRY_AUTH_HEADER,PATH_STAT_HEADER,BUILD_FILE,BUILD_FILE_PARAM}=require("./wire"),{muxFrame,readEntries,follow,formatEntry}=require("../runtime/logs"),{labelMatch}=require("../engine"),GO_ARCH={x64:"amd64",arm64:"arm64",arm:"arm",ia32:"386"},UNAME_ARCH={x64:"x86_64",arm64:"aarch64",arm:"armv7l",ia32:"i386"};function httpError(e,t){const r=new Error(t);return r.statusCode=e,r}function parseFilters(e){const t=e.query.get("filters");if(!t)return{};try{const e=JSON.parse(t);return Object.fromEntries(Object.entries(e).map(([e,t])=>[e,Array.isArray(t)?t:Object.keys(t)]))}catch(e){throw httpError(400,"invalid filters")}}function parseTime(e,t){if(null==e||""===e)return t;if(/^\d+(\.\d+)?$/.test(e))return Math.floor(1e3*parseFloat(e));const r=Date.parse(e);if(Number.isNaN(r))throw httpError(400,`invalid timestamp: ${e}`);return r}function requestCredentials(e){const t=e.req.headers[REGISTRY_AUTH_HEADER];if(!t)return null;try{const e=JSON.parse(Buffer.from(String(t).replace(/-/g,"+").replace(/_/g,"/"),"base64").toString("utf8"));return e&&e.username&&"string"==typeof e.password?{username:e.username,password:e.password}:null}catch(e){return null}}async function progressStream(e,t){let r=null;const s=t=>{r||(r=e.stream("application/json")),r.write(JSON.stringify(t)+"\r\n")};try{await t(s),r||(r=e.stream("application/json")),r.end()}catch(e){if(!r)throw e;s({errorDetail:{message:e.message},error:e.message}),r.end()}}function toApiEvent(e,t){const r={...e.attributes},s={Type:e.type,Action:e.action,Actor:{ID:e.actorId,Attributes:r},scope:"local",time:Math.floor(e.time/1e3),timeNano:1e6*e.time};return"container"!==e.type&&"image"!==e.type||(s.status=e.action,s.id=e.actorId,r.image&&(s.from=r.image)),s}function eventMatches(e,t){const r=(e,r)=>!t[e]?.length||t[e].some(r);return r("type",t=>t===e.type)&&r("event",t=>e.action===t||e.action.startsWith(t+":"))&&r("container",t=>"container"===e.type&&(e.actorId.startsWith(t)||e.attributes.name===t))&&r("image",t=>e.attributes.image===t||e.actorId===t)&&r("volume",t=>"volume"===e.type&&e.actorId===t)&&(t.label||[]).every(t=>labelMatch(e.attributes,t))}function buildRoutes({store:e,engine:t,home:r}){const s=[],a=(e,t,r,a,n={})=>{const o=t instanceof RegExp?t:new RegExp("^"+t.replace(/:\w+/g,"([^/]+)")+"$");s.push({method:e,re:o,name:r,handler:a,...n})},n=e=>e.text(200,"OK","text/plain; charset=utf-8",{"Cache-Control":"no-cache, no-store, must-revalidate",Pragma:"no-cache","Builder-Version":"0"===process.env.TNC_DOCK_BUILDKIT?"1":"2"});a("GET","/_ping","ping",n),a("HEAD","/_ping","ping",n),a("GET","/version","version",e=>{const r={ApiVersion:API_VERSION,MinAPIVersion:MIN_API_VERSION,Os:"linux",Arch:GO_ARCH[process.arch]||process.arch};e.json(200,{Platform:{Name:"ThinkNCollab dock"},Components:[{Name:"Engine",Version:DOCK_VERSION,Details:r}],Version:DOCK_VERSION,ApiVersion:API_VERSION,MinAPIVersion:MIN_API_VERSION,GitCommit:"",GoVersion:`node ${process.version}`,Os:"linux",Arch:r.Arch,KernelVersion:t.kernelVersion||"",BuildTime:""})});const o=e=>{try{return e()}catch(e){return null}};a("GET","/info","info",s=>{const a=e.counts();let n=null;try{n=t.host}catch(e){}const i="rootless"===n?.kind,c="local"===n?.kind&&!require("fs").existsSync("/sys/fs/cgroup/cgroup.controllers");s.json(200,{ID:"",Name:os.hostname(),ServerVersion:DOCK_VERSION,Containers:a.containers,ContainersRunning:a.running,ContainersPaused:a.paused,ContainersStopped:a.stopped,Images:a.images,Driver:"overlayfs",DriverStatus:"0"===process.env.TNC_DOCK_MULTI_PLATFORM?[]:[MULTI_PLATFORM_STORE_STATUS],OperatingSystem:"ThinkNCollab dock",OSType:"linux",OSVersion:"",KernelVersion:t.kernelVersion||"",Architecture:UNAME_ARCH[process.arch]||process.arch,NCPU:os.cpus().length,MemTotal:os.totalmem(),[ROOT_DIR_FIELD]:r,IndexServerAddress:DEFAULT_REGISTRY,RegistryConfig:{IndexConfigs:{},InsecureRegistryCIDRs:[],Mirrors:[]},Plugins:{Volume:["local"],Network:["bridge","host","null"],Authorization:null,Log:["json-file"]},LoggingDriver:"json-file",CgroupDriver:i?"none":"cgroupfs",CgroupVersion:c?"1":"2",Runtimes:{crun:{path:"crun"}},DefaultRuntime:"crun",Labels:[],ExperimentalBuild:!1,Swarm:{LocalNodeState:"inactive",ControlAvailable:!1,NodeID:"",NodeAddr:"",RemoteManagers:null},SecurityOptions:[...i?["name=rootless"]:[],...!i&&"local"===n?.kind&&o(()=>t.usernsRange())?["name=userns"]:[],"name=seccomp,profile=builtin"],Warnings:n?.warnings?.()||[]})});const i=Date.now();a("GET","/metrics","metrics",r=>{const s=e.counts(),a=e=>String(e).replace(/\\/g,"\\\\").replace(/"/g,'\\"').replace(/\n/g,"\\n"),n=[],o=(e,t,r,s)=>{n.push(`# HELP ${e} ${t}`,`# TYPE ${e} ${r}`);for(const[t,r]of s){const s=Object.entries(t).map(([e,t])=>`${e}="${a(t)}"`).join(",");n.push(`${e}${s?`{${s}}`:""} ${r}`)}},c=process.memoryUsage();o("dock_info","dock version","gauge",[[{version:DOCK_VERSION,api_version:API_VERSION,node:process.version},1]]),o("dock_uptime_seconds","Seconds since the daemon started","gauge",[[{},((Date.now()-i)/1e3).toFixed(0)]]),o("dock_containers","Containers by state","gauge",[[{state:"running"},s.running],[{state:"paused"},s.paused],[{state:"stopped"},s.stopped]]),o("dock_images","Images in the store","gauge",[[{},s.images]]),o("dock_volumes","Volumes","gauge",[[{},e.listVolumes().length]]),o("dock_networks","Networks","gauge",[[{},e.listNetworks().length]]),o("dock_process_resident_memory_bytes","Resident memory of the daemon process","gauge",[[{},c.rss]]),o("dock_process_heap_used_bytes","V8 heap in use by the daemon","gauge",[[{},c.heapUsed]]),o("dock_api_requests_total","Engine API requests by endpoint","counter",[...(t.apiRequests||new Map).entries()].sort(([e],[t])=>e.localeCompare(t)).map(([e,t])=>[{route:e},t])),r.text(200,n.join("\n")+"\n","text/plain; version=0.0.4; charset=utf-8")}),a("GET","/events","events",async r=>{const s=parseFilters(r),a=Date.now(),n=parseTime(r.query.get("since"),null),o=parseTime(r.query.get("until"),null),i=r.stream("application/json"),c=t=>{eventMatches(t,s)&&i.write(JSON.stringify(toApiEvent(t,e))+"\n")};if(null!==n||null!==o)for(const t of e.eventsBetween(n??0,Math.min(o??a,a)))c(t);if(null!==o&&o<=a)return i.end();let l=null;const m=e=>{null!==o&&e.time>o||c(e)};t.on("event",m);const u=()=>{t.off("event",m),l&&clearTimeout(l)};i.onClose(u),null!==o&&(l=setTimeout(()=>{u(),i.end()},o-a))}),a("POST","/auth","auth",async e=>{const{username:r,password:s,serveraddress:a}=e.body||{},n=await t.login({username:r,password:s,serveraddress:a});e.json(200,{Status:"Login Succeeded",IdentityToken:"",Registry:n})}),a("DELETE","/auth","auth.remove",e=>{const{host:r,removed:s}=t.logout(e.query.get("serveraddress")||"");e.json(200,{Registry:r,Removed:s})});const c=e=>{let t=0;const r=e=>{let s,a;try{s=require("fs").lstatSync(e)}catch(e){return}if(s.isDirectory()){try{a=require("fs").readdirSync(e)}catch(e){return}for(const t of a)r(require("path").join(e,t))}else t+=s.size};return r(e),t};a("GET","/system/df","system.df",r=>{const s=e.listImages().map(e=>t.imageSummary(e)),a=new Map;for(const t of e.listImages())for(const e of t.layers||[])a.set(e.digest,e.size);r.json(200,{LayersSize:[...a.values()].reduce((e,t)=>e+t,0),Images:s,Containers:e.listContainers().map(e=>({...t.containerSummary(e),SizeRw:0,SizeRootFs:0})),Volumes:e.listVolumes().map(e=>({...t.volumes.toApi(e),UsageData:{Size:c(e.mountpoint),RefCount:t.volumes.usedBy(e.name).length}})),BuildCache:[]})});const l=(e,t,r)=>!(r.label||[]).some(t=>!labelMatch(e,t))&&!(r["label!"]||[]).some(t=>labelMatch(e,t))&&(!r.until?.length||t<parseTime(r.until[0],1/0));a("POST","/containers/prune","containers.prune",async r=>{const s=parseFilters(r),a=[];for(const r of e.listContainers())"running"!==r.state&&"removing"!==r.state&&l(r.config.Config.Labels||{},r.createdAt,s)&&(await t.remove(r.id,{}),a.push(r.id));r.json(200,{ContainersDeleted:a,SpaceReclaimed:0})}),a("POST","/images/prune","images.prune",async r=>{const s=parseFilters(r),a="false"===s.dangling?.[0]||"0"===s.dangling?.[0],n=new Set(e.listContainers().map(e=>e.image)),o=[];let i=0;for(const r of e.listImages())n.has(r.id)||!a&&r.repoTags.length||(o.push(...await t.removeImage(r.id,{force:!0})),i+=r.size);r.json(200,{ImagesDeleted:o,SpaceReclaimed:i})}),a("POST","/volumes/prune","volumes.prune",r=>{const s=parseFilters(r),a="true"===s.all?.[0]||"1"===s.all?.[0],n=[];let o=0;for(const r of e.listVolumes())!a&&!r.anonymous||t.volumes.usedBy(r.name).length||l(r.labels||{},r.createdAt,s)&&(o+=c(r.mountpoint),t.volumes.remove(r.name),n.push(r.name));r.json(200,{VolumesDeleted:n,SpaceReclaimed:o})}),a("POST","/build/prune","build.prune",async e=>{const r=await t.pruneBuildCache();e.json(200,{CachesDeleted:Array.from({length:r.count},(e,t)=>String(t)),SpaceReclaimed:r.bytes})}),a("GET","/images/json","images.list",r=>{const s=parseFilters(r);let a=e.listImages();s.reference?.length&&(a=a.filter(e=>e.repoTags.some(e=>s.reference.some(t=>e===t||e.startsWith(t+":"))))),s.dangling?.length&&(a=a.filter(e=>0===e.repoTags.length==("true"===s.dangling[0]))),r.json(200,a.map(e=>t.imageSummary(e)))}),a("POST","/images/create","images.pull",async e=>{const r=e.query.get("fromSrc");if(r){if("-"!==r)throw httpError(501,"importing from a URL is not supported; send the tar as the request body (fromSrc=-)");return void await progressStream(e,async r=>{r({status:(await t.importImage(e.req,{repo:e.query.get("repo")||"",tag:e.query.get("tag")||"",changes:e.query.getAll("changes"),message:e.query.get("message")||"",platform:e.query.get("platform")||""})).id})})}e.req.resume();const s=e.query.get("fromImage");if(!s)throw httpError(400,"fromImage is required");const a=e.query.get("tag"),n=a?a.startsWith("sha256:")?`${s}@${a}`:`${s}:${a}`:s;await progressStream(e,r=>t.pull(n,r,{credentials:requestCredentials(e),platform:e.query.get("platform")||void 0}))},{rawBody:!0});const m=async(e,r)=>{const s=r.flatMap(e=>e.split(",")).map(e=>e.trim()).filter(Boolean);t.saveSelection(s);const a=e.stream("application/x-tar");try{await t.saveImages(s,a)}finally{a.end()}};a("GET","/images/get","images.save",e=>m(e,e.query.getAll("names"))),a("GET",/^\/images\/(.+)\/get$/,"images.save",e=>m(e,[e.params[0]])),a("POST","/images/load","images.load",async e=>{const r=e.flag("quiet");await progressStream(e,s=>t.loadImages(e.req,e=>{r&&!e.stream?.startsWith("Loaded")||s(e)}))},{rawBody:!0}),a("GET","/images/search","images.search",async e=>{e.json(200,await t.search(e.query.get("term")||"",{limit:Number(e.query.get("limit"))||25,filters:parseFilters(e)}))}),a("GET",/^\/distribution\/(.+)\/json$/,"distribution.inspect",async e=>{const r=await t.distribution(e.params[0],{credentials:requestCredentials(e)});e.json(200,{Descriptor:r.descriptor,Platforms:r.platforms})}),a("GET",/^\/tnc\/manifests\/(.+)$/,"tnc.manifest.inspect",async e=>{const r=await t.distribution(e.params[0],{credentials:requestCredentials(e)});e.json(200,{Descriptor:r.descriptor,Platforms:r.platforms,Manifest:r.manifest})}),a("POST",/^\/tnc\/manifests\/(.+)\/push$/,"tnc.manifest.push",async e=>{const r=e.body&&e.body.Images||[];for(const e of r)t.requireImage(e);await progressStream(e,s=>t.pushIndex(e.params[0],r,s,{credentials:requestCredentials(e)}))}),a("GET",/^\/images\/(.+)\/history$/,"images.history",e=>{e.json(200,t.imageHistory(e.params[0]))}),a("POST","/commit","commit",async e=>{const r=e.query.get("container");if(!r)throw httpError(400,"container is required");e.json(201,await t.commit(r,{repo:e.query.get("repo")||"",tag:e.query.get("tag")||"",comment:e.query.get("comment")||"",author:e.query.get("author")||"",pause:e.flag("pause",!0),changes:e.query.getAll("changes"),config:e.body||{}}))}),a("GET",/^\/images\/(.+)\/json$/,"images.inspect",e=>{e.json(200,t.imageInspect(t.requireImage(e.params[0])))}),a("POST",/^\/images\/(.+)\/tag$/,"images.tag",e=>{const r=e.query.get("repo");if(!r)throw httpError(400,"repo is required");t.tagImage(e.params[0],r,e.query.get("tag")||"latest"),e.empty(201)}),a("POST",/^\/images\/(.+)\/push$/,"images.push",async e=>{const r=e.params[0],s=e.query.get("tag")||"";await progressStream(e,a=>t.push(r,s,a,{credentials:requestCredentials(e)}))}),a("DELETE",/^\/images\/(.+)$/,"images.remove",async e=>{e.json(200,await t.removeImage(e.params[0],{force:e.flag("force")}))}),a("POST","/build","build",async e=>{if(e.query.get("remote"))throw httpError(501,"building from a remote URL is not supported yet; send a build context");const s=(t,r)=>{try{return JSON.parse(e.query.get(t)||"null")??r}catch(e){throw httpError(400,`invalid ${t}`)}},a=Object.fromEntries(Object.entries(s("buildargs",{})).filter(([,e])=>null!==e)),n=require("fs"),o=require("path").join(r,`build-context-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}.tar`);await new Promise((t,r)=>{const s=n.createWriteStream(o);e.req.pipe(s),s.on("finish",t),s.on("error",r),e.req.on("error",r)});const i=e.stream("application/json"),c=e=>i.write(JSON.stringify(e)+"\r\n");try{await t.build(o,{buildFile:e.query.get(BUILD_FILE_PARAM)||BUILD_FILE,tags:e.query.getAll("t"),buildArgs:a,labels:s("labels",{}),target:e.query.get("target")||"",noCache:e.flag("nocache"),pull:e.flag("pull"),quiet:e.flag("q"),networkMode:["","default",null].includes(e.query.get("networkmode"))?"bridge":e.query.get("networkmode"),platform:e.query.get("platform")||"",cacheFrom:s("cachefrom",[]).filter(e=>"string"==typeof e&&e),cacheTo:s("cacheto",[]).filter(e=>"string"==typeof e&&e),ssh:e.query.get("sshsession")||null},c)}catch(e){c({errorDetail:{code:e.code||1,message:e.message},error:e.message})}finally{n.rmSync(o,{force:!0}),i.end()}},{rawBody:!0}),a("POST","/tnc/images/sign","images.sign",async e=>{const r=e.body||{};if(!r.Image||!r.Key)throw httpError(400,"Image and Key (a PEM private key) are required");e.json(200,await require("../image/sign").signImage(t,r.Image,{key:r.Key,passphrase:r.Passphrase||"",annotations:r.Annotations||null}))}),a("POST","/tnc/images/verify","images.verify",async e=>{const r=e.body||{};if(!r.Image||!r.Key)throw httpError(400,"Image and Key (a PEM public key) are required");e.json(200,await require("../image/sign").verifyImage(t,r.Image,{key:r.Key}))}),a("GET","/tnc/images/scan","images.scan",async e=>{const r=e.query.get("image");if(!r)throw httpError(400,"image is required");const s=require("../image/scan"),a=await s.scanImage(t,r,{offline:e.flag("offline")}),n=e.query.get("sbom");e.json(200,n?s.sbom(n,r,a):a)}),a("POST","/tnc/ssh-agent","build.ssh",async e=>{const t=(e.query.get("ids")||"").split(",").map(e=>e.trim()).filter(Boolean),r=e.query.get("session");if(!/^[0-9a-f]{32}$/.test(r||""))throw httpError(400,"invalid ssh agent session");await require("../build/sshagent").acceptSession(r,t,e.hijack("application/octet-stream"))}),a("POST","/grpc","buildkit.grpc",async e=>{if("0"===process.env.TNC_DOCK_BUILDKIT)throw httpError(501,"BuildKit is turned off (TNC_DOCK_BUILDKIT=0)");await t.buildkit.ready();const r=e.upgrade("h2c");r&&await t.track(()=>t.buildkit.grpc(r))}),a("POST","/session","buildkit.session",async e=>{if("0"===process.env.TNC_DOCK_BUILDKIT)throw httpError(501,"BuildKit is turned off (TNC_DOCK_BUILDKIT=0)");await t.buildkit.ready();const r=e.upgradeStream(e.req.headers.upgrade||"h2c");r&&await t.track(()=>t.buildkit.session(r,e.req.rawHeaders))}),a("GET","/containers/json","containers.list",e=>{let r=t.listContainers({all:e.flag("all"),filters:parseFilters(e)});const s=parseInt(e.query.get("limit")||"-1",10);s>0&&(r=r.slice(0,s)),e.json(200,r)}),a("POST","/containers/create","containers.create",e=>{e.json(201,t.create(e.query.get("name"),e.body,{platform:e.query.get("platform")||e.body.Platform||""}))});const u=e=>Buffer.from(JSON.stringify(e)).toString("base64");a("HEAD","/containers/:id/archive","containers.archive.stat",async e=>{const r=await t.fs.stat(e.params[0],e.query.get("path"));e.head(200,{[PATH_STAT_HEADER]:u(r),"Content-Type":"application/json"})}),a("GET","/containers/:id/archive","containers.archive.get",async e=>{let r=null;await t.fs.getArchive(e.params[0],e.query.get("path"),t=>{r=e.stream("application/x-tar",{[PATH_STAT_HEADER]:u(t)})},{write:e=>r.write(e)}),r?.end()}),a("PUT","/containers/:id/archive","containers.archive.put",async e=>{await t.fs.putArchive(e.params[0],e.query.get("path"),e.req,{copyUIDGID:e.flag("copyUIDGID"),noOverwriteDirNonDir:e.flag("noOverwriteDirNonDir")}),e.empty(200)},{rawBody:!0}),a("GET","/containers/:id/export","containers.export",async e=>{t.findContainer(e.params[0]);const r=e.stream("application/x-tar");try{await t.fs.exportTar(e.params[0],r)}finally{r.end()}}),a("GET","/containers/:id/changes","containers.changes",async e=>{const r=await t.fs.changes(e.params[0]);e.json(200,r.length?r:null)}),a("GET","/containers/:id/stats","containers.stats",async e=>{t.findContainer(e.params[0]);const r=e.flag("stream",!0),s=e.stream("application/json");let a=!1;s.onClose(()=>{a=!0});try{await t.stats.run(e.params[0],{stream:r,oneShot:e.flag("one-shot")},e=>s.write(JSON.stringify(e)+"\n"),()=>a)}finally{s.end()}}),a("GET","/containers/:id/top","containers.top",async e=>e.json(200,await t.stats.top(e.params[0]))),a("POST","/containers/:id/pause","containers.pause",async e=>{await t.pause(e.params[0]),e.empty(204)}),a("POST","/containers/:id/unpause","containers.unpause",async e=>{await t.unpause(e.params[0]),e.empty(204)}),a("POST","/containers/:id/rename","containers.rename",e=>{t.rename(e.params[0],e.query.get("name")),e.empty(204)}),a("POST","/containers/:id/update","containers.update",async e=>{e.json(200,await t.update(e.params[0],e.body||{}))}),a("GET","/containers/:id/json","containers.inspect",e=>e.json(200,t.inspect(e.params[0]))),a("POST","/containers/:id/start","containers.start",async e=>{const r=await t.start(e.params[0]);e.empty("notModified"===r?304:204)}),a("POST","/containers/:id/stop","containers.stop",async e=>{const r=e.query.get("t")??e.query.get("timeout"),s=await t.stop(e.params[0],null===r?void 0:parseInt(r,10));e.empty("notModified"===s?304:204)}),a("POST","/containers/:id/restart","containers.restart",async e=>{const r=e.query.get("t");await t.restart(e.params[0],null===r?void 0:parseInt(r,10)),e.empty(204)}),a("POST","/containers/:id/kill","containers.kill",async e=>{await t.kill(e.params[0],e.query.get("signal")||"SIGKILL"),e.empty(204)}),a("POST","/containers/:id/wait","containers.wait",async e=>{const r=t.wait(e.params[0],e.query.get("condition")||"not-running"),s=e.stream("application/json"),a=await r;s.write(JSON.stringify(a)+"\n"),s.end()}),a("GET","/containers/:id/probe","containers.probe",async e=>{e.json(200,{ok:await t.probe(e.params[0],parseInt(e.query.get("port"),10))})}),a("POST","/containers/:id/resize","containers.resize",async e=>{await t.resize(e.params[0],parseInt(e.query.get("h"),10),parseInt(e.query.get("w"),10)),e.empty(200)}),a("DELETE","/containers/:id","containers.remove",async e=>{await t.remove(e.params[0],{force:e.flag("force"),volumes:e.flag("v")}),e.empty(204)}),a("GET","/containers/:id/logs","containers.logs",async r=>{const s=t.findContainer(r.params[0]),a={stdout:r.flag("stdout"),stderr:r.flag("stderr")};if(!a.stdout&&!a.stderr)throw httpError(400,"Bad parameters: you must choose at least one stream");const n={since:parseTime(r.query.get("since"),0),until:parseTime(r.query.get("until"),1/0),tail:r.query.get("tail")||"all"},o=r.flag("timestamps"),i=!!s.config.Config.Tty,c=r.stream(i?RAW_STREAM:MUX_STREAM),l=e=>{if(!a[e.stream])return;const t=formatEntry(e,{timestamps:o});c.write(i?t:muxFrame(e.stream,t))},m=t.logFile(s.id),{entries:u,offset:p}=readEntries(m,n);if(u.forEach(l),!r.flag("follow")||"running"!==s.state)return c.end();const d=follow(m,p,l,()=>{const t=e.getContainer(s.id);return!t||"running"!==t.state}),g=e=>{e.actorId!==s.id||"die"!==e.action&&"destroy"!==e.action||d.pin()};t.on("event",g),c.onClose(d.cancel);try{await d.done}finally{t.off("event",g)}c.end()});const p=async(r,s,a,{mux:n})=>{const o={stdout:r.flag("stdout"),stderr:r.flag("stderr")},i=t.logFile(s.id),c=e=>{o[e.stream]&&a.write(n?muxFrame(e.stream,formatEntry(e)):formatEntry(e))};let l=0;if(r.flag("logs")){const e=readEntries(i);e.entries.forEach(c),l=e.offset}else try{l=require("fs").statSync(i).size}catch(e){l=0}if(!r.flag("stream"))return a.end();let m=!1,u=null;const p=e=>{e.actorId!==s.id||"die"!==e.action&&"destroy"!==e.action||(m=!0,u?.pin())};if(t.on("event",p),r.flag("stdin")&&s.config.Config.OpenStdin){let r=null;const n=[],o=(async()=>{for(let t=0;t<200&&!m;t++){const t=e.getContainer(s.id);if("running"===t?.state)break;await new Promise(e=>setTimeout(e,50))}r=t.openStdin(s);for(const e of n.splice(0))r.write(e)})();a.onData(e=>{r?r.write(e):n.push(e)}),a.onEnd(async()=>{await o,await r.end()})}else a.onData(()=>{});u=follow(i,l,c,()=>m,n?100:25),a.onClose(()=>u.cancel());try{await u.done}finally{t.off("event",p)}a.end()};return a("POST","/containers/:id/attach","containers.attach",async e=>{const r=t.findContainer(e.params[0]);if("exited"===r.state)throw httpError(409,"You cannot attach to a stopped container, start it first");const s=!!r.config.Config.Tty;await p(e,r,e.hijack(s?RAW_STREAM:MUX_STREAM),{mux:!s})}),a("GET","/containers/:id/attach/ws","containers.attach.ws",async e=>{const r=require("./websocket");if(!e.socket||!r.isUpgrade(e.req))throw httpError(400,"attach/ws needs a WebSocket upgrade");const s=t.findContainer(e.params[0]);if("exited"===s.state)throw httpError(409,"You cannot attach to a stopped container, start it first");e.done=!0;const a=r.accept(e.req,e.socket,e.stdinPrefix);let n=!1;a.on("close",()=>{n=!0});const o={write:e=>a.send(Buffer.isBuffer(e)?e:Buffer.from(e),!0),end:()=>a.close(),onData:e=>a.on("message",t=>e(t)),onEnd:e=>a.on("close",e),onClose:e=>{n?setImmediate(e):a.on("close",e)}};e.query.has("stream")||e.query.set("stream","1");for(const t of["stdout","stderr","stdin"])e.query.has(t)||e.query.set(t,"1");await p(e,s,o,{mux:!1})}),a("POST","/containers/:id/exec","exec.create",e=>{e.json(201,t.createExec(e.params[0],e.body))}),a("POST","/exec/:id/start","exec.start",async e=>{const r=t.getExec(e.params[0]),s=await t.startExec(e.params[0]);if(e.body.Detach)return s.end(),s.stdout.resume(),s.stderr.resume(),e.empty(200);const a=!!r.config.Tty,n=e.hijack(a?RAW_STREAM:MUX_STREAM);s.stdout.on("data",e=>{!1!==r.config.AttachStdout&&n.write(a?e:muxFrame("stdout",e))}),s.stderr.on("data",e=>{!1!==r.config.AttachStderr&&n.write(a?e:muxFrame("stderr",e))}),r.config.AttachStdin?(n.onData(e=>s.write(e)),n.onEnd(()=>s.end())):s.end(),n.onClose(()=>{r.Running&&s.kill("SIGKILL")}),await s.exited,await new Promise(e=>setImmediate(e)),n.end()}),a("GET","/exec/:id/json","exec.inspect",e=>e.json(200,t.execInspect(e.params[0]))),a("POST","/exec/:id/resize","exec.resize",async e=>{await t.resizeExec(e.params[0],parseInt(e.query.get("h"),10),parseInt(e.query.get("w"),10)),e.empty(200)}),a("GET","/volumes","volumes.list",r=>{const s=parseFilters(r);let a=e.listVolumes();if(s.name?.length&&(a=a.filter(e=>s.name.some(t=>e.name.includes(t)))),s.label?.length&&(a=a.filter(e=>s.label.every(t=>labelMatch(e.labels,t)))),s.dangling?.length){const e="true"===s.dangling[0]||"1"===s.dangling[0];a=a.filter(r=>0===t.volumes.usedBy(r.name).length===e)}r.json(200,{Volumes:a.map(e=>t.volumes.toApi(e)),Warnings:[]})}),a("POST","/volumes/create","volumes.create",e=>{if(e.body.Driver&&"local"!==e.body.Driver)throw httpError(400,`volume driver "${e.body.Driver}" is not supported; only local`);e.json(201,t.volumes.toApi(t.volumes.create(e.body.Name||null,e.body.Labels||{})))}),a("GET","/volumes/:name","volumes.inspect",r=>{const s=e.getVolume(r.params[0]);if(!s)throw httpError(404,`get ${r.params[0]}: no such volume`);r.json(200,t.volumes.toApi(s))}),a("DELETE","/volumes/:name","volumes.remove",e=>{t.volumes.remove(e.params[0],e.flag("force")),e.empty(204)}),a("GET","/networks","networks.list",e=>{t.host;const r=parseFilters(e);e.json(200,t.network.list().filter(e=>((e,t)=>{const r=(e,r)=>!t[e]?.length||t[e].some(r);return r("name",t=>e.name.includes(t))&&r("id",t=>e.id.startsWith(t))&&r("driver",t=>e.driver===t)&&r("type",t=>"builtin"===t==!!e.builtin)&&(t.label||[]).every(t=>labelMatch(e.labels,t))})(e,r)).map(e=>t.networkInspect(e)))}),a("POST","/networks/create","networks.create",e=>{t.host;const r=t.network.create(e.body);t.emitEvent("network","create",r.id,{name:r.name,type:r.driver}),e.json(201,{Id:r.id,Warning:""})}),a("POST","/networks/prune","networks.prune",async r=>{t.host;const s=[];for(const r of e.listNetworks())e.listContainers().some(e=>(e.config.networks||[]).some(e=>e.name===r.name))||(await t.removeNetwork(r.id),s.push(r.name));r.json(200,{NetworksDeleted:s})}),a("GET","/networks/:id","networks.inspect",e=>{t.host,e.json(200,t.networkInspect(t.network.resolve(e.params[0])))}),a("DELETE","/networks/:id","networks.remove",async e=>{await t.removeNetwork(e.params[0]),e.empty(204)}),a("POST","/networks/:id/connect","networks.connect",async e=>{await t.connectNetwork(e.params[0],e.body.Container,e.body.EndpointConfig||{}),e.empty(200)}),a("POST","/networks/:id/disconnect","networks.disconnect",async e=>{await t.disconnectNetwork(e.params[0],e.body.Container),e.empty(200)}),s}module.exports={buildRoutes,toApiEvent,parseTime};
|
|
@@ -4,4 +4,4 @@
|
|
|
4
4
|
* PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
|
|
5
5
|
* UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
|
|
6
6
|
*/
|
|
7
|
-
"use strict";const http=require("http"),{DOCK_VERSION,API_VERSION,MIN_API_VERSION}=require("../version"),{buildRoutes}=require("./routes"),{MUX_STREAM}=require("./wire"),VERSION_PREFIX=/^\/v(\d+\.\d+)(\/.*)$/,MAX_BODY=16777216,SECRET_PARAMS=new Set(["buildargs"]);function redactSearch(e){if(![...e.keys()].length)return"";const t=new URLSearchParams;for(const[s,r]of e)t.append(s,SECRET_PARAMS.has(s)?"***":r);return`?${t}`}function compareVersions(e,t){const[s,r]=e.split(".").map(Number),[n,o]=t.split(".").map(Number);return s!==n?s-n:r-o}function baseHeaders(e={}){return{"Api-Version":API_VERSION,Ostype:"linux",Server:`tnc-dock/${DOCK_VERSION} (${process.platform})`,...e}}function readBody(e,t=Buffer.alloc(0)){return new Promise((s,r)=>{const n=[t];let o=t.length;e.on("data",t=>{if(o+=t.length,o>16777216)return r(Object.assign(new Error("request body too large"),{statusCode:413})),void e.destroy();n.push(t)}),e.on("end",()=>s(Buffer.concat(n))),e.on("error",r)})}function parseJson(e){if(!e||0===e.length)return{};try{return JSON.parse(e.toString("utf8"))}catch(e){throw Object.assign(new Error("invalid JSON in request body"),{statusCode:400})}}class Context{constructor({req:e,res:t,socket:s,url:r,params:n,body:o,stdinPrefix:i}){this.req=e,this.res=t,this.socket=s,this.url=r,this.query=r.searchParams,this.params=n,this.body=o,this.stdinPrefix=i||Buffer.alloc(0),this.done=!1}flag(e,t=!1){const s=this.query.get(e);return null===s?t:"1"===s||"true"===s||"True"===s}_rawHead(e,t){const s=[`HTTP/1.1 ${e} ${101===e?"UPGRADED":http.STATUS_CODES[e]}`];for(const[e,r]of Object.entries(baseHeaders(t)))s.push(`${e}: ${r}`);this.socket.write(s.join("\r\n")+"\r\n\r\n")}json(e,t){if(this.done)return;this.done=!0;const s=Buffer.from(JSON.stringify(t));if(this.socket)return this._rawHead(e,{"Content-Type":"application/json","Content-Length":s.length,Connection:"close"}),void this.socket.end(s);this.res.writeHead(e,baseHeaders({"Content-Type":"application/json","Content-Length":s.length})),this.res.end(s)}empty(e=204){if(!this.done){if(this.done=!0,this.socket)return this._rawHead(e,{"Content-Length":0,Connection:"close"}),void this.socket.end();this.res.writeHead(e,baseHeaders({"Content-Length":0})),this.res.end()}}text(e,t,s="text/plain; charset=utf-8",r={}){this.done=!0,this.res.writeHead(e,baseHeaders({"Content-Type":s,...r})),this.res.end("HEAD"===this.req.method?void 0:t)}_guard(e){let t=!1;e.on("error",()=>{t=!0}),e.on("close",()=>{t=!0});const s=()=>!t&&!e.destroyed&&!e.writableEnded;return{write:t=>!!s()&&e.write(t),drained:()=>new Promise(t=>{if(!s()||!e.writableNeedDrain)return t();const r=()=>{e.off("drain",r),e.off("close",r),t()};e.once("drain",r),e.once("close",r)}),closed:()=>!s(),end:()=>{s()&&e.end()},onClose:s=>{t||e.destroyed?setImmediate(s):e.on("close",s)}}}stream(e,t={}){return this.done=!0,this.socket?(this._rawHead(200,{"Content-Type":e,Connection:"close",...t}),this._guard(this.socket)):(this.res.writeHead(200,baseHeaders({"Content-Type":e,...t})),this.res.flushHeaders(),this._guard(this.res))}head(e,t={}){this.done=!0,this.res.writeHead(e,baseHeaders(t)),this.res.end()}upgrade(e){return this.socket?(this.done=!0,this._rawHead(101,{Connection:"Upgrade",Upgrade:e}),this.stdinPrefix.length&&this.socket.unshift(this.stdinPrefix),this.socket):(this.json(400,{message:`this endpoint needs a connection upgrade to ${e}`}),null)}upgradeStream(e){const t=this.upgrade(e);return t?{...this._guard(t),onData:e=>{t.on("data",e),t.resume()},onEnd:e=>t.on("end",e)}:null}hijack(e=MUX_STREAM){if(this.done=!0,this.socket){this._rawHead(101,{"Content-Type":e,Connection:"Upgrade",Upgrade:"tcp"});const t=this.socket,s=this.stdinPrefix;return{...this._guard(t),onData:e=>{s.length&&e(s),t.on("data",e),t.resume()},onEnd:e=>t.on("end",e)}}return this.res.writeHead(200,baseHeaders({"Content-Type":e})),this.res.flushHeaders(),{...this._guard(this.res),onData:()=>{},onEnd:e=>e()}}}class ApiServer{constructor({store:e,engine:t,socketPath:s,home:r,log:n=()=>{}}){this.store=e,this.engine=t,this.socketPath=s,this.home=r,this.log=n,this.server=null,this.upgraded=new Set,this.routes=buildRoutes({store:e,engine:t,home:r})}start(){return new Promise((e,t)=>{this.server=http.createServer((e,t)=>this._handle(e,t,null,null)),this.server.on("upgrade",(e,t,s)=>this._handle(e,null,t,s)),this.server.once("error",t),this.server.listen(this.socketPath,()=>{this.server.removeListener("error",t),e()})})}stop(){return new Promise(e=>{if(!this.server)return e();this.server.close(()=>e()),this.server.closeAllConnections?.();for(const e of this.upgraded)e.destroy()})}_match(e,t){for(const s of this.routes){if(s.method!==e&&("GET"!==s.method||"HEAD"!==e||!s.allowHead))continue;const r=s.re.exec(t);if(r)return{route:s,params:r.slice(1).map(decodeURIComponent)}}return null}async _handle(e,t,s,r){s&&(s.on("error",t=>this.log(`client socket ${e.method} ${e.url}: ${t.code||t.message}`)),this.upgraded.add(s),s.on("close",()=>this.upgraded.delete(s)));const n=new URL(e.url,"http://dock");let o=n.pathname;const i=(r,o)=>new Context({req:e,res:t,socket:s,url:n,params:[]}).json(r,{message:o}),a=VERSION_PREFIX.exec(o);if(a){if(compareVersions(a[1],API_VERSION)>0)return i(400,`client version ${a[1]} is too new. Maximum supported API version is ${API_VERSION}`);if(compareVersions(a[1],MIN_API_VERSION)<0)return i(400,`client version ${a[1]} is too old. Minimum supported API version is ${MIN_API_VERSION}, please upgrade your client to a newer version`);o=a[2]}"/_ping"!==o&&this.engine?.touch?.();const d=this._match(e.method,o);if(this.log(`${e.method} ${n.pathname}${redactSearch(n.searchParams)} -> ${d?d.route.name:"501"}`),!d)return e.resume(),i(501,`dock does not implement ${e.method} ${o} yet`);let h;try{let o={},i=Buffer.alloc(0);if(s){const t=parseInt(e.headers["content-length"]||"0",10);let n=r||Buffer.alloc(0);for(;n.length<t;){const e=await new Promise(e=>s.once("data",e));n=Buffer.concat([n,e])}s.pause(),o=parseJson(n.subarray(0,t)),i=n.subarray(t)}else d.route.rawBody||(o=parseJson(await readBody(e)));h=new Context({req:e,res:t,socket:s,url:n,params:d.params,body:o,stdinPrefix:i}),await d.route.handler(h)}catch(r){const o=r.statusCode||500;o>=500&&this.log(`error ${e.method} ${n.pathname}: ${r.stack||r.message}`),h=h||new Context({req:e,res:t,socket:s,url:n,params:[]}),h.done||h.json(o,{message:r.message})}}}module.exports=ApiServer;
|
|
7
|
+
"use strict";const http=require("http"),{DOCK_VERSION,API_VERSION,MIN_API_VERSION}=require("../version"),{buildRoutes}=require("./routes"),{MUX_STREAM}=require("./wire"),VERSION_PREFIX=/^\/v(\d+\.\d+)(\/.*)$/,MAX_BODY=16777216,SECRET_PARAMS=new Set(["buildargs"]);function redactSearch(e){if(![...e.keys()].length)return"";const t=new URLSearchParams;for(const[s,r]of e)t.append(s,SECRET_PARAMS.has(s)?"***":r);return`?${t}`}function compareVersions(e,t){const[s,r]=e.split(".").map(Number),[n,o]=t.split(".").map(Number);return s!==n?s-n:r-o}function baseHeaders(e={}){return{"Api-Version":API_VERSION,Ostype:"linux",Server:`tnc-dock/${DOCK_VERSION} (${process.platform})`,...e}}function readBody(e,t=Buffer.alloc(0)){return new Promise((s,r)=>{const n=[t];let o=t.length;e.on("data",t=>{if(o+=t.length,o>16777216)return r(Object.assign(new Error("request body too large"),{statusCode:413})),void e.destroy();n.push(t)}),e.on("end",()=>s(Buffer.concat(n))),e.on("error",r)})}function parseJson(e){if(!e||0===e.length)return{};try{return JSON.parse(e.toString("utf8"))}catch(e){throw Object.assign(new Error("invalid JSON in request body"),{statusCode:400})}}class Context{constructor({req:e,res:t,socket:s,url:r,params:n,body:o,stdinPrefix:i}){this.req=e,this.res=t,this.socket=s,this.url=r,this.query=r.searchParams,this.params=n,this.body=o,this.stdinPrefix=i||Buffer.alloc(0),this.done=!1}flag(e,t=!1){const s=this.query.get(e);return null===s?t:"1"===s||"true"===s||"True"===s}_rawHead(e,t){const s=[`HTTP/1.1 ${e} ${101===e?"UPGRADED":http.STATUS_CODES[e]}`];for(const[e,r]of Object.entries(baseHeaders(t)))s.push(`${e}: ${r}`);this.socket.write(s.join("\r\n")+"\r\n\r\n")}json(e,t){if(this.done)return;this.done=!0;const s=Buffer.from(JSON.stringify(t));if(this.socket)return this._rawHead(e,{"Content-Type":"application/json","Content-Length":s.length,Connection:"close"}),void this.socket.end(s);this.res.writeHead(e,baseHeaders({"Content-Type":"application/json","Content-Length":s.length})),this.res.end(s)}empty(e=204){if(!this.done){if(this.done=!0,this.socket)return this._rawHead(e,{"Content-Length":0,Connection:"close"}),void this.socket.end();this.res.writeHead(e,baseHeaders({"Content-Length":0})),this.res.end()}}text(e,t,s="text/plain; charset=utf-8",r={}){this.done=!0,this.res.writeHead(e,baseHeaders({"Content-Type":s,...r})),this.res.end("HEAD"===this.req.method?void 0:t)}_guard(e){let t=!1;e.on("error",()=>{t=!0}),e.on("close",()=>{t=!0});const s=()=>!t&&!e.destroyed&&!e.writableEnded;return{write:t=>!!s()&&e.write(t),drained:()=>new Promise(t=>{if(!s()||!e.writableNeedDrain)return t();const r=()=>{e.off("drain",r),e.off("close",r),t()};e.once("drain",r),e.once("close",r)}),closed:()=>!s(),end:()=>{s()&&e.end()},onClose:s=>{t||e.destroyed?setImmediate(s):e.on("close",s)}}}stream(e,t={}){return this.done=!0,this.socket?(this._rawHead(200,{"Content-Type":e,Connection:"close",...t}),this._guard(this.socket)):(this.res.writeHead(200,baseHeaders({"Content-Type":e,...t})),this.res.flushHeaders(),this._guard(this.res))}head(e,t={}){this.done=!0,this.res.writeHead(e,baseHeaders(t)),this.res.end()}upgrade(e){return this.socket?(this.done=!0,this._rawHead(101,{Connection:"Upgrade",Upgrade:e}),this.stdinPrefix.length&&this.socket.unshift(this.stdinPrefix),this.socket):(this.json(400,{message:`this endpoint needs a connection upgrade to ${e}`}),null)}upgradeStream(e){const t=this.upgrade(e);return t?{...this._guard(t),onData:e=>{t.on("data",e),t.resume()},onEnd:e=>t.on("end",e)}:null}hijack(e=MUX_STREAM){if(this.done=!0,this.socket){this._rawHead(101,{"Content-Type":e,Connection:"Upgrade",Upgrade:"tcp"});const t=this.socket,s=this.stdinPrefix;return{...this._guard(t),onData:e=>{s.length&&e(s),t.on("data",e),t.resume()},onEnd:e=>t.on("end",e)}}return this.res.writeHead(200,baseHeaders({"Content-Type":e})),this.res.flushHeaders(),{...this._guard(this.res),onData:()=>{},onEnd:e=>e()}}}class ApiServer{constructor({store:e,engine:t,socketPath:s,home:r,log:n=()=>{}}){this.store=e,this.engine=t,this.socketPath=s,this.home=r,this.log=n,this.server=null,this.upgraded=new Set,this.routes=buildRoutes({store:e,engine:t,home:r})}start(){return new Promise((e,t)=>{this.server=http.createServer((e,t)=>this._handle(e,t,null,null)),this.server.on("upgrade",(e,t,s)=>this._handle(e,null,t,s)),this.server.once("error",t),this.server.listen(this.socketPath,()=>{this.server.removeListener("error",t),e()})})}stop(){return new Promise(e=>{if(!this.server)return e();this.server.close(()=>e()),this.server.closeAllConnections?.();for(const e of this.upgraded)e.destroy()})}_match(e,t){for(const s of this.routes){if(s.method!==e&&("GET"!==s.method||"HEAD"!==e||!s.allowHead))continue;const r=s.re.exec(t);if(r)return{route:s,params:r.slice(1).map(decodeURIComponent)}}return null}async _handle(e,t,s,r){s&&(s.on("error",t=>this.log(`client socket ${e.method} ${e.url}: ${t.code||t.message}`)),this.upgraded.add(s),s.on("close",()=>this.upgraded.delete(s)));const n=new URL(e.url,"http://dock");let o=n.pathname;const i=(r,o)=>new Context({req:e,res:t,socket:s,url:n,params:[]}).json(r,{message:o}),a=VERSION_PREFIX.exec(o);if(a){if(compareVersions(a[1],API_VERSION)>0)return i(400,`client version ${a[1]} is too new. Maximum supported API version is ${API_VERSION}`);if(compareVersions(a[1],MIN_API_VERSION)<0)return i(400,`client version ${a[1]} is too old. Minimum supported API version is ${MIN_API_VERSION}, please upgrade your client to a newer version`);o=a[2]}"/_ping"!==o&&this.engine?.touch?.();const d=this._match(e.method,o);if(this.engine){const e=d?d.route.name:"unimplemented";(this.engine.apiRequests||=new Map).set(e,(this.engine.apiRequests.get(e)||0)+1)}if(this.log(`${e.method} ${n.pathname}${redactSearch(n.searchParams)} -> ${d?d.route.name:"501"}`),!d)return e.resume(),i(501,`dock does not implement ${e.method} ${o} yet`);let h;try{let o={},i=Buffer.alloc(0);if(s){const t=parseInt(e.headers["content-length"]||"0",10);let n=r||Buffer.alloc(0);for(;n.length<t;){const e=await new Promise(e=>s.once("data",e));n=Buffer.concat([n,e])}s.pause(),o=parseJson(n.subarray(0,t)),i=n.subarray(t)}else d.route.rawBody||(o=parseJson(await readBody(e)));h=new Context({req:e,res:t,socket:s,url:n,params:d.params,body:o,stdinPrefix:i}),await d.route.handler(h)}catch(r){const o=r.statusCode||500;o>=500&&this.log(`error ${e.method} ${n.pathname}: ${r.stack||r.message}`),h=h||new Context({req:e,res:t,socket:s,url:n,params:[]}),h.done||h.json(o,{message:r.message})}}}module.exports=ApiServer;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license ThinkNCollab Enterprise & Open Core Ecosystem
|
|
3
|
+
* Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
|
|
4
|
+
* PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
|
|
5
|
+
* UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
|
|
6
|
+
*/
|
|
7
|
+
"use strict";const crypto=require("crypto"),{EventEmitter}=require("events"),GUID="258EAFA5-E914-47DA-95CA-C5AB0DC85B11",MAX_MESSAGE=16<<20;function isUpgrade(e){return/websocket/i.test(e.headers.upgrade||"")&&!!e.headers["sec-websocket-key"]}function acceptKey(e){return crypto.createHash("sha1").update(e+GUID).digest("base64")}function frame(e,r){const t=Buffer.isBuffer(r)?r:Buffer.from(String(r));let o;return t.length<126?o=Buffer.from([128|e,t.length]):t.length<65536?(o=Buffer.alloc(4),o[0]=128|e,o[1]=126,o.writeUInt16BE(t.length,2)):(o=Buffer.alloc(10),o[0]=128|e,o[1]=127,o.writeBigUInt64BE(BigInt(t.length),2)),Buffer.concat([o,t])}function parseFrames(e,{requireMask:r=!0}={}){const t=[];let o=0;for(;e.length-o>=2;){const c=e[o],n=e[o+1],s=!!(128&n);if(r&&!s)throw new Error("client frames must be masked");let a=127&n,f=o+2;if(126===a){if(e.length<f+2)break;a=e.readUInt16BE(f),f+=2}else if(127===a){if(e.length<f+8)break;const r=e.readBigUInt64BE(f);if(r>BigInt(16777216))throw new Error("frame too large");a=Number(r),f+=8}if(a>16777216)throw new Error("frame too large");const i=f;if(s&&(f+=4),e.length<f+a)break;let l=e.subarray(f,f+a);if(s){const r=e.subarray(i,i+4);l=Buffer.from(l);for(let e=0;e<l.length;e++)l[e]^=r[3&e]}t.push({fin:!!(128&c),opcode:15&c,payload:l}),o=f+a}return{frames:t,rest:e.subarray(o)}}function accept(e,r,t=Buffer.alloc(0),{protocol:o=null}={}){const c=["HTTP/1.1 101 Switching Protocols","Upgrade: websocket","Connection: Upgrade",`Sec-WebSocket-Accept: ${acceptKey(e.headers["sec-websocket-key"])}`];o&&c.push(`Sec-WebSocket-Protocol: ${o}`),r.write(c.join("\r\n")+"\r\n\r\n"),r.setNoDelay?.(!0);const n=new EventEmitter;let s=Buffer.from(t),a=[],f=0,i=!1;const l=()=>{i||(i=!0,n.emit("close"))};n.send=(e,t=Buffer.isBuffer(e))=>!i&&!r.destroyed&&r.write(frame(t?2:1,e)),n.close=(e=1e3)=>{if(!i&&!r.destroyed){const t=Buffer.alloc(2);t.writeUInt16BE(e),r.write(frame(8,t)),r.end()}l()},n.socket=r;const u=e=>{let t;s=s.length?Buffer.concat([s,e]):e;try{t=parseFrames(s)}catch(e){return n.close(1002),void r.destroy()}s=Buffer.from(t.rest);for(const e of t.frames){if(8===e.opcode)return void n.close(1e3);if(9!==e.opcode){if(10!==e.opcode){if(1===e.opcode||2===e.opcode)f=e.opcode,a=[e.payload];else{if(0!==e.opcode)continue;a.push(e.payload)}if(e.fin){const e=Buffer.concat(a);a=[],n.emit("message",e,2===f)}}}else r.write(frame(10,e.payload))}};return r.on("data",u),r.on("end",()=>{r.destroyed||r.end(),l()}),r.on("close",l),r.on("error",l),r.resume(),s.length&&setImmediate(()=>u(Buffer.alloc(0))),n}module.exports={accept,isUpgrade,acceptKey,frame,parseFrames};
|
|
@@ -4,4 +4,4 @@
|
|
|
4
4
|
* PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
|
|
5
5
|
* UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
|
|
6
6
|
*/
|
|
7
|
-
"use strict";module.exports={DEFAULT_REGISTRY:"https://index.docker.io/v1/",DEFAULT_REGISTRY_HOST:"docker.io",DEFAULT_REGISTRY_API:"registry-1.docker.io",MANIFEST_LIST_V2:"application/vnd.docker.distribution.manifest.list.v2+json",MANIFEST_V2:"application/vnd.docker.distribution.manifest.v2+json",LAYER_V2_GZIP:"application/vnd.docker.image.rootfs.diff.tar.gzip",RAW_STREAM:"application/vnd.docker.raw-stream",MUX_STREAM:"application/vnd.docker.multiplexed-stream",ROOT_DIR_FIELD:"DockerRootDir",REGISTRY_AUTH_HEADER:"x-registry-auth",PATH_STAT_HEADER:"X-Docker-Container-Path-Stat",SESSION_HEADER_PREFIX:"x-docker-expose-session-",BUILDKIT_GRPC_PACKAGE:"moby.buildkit.v1",ENGINE_EXPORTER:"moby",BUILDKIT_RELEASES:"https://github.com/moby/buildkit/releases/download",BUILD_FILE:"Dockerfile",BUILD_IGNORE_FILE:".dockerignore",BUILD_FILE_PARAM:"dockerfile",COMPOSE_LEGACY_FILES:["docker-compose.yaml","docker-compose.yml"],HOST_ALIAS:"host.docker.internal",ENGINE_SOCKET:"/var/run/docker.sock",ENGINE_SOCKETS:["/var/run/docker.sock","/run/docker.sock"],ENV_MARKER:"/.dockerenv",INDEX_HOST:"index.docker.io",ENGINE_CLI:"docker",CONTEXT_ENDPOINT_FLAG:"--docker",ENGINE_HOST_VAR:"DOCKER_HOST",TESTCONTAINERS_HOST_KEY:"docker.host",ENGINE_CLI_IMAGE:"docker:27-cli",LEGACY_PREVIOUS_CONTEXT_FILE:"previous-docker-context"};
|
|
7
|
+
"use strict";module.exports={DEFAULT_REGISTRY:"https://index.docker.io/v1/",DEFAULT_REGISTRY_HOST:"docker.io",DEFAULT_REGISTRY_API:"registry-1.docker.io",DEFAULT_REGISTRY_SEARCH:"https://index.docker.io/v1/search",MANIFEST_LIST_V2:"application/vnd.docker.distribution.manifest.list.v2+json",MANIFEST_V2:"application/vnd.docker.distribution.manifest.v2+json",LAYER_V2_GZIP:"application/vnd.docker.image.rootfs.diff.tar.gzip",RAW_STREAM:"application/vnd.docker.raw-stream",MUX_STREAM:"application/vnd.docker.multiplexed-stream",ROOT_DIR_FIELD:"DockerRootDir",REGISTRY_AUTH_HEADER:"x-registry-auth",PATH_STAT_HEADER:"X-Docker-Container-Path-Stat",SESSION_HEADER_PREFIX:"x-docker-expose-session-",BUILDKIT_GRPC_PACKAGE:"moby.buildkit.v1",ENGINE_EXPORTER:"moby",BUILDKIT_RELEASES:"https://github.com/moby/buildkit/releases/download",BUILD_FILE:"Dockerfile",BUILD_IGNORE_FILE:".dockerignore",BUILD_FILE_PARAM:"dockerfile",COMPOSE_LEGACY_FILES:["docker-compose.yaml","docker-compose.yml"],HOST_ALIAS:"host.docker.internal",ENGINE_SOCKET:"/var/run/docker.sock",ENGINE_SOCKETS:["/var/run/docker.sock","/run/docker.sock"],ENV_MARKER:"/.dockerenv",INDEX_HOST:"index.docker.io",ENGINE_CLI:"docker",CONTEXT_ENDPOINT_FLAG:"--docker",ENGINE_HOST_VAR:"DOCKER_HOST",TESTCONTAINERS_HOST_KEY:"docker.host",ENGINE_CLI_IMAGE:"docker:27-cli",CREDENTIAL_HELPER_PREFIX:"docker-credential-",ENGINE_CLI_CONFIG_DIR:".docker",ENGINE_CLI_CONFIG_VAR:"DOCKER_CONFIG",LEGACY_PREVIOUS_CONTEXT_FILE:"previous-docker-context",SIGNING_IDENTITY_KEY:"docker-reference",SIGNING_DIGEST_KEY:"docker-manifest-digest",SIGNING_DEFAULT_REGISTRY:"index.docker.io",MULTI_PLATFORM_STORE_STATUS:["driver-type","io.containerd.snapshotter.v1"]};
|
|
@@ -4,4 +4,4 @@
|
|
|
4
4
|
* PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
|
|
5
5
|
* UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
|
|
6
6
|
*/
|
|
7
|
-
"use strict";const fs=require("fs"),path=require("path"),crypto=require("crypto"),{parseBuildFile,expand,splitWords,parseKeyValues,parseDuration,buildError,HEREDOC}=require("./buildfile"),{parsePlatform,platformString}=require("../runtime/emulation"),{buildSpec,resolveUser}=require("../runtime/spec"),{parseReference}=require("../image/reference"),{IO_CGROUP}=require("../runtime/rootfs"),LAYER_MEDIA="application/vnd.oci.image.layer.v1.tar+gzip",DEFAULT_SHELL=["/bin/sh","-c"],GO_ARCH={x64:"amd64",arm64:"arm64"},sha256=t=>crypto.createHash("sha256").update(t).digest("hex"),short=t=>t.replace(/^sha256:/,"").slice(0,12),COMMIT=`\nset -e\nupper="$1"; out="$2"; shim="$3"; DOCK_IO_CGROUP="$4"\n${IO_CGROUP}\n"$shim" oci-whiteouts "$upper"\ncd "$upper"\nif [ -z "$(ls -A)" ]; then echo EMPTY; exit 0; fi\ntar -cf - . | gzip -n > "$out"\nd=$(sha256sum "$out" | cut -d' ' -f1)\ni=$(gzip -dc "$out" | sha256sum | cut -d' ' -f1)\ns=$(stat -c %s "$out")\necho "$d $i $s"\n`,COPY=`\nset -e\nbase="$1"; tmp="$2"; merged="$3"; dest="$4"; forcedir="$5"; owner="$6"; mode="$7"; extract="$8"; DOCK_IO_CGROUP="$9"; shift 9\n${IO_CGROUP}\nrm -rf "$tmp"; mkdir -p "$tmp"\nlist="$tmp.list"; : > "$list"\nfor pat; do\n found=0\n for src in "$base"/$pat; do\n if [ -e "$src" ] || [ -L "$src" ]; then printf '%s\\n' "$src" >> "$list"; found=1; fi\n done\n if [ $found = 0 ]; then echo "file not found in build context or excluded by the ignore file: $pat" >&2; exit 2; fi\ndone\ncount=$(wc -l < "$list")\ndirmode=$forcedir\n[ "$count" -gt 1 ] && dirmode=1\n[ -d "$merged$dest" ] && dirmode=1\nx="$tmp/x"\nwhile IFS= read -r src; do\n if [ -d "$src" ] && [ ! -L "$src" ]; then\n mkdir -p "$x"; cp -a "$src/." "$x/"\n elif [ "$extract" = 1 ] && case "$src" in *.tar|*.tar.gz|*.tgz|*.tar.bz2|*.tar.xz) true;; *) false;; esac; then\n mkdir -p "$x"; tar -xf "$src" -C "$x"\n elif [ "$dirmode" = 1 ]; then\n mkdir -p "$x"; cp -a "$src" "$x/"\n else\n cp -a "$src" "$x"\n fi\ndone < "$list"\n[ -n "$owner" ] && chown -R "$owner" "$x"\n[ -n "$mode" ] && chmod -R "$mode" "$x"\nmkdir -p "$merged$(dirname "$dest")"\nif [ -d "$x" ] && [ -d "$merged$dest" ]; then cp -a "$x/." "$merged$dest/"; else rm -rf "$merged$dest"; cp -a "$x" "$merged$dest"; fi\nrm -rf "$tmp" "$list"\n`;function globToRegex(t){let e="";for(const s of t)e+="*"===s?"[^/]*":"?"===s?"[^/]":s.replace(/[.+^${}()|[\]\\]/g,"\\$&");return new RegExp(`^${e}$`)}function matchContext(t,e){const s=e.split("/").filter(t=>t&&"."!==t);let i=[t];for(const t of s){const e=[],s=/[*?]/.test(t)?globToRegex(t):null;for(const r of i){if(!s){const s=path.join(r,t);(fs.existsSync(s)||isLink(s))&&e.push(s);continue}let i=[];try{i=fs.readdirSync(r)}catch(t){}for(const t of i)s.test(t)&&e.push(path.join(r,t))}i=e}return s.length?i:[t]}function isLink(t){try{return fs.lstatSync(t).isSymbolicLink()}catch(t){return!1}}function hashPaths(t,e){const s=crypto.createHash("sha256"),i=e=>{const r=fs.lstatSync(e);if(s.update(`${path.relative(t,e)}\0${r.mode}\0`),r.isSymbolicLink())s.update(fs.readlinkSync(e));else if(r.isDirectory())for(const t of fs.readdirSync(e).sort())i(path.join(e,t));else s.update(fs.readFileSync(e))};for(const t of e.sort())i(t);return s.digest("hex")}function parseMount(t,e){const s={type:"bind"};for(const e of String(t).split(",")){if(!e)continue;const t=e.indexOf("="),i=(-1===t?e:e.slice(0,t)).toLowerCase(),r=-1===t?"true":e.slice(t+1);s[{dst:"target",destination:"target",src:"source",ro:"readonly","read-only":"readonly",readwrite:"rw"}[i]||i]=r}if(!["cache","secret","bind","tmpfs","ssh"].includes(s.type))throw buildError(`line ${e}: RUN --mount type "${s.type}" is not supported (cache, secret, bind, tmpfs)`);if("ssh"===s.type)throw buildError(`line ${e}: RUN --mount=type=ssh is not supported yet; use --mount=type=secret for keys or tokens`);if("secret"===s.type){if(!s.id&&!s.target)throw buildError(`line ${e}: RUN --mount=type=secret needs id=…`);s.id=s.id||path.posix.basename(s.target),s.target||s.env||(s.target=`/run/secrets/${s.id}`)}else if(!s.target)throw buildError(`line ${e}: RUN --mount=type=${s.type} needs target=…`);if(s.target&&!s.target.startsWith("/"))throw buildError(`line ${e}: mount target must be absolute: ${s.target}`);return s}const GO_ARCH_NAME=t=>GO_ARCH[t]||t;class Builder{constructor(t,{contextDir:e,buildDir:s,out:i,quiet:r=!1,noCache:n=!1,pull:o=!1,buildArgs:a={},target:c="",labels:h={},networkMode:l="bridge",platform:d=null,secretsDir:u=null}){this.engine=t,this.store=t.store,this.host=t.host,this.contextDir=e,this.buildDir=s,this.out=i,this.quiet=r,this.noCache=n,this.pull=o,this.buildArgs=a,this.usedArgs=new Set,this.target=c,this.labels=h,this.networkMode=l,this.bid=crypto.randomBytes(6).toString("hex"),this.mounted=new Set,this.step=0,this.secretsDir=u;const f={os:"linux",arch:GO_ARCH_NAME(process.arch)};this.buildPlatform=f,this.targetPlatform=d||f;const p=(t,e)=>({[`${t}PLATFORM`]:platformString(e),[`${t}OS`]:e.os||"linux",[`${t}ARCH`]:e.arch,[`${t}VARIANT`]:e.variant||""});this.autoArgs={...p("TARGET",this.targetPlatform),...p("BUILD",f)}}say(t){this.quiet||this.out({stream:t.endsWith("\n")?t:t+"\n"})}async run(t){const e=fs.readFileSync(t,"utf8"),{instructions:s}=parseBuildFile(e);if(!s.length)throw buildError("the build file cannot be empty");const i={},r=[];for(const t of s)if("FROM"===t.cmd){const e=splitWords(t.args),s=e.findIndex(t=>"as"===t.toLowerCase());r.push({index:r.length,name:-1!==s?e[s+1].toLowerCase():null,baseRaw:e[0],from:t,instructions:[]})}else if(r.length)r[r.length-1].instructions.push(t);else{if("ARG"!==t.cmd)throw buildError(`line ${t.line}: ${t.cmd} must come after FROM (only ARG may come before)`);for(const[e,s]of parseKeyValues(t.args,"ARG"))i[e]=this.buildArgs[e]??s??this.autoArgs[e]??"",e in this.buildArgs&&this.usedArgs.add(e)}this.globalArgs=i;const n={...this.autoArgs,...i};for(const t of r)t.platform=t.from.flags.platform?parsePlatform(expand(t.from.flags.platform,n)):this.targetPlatform;const o=this.target?r.find(t=>t.name===this.target.toLowerCase()):r[r.length-1];if(!o)throw buildError(`failed to reach build target ${this.target} in the build file`);const a=new Set,c=t=>{if(a.has(t.index))return;a.add(t.index);const e=this._stageRef(r,expand(t.baseRaw,{...this.autoArgs,...i}),t.index);e&&c(e);for(const e of t.instructions)if(("COPY"===e.cmd||"ADD"===e.cmd)&&e.flags.from){const s=this._stageRef(r,e.flags.from,t.index);s&&c(s)}};c(o);const h=r.filter(t=>a.has(t.index)&&t.index<=o.index);this.total=h.reduce((t,e)=>t+1+e.instructions.length,0);try{for(const t of h)t.state=await this._buildStage(t,r,i);const t=Object.keys(this.buildArgs).filter(t=>!this.usedArgs.has(t));return t.length&&this.say(`[Warning] One or more build-args [${t.join(" ")}] were not consumed`),this._finish(o.state,o.platform)}finally{for(const t of this.mounted)await this.engine.rootfs.remove(t).catch(()=>{})}}_stageRef(t,e,s){return/^\d+$/.test(e)?t[Number(e)]:t.find(t=>t.name&&t.name===e.toLowerCase()&&t.index<s)||null}async _image(t,e){let s=this.pull?null:this.engine.findImage(t);return s&&s.architecture&&e&&s.architecture!==e&&(s=null),s||(this.say(`Pulling ${t}${e!==this.buildPlatform.arch?` (linux/${e})`:""}`),s=await this.engine.pull(t,()=>{},{platform:{os:"linux",arch:e}})),s}async _baseState(t,e,s){const i=expand(t.baseRaw,{...this.autoArgs,...s}),r=this._stageRef(e,i,t.index);if(r)return{...JSON.parse(JSON.stringify(r.state)),arch:r.platform.arch};if("scratch"===i)return{layers:[],diffIds:[],config:{Env:["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"]},history:[],baseId:"scratch",arch:t.platform.arch};const n=await this._image(i,t.platform.arch);return{arch:n.architecture||t.platform.arch,layers:(n.layers||[]).map(t=>({...t})),diffIds:[...n.config?.rootfs?.diff_ids||[]],config:JSON.parse(JSON.stringify(n.config?.config||{})),history:[...n.config?.history||[]],baseId:n.id}}async _buildStage(t,e,s){this.say(`Step ${++this.step}/${this.total} : ${t.from.original}`);const i=await this._baseState(t,e,s);i.args={},i.cmdSet=!1,this.say(` ---\x3e ${short("scratch"===i.baseId?sha256("scratch"):i.baseId)}`);for(const s of t.instructions)this.say(`Step ${++this.step}/${this.total} : ${s.original}`),await this._instruction(i,s,e,t);return i}_env(t){const e={...t.args};for(const s of t.config.Env||[]){const t=s.indexOf("=");e[s.slice(0,t)]=s.slice(t+1)}return e}_setEnv(t,e,s){const i=(t.config.Env||[]).filter(t=>t.split("=")[0]!==e);i.push(`${e}=${s}`),t.config.Env=i}_shellForm(t,e){return[...t.config.Shell||DEFAULT_SHELL,e]}_chainKey(t){return sha256(JSON.stringify(t.diffIds))}_stepId(t){return short(sha256(JSON.stringify([t.diffIds,t.config])))}async _instruction(t,e,s,i){const r=this._env(t),n=t=>expand(t,r);switch(e.cmd){case"ENV":for(const[s,i]of parseKeyValues(e.args,"ENV"))this._setEnv(t,s,expand(i,this._env(t)));return this._meta(t,e);case"ARG":for(const[s,i]of parseKeyValues(e.args,"ARG"))s in this.buildArgs?(t.args[s]=this.buildArgs[s],this.usedArgs.add(s)):t.args[s]=void 0!==i?n(i):this.globalArgs[s]??this.autoArgs[s]??"";return this._meta(t,e);case"LABEL":t.config.Labels={...t.config.Labels||{}};for(const[s,i]of parseKeyValues(e.args,"LABEL"))t.config.Labels[n(s)]=n(i);return this._meta(t,e);case"MAINTAINER":return t.author=e.args,this._meta(t,e);case"EXPOSE":t.config.ExposedPorts={...t.config.ExposedPorts||{}};for(const s of splitWords(n(e.args)))t.config.ExposedPorts[s.includes("/")?s:`${s}/tcp`]={};return this._meta(t,e);case"VOLUME":t.config.Volumes={...t.config.Volumes||{}};for(const s of(e.json||splitWords(e.args)).map(n))t.config.Volumes[s]={};return this._meta(t,e);case"USER":return t.config.User=n(e.args),this._meta(t,e);case"STOPSIGNAL":return t.config.StopSignal=n(e.args),this._meta(t,e);case"SHELL":if(!e.json)throw buildError(`line ${e.line}: SHELL requires the arguments to be in JSON form`);return t.config.Shell=e.json,this._meta(t,e);case"CMD":return t.config.Cmd=e.json||this._shellForm(t,e.args),t.cmdSet=!0,this._meta(t,e);case"ENTRYPOINT":return t.config.Entrypoint=e.json||this._shellForm(t,e.args),t.cmdSet||(t.config.Cmd=null),this._meta(t,e);case"HEALTHCHECK":return t.config.Healthcheck=this._healthcheck(e),this._meta(t,e);case"ONBUILD":return t.config.OnBuild=[...t.config.OnBuild||[],e.args],this.say(" ---\x3e ONBUILD triggers are stored but not executed by dock yet"),this._meta(t,e);case"WORKDIR":{const s=path.posix.resolve(t.config.WorkingDir||"/",n(e.args));return t.config.WorkingDir=s,this._fsStep(t,e,`WORKDIR ${s}`,async()=>{})}case"RUN":return this._run(t,e,r,s,i);case"COPY":case"ADD":return this._copy(t,e,r,s,i);default:throw buildError(`line ${e.line}: unknown instruction: ${e.cmd}`)}}_healthcheck(t){const e=t.args.trim();if(/^NONE$/i.test(e))return{Test:["NONE"]};const s=/^CMD\s+(.*)$/is.exec(e);if(!s)throw buildError(`line ${t.line}: HEALTHCHECK must be NONE or CMD ...`);let i;try{const t=JSON.parse(s[1]);i=Array.isArray(t)?["CMD",...t]:null}catch(t){}const r={Test:i||["CMD-SHELL",s[1]]},n=t.flags;return n.interval&&(r.Interval=parseDuration(n.interval)),n.timeout&&(r.Timeout=parseDuration(n.timeout)),n["start-period"]&&(r.StartPeriod=parseDuration(n["start-period"])),n["start-interval"]&&(r.StartInterval=parseDuration(n["start-interval"])),n.retries&&(r.Retries=parseInt(n.retries,10)),r}_meta(t,e){t.history.push({created:(new Date).toISOString(),created_by:`/bin/sh -c #(nop) ${e.original}`,empty_layer:!0}),this.say(` ---\x3e ${this._stepId(t)}`)}async _fsStep(t,e,s,i,{cacheable:r=!0,createdBy:n}={}){const o=sha256(`${this._chainKey(t)}\n${s}`),a=r&&!this.noCache?this.store.getBuildCache(o):null;if(a&&(""===a.digest||fs.existsSync(this._blobPath(a.digest))))return this.say(" ---\x3e Using cache"),this._addLayer(t,a,n||e.original),void this.say(` ---\x3e ${this._stepId(t)}`);const c=`build-${this.bid}-${this.step}`,h=await this.engine.rootfs.prepare(c,{layers:t.layers},t.config.WorkingDir||"/");this.mounted.add(c);try{await i(h,c);const s=await this._commit(c);this.store.putBuildCache(o,s),this._addLayer(t,s,n||e.original)}finally{await this.engine.rootfs.remove(c).catch(()=>{}),this.mounted.delete(c)}this.say(` ---\x3e ${this._stepId(t)}`)}_blobPath(t){return path.join(this.engine.paths.blobs,t.replace(/^sha256:/,""))}async _commit(t){const e=path.join(this.engine.paths.blobs,`build-${t}.tmp`),{shim:s}=this.host.binaries(),i=`${this.engine.rootfs.root}/containers/${t}/upper`,r=(await this.host.sh(COMMIT,[i,this.host.guestPath(e),s,"vm"===this.host.kind?1:0])).stdout.trim().split("\n").pop();if("EMPTY"===r)return{digest:"",diffId:"",size:0};const[n,o,a]=r.split(" ");return fs.renameSync(e,this._blobPath(n)),{digest:`sha256:${n}`,diffId:`sha256:${o}`,size:Number(a)}}_addLayer(t,e,s){const i={created:(new Date).toISOString(),created_by:s};e.digest?(t.layers.push({digest:e.digest,size:e.size,mediaType:LAYER_MEDIA}),t.diffIds.push(e.diffId)):i.empty_layer=!0,t.history.push(i)}_runArgv(t,e){if(e.json)return{argv:e.json,stdin:null};if(e.heredocs?.length){if(!e.args.replace(new RegExp(HEREDOC.source,"g"),"").trim()&&1===e.heredocs.length){const s=e.heredocs[0].content,i=/^#!(.*)\n/.exec(s);return i?{argv:splitWords(i[1].trim()),stdin:s}:{argv:[...t.config.Shell||DEFAULT_SHELL,s],stdin:null}}return{argv:this._shellForm(t,e.heredocText),stdin:null}}return{argv:this._shellForm(t,e.args),stdin:null}}async _runMounts(t,e,s,i,r,n){const o=(e.mounts||[]).map(t=>parseMount(expand(t,s),e.line)),a=t=>this.host.guestPath(t),c=this.engine.rootfs.root,h=[],l=[],d=[],u=[];for(const f of o)if("cache"===f.type){const t=f.id||f.target,e=`${c}/cache-mounts/${sha256(t).slice(0,32)}`,s=`${f.uid||0}:${f.gid||0}`;await this.host.sh('[ -d "$1" ] || { mkdir -p "$1"; chown "$2" "$1"; chmod "$3" "$1"; }',[e,s,f.mode||"0755"]),h.push({destination:f.target,type:"bind",source:e,options:["rbind","rw"]}),l.push(["cache",f.target,t])}else if("tmpfs"===f.type)h.push({destination:f.target,type:"tmpfs",source:"tmpfs",options:["nosuid","nodev",...f.size?[`size=${f.size}`]:[]]}),l.push(["tmpfs",f.target]);else if("secret"===f.type){const t=this.secretsDir&&path.join(this.secretsDir,f.id);if(!t||!fs.existsSync(t)){if("true"===f.required)throw buildError(`line ${e.line}: secret ${f.id} is required; pass it with: thinknsh build --secret id=${f.id},src=FILE`);continue}if(f.env&&d.push(`${f.env}=${fs.readFileSync(t,"utf8")}`),f.target){const e=`/run/tnc-secrets/${n}`;await this.host.sh('umask 077; mkdir -p "$1"; cat > "$1/$2"; chown "$3" "$1/$2"; chmod "$4" "$1/$2"',[e,f.id,`${f.uid||0}:${f.gid||0}`,f.mode||"0400"],{input:fs.readFileSync(t)}),h.push({destination:f.target,type:"bind",source:`${e}/${f.id}`,options:["rbind","ro"]}),u.push(()=>this.host.sh('rm -rf "$1"',[e]))}l.push(["secret",f.id,f.target||"",f.env||""])}else if("bind"===f.type){const o=path.posix.normalize("/"+(f.source||"/")).replace(/\/+$/,"")||"/",d="true"===f.rw||"true"===f.readwrite;let p;if(f.from){const e=expand(f.from,s),a=this._stageRef(i,e,r.index);let c;if(a)c=a.state.layers,l.push(["bind",f.target,"stage",a.state.diffIds,o]);else{const s=await this._image(e,t.arch);c=s.layers,l.push(["bind",f.target,"image",s.id,o])}const d=`${n}-bind-${h.length}`,g=await this.engine.rootfs.prepare(d,{layers:c},"/");this.mounted.add(d),u.push(async()=>{await this.engine.rootfs.remove(d).catch(()=>{}),this.mounted.delete(d)}),p=`${g}${"/"===o?"":o}`}else{const t=o.replace(/^\//,"");if(t.startsWith(".."))throw buildError(`line ${e.line}: bind source outside the build context: ${f.source}`);l.push(["bind",f.target,"context",hashPaths(this.contextDir,matchContext(this.contextDir,t||"."))]),p=a(path.join(this.contextDir,t))}if(d){const t=`${c}/containers/${n}/bind-rw-${h.length}`;await this.host.sh('mkdir -p "$2" && cp -a "$1/." "$2/" 2>/dev/null || cp -a "$1" "$2"',[p,t]),p=t}h.push({destination:f.target,type:"bind",source:p,options:["rbind",d?"rw":"ro"]})}return{mounts:h,cacheText:l,env:d,cleanup:u}}async _run(t,e,s,i,r){const n=Object.keys(e.flags).filter(t=>!["network","mount","security"].includes(t));if(n.length)throw buildError(`line ${e.line}: RUN --${n[0]} is not supported yet`);if(e.flags.security&&!["insecure","sandbox"].includes(e.flags.security))throw buildError(`line ${e.line}: RUN --security must be insecure or sandbox`);const{argv:o,stdin:a}=this._runArgv(t,e),c=Object.entries(s).map(([t,e])=>`${t}=${e}`),h=e.flags.network||this.networkMode,l="insecure"===e.flags.security,d=(e.mounts||[]).length?(e.mounts||[]).map(t=>parseMount(expand(t,s),e.line)).map(t=>[t.type,t.target,t.id||"",t.source||"",t.from||""]):[],u=a||d.length||l||t.arch!==this.buildPlatform.arch?JSON.stringify(["RUN",o,a,c.sort(),t.config.User||"",t.config.WorkingDir||"/",d,l,t.arch]):JSON.stringify(["RUN",o,c.sort(),t.config.User||"",t.config.WorkingDir||"/"]);await this.engine.emulation.ensure(t.arch),await this._fsStep(t,e,u,async(n,d)=>{this.say(` ---\x3e Running in ${short(sha256(d))}`);const u=await this._runMounts(t,e,s,i,r,d),f=t.config.User||"",p=f&&f.split(":").some(t=>!/^\d+$/.test(t))?resolveUser(f,await this.engine.rootfs.readFile(d,"/etc/passwd"),await this.engine.rootfs.readFile(d,"/etc/group")):resolveUser(f),g=path.join(this.buildDir,`step-${this.step}`);fs.mkdirSync(g,{recursive:!0});const m=t=>this.host.guestPath(t),$="none"===h?await this.engine.network.attachAll(d,[{name:"none"}]):"host"===h?{nsPath:null,endpoints:{host:{}}}:await this.engine.network.attachAll(d,[{name:"bridge"}]);try{fs.writeFileSync(path.join(g,"hosts"),"127.0.0.1\tlocalhost\n::1\tlocalhost\n"),fs.writeFileSync(path.join(g,"resolv.conf"),await this.engine._resolvConf());const e=buildSpec({id:d,rootfs:n,args:o,env:[...c,...u.env],cwd:t.config.WorkingDir||"/",user:p,hostname:short(sha256(d)),nsPath:$.nsPath,hostConfig:l?{Privileged:!0}:{},mounts:[{destination:"/etc/hosts",type:"bind",source:m(path.join(g,"hosts")),options:["rbind","rw"]},{destination:"/etc/resolv.conf",type:"bind",source:m(path.join(g,"resolv.conf")),options:["rbind","rw"]},...u.mounts]});fs.writeFileSync(path.join(g,"config.json"),JSON.stringify(e));const{crun:s}=this.host.binaries(),i=this.host.spawn([s,"--cgroup-manager=cgroupfs","run","--bundle",m(g),d]);a&&i.write(a),i.end();const r=t=>{this.quiet||this.out({stream:t.toString("utf8")})};i.stdout.on("data",r),i.stderr.on("data",r);const h=await i.exited;if(this.say(` ---\x3e Removed intermediate container ${short(sha256(d))}`),0!==h){const t=buildError(`The command '${o.join(" ")}' returned a non-zero code: ${h}`);throw t.code=h,t}}finally{$.nsPath&&await this.engine.network.detachAll(d,$.endpoints);for(const t of u.cleanup)await t().catch(()=>{});await this._dropMountTargets(d,u.mounts)}},{createdBy:e.heredocs?.length?`RUN ${e.original.replace(/^RUN\s+/i,"")}`:`${o.join(" ")}`})}async _dropMountTargets(t,e){if(!e.length)return;const s=`${this.engine.rootfs.root}/containers/${t}/upper`;await this.host.sh('u="$1"; shift; for t; do p="$u$t"; while [ "$p" != "$u" ]; do if [ -d "$p" ] && [ ! -L "$p" ]; then rmdir "$p" 2>/dev/null || break; elif [ -f "$p" ] && [ ! -s "$p" ]; then rm -f "$p"; else break; fi; p=$(dirname "$p"); done; done; true',[s,...e.map(t=>t.destination)]).catch(()=>{})}async _copy(t,e,s,i,r){const n=t=>expand(t,s);let o;if(e.heredocs?.length&&!e.flags.from){const t=`.tnc-heredoc-${this.step}`;fs.mkdirSync(path.join(this.contextDir,t),{recursive:!0});const i=new Map(e.heredocs.map(t=>[t.name,t]));o=splitWords(e.args).map(e=>{const r=/^<<-?["']?([A-Za-z_][A-Za-z0-9_]*)["']?$/.exec(e);if(!r||!i.has(r[1]))return n(e);const o=i.get(r[1]);return fs.writeFileSync(path.join(this.contextDir,t,o.name),o.expand?expand(o.content,s):o.content),`${t}/${o.name}`})}else o=(e.json||splitWords(e.args)).map(n);if(o.length<2)throw buildError(`line ${e.line}: ${e.cmd} requires at least two arguments`);let a=o.pop();const c=a.endsWith("/")||o.length>1;a=path.posix.resolve(t.config.WorkingDir||"/",a);let h,l,d=o,u=null;if(e.flags.from){const s=n(e.flags.from),o=this._stageRef(i,s,r.index);let a;if(o)a=o.state.layers,l=sha256(JSON.stringify(o.state.diffIds)+JSON.stringify(d));else{const e=await this._image(s,t.arch);a=e.layers,l=sha256(e.id+JSON.stringify(d))}u=`build-${this.bid}-from-${this.step}`,h=await this.engine.rootfs.prepare(u,{layers:a},"/"),this.mounted.add(u),d=d.map(t=>t.replace(/^\/+/,""))}else{"ADD"===e.cmd&&(d=await Promise.all(d.map(t=>/^https?:\/\//.test(t)?this._download(t):t)));for(const t of d)if(path.posix.normalize(t.replace(/^\/+/,"")).startsWith(".."))throw buildError(`line ${e.line}: forbidden path outside the build context: ${t}`);d=d.map(t=>path.posix.normalize(t.replace(/^\/+/,"")));const t=d.flatMap(t=>matchContext(this.contextDir,t));l=hashPaths(this.contextDir,t),h=this.host.guestPath(this.contextDir)}const f="ADD"===e.cmd&&!e.flags.from,p=JSON.stringify([e.cmd,e.flags,d,a,l]);try{await this._fsStep(t,e,p,async(t,s)=>{let i=e.flags.from?"":"0:0";if(e.flags.chown){const t=resolveUser(n(e.flags.chown),await this.engine.rootfs.readFile(s,"/etc/passwd"),await this.engine.rootfs.readFile(s,"/etc/group"));i=`${t.uid}:${t.gid}`}const r=`${this.engine.rootfs.root}/containers/${s}/copy`;await this.host.sh(COPY,[h,r,t,a,c?1:0,i,e.flags.chmod||"",f?1:0,"vm"===this.host.kind?1:0,...d])})}finally{u&&(await this.engine.rootfs.remove(u).catch(()=>{}),this.mounted.delete(u))}}async _download(t){const e=await fetch(t);if(!e.ok)throw buildError(`failed to download ${t}: HTTP ${e.status}`);const s=path.posix.basename(new URL(t).pathname)||"download",i=`.dock-add-${this.step}/${s}`;return fs.mkdirSync(path.join(this.contextDir,path.dirname(i)),{recursive:!0}),fs.writeFileSync(path.join(this.contextDir,i),Buffer.from(await e.arrayBuffer())),i}_finish(t,e){const s={architecture:e?.arch||t.arch||GO_ARCH[process.arch]||process.arch,...e?.variant?{variant:e.variant}:{},os:"linux",created:(new Date).toISOString(),...t.author?{author:t.author}:{},config:{...t.config,Labels:{...t.config.Labels||{},...this.labels}},rootfs:{type:"layers",diff_ids:t.diffIds},history:t.history},i=JSON.stringify(s),r=`sha256:${sha256(i)}`;return fs.writeFileSync(this._blobPath(r),i),{id:r,repoTags:[],repoDigests:[],digest:null,size:t.layers.reduce((t,e)=>t+e.size,0),layers:t.layers,config:s,architecture:s.architecture,os:"linux",createdAt:Date.now()}}}function normalizeTag(t){return parseReference(t).familiar}module.exports={Builder,normalizeTag,matchContext,hashPaths,parseMount,COMMIT};
|
|
7
|
+
"use strict";const fs=require("fs"),path=require("path"),crypto=require("crypto"),{parseBuildFile,expand,splitWords,parseKeyValues,parseDuration,buildError,HEREDOC}=require("./buildfile"),{parsePlatform,platformString}=require("../runtime/emulation"),{buildSpec,resolveUser}=require("../runtime/spec"),{parseReference}=require("../image/reference"),{IO_CGROUP}=require("../runtime/rootfs"),LAYER_MEDIA="application/vnd.oci.image.layer.v1.tar+gzip",DEFAULT_SHELL=["/bin/sh","-c"],GO_ARCH={x64:"amd64",arm64:"arm64"},sha256=t=>crypto.createHash("sha256").update(t).digest("hex"),short=t=>t.replace(/^sha256:/,"").slice(0,12),COMMIT=`\nset -e\nupper="$1"; out="$2"; shim="$3"; DOCK_IO_CGROUP="$4"\n${IO_CGROUP}\nif [ -n "\${REMAP:-}" ]; then "$shim" idshift "$upper" $REMAP from; fi\n"$shim" oci-whiteouts "$upper"\ncd "$upper"\nif [ -z "$(ls -A)" ]; then echo EMPTY; exit 0; fi\ntar -cf - . | gzip -n > "$out"\nd=$(sha256sum "$out" | cut -d' ' -f1)\ni=$(gzip -dc "$out" | sha256sum | cut -d' ' -f1)\ns=$(stat -c %s "$out")\necho "$d $i $s"\n`,STEP_STDIO='\nd=$(mktemp -d) || exit 1\nmkfifo "$d/o" "$d/e" || exit 1\n[ -n "$1" ] && chown "$1" "$d" "$d/o" "$d/e"\nshift\ncat "$d/o" & a=$!\ncat "$d/e" >&2 & b=$!\n"$@" > "$d/o" 2> "$d/e"\nrc=$?\nwait $a $b\nrm -rf "$d"\nexit $rc\n',COPY=`\nset -e\nbase="$1"; tmp="$2"; merged="$3"; dest="$4"; forcedir="$5"; owner="$6"; mode="$7"; extract="$8"; DOCK_IO_CGROUP="$9"; shift 9\n${IO_CGROUP}\nrm -rf "$tmp"; mkdir -p "$tmp"\nlist="$tmp.list"; : > "$list"\nfor pat; do\n found=0\n for src in "$base"/$pat; do\n if [ -e "$src" ] || [ -L "$src" ]; then printf '%s\\n' "$src" >> "$list"; found=1; fi\n done\n if [ $found = 0 ]; then echo "file not found in build context or excluded by the ignore file: $pat" >&2; exit 2; fi\ndone\ncount=$(wc -l < "$list")\ndirmode=$forcedir\n[ "$count" -gt 1 ] && dirmode=1\n[ -d "$merged$dest" ] && dirmode=1\nx="$tmp/x"\nwhile IFS= read -r src; do\n if [ -d "$src" ] && [ ! -L "$src" ]; then\n mkdir -p "$x"; cp -a "$src/." "$x/"\n elif [ "$extract" = 1 ] && case "$src" in *.tar|*.tar.gz|*.tgz|*.tar.bz2|*.tar.xz) true;; *) false;; esac; then\n mkdir -p "$x"; tar -xf "$src" -C "$x"\n elif [ "$dirmode" = 1 ]; then\n mkdir -p "$x"; cp -a "$src" "$x/"\n else\n cp -a "$src" "$x"\n fi\ndone < "$list"\n[ -n "$owner" ] && chown -R "$owner" "$x"\n[ -n "$mode" ] && chmod -R "$mode" "$x"\nmkdir -p "$merged$(dirname "$dest")"\nif [ -d "$x" ] && [ -d "$merged$dest" ]; then cp -a "$x/." "$merged$dest/"; else rm -rf "$merged$dest"; cp -a "$x" "$merged$dest"; fi\nrm -rf "$tmp" "$list"\n`;function globToRegex(t){let e="";for(const s of t)e+="*"===s?"[^/]*":"?"===s?"[^/]":s.replace(/[.+^${}()|[\]\\]/g,"\\$&");return new RegExp(`^${e}$`)}function matchContext(t,e){const s=e.split("/").filter(t=>t&&"."!==t);let i=[t];for(const t of s){const e=[],s=/[*?]/.test(t)?globToRegex(t):null;for(const r of i){if(!s){const s=path.join(r,t);(fs.existsSync(s)||isLink(s))&&e.push(s);continue}let i=[];try{i=fs.readdirSync(r)}catch(t){}for(const t of i)s.test(t)&&e.push(path.join(r,t))}i=e}return s.length?i:[t]}function isLink(t){try{return fs.lstatSync(t).isSymbolicLink()}catch(t){return!1}}function hashPaths(t,e){const s=crypto.createHash("sha256"),i=e=>{const r=fs.lstatSync(e);if(s.update(`${path.relative(t,e)}\0${r.mode}\0`),r.isSymbolicLink())s.update(fs.readlinkSync(e));else if(r.isDirectory())for(const t of fs.readdirSync(e).sort())i(path.join(e,t));else s.update(fs.readFileSync(e))};for(const t of e.sort())i(t);return s.digest("hex")}function parseMount(t,e){const s={type:"bind"};for(const e of String(t).split(",")){if(!e)continue;const t=e.indexOf("="),i=(-1===t?e:e.slice(0,t)).toLowerCase(),r=-1===t?"true":e.slice(t+1);s[{dst:"target",destination:"target",src:"source",ro:"readonly","read-only":"readonly",readwrite:"rw"}[i]||i]=r}if(!["cache","secret","bind","tmpfs","ssh"].includes(s.type))throw buildError(`line ${e}: RUN --mount type "${s.type}" is not supported (cache, secret, bind, tmpfs, ssh)`);if("ssh"===s.type)s.id=s.id||"default";else if("secret"===s.type){if(!s.id&&!s.target)throw buildError(`line ${e}: RUN --mount=type=secret needs id=…`);s.id=s.id||path.posix.basename(s.target),s.target||s.env||(s.target=`/run/secrets/${s.id}`)}else if(!s.target)throw buildError(`line ${e}: RUN --mount=type=${s.type} needs target=…`);if(s.target&&!s.target.startsWith("/"))throw buildError(`line ${e}: mount target must be absolute: ${s.target}`);return s}const GO_ARCH_NAME=t=>GO_ARCH[t]||t;class Builder{constructor(t,{contextDir:e,buildDir:s,out:i,quiet:r=!1,noCache:n=!1,pull:o=!1,buildArgs:a={},target:h="",labels:c={},networkMode:d="bridge",platform:l=null,secretsDir:u=null,cacheFrom:f=[],cacheTo:p=[],ssh:g=null}){this.engine=t,this.store=t.store,this.host=t.host,this.contextDir=e,this.buildDir=s,this.out=i,this.quiet=r,this.noCache=n,this.pull=o,this.buildArgs=a,this.usedArgs=new Set,this.target=h,this.labels=c,this.networkMode=d,this.bid=crypto.randomBytes(6).toString("hex"),this.mounted=new Set,this.step=0,this.seq=0,this.secretsDir=u;const{parseCacheSpec:m}=require("./cache");this.cacheFrom=f.map(t=>m(t,"from")),this.cacheTo=p.map(t=>m(t,"to")),this.imported=new Map,this.records=[],this.ssh=g,this.userns=null;const $={os:"linux",arch:GO_ARCH_NAME(process.arch)};this.buildPlatform=$,this.targetPlatform=l||$;const y=(t,e)=>({[`${t}PLATFORM`]:platformString(e),[`${t}OS`]:e.os||"linux",[`${t}ARCH`]:e.arch,[`${t}VARIANT`]:e.variant||""});this.autoArgs={...y("TARGET",this.targetPlatform),...y("BUILD",$)}}say(t){this.quiet||this.out({stream:t.endsWith("\n")?t:t+"\n"})}_proxyEnv(t,e){const{containerProxyEnv:s,effective:i}=require("../config"),r=s(i(),this.engine._hostGateway(e)||void 0);return Object.entries(r).filter(([e])=>!(e in t)&&!(e in this.buildArgs)).map(([t,e])=>`${t}=${e}`)}async run(t){const e=fs.readFileSync(t,"utf8"),{instructions:s}=parseBuildFile(e);if(!s.length)throw buildError("the build file cannot be empty");const i={},r=[];for(const t of s)if("FROM"===t.cmd){const e=splitWords(t.args),s=e.findIndex(t=>"as"===t.toLowerCase());r.push({index:r.length,name:-1!==s?e[s+1].toLowerCase():null,baseRaw:e[0],from:t,instructions:[]})}else if(r.length)r[r.length-1].instructions.push(t);else{if("ARG"!==t.cmd)throw buildError(`line ${t.line}: ${t.cmd} must come after FROM (only ARG may come before)`);for(const[e,s]of parseKeyValues(t.args,"ARG"))i[e]=this.buildArgs[e]??s??this.autoArgs[e]??"",e in this.buildArgs&&this.usedArgs.add(e)}this.globalArgs=i;const n=this.engine.usernsRange?.();n&&!s.some(t=>"RUN"===t.cmd&&"insecure"===t.flags?.security)&&(this.userns={uid:n.uid,gid:n.gid,size:n.size});const o={...this.autoArgs,...i};for(const t of r)t.platform=t.from.flags.platform?parsePlatform(expand(t.from.flags.platform,o)):this.targetPlatform;const a=this.target?r.find(t=>t.name===this.target.toLowerCase()):r[r.length-1];if(!a)throw buildError(`failed to reach build target ${this.target} in the build file`);const h=new Set,c=t=>{if(h.has(t.index))return;h.add(t.index);const e=this._stageRef(r,expand(t.baseRaw,{...this.autoArgs,...i}),t.index);e&&c(e);for(const e of this._stageDeps(t,r,i))c(e)};c(a);const d=r.filter(t=>h.has(t.index)&&t.index<=a.index);if(this.total=d.reduce((t,e)=>t+1+e.instructions.length,0),this.cacheFrom.length&&!this.noCache){const{importCaches:t}=require("./cache");this.imported=await t(this.engine,this.cacheFrom,t=>this.say(t))}const l=[];try{const t=Math.max(1,Number(process.env.TNC_DOCK_BUILD_PARALLEL)||4);let e=0;const s=[],n=()=>e<t?(e++,Promise.resolve()):new Promise(t=>s.push(t)),o=()=>{const t=s.shift();t?t():e--};for(const t of d){const e=this._stageDeps(t,r,i).filter(t=>h.has(t.index));t.ready=(async()=>{await Promise.all(e.map(t=>t.ready)),await n();try{t.state=await this._buildStage(t,r,i)}finally{o()}return t.state})(),l.push(t.ready),t.ready.catch(()=>{})}await Promise.all(l);const c=Object.keys(this.buildArgs).filter(t=>!this.usedArgs.has(t));c.length&&this.say(`[Warning] One or more build-args [${c.join(" ")}] were not consumed`);const u=new Set;for(let t=a;t;t=this._stageRef(r,expand(t.baseRaw,{...this.autoArgs,...i}),t.index))u.add(t.index);const f=this._finish(a.state,a.platform,u);if(this.cacheTo.length){const{exportCaches:t}=require("./cache");await t(this.engine,this.cacheTo,this.records,u,t=>this.say(t))}return f}finally{await Promise.allSettled(l);for(const t of this.sshStops||[])t();this.sshListeners&&await this.host.sh('rm -rf "$1"',[`/run/tnc-ssh/${this.bid}`]).catch(()=>{});for(const t of this.mounted)await this.engine.rootfs.remove(t).catch(()=>{})}}_stageDeps(t,e,s){const i={...this.autoArgs,...s},r=[expand(t.baseRaw,i)];for(const e of t.instructions)if("COPY"!==e.cmd&&"ADD"!==e.cmd||!e.flags.from||r.push(expand(e.flags.from,i)),"RUN"===e.cmd)for(const t of e.mounts||[]){const e=/(?:^|,)from=([^,]+)/.exec(t);e&&r.push(expand(e[1],i))}return[...new Set(r.map(s=>this._stageRef(e,s,t.index)).filter(Boolean))]}async _builtStage(t,e,s){const i=this._stageRef(t,e,s);return i&&!i.state&&await i.ready,i}_stageRef(t,e,s){return/^\d+$/.test(e)?t[Number(e)]:t.find(t=>t.name&&t.name===e.toLowerCase()&&t.index<s)||null}async _image(t,e){let s=this.pull?null:this.engine.findImage(t);return s&&s.architecture&&e&&s.architecture!==e&&(s=null),s||(this.say(`Pulling ${t}${e!==this.buildPlatform.arch?` (linux/${e})`:""}`),s=await this.engine.pull(t,()=>{},{platform:{os:"linux",arch:e}})),s}async _baseState(t,e,s){const i=expand(t.baseRaw,{...this.autoArgs,...s}),r=await this._builtStage(e,i,t.index);if(r)return{...JSON.parse(JSON.stringify(r.state)),arch:r.platform.arch};if("scratch"===i)return{layers:[],diffIds:[],config:{Env:["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"]},history:[],baseId:"scratch",arch:t.platform.arch};const n=await this._image(i,t.platform.arch);return{arch:n.architecture||t.platform.arch,layers:(n.layers||[]).map(t=>({...t})),diffIds:[...n.config?.rootfs?.diff_ids||[]],config:JSON.parse(JSON.stringify(n.config?.config||{})),history:[...n.config?.history||[]],baseId:n.id}}async _buildStage(t,e,s){this.say(`Step ${++this.step}/${this.total} : ${t.from.original}`);const i=await this._baseState(t,e,s);i.stage=t.index,i.args={},i.cmdSet=!1,this.say(` ---\x3e ${short("scratch"===i.baseId?sha256("scratch"):i.baseId)}`);for(const s of t.instructions)this.say(`Step ${++this.step}/${this.total} : ${s.original}`),await this._instruction(i,s,e,t);return i}_env(t){const e={...t.args};for(const s of t.config.Env||[]){const t=s.indexOf("=");e[s.slice(0,t)]=s.slice(t+1)}return e}_setEnv(t,e,s){const i=(t.config.Env||[]).filter(t=>t.split("=")[0]!==e);i.push(`${e}=${s}`),t.config.Env=i}_shellForm(t,e){return[...t.config.Shell||DEFAULT_SHELL,e]}_chainKey(t){return sha256(JSON.stringify(t.diffIds))}_stepId(t){return short(sha256(JSON.stringify([t.diffIds,t.config])))}async _instruction(t,e,s,i){const r=this._env(t),n=t=>expand(t,r);switch(e.cmd){case"ENV":for(const[s,i]of parseKeyValues(e.args,"ENV"))this._setEnv(t,s,expand(i,this._env(t)));return this._meta(t,e);case"ARG":for(const[s,i]of parseKeyValues(e.args,"ARG"))s in this.buildArgs?(t.args[s]=this.buildArgs[s],this.usedArgs.add(s)):t.args[s]=void 0!==i?n(i):this.globalArgs[s]??this.autoArgs[s]??"";return this._meta(t,e);case"LABEL":t.config.Labels={...t.config.Labels||{}};for(const[s,i]of parseKeyValues(e.args,"LABEL"))t.config.Labels[n(s)]=n(i);return this._meta(t,e);case"MAINTAINER":return t.author=e.args,this._meta(t,e);case"EXPOSE":t.config.ExposedPorts={...t.config.ExposedPorts||{}};for(const s of splitWords(n(e.args)))t.config.ExposedPorts[s.includes("/")?s:`${s}/tcp`]={};return this._meta(t,e);case"VOLUME":t.config.Volumes={...t.config.Volumes||{}};for(const s of(e.json||splitWords(e.args)).map(n))t.config.Volumes[s]={};return this._meta(t,e);case"USER":return t.config.User=n(e.args),this._meta(t,e);case"STOPSIGNAL":return t.config.StopSignal=n(e.args),this._meta(t,e);case"SHELL":if(!e.json)throw buildError(`line ${e.line}: SHELL requires the arguments to be in JSON form`);return t.config.Shell=e.json,this._meta(t,e);case"CMD":return t.config.Cmd=e.json||this._shellForm(t,e.args),t.cmdSet=!0,this._meta(t,e);case"ENTRYPOINT":return t.config.Entrypoint=e.json||this._shellForm(t,e.args),t.cmdSet||(t.config.Cmd=null),this._meta(t,e);case"HEALTHCHECK":return t.config.Healthcheck=this._healthcheck(e),this._meta(t,e);case"ONBUILD":return t.config.OnBuild=[...t.config.OnBuild||[],e.args],this.say(" ---\x3e ONBUILD triggers are stored but not executed by dock yet"),this._meta(t,e);case"WORKDIR":{const s=path.posix.resolve(t.config.WorkingDir||"/",n(e.args));return t.config.WorkingDir=s,this._fsStep(t,e,`WORKDIR ${s}`,async()=>{})}case"RUN":return this._run(t,e,r,s,i);case"COPY":case"ADD":return this._copy(t,e,r,s,i);default:throw buildError(`line ${e.line}: unknown instruction: ${e.cmd}`)}}_healthcheck(t){const e=t.args.trim();if(/^NONE$/i.test(e))return{Test:["NONE"]};const s=/^CMD\s+(.*)$/is.exec(e);if(!s)throw buildError(`line ${t.line}: HEALTHCHECK must be NONE or CMD ...`);let i;try{const t=JSON.parse(s[1]);i=Array.isArray(t)?["CMD",...t]:null}catch(t){}const r={Test:i||["CMD-SHELL",s[1]]},n=t.flags;return n.interval&&(r.Interval=parseDuration(n.interval)),n.timeout&&(r.Timeout=parseDuration(n.timeout)),n["start-period"]&&(r.StartPeriod=parseDuration(n["start-period"])),n["start-interval"]&&(r.StartInterval=parseDuration(n["start-interval"])),n.retries&&(r.Retries=parseInt(n.retries,10)),r}_meta(t,e){t.history.push({created:(new Date).toISOString(),created_by:`/bin/sh -c #(nop) ${e.original}`,empty_layer:!0}),this.say(` ---\x3e ${this._stepId(t)}`)}async _fsStep(t,e,s,i,{cacheable:r=!0,createdBy:n}={}){const o=sha256(`${this._chainKey(t)}\n${s}`);let a=r&&!this.noCache?this.store.getBuildCache(o):null;if(a&&""!==a.digest&&!fs.existsSync(this._blobPath(a.digest))&&(a=null),!a&&r&&!this.noCache&&this.imported.has(o)){const t=this.imported.get(o);try{await t.fetch(),a=t.entry,this.store.putBuildCache(o,a)}catch(t){this.say(`[Warning] cached layer for this step could not be fetched: ${t.message}`)}}if(a)return this.say(" ---\x3e Using cache"),this._addLayer(t,a,n||e.original),this.records.push({key:o,layer:a,stage:t.stage}),void this.say(` ---\x3e ${this._stepId(t)}`);const h=`build-${this.bid}-${++this.seq}`,c=await this.engine.rootfs.prepare(h,{layers:t.layers},t.config.WorkingDir||"/",{userns:this.userns});this.mounted.add(h);try{await i(c,h);const s=await this._commit(h);this.store.putBuildCache(o,s),this._addLayer(t,s,n||e.original),r&&this.records.push({key:o,layer:s,stage:t.stage})}finally{await this.engine.rootfs.remove(h).catch(()=>{}),this.mounted.delete(h)}this.say(` ---\x3e ${this._stepId(t)}`)}_blobPath(t){return path.join(this.engine.paths.blobs,t.replace(/^sha256:/,""))}async _commit(t){const e=path.join(this.engine.paths.blobs,`build-${t}.tmp`),{shim:s}=this.host.binaries(),i=`${this.engine.rootfs.root}/containers/${t}/upper`,r=(await this.host.sh(this.userns?`REMAP="${this.userns.uid} ${this.userns.gid} ${this.userns.size}"\n${COMMIT}`:COMMIT,[i,this.host.guestPath(e),s,"vm"===this.host.kind?1:0])).stdout.trim().split("\n").pop();if("EMPTY"===r)return{digest:"",diffId:"",size:0};const[n,o,a]=r.split(" ");return fs.renameSync(e,this._blobPath(n)),{digest:`sha256:${n}`,diffId:`sha256:${o}`,size:Number(a)}}_addLayer(t,e,s){const i={created:(new Date).toISOString(),created_by:s};e.digest?(t.layers.push({digest:e.digest,size:e.size,mediaType:LAYER_MEDIA}),t.diffIds.push(e.diffId)):i.empty_layer=!0,t.history.push(i)}_runArgv(t,e){if(e.json)return{argv:e.json,stdin:null};if(e.heredocs?.length){if(!e.args.replace(new RegExp(HEREDOC.source,"g"),"").trim()&&1===e.heredocs.length){const s=e.heredocs[0].content,i=/^#!(.*)\n/.exec(s);return i?{argv:splitWords(i[1].trim()),stdin:s}:{argv:[...t.config.Shell||DEFAULT_SHELL,s],stdin:null}}return{argv:this._shellForm(t,e.heredocText),stdin:null}}return{argv:this._shellForm(t,e.args),stdin:null}}async _runMounts(t,e,s,i,r,n){const o=(e.mounts||[]).map(t=>parseMount(expand(t,s),e.line)),a=t=>this.host.guestPath(t),h=this.engine.rootfs.root,c=[],d=[],l=[],u=[];let f=0;for(const p of o)if("ssh"===p.type){const t=require("./sshagent"),s=this.ssh?t.session(this.ssh):null;if(!s||!s.ids.has(p.id)){if("true"===p.required)throw buildError(`line ${e.line}: the SSH agent "${p.id}" is required; pass it with: thinknsh build --ssh ${p.id}`);continue}const i=p.target||t.DEFAULT_TARGET(f),r=await this._sshSocket(s,p.id);await this.host.sh('chown "$2" "$1" && chmod "$3" "$1"',[r,this._owner(p.uid,p.gid),p.mode||"0600"]),c.push({destination:i,type:"bind",source:r,options:["rbind","rw"]}),0===f++&&l.push(`SSH_AUTH_SOCK=${i}`),d.push(["ssh",p.id,i])}else if("cache"===p.type){const t=p.id||p.target,e=`${h}/cache-mounts/${sha256(t).slice(0,32)}${this.userns?`-${this.userns.uid}`:""}`,s=this._owner(p.uid,p.gid);await this.host.sh('[ -d "$1" ] || { mkdir -p "$1"; chown "$2" "$1"; chmod "$3" "$1"; }',[e,s,p.mode||"0755"]),c.push({destination:p.target,type:"bind",source:e,options:["rbind","rw"]}),d.push(["cache",p.target,t])}else if("tmpfs"===p.type)c.push({destination:p.target,type:"tmpfs",source:"tmpfs",options:["nosuid","nodev",...p.size?[`size=${p.size}`]:[]]}),d.push(["tmpfs",p.target]);else if("secret"===p.type){const t=this.secretsDir&&path.join(this.secretsDir,p.id);if(!t||!fs.existsSync(t)){if("true"===p.required)throw buildError(`line ${e.line}: secret ${p.id} is required; pass it with: thinknsh build --secret id=${p.id},src=FILE`);continue}if(p.env&&l.push(`${p.env}=${fs.readFileSync(t,"utf8")}`),p.target){const e=`/run/tnc-secrets/${n}`;await this.host.sh('umask 077; mkdir -p "$1"; cat > "$1/$2"; chown "$3" "$1/$2"; chmod "$4" "$1/$2"',[e,p.id,this._owner(p.uid,p.gid),p.mode||"0400"],{input:fs.readFileSync(t)}),c.push({destination:p.target,type:"bind",source:`${e}/${p.id}`,options:["rbind","ro"]}),u.push(()=>this.host.sh('rm -rf "$1"',[e]))}d.push(["secret",p.id,p.target||"",p.env||""])}else if("bind"===p.type){const o=path.posix.normalize("/"+(p.source||"/")).replace(/\/+$/,"")||"/",l="true"===p.rw||"true"===p.readwrite;let f;if(p.from){const e=expand(p.from,s),a=await this._builtStage(i,e,r.index);let h;if(a)h=a.state.layers,d.push(["bind",p.target,"stage",a.state.diffIds,o]);else{const s=await this._image(e,t.arch);h=s.layers,d.push(["bind",p.target,"image",s.id,o])}const l=`${n}-bind-${c.length}`,g=await this.engine.rootfs.prepare(l,{layers:h},"/",{userns:this.userns});this.mounted.add(l),u.push(async()=>{await this.engine.rootfs.remove(l).catch(()=>{}),this.mounted.delete(l)}),f=`${g}${"/"===o?"":o}`}else{const t=o.replace(/^\//,"");if(t.startsWith(".."))throw buildError(`line ${e.line}: bind source outside the build context: ${p.source}`);d.push(["bind",p.target,"context",hashPaths(this.contextDir,matchContext(this.contextDir,t||"."))]),f=a(path.join(this.contextDir,t))}if(l){const t=`${h}/containers/${n}/bind-rw-${c.length}`;await this.host.sh('mkdir -p "$2" && cp -a "$1/." "$2/" 2>/dev/null || cp -a "$1" "$2"',[f,t]),f=t}c.push({destination:p.target,type:"bind",source:f,options:["rbind",l?"rw":"ro"]})}return{mounts:c,cacheText:d,env:l,cleanup:u}}_owner(t=0,e=0){const s=this.userns;return s?`${s.uid+Number(t)}:${s.gid+Number(e)}`:`${Number(t)}:${Number(e)}`}async _sshSocket(t,e){if(this.sshListeners=this.sshListeners||new Map,!this.sshListeners.has(e)){const s=`/run/tnc-ssh/${this.bid}`,i=`${s}/${sha256(e).slice(0,16)}.sock`;this.sshListeners.set(e,(async()=>{await this.host.sh('mkdir -p "$1" && chmod 0700 "$1"',[s]);const r=require("./sshagent").startListener(this.host,t,e,i);return this.sshStops=[...this.sshStops||[],()=>r.stop()],await r.ready,i})())}return this.sshListeners.get(e)}async _run(t,e,s,i,r){const n=Object.keys(e.flags).filter(t=>!["network","mount","security"].includes(t));if(n.length)throw buildError(`line ${e.line}: RUN --${n[0]} is not supported yet`);if(e.flags.security&&!["insecure","sandbox"].includes(e.flags.security))throw buildError(`line ${e.line}: RUN --security must be insecure or sandbox`);const{argv:o,stdin:a}=this._runArgv(t,e),h=Object.entries(s).map(([t,e])=>`${t}=${e}`),c=e.flags.network||this.networkMode,d="insecure"===e.flags.security,l=(e.mounts||[]).length?(e.mounts||[]).map(t=>parseMount(expand(t,s),e.line)).map(t=>[t.type,t.target,t.id||"",t.source||"",t.from||""]):[],u=a||l.length||d||t.arch!==this.buildPlatform.arch?JSON.stringify(["RUN",o,a,h.sort(),t.config.User||"",t.config.WorkingDir||"/",l,d,t.arch]):JSON.stringify(["RUN",o,h.sort(),t.config.User||"",t.config.WorkingDir||"/"]);await this.engine.emulation.ensure(t.arch),await this._fsStep(t,e,u,async(n,l)=>{this.say(` ---\x3e Running in ${short(sha256(l))}`);const u=await this._runMounts(t,e,s,i,r,l),f=t.config.User||"",p=f&&f.split(":").some(t=>!/^\d+$/.test(t))?resolveUser(f,await this.engine.rootfs.readFile(l,"/etc/passwd"),await this.engine.rootfs.readFile(l,"/etc/group")):resolveUser(f),g=path.join(this.buildDir,l);fs.mkdirSync(g,{recursive:!0});const m=t=>this.host.guestPath(t),$="none"===c?await this.engine.network.attachAll(l,[{name:"none"}]):"host"===c?{nsPath:null,endpoints:{host:{}}}:await this.engine.network.attachAll(l,[{name:"bridge"}]);try{fs.writeFileSync(path.join(g,"hosts"),"127.0.0.1\tlocalhost\n::1\tlocalhost\n"),fs.writeFileSync(path.join(g,"resolv.conf"),await this.engine._resolvConf());const e=buildSpec({id:l,rootfs:n,args:o,env:[...h,...u.env,...this._proxyEnv(s,$.endpoints)],cwd:t.config.WorkingDir||"/",user:p,hostname:short(sha256(l)),nsPath:$.nsPath,hostConfig:d?{Privileged:!0}:{},cgroupParent:this.host.cgroupParent?.(),userns:this.userns,mounts:[{destination:"/etc/hosts",type:"bind",source:m(path.join(g,"hosts")),options:["rbind","rw"]},{destination:"/etc/resolv.conf",type:"bind",source:m(path.join(g,"resolv.conf")),options:["rbind","rw"]},...u.mounts]});fs.writeFileSync(path.join(g,"config.json"),JSON.stringify(e));const{crun:i}=this.host.binaries(),r=[i,`--cgroup-manager=${this.host.cgroupManager?.()||"cgroupfs"}`,"run","--bundle",m(g),l],c=this.host.spawn(["/bin/sh","-c",STEP_STDIO,"sh",this.userns?this._owner(0,0):"",...r]);a&&c.write(a),c.end();const f=t=>{this.quiet||this.out({stream:t.toString("utf8")})};c.stdout.on("data",f),c.stderr.on("data",f);const y=await c.exited;if(this.say(` ---\x3e Removed intermediate container ${short(sha256(l))}`),0!==y){const t=buildError(`The command '${o.join(" ")}' returned a non-zero code: ${y}`);throw t.code=y,t}}finally{$.nsPath&&await this.engine.network.detachAll(l,$.endpoints);for(const t of u.cleanup)await t().catch(()=>{});await this._dropMountTargets(l,u.mounts)}},{createdBy:e.heredocs?.length?`RUN ${e.original.replace(/^RUN\s+/i,"")}`:`${o.join(" ")}`})}async _dropMountTargets(t,e){if(!e.length)return;const s=`${this.engine.rootfs.root}/containers/${t}/upper`;await this.host.sh('u="$1"; shift; for t; do p="$u$t"; while [ "$p" != "$u" ]; do if [ -d "$p" ] && [ ! -L "$p" ]; then rmdir "$p" 2>/dev/null || break; elif [ -f "$p" ] && [ ! -s "$p" ]; then rm -f "$p"; else break; fi; p=$(dirname "$p"); done; done; true',[s,...e.map(t=>t.destination)]).catch(()=>{})}async _copy(t,e,s,i,r){const n=t=>expand(t,s);let o;if(e.heredocs?.length&&!e.flags.from){const t=`.tnc-heredoc-${r.index}-${e.line}`;fs.mkdirSync(path.join(this.contextDir,t),{recursive:!0});const i=new Map(e.heredocs.map(t=>[t.name,t]));o=splitWords(e.args).map(e=>{const r=/^<<-?["']?([A-Za-z_][A-Za-z0-9_]*)["']?$/.exec(e);if(!r||!i.has(r[1]))return n(e);const o=i.get(r[1]);return fs.writeFileSync(path.join(this.contextDir,t,o.name),o.expand?expand(o.content,s):o.content),`${t}/${o.name}`})}else o=(e.json||splitWords(e.args)).map(n);if(o.length<2)throw buildError(`line ${e.line}: ${e.cmd} requires at least two arguments`);let a=o.pop();const h=a.endsWith("/")||o.length>1;a=path.posix.resolve(t.config.WorkingDir||"/",a);let c,d,l=o,u=null;if(e.flags.from){const s=n(e.flags.from),o=await this._builtStage(i,s,r.index);let a;if(o)a=o.state.layers,d=sha256(JSON.stringify(o.state.diffIds)+JSON.stringify(l));else{const e=await this._image(s,t.arch);a=e.layers,d=sha256(e.id+JSON.stringify(l))}u=`build-${this.bid}-from-${++this.seq}`,c=await this.engine.rootfs.prepare(u,{layers:a},"/",{userns:this.userns}),this.mounted.add(u),l=l.map(t=>t.replace(/^\/+/,""))}else{"ADD"===e.cmd&&(l=await Promise.all(l.map(t=>/^https?:\/\//.test(t)?this._download(t):t)));for(const t of l)if(path.posix.normalize(t.replace(/^\/+/,"")).startsWith(".."))throw buildError(`line ${e.line}: forbidden path outside the build context: ${t}`);l=l.map(t=>path.posix.normalize(t.replace(/^\/+/,"")));const t=l.flatMap(t=>matchContext(this.contextDir,t));d=hashPaths(this.contextDir,t),c=this.host.guestPath(this.contextDir)}const f="ADD"===e.cmd&&!e.flags.from,p=JSON.stringify([e.cmd,e.flags,l,a,d]);try{await this._fsStep(t,e,p,async(t,s)=>{let i=e.flags.from?"":this._owner(0,0);if(e.flags.chown){const t=resolveUser(n(e.flags.chown),await this.engine.rootfs.readFile(s,"/etc/passwd"),await this.engine.rootfs.readFile(s,"/etc/group"));i=this._owner(t.uid,t.gid)}const r=`${this.engine.rootfs.root}/containers/${s}/copy`;await this.host.sh(COPY,[c,r,t,a,h?1:0,i,e.flags.chmod||"",f?1:0,"vm"===this.host.kind?1:0,...l])})}finally{u&&(await this.engine.rootfs.remove(u).catch(()=>{}),this.mounted.delete(u))}}async _download(t){const e=await fetch(t);if(!e.ok)throw buildError(`failed to download ${t}: HTTP ${e.status}`);const s=path.posix.basename(new URL(t).pathname)||"download",i=`.dock-add-${sha256(t).slice(0,12)}/${s}`;return fs.mkdirSync(path.join(this.contextDir,path.dirname(i)),{recursive:!0}),fs.writeFileSync(path.join(this.contextDir,i),Buffer.from(await e.arrayBuffer())),i}_finish(t,e,s=new Set([t.stage])){const i=this.cacheTo.some(t=>"inline"===t.type)?require("./cache").selectEntries(this.records,"min",s):null,r={architecture:e?.arch||t.arch||GO_ARCH[process.arch]||process.arch,...e?.variant?{variant:e.variant}:{},os:"linux",created:(new Date).toISOString(),...t.author?{author:t.author}:{},config:{...t.config,Labels:{...t.config.Labels||{},...this.labels}},rootfs:{type:"layers",diff_ids:t.diffIds},history:t.history,...i?{[require("./cache").INLINE_KEY]:i}:{}},n=JSON.stringify(r),o=`sha256:${sha256(n)}`;return fs.writeFileSync(this._blobPath(o),n),{id:o,repoTags:[],repoDigests:[],digest:null,size:t.layers.reduce((t,e)=>t+e.size,0),layers:t.layers,config:r,architecture:r.architecture,os:"linux",createdAt:Date.now()}}}function normalizeTag(t){return parseReference(t).familiar}module.exports={Builder,normalizeTag,matchContext,hashPaths,parseMount,COMMIT};
|
|
@@ -4,4 +4,4 @@
|
|
|
4
4
|
* PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
|
|
5
5
|
* UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
|
|
6
6
|
*/
|
|
7
|
-
"use strict";const fs=require("fs"),path=require("path"),http2=require("http2"),crypto=require("crypto"),{Duplex}=require("stream"),{spawnSync}=require("child_process"),{parseReference}=require("../image/reference"),{SESSION_HEADER_PREFIX,BUILDKIT_GRPC_PACKAGE,ENGINE_EXPORTER,BUILDKIT_RELEASES}=require("../api/wire"),VERSION="v0.33.1",RELEASES={arm64:"4e1ba91f139761f249a1fa6c71e632dfdbdafeda416176ecadefbb95225e56c4",amd64:"4e044bcd62a0c0bbe6a8c94d73989de2bfe4c04dbc0f9d6021cf96b72cd1d965"},releaseUrl=t=>`${BUILDKIT_RELEASES}/${VERSION}/buildkit-${VERSION}.linux-${t}.tar.gz`,ROOT="/var/lib/tnc-dock/buildkit",RUN_DIR="/run/tnc-buildkit",ADDR=`unix://${RUN_DIR}/buildkitd.sock`,CONTENT=`${ROOT}/runc-overlayfs/content/blobs/sha256`,BUILDKITD_ARGV0="^[^ ]*/buildkitd( |$)",SOLVE=`/${BUILDKIT_GRPC_PACKAGE}.Control/Solve`,SESSION=`/${BUILDKIT_GRPC_PACKAGE}.Control/Session`;function httpError(t,e){const i=new Error(e);return i.statusCode=t,i}function varint(t){const e=[];for(;t>127;)e.push(127&t|128),t=Math.floor(t/128);return e.push(t),Buffer.from(e)}function readVarint(t,e){let i,r=0,s=1;do{if(e>=t.length)throw new Error("truncated protobuf");i=t[e++],r+=(127&i)*s,s*=128}while(128&i);return[r,e]}function eachField(t,e){let i=0;for(;i<t.length;){let r;[r,i]=readVarint(t,i);const s=Math.floor(r/8),n=7&r;if(0===n){let r;[r,i]=readVarint(t,i),e(s,n,r)}else if(2===n){let r;[r,i]=readVarint(t,i),e(s,n,t.subarray(i,i+r)),i+=r}else if(1===n)i+=8;else{if(5!==n)throw new Error(`unsupported protobuf wire type ${n}`);i+=4}}}function grpcFrame(t){const e=Buffer.alloc(5);return e.writeUInt32BE(t.length,1),Buffer.concat([e,t])}const bytesMessage=t=>Buffer.concat([Buffer.from([10]),varint(t.length),t]);function bytesMessageData(t){let e=Buffer.alloc(0);return eachField(t,(t,i,r)=>{1===t&&2===i&&(e=r)}),e}class FrameReader{constructor(){this.buf=Buffer.alloc(0)}push(t){this.buf=Buffer.concat([this.buf,t]);const e=[];for(;this.buf.length>=5;){const t=this.buf.readUInt32BE(1);if(this.buf.length<5+t)break;if(0!==this.buf[0])throw new Error("compressed gRPC messages are not supported");e.push(this.buf.subarray(5,5+t)),this.buf=this.buf.subarray(5+t)}return e}}function solveExporterResponse(t){const e={};return eachField(t,(t,i,r)=>{if(1!==t||2!==i)return;let s="",n="";eachField(r,(t,e,i)=>{2===e&&(1===t&&(s=i.toString("utf8")),2===t&&(n=i.toString("utf8")))}),e[s]=n}),e}function rewriteFields(t,e){const i=[];let r=0;for(;r<t.length;){const s=r;let n;[n,r]=readVarint(t,r);const o=Math.floor(n/8),a=7&n;if(2===a){let l;[l,r]=readVarint(t,r);const c=t.subarray(r,r+l);r+=l;const u=e(o,a,c);i.push(void 0===u?t.subarray(s,r):Buffer.concat([varint(n),varint(u.length),u]));continue}if(0===a)[,r]=readVarint(t,r);else if(1===a)r+=8;else{if(5!==a)throw new Error(`unsupported protobuf wire type ${a}`);r+=4}i.push(t.subarray(s,r))}return Buffer.concat(i)}const ENGINE=Buffer.from(ENGINE_EXPORTER),IMAGE=Buffer.from("image");function rewriteSolveRequest(t){return rewriteFields(t,(t,e,i)=>3===t&&i.equals(ENGINE)?IMAGE:13===t?rewriteFields(i,(t,e,i)=>1===t&&i.equals(ENGINE)?IMAGE:void 0):void 0)}function setExporterResponse(t,e){const i=(t,e)=>{const i=Buffer.from(e);return Buffer.concat([varint(t<<3|2),varint(i.length),i])};return rewriteFields(t,(t,r,s)=>{if(1!==t)return;let n=null;return eachField(s,(t,e,i)=>{1===t&&2===e&&(n=i.toString("utf8"))}),n in e?Buffer.concat([i(1,n),i(2,e[n])]):void 0})}function sessionHeaders(t){const e={};for(let i=0;i+1<t.length;i+=2){const r=t[i].toLowerCase();r.startsWith(SESSION_HEADER_PREFIX)&&(e[r]||=[]).push(t[i+1])}return e}const buildKitDir=(t,e)=>path.join(t,"buildkit",`${VERSION}-${e}`),buildKitInstalled=t=>["buildkitd","buildctl","runc"].every(e=>fs.existsSync(path.join(t,e)));async function installBuildKit(t,e){const i=RELEASES[e];if(!i)throw httpError(501,`BuildKit is not available for linux/${e}`);const r=path.dirname(t);fs.mkdirSync(r,{recursive:!0});const s=fs.mkdtempSync(path.join(r,".download-"));try{const r=path.join(s,"buildkit.tar.gz"),n=await fetch(releaseUrl(e));if(!n.ok)throw new Error(`HTTP ${n.status}`);fs.writeFileSync(r,Buffer.from(await n.arrayBuffer()));const o=crypto.createHash("sha256").update(fs.readFileSync(r)).digest("hex");if(o!==i)throw new Error(`sha256 mismatch for ${path.basename(releaseUrl(e))}: ${o}`);const a=path.join(s,"bin");fs.mkdirSync(a);const l=spawnSync("tar",["-xzf",r,"-C",a,"--strip-components=1","bin/buildkitd","bin/buildctl","bin/buildkit-runc"],{encoding:"utf8"});if(0!==l.status)throw new Error(`unpacking: ${(l.stderr||"").trim()}`);fs.renameSync(path.join(a,"buildkit-runc"),path.join(a,"runc")),fs.rmSync(t,{recursive:!0,force:!0}),fs.renameSync(a,t)}catch(t){throw httpError(t.statusCode||500,`could not install BuildKit ${VERSION}: ${t.message}`)}finally{fs.rmSync(s,{recursive:!0,force:!0})}}class BuildKit{constructor(t){this.engine=t,this.arch=t.linuxArch(),this.binDir=buildKitDir(t.paths.home,this.arch),this._starting=null,this._emulated=null}get host(){return this.engine.host}async _download(){await installBuildKit(this.binDir,this.arch)}_installed(){return buildKitInstalled(this.binDir)}_bin(t){return`${this.host.guestPath(this.binDir)}/${t}`}async _running(){if(0!==(await this.host.run(["sh","-c",'test -S "$1/buildkitd.sock" && ! test -e "$1/exit" && test "$(cat "$1/bin" 2>/dev/null)" = "$2"',"dock",RUN_DIR,this.host.guestPath(this.binDir)])).code)return!1;const t=await this.host.run([this._bin("buildctl"),"--addr",ADDR,"--timeout","5","debug","workers"]);return 0===t.code&&(!this._emulated||t.stdout.includes(`linux/${this._emulated}`))}async _emulate(){if(this._emulated=null,"vm"===this.host.kind&&"arm64"===this.engine.emulation.hostArch())try{await this.engine.emulation.ensure("amd64"),this._emulated="amd64"}catch(t){this.engine.log(`buildkit: linux/amd64 builds unavailable: ${t.message}`)}}async ready(){if(await this.engine.ready(),await this._emulate(),!this._installed()||!await this._running())return this._starting||(this._starting=this._start().finally(()=>{this._starting=null})),this._starting}async _start(){if(this._installed()||(this.engine.log(`installing BuildKit ${VERSION} (linux/${this.arch})`),await this._download()),await this._running())return;this.engine.log(`starting buildkitd ${VERSION}`);const{shim:t}=this.host.binaries();await this.host.run(["sh","-c",'p="$3"; pkill -f "$p"; for i in 1 2 3 4 5 6 7 8 9 10; do pgrep -f "$p" >/dev/null || break; sleep 0.5; done; pkill -9 -f "$p"; rm -rf "$1"; mkdir -p "$1" "$2"',"dock",RUN_DIR,ROOT,BUILDKITD_ARGV0]),await this.host.run(["sh","-c",'printf "%s\n" "$2" > "$1/bin"',"dock",RUN_DIR,this.host.guestPath(this.binDir)]),await this.host.check([t,"--log",`${ROOT}/buildkitd.log`,"--exit",`${RUN_DIR}/exit`,"--","/bin/env",`PATH=${this.host.guestPath(this.binDir)}:/usr/sbin:/usr/bin:/sbin:/bin`,this._bin("buildkitd"),"--root",ROOT,"--addr",ADDR,"--oci-worker-binary","runc","--oci-worker-net","host","--containerd-worker","false",...this._emulated?["--oci-worker-platform",`linux/${this.arch}`,"--oci-worker-platform",`linux/${this._emulated}`]:[]]);for(let t=0;t<100;t++){if(await this._running())return void this.engine.log(`buildkitd ${VERSION} running`);await new Promise(t=>setTimeout(t,200))}const e=(await this.host.run(["tail","-c","20000",`${ROOT}/buildkitd.log`])).stdout.split("\n").map(t=>{try{return JSON.parse(t).log.trim()}catch(t){return""}});throw httpError(500,`buildkitd did not start: ${(e.filter(t=>/^(buildkitd:|time=.*level=(error|fatal))/.test(t)).pop()||e.filter(Boolean).pop()||"no log output").slice(0,500)}`)}connect(){const t=this.host.spawn([this._bin("buildctl"),"--addr",ADDR,"dial-stdio"]),e=new Duplex({read(){},write(e,i,r){t.write(e),r()},final(e){t.end(),e()},destroy(e,i){t.kill("SIGTERM"),i(e)}});return t.stdout.on("data",t=>e.push(t)),t.stdout.on("end",()=>e.push(null)),t.stderr.on("data",t=>this.engine.log(`buildctl dial-stdio: ${String(t).trim()}`)),t.exited.then(()=>e.push(null)),e}_client(){const t=this.connect(),e=http2.connect("http://buildkitd",{createConnection:()=>t});return e.on("error",t=>this.engine.log(`buildkitd connection: ${t.message}`)),e.on("close",()=>t.destroy()),e}async session(t,e){await this.ready();const i=this._client(),r=i.request({":method":"POST",":path":SESSION,"content-type":"application/grpc",te:"trailers",...sessionHeaders(e)}),s=new FrameReader;r.on("data",e=>{for(const i of s.push(e))t.write(bytesMessageData(i))}),t.onData(t=>r.write(grpcFrame(bytesMessage(Buffer.from(t))))),t.onEnd(()=>r.end()),await new Promise(e=>{const s=()=>{i.close(),t.end(),e()};r.on("close",s),r.on("error",s),t.onClose(()=>{r.close(),s()})})}async grpc(t){await this.ready();const e=this._client(),i=http2.createServer();i.on("stream",(t,i)=>this._proxyStream(e,t,i)),i.on("sessionError",t=>this.engine.log(`buildkit proxy: ${t.message}`)),await new Promise(e=>{t.on("close",e),i.emit("connection",t),t.resume()}),e.close()}_proxyStream(t,e,i){const r={};for(const[t,e]of Object.entries(i))":authority"!==t&&":scheme"!==t&&"connection"!==t&&(r[t]=e);const s=t.request(r),n=i[":path"]===SOLVE;let o={};if(n){const t=[];e.on("data",e=>t.push(e)),e.on("end",()=>{try{const e=(new FrameReader).push(Buffer.concat(t));s.end(Buffer.concat(e.map(t=>grpcFrame(rewriteSolveRequest(t)))))}catch(t){this.engine.log(`buildkit solve request: ${t.message}`),s.close(http2.constants.NGHTTP2_INTERNAL_ERROR)}})}else e.pipe(s);s.on("trailers",t=>{o=t}),s.on("response",(t,i)=>{const r={...t};if(i&http2.constants.NGHTTP2_FLAG_END_STREAM||void 0!==t["grpc-status"])return void e.respond(r,{endStream:!0});if(e.respond(r,{waitForTrailers:!0}),e.on("wantTrailers",()=>e.sendTrailers(o)),!n)return void s.pipe(e);const a=[];s.on("data",t=>a.push(t)),s.on("end",async()=>{let t=Buffer.concat(a);try{if("0"===String(o["grpc-status"])){const[e]=(new FrameReader).push(t),i=e&&await this.importResult(solveExporterResponse(e));i&&(t=grpcFrame(setExporterResponse(e,{"containerimage.digest":i.id})))}}catch(t){return this.engine.log(`buildkit import: ${t.stack||t.message}`),o={"grpc-status":"13","grpc-message":encodeURIComponent(`dock: ${t.message}`)},e.end()}e.end(t)})}),s.on("error",()=>e.close(http2.constants.NGHTTP2_INTERNAL_ERROR)),e.on("error",()=>s.close()),e.on("close",()=>{s.closed||s.close()})}async _blobText(t){const e=t.replace(/^sha256:/,"");if(!/^[0-9a-f]{64}$/.test(e))throw new Error(`bad digest ${t}`);const i=await this.host.run(["cat",`${CONTENT}/${e}`]);if(0!==i.code)throw new Error(`BuildKit has no blob ${t}`);return i.stdout}async importResult(t){const e=t["containerimage.digest"];if(!e)return null;let i=JSON.parse(await this._blobText(e)),r=e;if(i.manifests){const t=this.arch,e=i.manifests.find(e=>"linux"===e.platform?.os&&e.platform.architecture===t)||i.manifests.find(t=>"unknown"!==t.platform?.architecture);if(!e)throw new Error("the built index has no image manifest");r=e.digest,i=JSON.parse(await this._blobText(e.digest))}const s=await this._blobText(i.config.digest),n=JSON.parse(s),o=(i.layers||[]).map(t=>({digest:t.digest,size:t.size,mediaType:t.mediaType}));if(o.some(t=>/zstd/.test(t.mediaType||"")))throw new Error("zstd layers are not supported; build with compression=gzip");const a=this.engine.paths.blobs,l=[i.config.digest,...o.map(t=>t.digest)].map(t=>t.replace(/^sha256:/,"")).filter(t=>!fs.existsSync(path.join(a,t)));l.length&&await this.host.check(["sh","-c",'src="$1"; dst="$2"; shift 2; for h; do cp "$src/$h" "$dst/.$h.tmp" && mv "$dst/.$h.tmp" "$dst/$h"; done',"dock",CONTENT,this.host.guestPath(a),...l]);const c=[];for(const e of(t["image.name"]||"").split(",").map(t=>t.trim()).filter(Boolean))try{const t=parseReference(e);t.tagged&&c.push(t.tagged)}catch(t){}const u=i.config.digest,d=this.engine.store.saveImage({id:u,repoTags:c,repoDigests:[],digest:null,size:o.reduce((t,e)=>t+(e.size||0),0),layers:o,config:n,architecture:n.architecture,os:n.os,createdAt:Date.parse(n.created)||Date.now()});for(const t of c)this.engine.emitEvent("image","tag",d.id,{name:t});return this.engine.log(`buildkit: imported ${u.slice(7,19)} (${r.slice(7,19)}) ${c.join(" ")}`),d}}module.exports={BuildKit,VERSION,RELEASES,installBuildKit,buildKitDir,buildKitInstalled,grpcFrame,bytesMessage,bytesMessageData,FrameReader,solveExporterResponse,sessionHeaders,varint,rewriteSolveRequest,setExporterResponse};
|
|
7
|
+
"use strict";const fs=require("fs"),path=require("path"),http2=require("http2"),crypto=require("crypto"),{Duplex}=require("stream"),{spawnSync}=require("child_process"),{parseReference}=require("../image/reference"),{SESSION_HEADER_PREFIX,BUILDKIT_GRPC_PACKAGE,ENGINE_EXPORTER,BUILDKIT_RELEASES}=require("../api/wire"),VERSION="v0.33.1",RELEASES={arm64:"4e1ba91f139761f249a1fa6c71e632dfdbdafeda416176ecadefbb95225e56c4",amd64:"4e044bcd62a0c0bbe6a8c94d73989de2bfe4c04dbc0f9d6021cf96b72cd1d965"},releaseUrl=t=>`${BUILDKIT_RELEASES}/${VERSION}/buildkit-${VERSION}.linux-${t}.tar.gz`,ROOT="/var/lib/tnc-dock/buildkit",RUN_DIR="/run/tnc-buildkit",ADDR=`unix://${RUN_DIR}/buildkitd.sock`,CONTENT=`${ROOT}/runc-overlayfs/content/blobs/sha256`,BUILDKITD_ARGV0="^[^ ]*/buildkitd( |$)",SOLVE=`/${BUILDKIT_GRPC_PACKAGE}.Control/Solve`,SESSION=`/${BUILDKIT_GRPC_PACKAGE}.Control/Session`;function httpError(t,e){const i=new Error(e);return i.statusCode=t,i}function varint(t){const e=[];for(;t>127;)e.push(127&t|128),t=Math.floor(t/128);return e.push(t),Buffer.from(e)}function readVarint(t,e){let i,r=0,s=1;do{if(e>=t.length)throw new Error("truncated protobuf");i=t[e++],r+=(127&i)*s,s*=128}while(128&i);return[r,e]}function eachField(t,e){let i=0;for(;i<t.length;){let r;[r,i]=readVarint(t,i);const s=Math.floor(r/8),n=7&r;if(0===n){let r;[r,i]=readVarint(t,i),e(s,n,r)}else if(2===n){let r;[r,i]=readVarint(t,i),e(s,n,t.subarray(i,i+r)),i+=r}else if(1===n)i+=8;else{if(5!==n)throw new Error(`unsupported protobuf wire type ${n}`);i+=4}}}function grpcFrame(t){const e=Buffer.alloc(5);return e.writeUInt32BE(t.length,1),Buffer.concat([e,t])}const bytesMessage=t=>Buffer.concat([Buffer.from([10]),varint(t.length),t]);function bytesMessageData(t){let e=Buffer.alloc(0);return eachField(t,(t,i,r)=>{1===t&&2===i&&(e=r)}),e}class FrameReader{constructor(){this.buf=Buffer.alloc(0)}push(t){this.buf=Buffer.concat([this.buf,t]);const e=[];for(;this.buf.length>=5;){const t=this.buf.readUInt32BE(1);if(this.buf.length<5+t)break;if(0!==this.buf[0])throw new Error("compressed gRPC messages are not supported");e.push(this.buf.subarray(5,5+t)),this.buf=this.buf.subarray(5+t)}return e}}function solveExporterResponse(t){const e={};return eachField(t,(t,i,r)=>{if(1!==t||2!==i)return;let s="",n="";eachField(r,(t,e,i)=>{2===e&&(1===t&&(s=i.toString("utf8")),2===t&&(n=i.toString("utf8")))}),e[s]=n}),e}function rewriteFields(t,e){const i=[];let r=0;for(;r<t.length;){const s=r;let n;[n,r]=readVarint(t,r);const o=Math.floor(n/8),a=7&n;if(2===a){let l;[l,r]=readVarint(t,r);const c=t.subarray(r,r+l);r+=l;const u=e(o,a,c);i.push(void 0===u?t.subarray(s,r):Buffer.concat([varint(n),varint(u.length),u]));continue}if(0===a)[,r]=readVarint(t,r);else if(1===a)r+=8;else{if(5!==a)throw new Error(`unsupported protobuf wire type ${a}`);r+=4}i.push(t.subarray(s,r))}return Buffer.concat(i)}const ENGINE=Buffer.from(ENGINE_EXPORTER),IMAGE=Buffer.from("image"),REGISTRY=Buffer.from("registry");function rewriteSolveRequest(t,e={}){const i=(t,e)=>{const i=Buffer.from(e);return Buffer.concat([varint(t<<3|2),varint(i.length),i])},r=t=>{let r=null,s="";if(eachField(t,(t,e,i)=>{2===e&&1===t&&(r=i.toString("utf8")),2===e&&2===t&&(s=i.toString("utf8"))}),"name"===r&&(e.names=s),"push"===r&&/^(1|true)$/i.test(s))return e.push=!0,Buffer.concat([i(1,"push"),i(2,"false")])};return rewriteFields(t,(t,i,s)=>3===t&&s.equals(ENGINE)?IMAGE:3===t&&s.equals(REGISTRY)?(e.push=!0,IMAGE):4===t?r(s):13===t?rewriteFields(s,(t,i,s)=>1===t&&s.equals(ENGINE)?IMAGE:1===t&&s.equals(REGISTRY)?(e.push=!0,IMAGE):2===t?r(s):void 0):void 0)}function setExporterResponse(t,e){const i=(t,e)=>{const i=Buffer.from(e);return Buffer.concat([varint(t<<3|2),varint(i.length),i])};return rewriteFields(t,(t,r,s)=>{if(1!==t)return;let n=null;return eachField(s,(t,e,i)=>{1===t&&2===e&&(n=i.toString("utf8"))}),n in e?Buffer.concat([i(1,n),i(2,e[n])]):void 0})}function sessionHeaders(t){const e={};for(let i=0;i+1<t.length;i+=2){const r=t[i].toLowerCase();r.startsWith(SESSION_HEADER_PREFIX)&&(e[r]||=[]).push(t[i+1])}return e}const buildKitDir=(t,e)=>path.join(t,"buildkit",`${VERSION}-${e}`),buildKitInstalled=t=>["buildkitd","buildctl","runc"].every(e=>fs.existsSync(path.join(t,e)));async function installBuildKit(t,e){const i=RELEASES[e];if(!i)throw httpError(501,`BuildKit is not available for linux/${e}`);const r=path.dirname(t);fs.mkdirSync(r,{recursive:!0});const s=fs.mkdtempSync(path.join(r,".download-"));try{const r=path.join(s,"buildkit.tar.gz"),n=await fetch(releaseUrl(e));if(!n.ok)throw new Error(`HTTP ${n.status}`);fs.writeFileSync(r,Buffer.from(await n.arrayBuffer()));const o=crypto.createHash("sha256").update(fs.readFileSync(r)).digest("hex");if(o!==i)throw new Error(`sha256 mismatch for ${path.basename(releaseUrl(e))}: ${o}`);const a=path.join(s,"bin");fs.mkdirSync(a);const l=spawnSync("tar",["-xzf",r,"-C",a,"--strip-components=1","bin/buildkitd","bin/buildctl","bin/buildkit-runc"],{encoding:"utf8"});if(0!==l.status)throw new Error(`unpacking: ${(l.stderr||"").trim()}`);fs.renameSync(path.join(a,"buildkit-runc"),path.join(a,"runc")),fs.rmSync(t,{recursive:!0,force:!0}),fs.renameSync(a,t)}catch(t){throw httpError(t.statusCode||500,`could not install BuildKit ${VERSION}: ${t.message}`)}finally{fs.rmSync(s,{recursive:!0,force:!0})}}class BuildKit{constructor(t){this.engine=t,this.arch=t.linuxArch(),this.binDir=buildKitDir(t.paths.home,this.arch),this._starting=null,this._emulated=null}get host(){return this.engine.host}async _download(){await installBuildKit(this.binDir,this.arch)}_installed(){return buildKitInstalled(this.binDir)}_bin(t){return`${this.host.guestPath(this.binDir)}/${t}`}async _running(){if(0!==(await this.host.run(["sh","-c",'test -S "$1/buildkitd.sock" && ! test -e "$1/exit" && test "$(cat "$1/bin" 2>/dev/null)" = "$2"',"dock",RUN_DIR,this.host.guestPath(this.binDir)])).code)return!1;const t=await this.host.run([this._bin("buildctl"),"--addr",ADDR,"--timeout","5","debug","workers"]);return 0===t.code&&(!this._emulated||t.stdout.includes(`linux/${this._emulated}`))}async _emulate(){if(this._emulated=null,"vm"===this.host.kind&&"arm64"===this.engine.emulation.hostArch())try{await this.engine.emulation.ensure("amd64"),this._emulated="amd64"}catch(t){this.engine.log(`buildkit: linux/amd64 builds unavailable: ${t.message}`)}}async ready(){if(await this.engine.ready(),await this._emulate(),!this._installed()||!await this._running())return this._starting||(this._starting=this._start().finally(()=>{this._starting=null})),this._starting}async _start(){if(this._installed()||(this.engine.log(`installing BuildKit ${VERSION} (linux/${this.arch})`),await this._download()),await this._running())return;this.engine.log(`starting buildkitd ${VERSION}`);const{shim:t}=this.host.binaries();await this.host.run(["sh","-c",'p="$3"; pkill -f "$p"; for i in 1 2 3 4 5 6 7 8 9 10; do pgrep -f "$p" >/dev/null || break; sleep 0.5; done; pkill -9 -f "$p"; rm -rf "$1"; mkdir -p "$1" "$2"',"dock",RUN_DIR,ROOT,BUILDKITD_ARGV0]),await this.host.run(["sh","-c",'printf "%s\n" "$2" > "$1/bin"',"dock",RUN_DIR,this.host.guestPath(this.binDir)]),await this.host.check([t,"--log",`${ROOT}/buildkitd.log`,"--exit",`${RUN_DIR}/exit`,"--","env",`PATH=${this.host.guestPath(this.binDir)}:/usr/sbin:/usr/bin:/sbin:/bin`,this._bin("buildkitd"),"--root",ROOT,"--addr",ADDR,"--oci-worker-binary","runc","--oci-worker-net","host","--containerd-worker","false",...this._emulated?["--oci-worker-platform",`linux/${this.arch}`,"--oci-worker-platform",`linux/${this._emulated}`]:[]]);for(let t=0;t<100;t++){if(await this._running())return void this.engine.log(`buildkitd ${VERSION} running`);await new Promise(t=>setTimeout(t,200))}const e=(await this.host.run(["tail","-c","20000",`${ROOT}/buildkitd.log`])).stdout.split("\n").map(t=>{try{return JSON.parse(t).log.trim()}catch(t){return""}});throw httpError(500,`buildkitd did not start: ${(e.filter(t=>/^(buildkitd:|time=.*level=(error|fatal))/.test(t)).pop()||e.filter(Boolean).pop()||"no log output").slice(0,500)}`)}connect(){const t=this.host.spawn([this._bin("buildctl"),"--addr",ADDR,"dial-stdio"]),e=new Duplex({read(){},write(e,i,r){t.write(e),r()},final(e){t.end(),e()},destroy(e,i){t.kill("SIGTERM"),i(e)}});return t.stdout.on("data",t=>e.push(t)),t.stdout.on("end",()=>e.push(null)),t.stderr.on("data",t=>this.engine.log(`buildctl dial-stdio: ${String(t).trim()}`)),t.exited.then(()=>e.push(null)),e}_client(){const t=this.connect(),e=http2.connect("http://buildkitd",{createConnection:()=>t});return e.on("error",t=>this.engine.log(`buildkitd connection: ${t.message}`)),e.on("close",()=>t.destroy()),e}async session(t,e){await this.ready();const i=this._client(),r=i.request({":method":"POST",":path":SESSION,"content-type":"application/grpc",te:"trailers",...sessionHeaders(e)}),s=new FrameReader;r.on("data",e=>{for(const i of s.push(e))t.write(bytesMessageData(i))}),t.onData(t=>r.write(grpcFrame(bytesMessage(Buffer.from(t))))),t.onEnd(()=>r.end()),await new Promise(e=>{const s=()=>{i.close(),t.end(),e()};r.on("close",s),r.on("error",s),t.onClose(()=>{r.close(),s()})})}async grpc(t){await this.ready();const e=this._client(),i=http2.createServer();i.on("stream",(t,i)=>this._proxyStream(e,t,i)),i.on("sessionError",t=>this.engine.log(`buildkit proxy: ${t.message}`)),await new Promise(e=>{t.on("close",e),i.emit("connection",t),t.resume()}),e.close()}_proxyStream(t,e,i){const r={};for(const[t,e]of Object.entries(i))":authority"!==t&&":scheme"!==t&&"connection"!==t&&(r[t]=e);const s=t.request(r),n=i[":path"]===SOLVE,o={};let a={};if(n){const t=[];e.on("data",e=>t.push(e)),e.on("end",()=>{try{const e=(new FrameReader).push(Buffer.concat(t));s.end(Buffer.concat(e.map(t=>grpcFrame(rewriteSolveRequest(t,o)))))}catch(t){this.engine.log(`buildkit solve request: ${t.message}`),s.close(http2.constants.NGHTTP2_INTERNAL_ERROR)}})}else e.pipe(s);s.on("trailers",t=>{a=t}),s.on("response",(t,i)=>{const r={...t};if(i&http2.constants.NGHTTP2_FLAG_END_STREAM||void 0!==t["grpc-status"])return void e.respond(r,{endStream:!0});if(e.respond(r,{waitForTrailers:!0}),e.on("wantTrailers",()=>e.sendTrailers(a)),!n)return void s.pipe(e);const l=[];s.on("data",t=>l.push(t)),s.on("end",async()=>{let t=Buffer.concat(l);try{if("0"===String(a["grpc-status"])){const[e]=(new FrameReader).push(t),i=e?solveExporterResponse(e):{},r=e&&await this.importResult(i,{all:!0});r&&o.push?t=grpcFrame(setExporterResponse(e,{"containerimage.digest":await this.pushResult(r,o.names||i["image.name"]||"")})):r&&(t=grpcFrame(setExporterResponse(e,{"containerimage.digest":r.id})))}}catch(t){return this.engine.log(`buildkit import: ${t.stack||t.message}`),a={"grpc-status":"13","grpc-message":encodeURIComponent(`dock: ${t.message}`)},e.end()}e.end(t)})}),s.on("error",()=>e.close(http2.constants.NGHTTP2_INTERNAL_ERROR)),e.on("error",()=>s.close()),e.on("close",()=>{s.closed||s.close()})}async _blobText(t){const e=t.replace(/^sha256:/,"");if(!/^[0-9a-f]{64}$/.test(e))throw new Error(`bad digest ${t}`);const i=await this.host.run(["cat",`${CONTENT}/${e}`]);if(0!==i.code)throw new Error(`BuildKit has no blob ${t}`);return i.stdout}async importResult(t,{all:e=!1}={}){const i=t["containerimage.digest"];if(!i)return null;const r=JSON.parse(await this._blobText(i));if(!r.manifests)return this._importManifest(t,r,i,!0);const s=r.manifests.filter(t=>t.platform&&"unknown"!==t.platform.architecture),n=this.arch,o=s.find(t=>"linux"===t.platform.os&&t.platform.architecture===n)||s[0];if(!o)throw new Error("the built index has no image manifest");const a=await this._importManifest(t,JSON.parse(await this._blobText(o.digest)),o.digest,!0);if(a.platforms=[{image:a,platform:o.platform}],e)for(const e of s){if(e===o)continue;const i=await this._importManifest(t,JSON.parse(await this._blobText(e.digest)),e.digest,!1);a.platforms.push({image:i,platform:e.platform})}return a}async pushResult(t,e){const i=String(e).split(",").map(t=>t.trim()).filter(Boolean);if(!i.length)throw new Error("push needs a name: build with -t registry/repo:tag");const r=t.platforms||[{image:t}];let s=t.id;for(const t of i)this.engine.log(`buildkit: pushing ${t} (${r.length} platform${1===r.length?"":"s"})`),s=r.length>1?(await this.engine.pushIndex(t,r.map(t=>t.image.id))).digest:(await this.engine.push(t,null))[0]?.digest||s;return s}async _importManifest(t,e,i,r){const s=await this._blobText(e.config.digest),n=JSON.parse(s),o=(e.layers||[]).map(t=>({digest:t.digest,size:t.size,mediaType:t.mediaType}));if(o.some(t=>/zstd/.test(t.mediaType||"")))throw new Error("zstd layers are not supported; build with compression=gzip");const a=this.engine.paths.blobs,l=[e.config.digest,...o.map(t=>t.digest)].map(t=>t.replace(/^sha256:/,"")).filter(t=>!fs.existsSync(path.join(a,t)));l.length&&await this.host.check(["sh","-c",'src="$1"; dst="$2"; shift 2; for h; do cp "$src/$h" "$dst/.$h.tmp" && mv "$dst/.$h.tmp" "$dst/$h"; done',"dock",CONTENT,this.host.guestPath(a),...l]);const c=[];for(const e of(r&&t["image.name"]||"").split(",").map(t=>t.trim()).filter(Boolean))try{const t=parseReference(e);t.tagged&&c.push(t.tagged)}catch(t){}const u=e.config.digest,h=this.engine.store.saveImage({id:u,repoTags:c,repoDigests:[],digest:null,size:o.reduce((t,e)=>t+(e.size||0),0),layers:o,config:n,architecture:n.architecture,os:n.os,createdAt:Date.parse(n.created)||Date.now()});for(const t of c)this.engine.emitEvent("image","tag",h.id,{name:t});return this.engine.log(`buildkit: imported ${u.slice(7,19)} (${i.slice(7,19)}) ${c.join(" ")}`),h}}module.exports={BuildKit,VERSION,RELEASES,installBuildKit,buildKitDir,buildKitInstalled,grpcFrame,bytesMessage,bytesMessageData,FrameReader,solveExporterResponse,sessionHeaders,varint,rewriteSolveRequest,setExporterResponse};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license ThinkNCollab Enterprise & Open Core Ecosystem
|
|
3
|
+
* Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
|
|
4
|
+
* PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
|
|
5
|
+
* UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
|
|
6
|
+
*/
|
|
7
|
+
"use strict";const fs=require("fs"),os=require("os"),path=require("path"),crypto=require("crypto"),CACHE_CONFIG="application/vnd.tnc.dock.buildcache.config.v1+json",INLINE_KEY="tnc.buildcache",LAYER_MEDIA="application/vnd.oci.image.layer.v1.tar+gzip",OCI_MANIFEST="application/vnd.oci.image.manifest.v1+json";function cacheError(e){const t=new Error(e);return t.statusCode=400,t}function parseCacheSpec(e,t){const r=String(e).trim();if(!r)throw cacheError(`empty --cache-${t}`);if(!r.includes("="))return{type:"registry",ref:r,mode:"min"};const i={};for(const e of r.split(",")){const s=e.indexOf("=");if(-1===s)throw cacheError(`invalid --cache-${t} "${r}": ${e} is not key=value`);i[e.slice(0,s).trim().toLowerCase()]=e.slice(s+1).trim()}const s=i.type||(i.ref?"registry":i.src||i.dest?"local":""),n=i.mode||"min";if(!["min","max"].includes(n))throw cacheError(`--cache-${t} mode must be min or max`);if("registry"===s){if(!i.ref)throw cacheError(`--cache-${t} type=registry needs ref=…`);return{type:s,ref:i.ref,mode:n}}if("local"===s){const e="from"===t?i.src:i.dest;if(!e)throw cacheError(`--cache-${t} type=local needs ${"from"===t?"src":"dest"}=…`);return{type:s,dir:e,mode:n}}if("inline"===s){if("from"===t)throw cacheError("--cache-from has no inline type: name the image instead (--cache-from IMAGE)");return{type:s,mode:"min"}}if("gha"===s||"s3"===s||"azblob"===s)throw cacheError(`cache type ${s} is not supported; use registry or local`);throw cacheError(`unknown cache type "${s}" (registry, local, inline)`)}function cleanEntries(e){const t=/^sha256:[0-9a-f]{64}$/;return(Array.isArray(e)?e:[]).filter(e=>e&&/^[0-9a-f]{64}$/.test(e.key)&&(""===e.digest&&""===e.diffId||t.test(e.digest)&&t.test(e.diffId)&&Number.isFinite(e.size)))}async function importCaches(e,t,r=()=>{}){const i=new Map;for(const s of t)try{const{entries:t,fetchBlob:n}="local"===s.type?readLocal(s.dir):await readRegistry(e,s.ref);let a=0;for(const r of cleanEntries(t))i.has(r.key)||(i.set(r.key,{entry:r,fetch:()=>ensureBlob(e,r,n)}),a++);r(`importing cache from ${s.ref||s.dir}: ${a} step${1===a?"":"s"}`)}catch(e){r(`[Warning] cache ${s.ref||s.dir} not used: ${e.message}`)}return i}async function ensureBlob(e,t,r){if(!t.digest)return;const i=path.join(e.paths.blobs,t.digest.replace(/^sha256:/,""));fs.existsSync(i)&&fs.statSync(i).size===t.size||await r(t.digest,i,t.size)}function readLocal(e){const t=t=>path.join(e,"blobs","sha256",t.replace(/^sha256:/,""));let r;try{r=JSON.parse(fs.readFileSync(path.join(e,"index.json"),"utf8"))}catch(t){throw new Error(`no cache in ${e} (index.json missing)`)}const i=(r.manifests||[]).find(e=>"cache"===e.annotations?.["org.opencontainers.image.ref.name"])||r.manifests?.[0];if(!i)throw new Error(`no cache in ${e}`);const s=JSON.parse(fs.readFileSync(t(i.digest),"utf8"));return{entries:JSON.parse(fs.readFileSync(t(s.config.digest),"utf8")).entries,fetchBlob:async(r,i)=>{const s=t(r);if("sha256:"+crypto.createHash("sha256").update(fs.readFileSync(s)).digest("hex")!==r)throw new Error(`cache blob ${r} in ${e} is damaged`);copyFile(s,i)}}}async function readRegistry(e,t){const{parseReference:r}=require("../image/reference"),{RegistryClient:i}=require("../image/registry"),s=r(t),n=new i(s,{credentials:e._credentials(s.domain)});let a=await n.manifest(s.digest||s.tag);if(a.body.manifests){const t=e.linuxArch?.()||{x64:"amd64",arm64:"arm64"}[process.arch],r=a.body.manifests.find(e=>e.platform?.architecture===t)||a.body.manifests[0];a=await n.manifest(r.digest)}const c=a.body.config,o=path.join(os.tmpdir(),`tnc-cache-${process.pid}-${crypto.randomBytes(4).toString("hex")}`);let f;try{await n.blob(c.digest,o,c.size),f=JSON.parse(fs.readFileSync(o,"utf8"))}finally{fs.rmSync(o,{force:!0})}const h=c.mediaType===CACHE_CONFIG?f.entries:f[INLINE_KEY];if(!h)throw new Error(`${t} holds no build cache (push one with --cache-to type=registry,ref=${t}, or build the image with --cache-to type=inline)`);return{entries:h,fetchBlob:(e,t,r)=>n.blob(e,t,r)}}function selectEntries(e,t,r){const i=new Set,s=[];for(const n of e)("max"===t||r.has(n.stage))&&(i.has(n.key)||(i.add(n.key),s.push({key:n.key,digest:n.layer.digest||"",diffId:n.layer.diffId||"",size:n.layer.size||0})));return s}function cacheConfig(e){return JSON.stringify({schemaVersion:1,created:(new Date).toISOString(),entries:e})}function layerList(e){const t=new Map;for(const r of e)r.digest&&!t.has(r.digest)&&t.set(r.digest,{mediaType:LAYER_MEDIA,digest:r.digest,size:r.size});return[...t.values()]}function copyFile(e,t){if(fs.existsSync(t))return;const r=`${t}.${process.pid}.tmp`;try{fs.linkSync(e,r)}catch(t){fs.copyFileSync(e,r)}fs.renameSync(r,t)}function writeLocal(e,t,r){const i=path.join(t,"blobs","sha256");fs.mkdirSync(i,{recursive:!0});const s=e=>{const t=crypto.createHash("sha256").update(e).digest("hex");return fs.writeFileSync(path.join(i,t),e),{digest:`sha256:${t}`,size:e.length}},n=layerList(r);for(const t of n)copyFile(path.join(e.paths.blobs,t.digest.replace(/^sha256:/,"")),path.join(i,t.digest.replace(/^sha256:/,"")));const a=s(Buffer.from(cacheConfig(r))),c={schemaVersion:2,mediaType:OCI_MANIFEST,config:{mediaType:CACHE_CONFIG,...a},layers:n},o=s(Buffer.from(JSON.stringify(c)));fs.writeFileSync(path.join(t,"oci-layout"),JSON.stringify({imageLayoutVersion:"1.0.0"})),fs.writeFileSync(path.join(t,"index.json"),JSON.stringify({schemaVersion:2,mediaType:"application/vnd.oci.image.index.v1+json",manifests:[{mediaType:OCI_MANIFEST,...o,annotations:{"org.opencontainers.image.ref.name":"cache"}}]},null,2))}async function writeRegistry(e,t,r,i){const{parseReference:s}=require("../image/reference"),{pushArtifact:n}=require("../image/registry"),a=s(t),c=layerList(r).map(t=>({...t,file:path.join(e.paths.blobs,t.digest.replace(/^sha256:/,""))}));return n(a,{config:{mediaType:CACHE_CONFIG,raw:cacheConfig(r)},layers:c,credentials:e._credentials(a.domain),tag:a.tag||"latest",progress:i})}async function exportCaches(e,t,r,i,s=()=>{}){for(const n of t){if("inline"===n.type)continue;const t=selectEntries(r,n.mode,i);if("local"===n.type)writeLocal(e,n.dir,t),s(`exported cache to ${n.dir} (${t.length} steps)`);else{const r=await writeRegistry(e,n.ref,t,()=>{});s(`exported cache to ${n.ref}@${r.digest} (${t.length} steps)`)}}}module.exports={parseCacheSpec,importCaches,exportCaches,selectEntries,readLocal,writeLocal,cleanEntries,CACHE_CONFIG,INLINE_KEY};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license ThinkNCollab Enterprise & Open Core Ecosystem
|
|
3
|
+
* Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
|
|
4
|
+
* PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
|
|
5
|
+
* UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
|
|
6
|
+
*/
|
|
7
|
+
"use strict";const net=require("net"),crypto=require("crypto"),OPEN=1,DATA=2,CLOSE=3,HEADER=9,DEFAULT_TARGET=e=>`/run/buildkit/ssh_agent.${e}`;function frame(e,s,t=Buffer.alloc(0)){const n=Buffer.alloc(9+t.length);return n[0]=e,n.writeUInt32BE(s>>>0,1),n.writeUInt32BE(t.length,5),t.copy(n,9),n}function decoder(e){let s=Buffer.alloc(0);return t=>{for(s=s.length?Buffer.concat([s,t]):t;s.length>=9;){const t=s.readUInt32BE(5);if(s.length<9+t)break;e(s[0],s.readUInt32BE(1),s.subarray(9,9+t)),s=s.subarray(9+t)}}}const WINDOWS_AGENT="\\\\.\\pipe\\openssh-ssh-agent";function parseSshSpec(e,s=process.env,t=require("fs")){const n=String(e).trim(),o=n.indexOf("="),r=(-1===o?n:n.slice(0,o)).trim()||"default";let a=-1===o?"":n.slice(o+1).trim();if(a.includes(","))throw new Error(`--ssh ${n}: give one agent socket per id`);if(a||(a=s.SSH_AUTH_SOCK||("win32"===process.platform?WINDOWS_AGENT:"")),!a)throw new Error(`--ssh ${r}: no SSH agent (SSH_AUTH_SOCK is not set); start one with: eval "$(ssh-agent)" && ssh-add`);if(!a.startsWith("\\\\.\\pipe\\")){let e;try{e=t.statSync(a)}catch(e){throw new Error(`--ssh ${r}: ${a} does not exist`)}if(!e.isSocket())throw new Error(`--ssh ${r}: ${a} is not an agent socket; load the key into an agent instead: ssh-add ${a}`)}return{id:r,socket:a}}async function openSession(e,s){const t=new Map(s.map(e=>[e.id,e.socket])),n=crypto.randomBytes(16).toString("hex"),{socket:o}=await e.hijack(`/tnc/ssh-agent?session=${n}&ids=${encodeURIComponent([...t.keys()].join(","))}`),r=new Map;return o.on("data",decoder((e,s,n)=>{if(1===e){const e=t.get(n.toString("utf8"));if(!e)return void o.write(frame(3,s));const a=net.connect(e);r.set(s,a),a.on("data",e=>o.write(frame(2,s,e)));const i=()=>{r.delete(s)&&o.write(frame(3,s))};a.on("end",i),a.on("close",i),a.on("error",()=>a.destroy())}else if(2===e)r.get(s)?.write(n);else if(3===e){const e=r.get(s);r.delete(s),e?.end()}})),o.on("error",()=>{}),{session:n,ids:[...t.keys()],close(){for(const e of r.values())e.destroy();o.destroy()}}}const sessions=new Map;function acceptSession(e,s,t){if(!/^[0-9a-f]{32}$/.test(e||""))throw Object.assign(new Error("invalid ssh agent session"),{statusCode:400});const n={ids:new Set(s.filter(Boolean)),send:e=>t.write(e),channels:new Map,next:1};return sessions.set(e,n),t.onData(decoder((e,s,t)=>{const o=n.channels.get(s);o&&(2===e?o.data(t):3===e&&(n.channels.delete(s),o.close()))})),new Promise(s=>{const o=()=>{if(sessions.get(e)!==n)return s();sessions.delete(e);for(const e of n.channels.values())e.close();s()};t.onEnd(o),t.onClose?.(o)})}function session(e){return e&&sessions.get(e)||null}function startListener(e,s,t,n){const{shim:o}=e.binaries(),r=e.spawn([o,"muxlisten",n]),a=new Map;r.stdout.on("data",decoder((e,n,o)=>{if(1===e){const e=s.next++;a.set(n,e),s.channels.set(e,{data:e=>r.write(frame(2,n,e)),close:()=>{a.delete(n)&&r.write(frame(3,n))}}),s.send(frame(1,e,Buffer.from(t)))}else if(2===e){const e=a.get(n);e&&s.send(frame(2,e,o))}else if(3===e){const e=a.get(n);a.delete(n),e&&s.channels.delete(e)&&s.send(frame(3,e))}}));let i="";return r.stderr.on("data",e=>{i+=e}),{ready:new Promise((e,s)=>{let t=!1;const n=setInterval(()=>{/listening/.test(i)&&(t=!0,clearInterval(n),e())},20);r.exited.then(e=>{clearInterval(n),t||s(new Error(/unknown mode|usage/.test(i)||2===e?"RUN --mount=type=ssh needs a newer dock runtime: run thinknsh dock setup":`ssh agent socket: ${i.trim()||`exit ${e}`}`))})}),stop(){for(const e of a.values())s.channels.delete(e)&&s.send(frame(3,e));a.clear(),r.end(),r.kill?.()}}}module.exports={frame,decoder,parseSshSpec,openSession,acceptSession,session,startListener,DEFAULT_TARGET,OPEN:1,DATA:2,CLOSE:3,WINDOWS_AGENT};
|