thinkncollab-shell 2.0.0-beta.2 → 2.0.0-beta.3

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.
Files changed (50) hide show
  1. package/Readme.md +41 -10
  2. package/dist/native/tnc-tray/build-macos.sh +54 -0
  3. package/dist/native/tnc-tray/main.swift +201 -0
  4. package/dist/native/tnc-tray/tray.ps1 +136 -0
  5. package/dist/src/commands/dock/containers.js +1 -1
  6. package/dist/src/commands/dock/dock.js +1 -1
  7. package/dist/src/dock/api/routes.js +1 -1
  8. package/dist/src/dock/api/server.js +1 -1
  9. package/dist/src/dock/api/websocket.js +7 -0
  10. package/dist/src/dock/api/wire.js +1 -1
  11. package/dist/src/dock/build/builder.js +1 -1
  12. package/dist/src/dock/build/buildkit.js +1 -1
  13. package/dist/src/dock/build/cache.js +7 -0
  14. package/dist/src/dock/build/sshagent.js +7 -0
  15. package/dist/src/dock/cli/api.js +1 -1
  16. package/dist/src/dock/cli/commands.js +1 -1
  17. package/dist/src/dock/cli/compose.js +1 -1
  18. package/dist/src/dock/cli/trust.js +7 -0
  19. package/dist/src/dock/compat/cases.js +1 -1
  20. package/dist/src/dock/config.js +1 -1
  21. package/dist/src/dock/daemon-main.js +1 -1
  22. package/dist/src/dock/daemon.js +1 -1
  23. package/dist/src/dock/engine.js +1 -1
  24. package/dist/src/dock/image/auth.js +1 -1
  25. package/dist/src/dock/image/credhelpers.js +7 -0
  26. package/dist/src/dock/image/keychain.js +7 -0
  27. package/dist/src/dock/image/registry.js +1 -1
  28. package/dist/src/dock/image/scan.js +7 -0
  29. package/dist/src/dock/image/sign.js +7 -0
  30. package/dist/src/dock/linux/host.js +1 -1
  31. package/dist/src/dock/paths.js +1 -1
  32. package/dist/src/dock/runtime/cdi.js +7 -0
  33. package/dist/src/dock/runtime/fsops.js +1 -1
  34. package/dist/src/dock/runtime/network.js +1 -1
  35. package/dist/src/dock/runtime/overlaydns.js +7 -0
  36. package/dist/src/dock/runtime/ports.js +1 -1
  37. package/dist/src/dock/runtime/rootfs.js +1 -1
  38. package/dist/src/dock/runtime/spec.js +1 -1
  39. package/dist/src/dock/runtime/stats.js +1 -1
  40. package/dist/src/dock/runtime/userns.js +7 -0
  41. package/dist/src/dock/runtime/volumes.js +1 -1
  42. package/dist/src/dock/setup/autostart.js +1 -1
  43. package/dist/src/dock/setup/diagnose.js +7 -0
  44. package/dist/src/dock/setup/index.js +1 -1
  45. package/dist/src/dock/tray/agent.js +7 -0
  46. package/dist/src/dock/tray/install.js +7 -0
  47. package/dist/src/dock/ui/page.js +228 -15
  48. package/dist/src/dock/ui/server.js +1 -1
  49. package/package.json +1 -1
  50. 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 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};
@@ -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"),{spawn}=require("child_process"),path=require("path"),fs=require("fs"),{dockPaths}=require("../paths"),{ping}=require("../client");class EngineError extends Error{constructor(e,t){super(e),this.status=t}}const enc=encodeURIComponent;function qs(e={}){const t=[];for(const[n,r]of Object.entries(e))null!=r&&!1!==r&&(Array.isArray(r)?r.forEach(e=>t.push(`${n}=${enc(e)}`)):"object"==typeof r?t.push(`${n}=${enc(JSON.stringify(r))}`):t.push(`${n}=${enc(!0===r?"1":r)}`));return t.length?`?${t.join("&")}`:""}class DockApi{constructor(e=dockPaths().socket){this.socket=e}_request(e,t,n={}){return http.request({socketPath:this.socket,method:e,path:t,headers:n})}_send(e,t,n,r={}){return new Promise((s,o)=>{const c=null!=n&&"function"==typeof n.pipe,i=null==n||c?null:Buffer.isBuffer(n)?n:Buffer.from(JSON.stringify(n)),a={...r};i&&(a["Content-Length"]=i.length,a["Content-Type"]||="application/json");const u=this._request(e,t,a);u.on("response",s),u.on("error",e=>o(this._connError(e))),c?(n.on("error",e=>u.destroy(e)),n.pipe(u)):u.end(i)})}_connError(e){return["ENOENT","ECONNREFUSED"].includes(e.code)?new EngineError("dock is not running. Start it with: thinknsh dock start",0):e}static async _read(e){const t=[];for await(const n of e)t.push(n);const n=Buffer.concat(t).toString("utf8");if(!(e.headers["content-type"]||"").includes("json")||!n)return n;try{return JSON.parse(n)}catch(e){return n}}static async _check(e){if(e.statusCode<400)return e;const t=await DockApi._read(e);throw new EngineError("object"==typeof t&&t?t.message:String(t||`HTTP ${e.statusCode}`),e.statusCode)}async json(e,t,n){const r=await DockApi._check(await this._send(e,t,n));return DockApi._read(r)}async stream(e,t,n,r){return DockApi._check(await this._send(e,t,n,r))}async progress(e,t,n,r,s){const o=await this.stream(e,t,n,s);let c="",i=null;const a=e=>{if(!e.trim())return;let t;try{t=JSON.parse(e)}catch(e){return}t.error?i=i||t.error:r(t)};for await(const e of o){let t;for(c+=e.toString("utf8");-1!==(t=c.indexOf("\n"));)a(c.slice(0,t)),c=c.slice(t+1)}if(a(c),i)throw new EngineError(i,500)}hijack(e,t){return new Promise((n,r)=>{const s=null==t?null:Buffer.from(JSON.stringify(t)),o={Connection:"Upgrade",Upgrade:"tcp"};s&&(o["Content-Type"]="application/json",o["Content-Length"]=s.length);const c=this._request("POST",e,o);c.on("upgrade",(e,t,r)=>{r?.length&&t.unshift(r),n({socket:t,tty:(e.headers["content-type"]||"").includes("raw-stream")})}),c.on("response",async e=>{try{await DockApi._check(e),n({socket:e,tty:!1,readOnly:!0})}catch(e){r(e)}}),c.on("error",e=>r(this._connError(e))),c.end(s)})}async ensureRunning({quiet:e=!1}={}){if(await ping(this.socket))return;const t=dockPaths();e||process.stderr.write("starting dock…\n"),fs.mkdirSync(t.home,{recursive:!0,mode:448});const n=fs.openSync(t.logFile,"a"),r=spawn(process.execPath,[path.join(__dirname,"..","daemon-main.js")],{detached:!0,stdio:["ignore",n,n],env:process.env,windowsHide:!0});r.unref(),fs.closeSync(n);for(let e=0;e<100;e++){if(await ping(this.socket))return;if(null!==r.exitCode)break;await new Promise(e=>setTimeout(e,100))}throw new EngineError(`dock did not start; see ${t.logFile}`,0)}}function demuxer(e,t){let n=Buffer.alloc(0);return r=>{for(n=n.length?Buffer.concat([n,r]):r;n.length>=8;){const r=n.readUInt32BE(4);if(n.length<8+r)break;const s=n.subarray(8,8+r);(2===n[0]?t:e)(s),n=n.subarray(8+r)}}}module.exports={DockApi,EngineError,demuxer,qs,enc};
7
+ "use strict";const http=require("http"),{spawn}=require("child_process"),path=require("path"),fs=require("fs"),{dockPaths}=require("../paths"),{ping}=require("../client");class EngineError extends Error{constructor(e,t){super(e),this.status=t}}const enc=encodeURIComponent;function qs(e={}){const t=[];for(const[r,n]of Object.entries(e))null!=n&&!1!==n&&(Array.isArray(n)?n.forEach(e=>t.push(`${r}=${enc(e)}`)):"object"==typeof n?t.push(`${r}=${enc(JSON.stringify(n))}`):t.push(`${r}=${enc(!0===n?"1":n)}`));return t.length?`?${t.join("&")}`:""}class DockApi{constructor(e=dockPaths().socket){this.socket=e}_request(e,t,r={}){return http.request({socketPath:this.socket,method:e,path:t,headers:r})}_send(e,t,r,n={}){return new Promise((s,o)=>{const c=null!=r&&"function"==typeof r.pipe,i=null==r||c?null:Buffer.isBuffer(r)?r:Buffer.from(JSON.stringify(r)),a={...n};i&&(a["Content-Length"]=i.length,a["Content-Type"]||="application/json");const u=this._request(e,t,a);u.on("response",s),u.on("error",e=>o(this._connError(e))),c?(r.on("error",e=>u.destroy(e)),r.pipe(u)):u.end(i)})}_connError(e){return["ENOENT","ECONNREFUSED"].includes(e.code)?new EngineError("dock is not running. Start it with: thinknsh dock start",0):e}static async _read(e){const t=[];for await(const r of e)t.push(r);const r=Buffer.concat(t).toString("utf8");if(!(e.headers["content-type"]||"").includes("json")||!r)return r;try{return JSON.parse(r)}catch(e){return r}}static async _check(e){if(e.statusCode<400)return e;const t=await DockApi._read(e);throw new EngineError("object"==typeof t&&t?t.message:String(t||`HTTP ${e.statusCode}`),e.statusCode)}async json(e,t,r){const n=await DockApi._check(await this._send(e,t,r));return DockApi._read(n)}async stream(e,t,r,n){return DockApi._check(await this._send(e,t,r,n))}async progress(e,t,r,n,s){const o=await this.stream(e,t,r,s);let c="",i=null;const a=e=>{if(!e.trim())return;let t;try{t=JSON.parse(e)}catch(e){return}t.error?i=i||t.error:n(t)};for await(const e of o){let t;for(c+=e.toString("utf8");-1!==(t=c.indexOf("\n"));)a(c.slice(0,t)),c=c.slice(t+1)}if(a(c),i)throw new EngineError(i,500)}hijack(e,t){return new Promise((r,n)=>{const s=null==t?null:Buffer.from(JSON.stringify(t)),o={Connection:"Upgrade",Upgrade:"tcp"};s&&(o["Content-Type"]="application/json",o["Content-Length"]=s.length);const c=this._request("POST",e,o);c.on("upgrade",(e,t,n)=>{n?.length&&t.unshift(n),r({socket:t,tty:(e.headers["content-type"]||"").includes("raw-stream")})}),c.on("response",async e=>{try{await DockApi._check(e),r({socket:e,tty:!1,readOnly:!0})}catch(e){n(e)}}),c.on("error",e=>n(this._connError(e))),c.end(s)})}async ensureRunning({quiet:e=!1}={}){if(await ping(this.socket))return;const t=dockPaths();if("linux"===process.platform&&0!==process.getuid?.()&&!process.env.TNC_DOCK_HOME&&!require("../paths").rootlessMode())throw new EngineError("dock is not running. Start it with: sudo systemctl start tnc-dock (installer) or sudo thinknsh dock start",0);e||process.stderr.write("starting dock…\n"),fs.mkdirSync(t.home,{recursive:!0,mode:448});const r=fs.openSync(t.logFile,"a"),n=spawn(process.execPath,[path.join(__dirname,"..","daemon-main.js")],{detached:!0,stdio:["ignore",r,r],env:process.env,windowsHide:!0});n.unref(),fs.closeSync(r);for(let e=0;e<100;e++){if(await ping(this.socket))return;if(null!==n.exitCode)break;await new Promise(e=>setTimeout(e,100))}throw new EngineError(`dock did not start; see ${t.logFile}`,0)}}function demuxer(e,t){let r=Buffer.alloc(0);return n=>{for(r=r.length?Buffer.concat([r,n]):n;r.length>=8;){const n=r.readUInt32BE(4);if(r.length<8+n)break;const s=r.subarray(8,8+n);(2===r[0]?t:e)(s),r=r.subarray(8+n)}}}module.exports={DockApi,EngineError,demuxer,qs,enc};