thinkncollab-shell 2.0.0-beta.5 → 2.0.0-beta.7

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 (86) hide show
  1. package/Readme.md +64 -17
  2. package/dist/bin/thinknsh.js +1 -1
  3. package/dist/src/commands/auth/auth.js +7 -0
  4. package/dist/src/commands/auth/login.js +1 -1
  5. package/dist/src/commands/auth/logout.js +1 -1
  6. package/dist/src/commands/auth/register.js +1 -1
  7. package/dist/src/commands/auth/whoami.js +1 -1
  8. package/dist/src/commands/dock/cluster.js +1 -1
  9. package/dist/src/commands/dock/containers.js +1 -1
  10. package/dist/src/commands/index.js +1 -1
  11. package/dist/src/commands/room/connect.js +7 -0
  12. package/dist/src/commands/room/join.js +1 -1
  13. package/dist/src/commands/room/sync.js +1 -1
  14. package/dist/src/commands/sandbox/common.js +7 -0
  15. package/dist/src/commands/sandbox/console.js +1 -1
  16. package/dist/src/commands/sandbox/dep.js +1 -1
  17. package/dist/src/commands/sandbox/deploy.js +1 -1
  18. package/dist/src/commands/sandbox/dev.js +1 -1
  19. package/dist/src/commands/sandbox/exec.js +1 -1
  20. package/dist/src/commands/sandbox/push.js +1 -1
  21. package/dist/src/commands/sandbox/sandbox.js +1 -1
  22. package/dist/src/commands/sandbox/share.js +1 -1
  23. package/dist/src/commands/sandbox/snapshot.js +1 -1
  24. package/dist/src/commands/shell/doctor.js +1 -1
  25. package/dist/src/commands/tasks/build.js +1 -1
  26. package/dist/src/commands/terminal/share.js +1 -1
  27. package/dist/src/core/api-client.js +1 -1
  28. package/dist/src/core/auth/browser-login.js +7 -0
  29. package/dist/src/core/auth/gate.js +7 -0
  30. package/dist/src/core/auth/session-store.js +7 -0
  31. package/dist/src/core/crypto/seal.js +7 -0
  32. package/dist/src/core/dependency-tracker.js +1 -1
  33. package/dist/src/core/foreground.js +7 -0
  34. package/dist/src/core/platform-socket.js +7 -0
  35. package/dist/src/core/room/sync-manager.js +7 -0
  36. package/dist/src/core/room-link.js +7 -0
  37. package/dist/src/core/shell.js +1 -1
  38. package/dist/src/core/{sandbox/snapshot-manager.js → snapshot-manager.js} +1 -1
  39. package/dist/src/core/tunnel/client.js +7 -0
  40. package/dist/src/core/tunnel/daemon.js +7 -0
  41. package/dist/src/core/tunnel/index.js +7 -0
  42. package/dist/src/core/websocket.js +1 -1
  43. package/dist/src/core/window-launcher.js +7 -0
  44. package/dist/src/dock/bench/memory.js +1 -1
  45. package/dist/src/dock/cli/commands.js +1 -1
  46. package/dist/src/dock/cli/k8s.js +7 -0
  47. package/dist/src/dock/cli/services.js +1 -1
  48. package/dist/src/dock/cli/session.js +1 -1
  49. package/dist/src/dock/cli/workspace.js +1 -1
  50. package/dist/src/dock/engine.js +1 -1
  51. package/dist/src/dock/image/certs.js +7 -0
  52. package/dist/src/dock/image/registry.js +1 -1
  53. package/dist/src/dock/room-data.js +7 -0
  54. package/dist/src/dock/runtime/network.js +1 -1
  55. package/dist/src/dock/sandbox.js +7 -0
  56. package/dist/src/dock/setup/index.js +1 -1
  57. package/dist/src/dock/workspace/share.js +7 -0
  58. package/dist/src/dock/workspace/workspace.js +1 -1
  59. package/package.json +1 -1
  60. package/dist/src/core/sandbox/arch-shim.js +0 -7
  61. package/dist/src/core/sandbox/capsule-builder.js +0 -7
  62. package/dist/src/core/sandbox/cgroups.js +0 -7
  63. package/dist/src/core/sandbox/container-monitor.js +0 -7
  64. package/dist/src/core/sandbox/crun-manager.js +0 -7
  65. package/dist/src/core/sandbox/engine.js +0 -7
  66. package/dist/src/core/sandbox/image-manager.js +0 -7
  67. package/dist/src/core/sandbox/image-store.js +0 -7
  68. package/dist/src/core/sandbox/index.js +0 -7
  69. package/dist/src/core/sandbox/macos-sandbox.js +0 -7
  70. package/dist/src/core/sandbox/microvm-manager.js +0 -7
  71. package/dist/src/core/sandbox/native-service-manager.js +0 -7
  72. package/dist/src/core/sandbox/network.js +0 -7
  73. package/dist/src/core/sandbox/oci-exporter.js +0 -7
  74. package/dist/src/core/sandbox/oci-puller.js +0 -7
  75. package/dist/src/core/sandbox/oci-runtime-spec.js +0 -7
  76. package/dist/src/core/sandbox/port-proxy-daemon.js +0 -7
  77. package/dist/src/core/sandbox/room-sync-manager.js +0 -7
  78. package/dist/src/core/sandbox/runtime-manager.js +0 -7
  79. package/dist/src/core/sandbox/storage.js +0 -7
  80. package/dist/src/core/sandbox/supervisor.js +0 -7
  81. package/dist/src/core/sandbox/tunnel-daemon.js +0 -7
  82. package/dist/src/core/sandbox/tunnel.js +0 -7
  83. package/dist/src/core/sandbox/window-launcher.js +0 -7
  84. package/dist/src/core/sandbox/windows-wsl2.js +0 -7
  85. package/dist/src/core/sandbox-window.js +0 -8
  86. /package/dist/src/core/{sandbox/policy-engine.js → policy-engine.js} +0 -0
@@ -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"),{EventEmitter}=require("events"),{parseReference}=require("./image/reference"),{pullImage,pushImage,pushIndex,describe:describeRemote,verifyLogin}=require("./image/registry"),{AuthStore,normalizeRegistry}=require("./image/auth"),{createLinuxHost}=require("./linux/host"),{Rootfs}=require("./runtime/rootfs"),{Network}=require("./runtime/network"),{PortProxies}=require("./runtime/ports"),{Volumes}=require("./runtime/volumes"),{buildSpec,mergeEnv,processArgs,resolveUser,additionalGids}=require("./runtime/spec"),{ContainerFs}=require("./runtime/fsops"),{Stats}=require("./runtime/stats"),{Emulation,parsePlatform}=require("./runtime/emulation"),{HostForward,HOST_IP,ENGINE_SOCK}=require("./runtime/hostfwd"),{FsEventRelay}=require("./runtime/fsevents"),archive=require("./image/archive"),{BUILD_FILE,HOST_ALIAS}=require("./api/wire"),NAME_RE=/^\/?[a-zA-Z0-9][a-zA-Z0-9_.-]+$/,CRUN_CREATE_ERROR=[/^crun: /,/^executable file `.*` not found/,/^open executable: /,/^exec(ve)? `.*`: /,/^chdir to `.*`: /],GO_ARCH={x64:"amd64",arm64:"arm64"},ADJECTIVES=["brave","calm","eager","fancy","gentle","happy","jolly","keen","lucid","merry","nifty","proud","quick","sharp","sunny","witty"],NOUNS=["aryabhata","bose","raman","kalam","sarabhai","ramanujan","bhabha","saha","chandra","khorana","visvesvaraya","dhawan","swaminathan","mahalanobis"];function httpError(t,e){const s=new Error(e);return s.statusCode=t,s}const shortId=t=>t.replace(/^sha256:/,"").slice(0,12),iso=t=>t?new Date(t).toISOString():"0001-01-01T00:00:00Z",sleep=t=>new Promise(e=>setTimeout(e,t));function redactEnv(t){return Array.isArray(t)?t.map(t=>String(t).replace(/=.*$/s,"=***")):t}function redactExecBody(t){return t&&t.Env?{...t,Env:redactEnv(t.Env)}:t}const LAYER_REMOVE='root="$1"; shift; for d in "$root"/layers "$root"/layers-*; do [ -d "$d" ] || continue; for h; do rm -rf "$d/$h" "$d/$h.done"; done; done; true',LAYER_GC='\nroot="$1"; keep=$(cat); n=0\nfor d in "$root"/layers "$root"/layers-*; do\n [ -d "$d" ] || continue\n for e in "$d"/*; do\n h=${e##*/}; h=${h%.done}\n case "$h" in empty|*.tmp.*|*.lock|*\'*\') continue;; esac\n printf \'%s\\n\' "$keep" | grep -qx "$h" && continue\n if [ -e "$d/$h" ] || [ -e "$d/$h.done" ]; then rm -rf "$d/$h" "$d/$h.done"; n=$((n+1)); fi\n done\ndone\necho "$n"\n';class Engine extends EventEmitter{constructor({store:t,paths:e,log:s=()=>{},hostFactory:i=createLinuxHost}){super(),this.store=t,this.paths=e,this.log=s,this.hostFactory=i,this._host=null,this.execs=new Map,this.waiters=new Map,this.exiting=new Set,this.timer=null,this.lastLiveness=0,this.healthTimers=new Map,this.starting=new Map,this.auth=new AuthStore(e.auth||path.join(e.home,"auth.json")),this.fs=new ContainerFs(this),this.stats=new Stats(this),this.emulation=new Emulation(this),this.hostfwd=new HostForward({engine:this,socketPath:e.socket,log:s}),this.fsevents=new FsEventRelay({engine:this,log:s});for(const t of[e.blobs,e.containers,e.volumes])fs.mkdirSync(t,{recursive:!0})}get host(){if(!this._host){try{this._host=this.hostFactory({log:this.log})}catch(t){throw httpError(501,t.message)}this.rootfs=new Rootfs({host:this._host,blobsDir:this.paths.blobs}),this.network=new Network({host:this._host,store:this.store}),this.ports=new PortProxies({host:this._host,log:this.log})}return this._host}get buildkit(){if(!this._buildkit){const{BuildKit:t}=require("./build/buildkit");this._buildkit=new t(this)}return this._buildkit}get volumes(){return this._volumes||(this._volumes=new Volumes({store:this.store,host:this.host,dir:this.paths.volumes,emit:(t,e,s,i)=>this.emitEvent(t,e,s,i),engineSocket:this.paths.socket,guestEngineSocket:ENGINE_SOCK})),this._volumes}async ready(){try{await this.host.ready()}catch(t){throw t.statusCode?t:httpError(500,t.message)}if(!this.kernelVersion){const t=await this.host.run(["uname","-r"]);0===t.code&&(this.kernelVersion=t.stdout.trim())}if(this._layersChecked||(this._layersChecked=!0,setImmediate(()=>this.gcLayers().catch(()=>{}))),"vm"===this.host.kind)await this.hostfwd.listen(),await this.hostfwd.ensureGuest();else if("wsl"===this.host.kind&&!this._wslGateway){const t=await this.host.run(["/bin/sh","-c","ip route show default | awk '{print $3; exit}'"]);this._wslGateway=t.stdout.trim()||null}}_hostGateway(t={}){return"vm"===this._host?.kind?HOST_IP:"wsl"===this._host?.kind?this._wslGateway:"rootless"===this._host?.kind?this._host.gateway:Object.values(t).find(t=>t.gateway)?.gateway||null}emitEvent(t,e,s,i={}){const n=this.store.recordEvent(t,e,s,i);this.emit("event",n)}_attrs(t,e={}){return{...this.store.getImage(t.image)?.config?.config?.Labels||{},...t.config.Config.Labels||{},image:t.config.imageRef,name:t.name,...e}}containerDir(t){return path.join(this.paths.containers,t)}logFile(t){return path.join(this.containerDir(t),"json.log")}exitFile(t){return path.join(this.containerDir(t),"exit")}linuxArch(){return GO_ARCH[process.arch]||process.arch}findImage(t){let e=null;try{e=parseReference(t).familiar}catch(t){}return this.store.findImage(t,e)}requireImage(t){const e=this.findImage(t);if(!e)throw httpError(404,`No such image: ${t}`);return e}_credentials(t,e){if(e&&e.username&&"string"==typeof e.password)return{username:e.username,password:e.password};const s=this.auth.get(t);if(s)return s;let i;try{i=normalizeRegistry(t)}catch(t){return null}const n=require("./image/credhelpers").lookup(i);return n?{username:n.username,password:n.password}:null}async pull(t,e=()=>{},{credentials:s,platform:i}={}){const n=parseReference(t),r="string"==typeof i?parsePlatform(i):i,{image:o,fresh:a}=await pullImage(n,{blobsDir:this.paths.blobs,arch:r?.arch||this.linuxArch(),variant:r?.variant,progress:e,credentials:this._credentials(n.domain,s)}),h=require("./image/sign");if(h.policyFor(n).length){const t=(o.repoDigests||[]).find(t=>t.startsWith(`${n.familiarName}@`));await h.enforcePolicy(this,n,n.digest||t?.split("@")[1]||null),e({status:`Signature verified for ${n.familiar}`})}const c=this.store.saveImage(o);return e({status:a?`Status: Downloaded newer image for ${n.familiar}`:`Status: Image is up to date for ${n.familiar}`}),this.emitEvent("image","pull",n.familiar,{name:n.familiar}),c}async push(t,e,s=()=>{},{credentials:i}={}){const n=parseReference(t);if(n.digest)throw httpError(400,`cannot push a digest reference: ${t}`);let r;if(e||t.lastIndexOf(":")>t.lastIndexOf("/"))r=[parseReference(e?`${n.familiarName}:${e}`:t)];else{const t=`${n.familiarName}:`;if(r=this.store.listImages().flatMap(t=>t.repoTags).filter(e=>e.startsWith(t)).map(t=>parseReference(t)),!r.length)throw httpError(404,`An image does not exist locally with the tag: ${n.familiarName}`)}const o=this._credentials(n.domain,i),a=[];for(const t of r){const e=this.findImage(t.familiar);if(!e)throw httpError(404,`An image does not exist locally with the tag: ${t.familiar}`);const i=(e.repoDigests||[]).map(t=>{try{return parseReference(t)}catch(t){return null}}).filter(e=>e&&e.domain===t.domain).map(t=>t.path),n=await pushImage(t,e,{blobsDir:this.paths.blobs,credentials:o,sources:i,progress:s}),r=`${t.familiarName}@${n.digest}`,h=(e.repoDigests||[]).filter(e=>!e.startsWith(`${t.familiarName}@`));this.store.saveImage({...e,repoTags:[],repoDigests:[...h,r]}),this.emitEvent("image","push",t.familiar,{name:t.familiar}),a.push({ref:t.familiar,...n})}return a}async pushIndex(t,e,s=()=>{},{credentials:i}={}){const n=parseReference(t);if(!n.tag||n.digest)throw httpError(400,`a multi-platform push needs a tag: ${t}`);if(!Array.isArray(e)||0===e.length)throw httpError(400,"no images to push");const r=e.map(t=>{const e=this.requireImage(t),s=e.config||{};return{image:e,platform:{os:e.os||s.os||"linux",architecture:e.architecture||s.architecture,variant:s.variant}}}),o=r.flatMap(t=>(t.image.repoDigests||[]).map(t=>{try{return parseReference(t)}catch(t){return null}})).filter(t=>t&&t.domain===n.domain).map(t=>t.path),a=await pushIndex(n,r,{blobsDir:this.paths.blobs,credentials:this._credentials(n.domain,i),sources:o,progress:s});return this.emitEvent("image","push",n.familiar,{name:n.familiar}),a}async distribution(t,{credentials:e}={}){const s=parseReference(t);return await describeRemote(s,{blobsDir:this.paths.blobs,credentials:this._credentials(s.domain,e)})}async search(t,{limit:e=25,filters:s={}}={}){if(!t)throw httpError(400,"a search term is required");const{DEFAULT_REGISTRY_SEARCH:i}=require("./api/wire"),n=new URL(i);let r;n.searchParams.set("q",t),n.searchParams.set("n",String(Math.min(100,Math.max(1,e))));try{r=await fetch(n,{headers:{"User-Agent":"tnc-dock"},signal:AbortSignal.timeout(2e4)})}catch(t){throw httpError(502,`image search failed: ${t.cause?.code||t.message}`)}if(!r.ok)throw await(r.body?.cancel()),httpError(502,`image search failed: HTTP ${r.status}`);let o=((await r.json().catch(()=>({}))).results||[]).map(t=>({name:t.name||t.repo_name,description:t.description||t.short_description||"",star_count:t.star_count||0,is_official:!!t.is_official,is_automated:!!t.is_automated}));const a=t=>(s[t]||[])[0];return void 0!==a("is-official")&&(o=o.filter(t=>String(t.is_official)===String(a("is-official")))),void 0!==a("is-automated")&&(o=o.filter(t=>String(t.is_automated)===String(a("is-automated")))),void 0!==a("stars")&&(o=o.filter(t=>t.star_count>=Number(a("stars")))),o.slice(0,e)}async login({username:t,password:e,serveraddress:s}={}){const i=normalizeRegistry(s);if(!t||"string"!=typeof e||!e)throw httpError(400,"username and password are required");return await verifyLogin(i,{username:t,password:e}),this.auth.set(i,{username:t,password:e}),this.emitEvent("registry","login",i,{name:i}),i}logout(t){const e=normalizeRegistry(t),s=this.auth.remove(e);return s&&this.emitEvent("registry","logout",e,{name:e}),{host:e,removed:s}}tagImage(t,e,s="latest"){const i=this.requireImage(t),n=parseReference(s?`${e}:${s}`:e);this.store.saveImage({...i,repoTags:[n.familiar]}),this.emitEvent("image","tag",i.id,{name:n.familiar})}async removeImage(t,{force:e=!1}={}){const s=this.requireImage(t);let i=null;try{i=parseReference(t).familiar}catch(t){}const n=this.store.listContainers().filter(t=>t.image===s.id);if(i&&s.repoTags.includes(i)&&s.repoTags.length>1)return this.store.setImageTags(s.id,s.repoTags.filter(t=>t!==i)),this.emitEvent("image","untag",s.id,{name:i}),[{Untagged:i}];if(n.length&&!e)throw httpError(409,`conflict: unable to remove repository reference "${t}" (must force) - container ${shortId(n[0].id)} is using its referenced image ${shortId(s.id)}`);if(n.length&&e)return this.store.setImageTags(s.id,[]),s.repoTags.map(t=>({Untagged:t}));this.store.deleteImage(s.id);const r=new Set;for(const t of this.store.listImages()){r.add(t.id);for(const e of t.layers||[])r.add(e.digest)}const o=[s.id,...(s.layers||[]).map(t=>t.digest)].filter(t=>!r.has(t));for(const t of o){const e=path.join(this.paths.blobs,t.replace(/^sha256:/,""));fs.rmSync(e,{force:!0}),fs.rmSync(`${e}.tar`,{force:!0})}if(this._host&&await this.host.isUp().catch(()=>!1)){const t=(s.layers||[]).map(t=>t.digest).filter(t=>!r.has(t)).map(t=>t.replace(/^sha256:/,""));t.length&&await this.host.sh(LAYER_REMOVE,[this.rootfs.root,...t]).catch(()=>{})}return this.emitEvent("image","delete",s.id,{}),[...s.repoTags.map(t=>({Untagged:t})),...(s.repoDigests||[]).map(t=>({Untagged:t})),{Deleted:s.id}]}async gcLayers(){if(this.building>0||!await this.host.isUp().catch(()=>!1))return 0;const t=new Set;for(const e of this.store.listImages())for(const s of e.layers||[])t.add(s.digest.replace(/^sha256:/,""));for(const e of this.store.db.prepare("SELECT digest FROM build_cache WHERE digest != ''").all())t.add(e.digest.replace(/^sha256:/,""));const e=await this.host.sh(LAYER_GC,[this.rootfs.root],{input:[...t].join("\n")+"\n"}).catch(()=>({stdout:""})),s=Number(e.stdout.trim().split("\n").pop())||0;return s&&this.log(`removed ${s} unpacked layer${1===s?"":"s"} nothing uses`),s}async pruneBuildCache(){const t=this.store.db.prepare("SELECT digest FROM build_cache").all();if(this.store.db.exec("DELETE FROM build_cache"),this.building>0)return{count:t.length,bytes:0};const e=new Set(this.store.listImages().flatMap(t=>[t.id,...(t.layers||[]).map(t=>t.digest)]));let s=0;for(const i of new Set(t.map(t=>t.digest).filter(Boolean))){if(e.has(i))continue;const t=path.join(this.paths.blobs,i.replace(/^sha256:/,""));try{s+=fs.statSync(t).size}catch(t){}fs.rmSync(t,{force:!0}),fs.rmSync(`${t}.tar`,{force:!0})}return await this.gcLayers().catch(()=>{}),{count:t.length,bytes:s}}imageSummary(t){return{Id:t.id,ParentId:"",RepoTags:t.repoTags,RepoDigests:t.repoDigests||[],Created:Math.floor(t.createdAt/1e3),Size:t.size,SharedSize:-1,VirtualSize:t.size,Labels:t.config?.config?.Labels||{},Containers:this.store.listContainers().filter(e=>e.image===t.id).length}}imageInspect(t){const e=t.config||{};return{Id:t.id,RepoTags:t.repoTags,RepoDigests:t.repoDigests||[],Parent:"",Comment:e.comment||"",Created:e.created||iso(t.createdAt),Author:e.author||"",Config:e.config||{},Architecture:t.architecture||e.architecture,Variant:e.variant,Os:t.os||e.os,Size:t.size,VirtualSize:t.size,GraphDriver:{Name:"overlayfs",Data:null},RootFS:{Type:"layers",Layers:e.rootfs?.diff_ids||[]},Metadata:{LastTagTime:"0001-01-01T00:00:00Z"}}}async build(t,e,s){const{Builder:i,normalizeTag:n}=require("./build/builder");await this.ready();const r=path.join(this.paths.home,"build",crypto.randomBytes(8).toString("hex")),o=path.join(r,"context");fs.mkdirSync(o,{recursive:!0});try{const{spawnSync:a}=require("child_process");completeTar(t);const h=a("tar",["-xf",t,"-C",o],{encoding:"utf8"});if(0!==h.status)throw httpError(400,`invalid build context: ${(h.stderr||"").trim()}`);const c=e.buildFile||BUILD_FILE,u=path.resolve(o,c);if(!u.startsWith(o+path.sep)||!fs.existsSync(u))throw httpError(400,`Cannot locate the build file: ${c}`);const l=(e.tags||[]).map(n);let d=null;const m=path.join(o,".tnc-build-secrets");fs.existsSync(m)&&(d=path.join(r,"secrets"),fs.renameSync(m,d));const f=new i(this,{...e,contextDir:o,buildDir:r,out:s,secretsDir:d,platform:parsePlatform(e.platform)});let g;this.building=(this.building||0)+1;try{g=await f.run(u)}finally{this.building--}const p=this.store.saveImage({...g,repoTags:l});if(s({aux:{ID:p.id}}),e.quiet)s({stream:`${p.id}\n`});else{s({stream:`Successfully built ${shortId(p.id)}\n`});for(const t of l)s({stream:`Successfully tagged ${t}\n`})}for(const t of l)this.emitEvent("image","tag",p.id,{name:t});return p}finally{fs.rmSync(r,{recursive:!0,force:!0})}}findContainer(t){const e=this.store.findContainer(t);if(!e)throw httpError(404,`No such container: ${t}`);return e}_generateName(){for(let t=0;t<50;t++){const t=`${ADJECTIVES[crypto.randomInt(ADJECTIVES.length)]}_${NOUNS[crypto.randomInt(NOUNS.length)]}`;if(!this.store.findContainer(t))return t}return`dock_${crypto.randomBytes(4).toString("hex")}`}create(t,e={},{platform:s}={}){if(t&&!NAME_RE.test(t))throw httpError(400,`Invalid container name (${t}), only [a-zA-Z0-9][a-zA-Z0-9_.-] are allowed`);const i=t?t.replace(/^\//,""):this._generateName(),n=this.store.findContainer(i);if(n&&n.name===i)throw httpError(409,`Conflict. The container name "/${i}" is already in use by container "${n.id}". You have to remove (or rename) that container to be able to reuse that name.`);if(!e.Image)throw httpError(400,"Config cannot be empty in order to create a container");const r=this.requireImage(e.Image),o=parsePlatform(s);if(o&&r.architecture&&r.architecture!==o.arch)throw httpError(404,`No such image: ${e.Image} for linux/${o.arch} (the local one is linux/${r.architecture})`);if(require("./runtime/cdi").requested(e.HostConfig||{}).requests.length&&"darwin"===process.platform)throw httpError(400,"GPUs and CDI devices cannot reach containers on macOS: Apple's Virtualization framework has no device passthrough");const{HostConfig:a={},NetworkingConfig:h,...c}=e,u=require("./config").containerProxyEnv(void 0,"host.tnc.internal"),l=new Set([...c.Env||[],...r.config?.config?.Env||[]].map(t=>String(t).split("=")[0])),d=Object.entries(u).filter(([t])=>!l.has(t)).map(([t,e])=>`${t}=${e}`);d.length&&(c.Env=[...c.Env||[],...d]);const m=joinedContainer(a.NetworkMode);if(m&&(this.findContainer(m),Object.keys(a.PortBindings||{}).length))throw httpError(400,"conflicting options: port publishing and the container network mode");const f=m?[]:this._wantedNetworks(a.NetworkMode,h?.EndpointsConfig);if(0===processArgs(c,r.config?.config||{}).length)throw httpError(400,"No command specified");const g=this._containerUserns(a),p=crypto.randomBytes(32).toString("hex"),w=this.volumes.resolve(a,r.config?.config||{});this._volumesFrom(a.VolumesFrom,w);const y=this.containerDir(p);fs.mkdirSync(y,{recursive:!0});const v=this.store.insertContainer({id:p,name:i,image:r.id,state:"created",config:{Config:c,HostConfig:a,imageRef:e.Image,networks:f},runtime:{restartCount:0,volumes:w.volumes,anonymousVolumes:w.anonymous,ociMounts:w.ociMounts,apiMounts:w.apiMounts,...g?{userns:g}:{}}});return this.emitEvent("container","create",p,{...r.config?.config?.Labels||{},...c.Labels||{},image:e.Image,name:i}),{Id:v.id,Warnings:[]}}usernsRange(){if(void 0!==this._userns)return this._userns;const{remapSetting:t,resolveRemap:e}=require("./runtime/userns"),s=t();return this._userns=null,s&&"off"!==s?"local"!==this.host.kind?(this.log(`userns-remap is for dock on Linux as root; not used here (${this.host.kind})`),null):(this._userns=e(s),this._userns):null}async _shimSupports(t,e){if(this._shimModes=this._shimModes||new Map,!this._shimModes.has(t)){const{shim:s}=this.host.binaries(),i=await this.host.run([s,t,...e]).catch(()=>({code:2,stderr:"usage: tnc-shim --log"}));this._shimModes.set(t,!(2===i.code&&/usage: tnc-shim --log/.test(i.stderr||"")))}return this._shimModes.get(t)}_containerUserns(t={}){const e=t.UsernsMode||"";if(e&&"host"!==e)throw httpError(400,`--userns=${e} is not supported (use host, or set dock config --userns-remap)`);const s=this.usernsRange();if(!s||"host"===e)return null;if(t.Privileged)throw httpError(400,"privileged mode is incompatible with user namespaces; run it in the host user namespace: --privileged --userns=host");for(const[e,s]of[["NetworkMode","--network"],["PidMode","--pid"],["IpcMode","--ipc"],["UTSMode","--uts"]])if("host"===t[e])throw httpError(400,`${s}=host is incompatible with user namespaces; add --userns=host`);return{uid:s.uid,gid:s.gid,size:s.size}}_chownForUserns(t,e){const s=t.runtime.userns;if(s){for(const t of["hostname","hosts","resolv.conf"])try{fs.chownSync(path.join(e,t),s.uid,s.gid)}catch(t){}for(const e of t.runtime.volumes||[]){const t=this.store.getVolume(e);if(t)try{0===fs.statSync(t.mountpoint).uid&&0===fs.readdirSync(t.mountpoint).length&&fs.chownSync(t.mountpoint,s.uid,s.gid)}catch(t){}}}}_volumesFrom(t=[],e){const s=new Set(e.ociMounts.map(t=>t.destination));for(const i of t||[]){const[t,n]=String(i).split(":"),r=this.findContainer(t),o="ro"===n;for(const t of r.runtime.ociMounts||[]){if("bind"!==t.type||s.has(t.destination))continue;const i=t.options.filter(t=>"ro"!==t&&"rw"!==t);e.ociMounts.push({...t,options:[...i,o||t.options.includes("ro")&&"rw"!==n?"ro":"rw"]});const a=(r.runtime.apiMounts||[]).find(e=>e.Destination===t.destination);a&&e.apiMounts.push({...a,RW:!o&&a.RW,Mode:o?"ro":a.Mode}),s.add(t.destination)}for(const t of r.runtime.volumes||[])e.volumes.includes(t)||e.volumes.push(t)}}_wantedNetworks(t,e={}){this.host;const s=this.network.resolve(t),i=[{name:s.name,aliases:(n=s,e?.[n.name]?.Aliases||e?.[t]?.Aliases||[])}];var n;for(const t of Object.keys(e||{})){const n=this.network.resolve(t);if(!i.some(t=>t.name===n.name)){if(["host","null"].includes(s.driver)||["host","null"].includes(n.driver))throw httpError(400,"container cannot be connected to multiple networks with one of the networks in host or none mode");i.push({name:n.name,aliases:e[t].Aliases||[]})}}return i}_hostname(t){return t.config.Config.Hostname||t.id.slice(0,12)}_hostsContent(t){const e=["127.0.0.1\tlocalhost","::1\tlocalhost ip6-localhost ip6-loopback"],s=t.runtime.networks||{},i=Object.values(s).find(t=>t.ip);i&&e.push(`${i.ip}\t${this._hostname(t)}`),i?.ip6&&e.push(`${i.ip6}\t${this._hostname(t)}`);const n=new Set;for(const[i,r]of Object.entries(s)){let s;try{s=this.network.resolve(i)}catch(t){continue}if(s.dns&&r.ip)for(const s of this.store.listContainers()){if(s.id===t.id||"running"!==s.state)continue;const r=s.runtime.networks?.[i];if(!r?.ip||n.has(`${i}:${s.id}`))continue;n.add(`${i}:${s.id}`);const o=[...new Set([s.name,...r.aliases||[],this._hostname(s),s.id.slice(0,12)])];e.push(`${r.ip}\t${o.join(" ")}`),r.ip6&&e.push(`${r.ip6}\t${o.join(" ")}`)}}const r=this._hostGateway(s),o=(t.config.HostConfig.ExtraHosts||[]).map(t=>{const e=t.search(/[:=]/);return[t.slice(0,e),t.slice(e+1)]});r&&!o.some(([t])=>t===HOST_ALIAS)&&e.push(`${r}\t${HOST_ALIAS} host.tnc.internal`);for(const[t,s]of o){const i="host-gateway"===s?r:s;i&&e.push(`${i}\t${t}`)}return e.join("\n")+"\n"}_refreshHosts(t){const e=new Set(t);for(const t of this.store.listContainers())if("running"===t.state&&Object.keys(t.runtime.networks||{}).some(t=>e.has(t)))try{fs.writeFileSync(path.join(this.containerDir(t.id),"hosts"),this._hostsContent(t))}catch(t){}this._refreshDns().catch(t=>this.log(`dns: ${t.message}`))}_dnsRecords(t=null){const e=[],s=this.store.listContainers().filter(e=>"running"===e.state&&e.id!==t?.c.id).map(t=>[t,t.runtime.networks||{}]);t&&s.push([t.c,t.endpoints]);for(const[t,i]of s){const s=t.id.slice(0,12);let n=!1;for(const[r,o]of Object.entries(i)){let i;try{i=this.network.resolve(r)}catch(t){continue}if(!i.dns||!o.ip)continue;const a=i.id.slice(0,12);n||(e.push(`s /var/run/netns/${this.network.names(t.id,0).ns} ${s}`),n=!0),e.push(`n ${s} ${a}`);for(const i of new Set([t.name,...o.aliases||[],this._hostname(t),s]))/^[A-Za-z0-9_][A-Za-z0-9_.-]*$/.test(i)&&(e.push(`a ${a} ${i} ${o.ip}`),o.ip6&&e.push(`a ${a} ${i} ${o.ip6}`))}}return this._overlayNames&&e.push(...this._overlayNames.dnsLines()),e.join("\n")+"\n"}async _refreshDns(t=null){if("0"===process.env.TNC_DOCK_DNS||!this._host||!1===this._dnsSupported)return;"local"===this.host.kind&&(this._overlayNames||(this._overlayNames=new(require("./runtime/overlaydns").OverlayNames)(this)),this._overlayNames.sync());const e=this._dnsRecords(t),s=path.join(this.paths.home,"dns","records");fs.mkdirSync(path.dirname(s),{recursive:!0});const i=`${s}.${process.pid}.tmp`;if(fs.writeFileSync(i,e),fs.renameSync(i,s),this._dns&&!this._dns.dead)return void this._dns.proc.kill("SIGHUP");if(!/^s /m.test(e))return;const n=((await this._resolvConf()).match(/^nameserver\s+\S+/gm)||[]).map(t=>t.split(/\s+/)[1]).join(",")||"1.1.1.1",r=this.host.spawn([this.host.binaries().shim,"dns",this.host.guestPath(s),n]),o={proc:r,dead:!1};r.stdout.resume(),r.stderr.on("data",t=>this.log(`dns: ${String(t).trim()}`)),r.exited.then(t=>{o.dead=!0,this._dns===o&&(this._dns=null,this.log(`dns resolver exited (${t})`))}),this._dns=o}async _usesEmbeddedDns(t,e){if("0"===process.env.TNC_DOCK_DNS||t.Dns?.length)return!1;const[s,i]=Object.entries(e||{})[0]||[];if(!s||!i?.ip)return!1;try{if(!this.network.resolve(s).dns)return!1}catch(t){return!1}if(void 0===this._dnsSupported){const t=await this.host.run([this.host.binaries().shim,"dns"]).catch(()=>({stderr:""}));this._dnsSupported=/tnc-shim dns/.test(t.stderr||""),this._dnsSupported||this.log("dns: this runtime's tnc-shim has no resolver; user networks use /etc/hosts only (update: thinknsh dock setup)")}return this._dnsSupported}async _resolvConf(){if(this._resolv)return this._resolv;if(this.host.resolvConf)return this._resolv=this.host.resolvConf();let t=await this.host.run(["cat","/run/systemd/resolve/resolv.conf"]);0!==t.code&&(t=await this.host.run(["cat","/etc/resolv.conf"]));const e=(t.stdout.match(/^nameserver\s+(\S+)/gm)||[]).map(t=>t.split(/\s+/)[1]).filter(t=>!t.startsWith("127."));return this._resolv=(e.length?e:["1.1.1.1","8.8.8.8"]).map(t=>`nameserver ${t}`).join("\n")+"\n",this._resolv}async _containerResolv(t,e,s=!1){if(s){const e=await this._resolvConf();return["nameserver 127.0.0.11",...t.DnsSearch?.length?[`search ${t.DnsSearch.join(" ")}`]:e.split("\n").filter(t=>/^search\s/.test(t)),`options ${(t.DnsOptions?.length?t.DnsOptions:["ndots:0"]).join(" ")}`].join("\n")+"\n"}const i=e&&!this.host.resolvConf?(await this.host.run(["cat","/etc/resolv.conf"])).stdout:await this._resolvConf();return[...t.Dns?.length?t.Dns.map(t=>`nameserver ${t}`):i.split("\n").filter(t=>/^nameserver\s/.test(t)),...t.DnsSearch?.length?[`search ${t.DnsSearch.join(" ")}`]:i.split("\n").filter(t=>/^search\s/.test(t)),...t.DnsOptions?.length?[`options ${t.DnsOptions.join(" ")}`]:i.split("\n").filter(t=>/^options\s/.test(t))].join("\n")+"\n"}async _devices(t=[]){const e=[];for(const s of t||[]){const t=await this.host.run(["stat","-L","-c","%t %T %F %u %g %a",s.PathOnHost]);if(0!==t.code)throw httpError(400,`error gathering device information while adding custom device "${s.PathOnHost}": no such file or directory`);const[i,n,...r]=t.stdout.trim().split(" "),o=r.slice(0,-3).join(" "),[a,h,c]=r.slice(-3);if(!/character|block/.test(o))throw httpError(400,`${s.PathOnHost} is not a device`);e.push({path:s.PathInContainer||s.PathOnHost,type:/block/.test(o)?"b":"c",major:parseInt(i,16),minor:parseInt(n,16),fileMode:parseInt(c,8)|(/block/.test(o)?24576:8192),uid:Number(a),gid:Number(h),CgroupPermissions:s.CgroupPermissions||"rwm"})}return e}async start(t){const e=this.findContainer(t).id,s=this.starting.get(e);if(s)return await s.catch(()=>{}),"running"===this.store.getContainer(e)?.state?"notModified":this.start(e);const i=this._start(e);this.starting.set(e,i);try{return await i}finally{this.starting.delete(e)}}async _start(t){let e=this.findContainer(t);if("running"===e.state)return"notModified";if("removing"===e.state)throw httpError(409,`container ${e.id} is marked for removal and cannot be started`);await this.ready();for(let t=0;t<600&&!this.fs.idle(e.id);t++)await sleep(50);const s=this.store.getImage(e.image);if(!s)throw httpError(404,`No such image: ${e.config.imageRef}`);s.architecture&&s.architecture!==this.linuxArch()&&await this.emulation.ensure(s.architecture);const i=s.config?.config||{},{Config:n,HostConfig:r}=e.config,o=e.id,a=this.containerDir(o),h=t=>this.host.guestPath(t),c=n.WorkingDir||i.WorkingDir||"/",u=n.Hostname||o.slice(0,12);let l=null,d={};try{if(this.volumes.linuxSide&&e.runtime.volumes?.length&&await this.host.sh('for d; do mkdir -p "$d"; done',e.runtime.volumes.map(t=>this.volumes.linuxDir(t))),e.runtime.userns&&!await this._shimSupports("idshift",["/proc/self/no-such-dir","0","0","1","to"]))throw httpError(501,"userns-remap needs dock runtime 0.2.3 or newer: run thinknsh dock setup (or start this container with --userns host)");const t=await this.rootfs.prepare(o,s,c,{userns:e.runtime.userns}),m=n.User||i.User||"",f=m&&m.split(":").some(t=>!/^\d+$/.test(t))?resolveUser(m,await this.rootfs.readFile(o,"/etc/passwd"),await this.rootfs.readFile(o,"/etc/group")):resolveUser(m);if(r.GroupAdd?.length){const t=r.GroupAdd.some(t=>!/^\d+$/.test(String(t)));f.additionalGids=additionalGids(r.GroupAdd,t?await this.rootfs.readFile(o,"/etc/group"):"")}const g=require("./runtime/cdi"),{requests:p,devices:w}=g.requested(r),y=await this._devices(w);if(r.Privileged){const t=new Set(y.map(t=>t.path));for(const e of await this._hostDevices())t.has(e.path)||y.push(e)}let v=null;if(p.length){const t=await this.host.run(["/bin/sh","-c",g.LIST_SCRIPT]);v=g.resolve(p,g.parseSpecs(t.stdout));for(const t of v.deviceNodes)y.push(...await this._devices([{PathOnHost:t.hostPath||t.path,PathInContainer:t.path,CgroupPermissions:t.permissions||"rwm"}]))}const S=joinedContainer(r.NetworkMode);let C=null;if(S){if(C=this.findContainer(S),"running"!==C.state||!C.runtime.network)throw httpError(409,`cannot join the network of ${C.name}: it is not running`);l={nsPath:C.runtime.network.nsPath,endpoints:{}}}else l=await this.network.attachAll(o,e.config.networks||this._wantedNetworks(r.NetworkMode));const _=Object.values(l.endpoints)[0]||{};if(fs.writeFileSync(path.join(a,"hostname"),u+"\n"),C)for(const t of["hosts","resolv.conf"])fs.copyFileSync(path.join(this.containerDir(C.id),t),path.join(a,t));else{fs.writeFileSync(path.join(a,"hosts"),this._hostsContent({...e,runtime:{...e.runtime,networks:l.endpoints}}));const t=await this._usesEmbeddedDns(r,l.endpoints);fs.writeFileSync(path.join(a,"resolv.conf"),await this._containerResolv(r,null===l.nsPath,t)),t&&await this._refreshDns({c:e,endpoints:l.endpoints}).catch(t=>this.log(`dns: ${t.message}`))}this._chownForUserns(e,a);const E=mergeEnv(i.Env,[`HOSTNAME=${u}`,...v?.env||[],...n.Env||[]]),{shim:b}=this.host.binaries(),$=processArgs(n,i),k=buildSpec({id:o,rootfs:t,cwd:c,user:f,hostname:u,nsPath:l.nsPath,hostConfig:r,terminal:!!n.Tty,devices:y,cgroupParent:this.host.cgroupParent?.(),userns:e.runtime.userns,args:r.Init?["/sbin/tnc-init","init","--",...$]:$,env:E,mounts:[...r.Init?[{destination:"/sbin/tnc-init",type:"bind",source:b,options:["rbind","ro"]}]:[],{destination:"/etc/hostname",type:"bind",source:h(path.join(a,"hostname")),options:["rbind","rw"]},{destination:"/etc/hosts",type:"bind",source:h(path.join(a,"hosts")),options:["rbind","rw"]},{destination:"/etc/resolv.conf",type:"bind",source:h(path.join(a,"resolv.conf")),options:["rbind","rw"]},...e.runtime.ociMounts||[],...v?g.ociMounts(v):[]]});v&&Object.keys(v.hooks).length&&(k.hooks=v.hooks),fs.writeFileSync(path.join(a,"config.json"),JSON.stringify(k,null,2)),fs.rmSync(this.exitFile(o),{force:!0}),_.ip&&(d=await this.ports.start(o,r.PortBindings,_.ip));const{shim:I,crun:x}=this.host.binaries(),A=fs.existsSync(this.logFile(o))?fs.statSync(this.logFile(o)).size:0,D=["--log",h(this.logFile(o)),"--exit",h(this.exitFile(o)),"--ctl",this.rootfs.ctlFifo(o),...logRotationArgs(r.LogConfig),...e.runtime.userns?["--stdio-owner",`${e.runtime.userns.uid}:${e.runtime.userns.gid}`]:[]],P=[x,`--cgroup-manager=${this.host.cgroupManager?.()||"cgroupfs"}`,"run","--bundle",h(a)];n.OpenStdin&&D.push("--stdin",this.rootfs.stdinFifo(o)),n.Tty&&(D.push("--console",this.rootfs.consoleSock(o)),P.push("--console-socket",this.rootfs.consoleSock(o))),await this.host.run([x,"delete","-f",o]),await this.host.check([I,...D,"--",...P,o]);const R=await this._waitForStart(o,A);return e=this.store.updateContainer(o,{state:"running",runtime:{...e.runtime,pid:R,health:null,networks:l.endpoints,network:{nsPath:l.nsPath},ip:_.ip||null,ports:d,startedAt:Date.now(),finishedAt:null,exitCode:0,error:"",manualStop:!1,paused:!1,cgroupPath:k.linux.cgroupsPath}}),this._refreshHosts(Object.keys(l.endpoints)),this.fsevents.track(o,e.runtime.apiMounts),this.emitEvent("container","start",o,this._attrs(e)),this._ensureReconciler(),this._startHealth(e),"started"}catch(t){this.ports&&this.ports.stop(o),l&&await this.network.detachAll(o,l.endpoints),await this.rootfs.unmount(o).catch(()=>{});const s=t.exitCode??128;throw this.store.updateContainer(o,{state:"created"===e.state?"created":"exited",runtime:{...e.runtime,error:t.message,exitCode:s,finishedAt:Date.now()}}),t.statusCode?t:httpError(500,t.message)}}async _waitForStart(t,e){const{crun:s}=this.host.binaries();for(let i=0;i<40;i++){if(fs.existsSync(this.exitFile(t))){const s=parseInt(fs.readFileSync(this.exitFile(t),"utf8"),10),i=this._crunErrorSince(t,e);if(i){fs.rmSync(this.exitFile(t),{force:!0});const e=httpError(400,`failed to create task for container: OCI runtime create failed: ${i}`);throw e.exitCode=/not found|No such file/.test(i)?127:/ermission denied/.test(i)?126:s||127,e}return 0}const i=await this.host.run([s,"state",t]);if(0===i.code)try{const t=JSON.parse(i.stdout);if("running"===t.status)return t.pid||0}catch(t){}await sleep(50)}return 0}_crunErrorSince(t,e){try{const s=fs.openSync(this.logFile(t),"r"),i=fs.fstatSync(s).size,n=Buffer.alloc(Math.max(0,i-e));fs.readSync(s,n,0,n.length,e),fs.closeSync(s);for(const t of n.toString("utf8").split("\n")){if(!t)continue;const e=JSON.parse(t);if("stderr"===e.stream&&CRUN_CREATE_ERROR.some(t=>t.test(e.log)))return e.log.replace(/^crun: /,"").trim()}}catch(t){}return null}_waitFor(t,e){return new Promise(s=>{this.waiters.has(t)||this.waiters.set(t,[]),this.waiters.get(t).push({condition:e,resolve:s})})}_resolveWaiters(t,e,s){const i=this.waiters.get(t)||[],n=[];for(const t of i)"removed"===e||"removed"!==t.condition?t.resolve({StatusCode:s,Error:null}):n.push(t);n.length?this.waiters.set(t,n):this.waiters.delete(t)}wait(t,e="not-running"){const s=this.findContainer(t);if(!["not-running","next-exit","removed"].includes(e))throw httpError(400,`invalid condition: "${e}"`);return"not-running"===e&&"running"!==s.state?Promise.resolve({StatusCode:s.runtime.exitCode||0,Error:null}):this._waitFor(s.id,e)}async _signal(t,e){const{crun:s}=this.host.binaries();return 0===(await this.host.run([s,"kill",t.id,signalArg(e)])).code}async _hostDevices(){const t="vm"===this.host.kind||"wsl"===this.host.kind,e=(await this.host.run(["/bin/sh","-c","find /dev -xdev \\( -type c -o -type b \\) ! -path '/dev/pts/*' ! -path '/dev/shm/*' ! -path '/dev/mqueue/*' 2>/dev/null"])).stdout.split("\n").filter(Boolean).filter(t=>!["/dev/console","/dev/ptmx","/dev/tty"].includes(t)).filter(e=>!(t&&/^\/dev\/(vd|sd|nvme|xvd|hd|dm-|mapper\/)/.test(e))),s=[];for(const t of e)try{s.push(...await this._devices([{PathOnHost:t,PathInContainer:t,CgroupPermissions:"rwm"}]))}catch(t){}return s}async stop(t,e){const s=this.findContainer(t);if("running"!==s.state)return"notModified";await this.ready(),await this._thaw(s),this.store.updateContainer(s.id,{runtime:{...s.runtime,manualStop:!0}});const i=this.store.getImage(s.image)?.config?.config||{},n=s.config.Config.StopSignal||i.StopSignal||"SIGTERM",r=Number.isFinite(e)?e:s.config.Config.StopTimeout??10,o=this._waitFor(s.id,"next-exit");return await this._signal(s,n),await Promise.race([o,sleep(1e3*r).then(()=>null)])||(await this._signal(s,"SIGKILL"),await Promise.race([o,sleep(1e4)])),"stopped"}async kill(t,e="SIGKILL"){const s=this.findContainer(t);if("running"!==s.state)throw httpError(409,`Cannot kill container: ${t}: Container ${s.id} is not running`);if(await this.ready(),"KILL"!==String(e).replace(/^SIG/,"")&&9!==e&&"9"!==e||await this._thaw(s),this.store.updateContainer(s.id,{runtime:{...s.runtime,manualStop:!0}}),!await this._signal(s,e))throw httpError(500,`Cannot kill container ${t}: crun kill failed`)}async restart(t,e){await this.stop(t,e),await this.start(t)}async remove(t,{force:e=!1,volumes:s=!1}={}){let i=this.findContainer(t);if("running"===i.state&&!e)throw httpError(409,`You cannot remove a running container ${i.id}. Stop the container before attempting removal or force remove`);if("removing"===i.state)return;const n="running"===i.state;if(i=this.store.updateContainer(i.id,{state:"removing",runtime:{...i.runtime,manualStop:!0}}),n){await this.ready(),await this._thaw(i);const t=this._waitFor(i.id,"next-exit");await this._signal(i,"SIGKILL"),await Promise.race([t,sleep(1e4)]),i=this.store.getContainer(i.id)}await this._destroy(i,s)}async _destroy(t,e){if(this._host&&await this.host.isUp().catch(()=>!1)&&await this.rootfs.remove(t.id).catch(e=>this.log(`rootfs cleanup for ${t.id}: ${e.message}`)),fs.rmSync(this.containerDir(t.id),{recursive:!0,force:!0}),this.store.deleteContainer(t.id),e)for(const e of t.runtime.anonymousVolumes||[])try{this.volumes.remove(e)}catch(t){}this.emitEvent("container","destroy",t.id,this._attrs(t)),this._resolveWaiters(t.id,"removed",t.runtime.exitCode||0)}async pause(t){const e=this.findContainer(t);if("running"!==e.state)throw httpError(409,`Container ${e.id} is not running`);if(e.runtime.paused)throw httpError(409,`Container ${e.id} is already paused`);if(await this.ready(),"disabled"===this.host.cgroupManager?.())throw httpError(501,"pause needs cgroups, which rootless mode does not have; stop the container instead");const{crun:s}=this.host.binaries(),i=await this.host.run([s,"pause",e.id]);if(0!==i.code)throw httpError(500,`Cannot pause container ${e.id}: ${(i.stderr||i.stdout).trim()}`);const n=this.store.getContainer(e.id);this.store.updateContainer(e.id,{runtime:{...n.runtime,paused:!0}}),this.emitEvent("container","pause",e.id,this._attrs(e))}async unpause(t){const e=this.findContainer(t);if(!e.runtime.paused||"running"!==e.state)throw httpError(409,`Container ${e.id} is not paused`);await this.ready(),await this._thaw(e,!0),this.emitEvent("container","unpause",e.id,this._attrs(e))}async _thaw(t,e=!1){const s=this.store.getContainer(t.id);if(!s?.runtime.paused)return;const{crun:i}=this.host.binaries(),n=await this.host.run([i,"resume",t.id]);if(0!==n.code&&e)throw httpError(500,`Cannot unpause container ${t.id}: ${(n.stderr||n.stdout).trim()}`);this.store.updateContainer(t.id,{runtime:{...this.store.getContainer(t.id).runtime,paused:!1}})}rename(t,e){const s=this.findContainer(t);if(!e||!NAME_RE.test(e))throw httpError(400,`Invalid container name (${e}), only [a-zA-Z0-9][a-zA-Z0-9_.-] are allowed`);const i=e.replace(/^\//,"");if(i===s.name)throw httpError(409,"Renaming a container with the same name as its current name");const n=this.store.findContainer(i);if(n&&n.name===i)throw httpError(409,`Conflict. The container name "/${i}" is already in use by container "${n.id}". You have to remove (or rename) that container to be able to reuse that name.`);this.store.updateContainer(s.id,{name:i}),this._refreshHosts(Object.keys(s.runtime.networks||{})),this.emitEvent("container","rename",s.id,this._attrs({...s,name:i},{oldName:`/${s.name}`}))}_saveConfig(t,e){return this.store.db.prepare("UPDATE containers SET config = ? WHERE id = ?").run(JSON.stringify(e),t),this.store.getContainer(t)}async update(t,e={}){const s=this.findContainer(t),i=Object.fromEntries(["Memory","MemorySwap","MemoryReservation","NanoCpus","CpuShares","CpuQuota","CpuPeriod","CpusetCpus","CpusetMems","PidsLimit","BlkioWeight"].filter(t=>void 0!==e[t]&&null!==e[t]&&""!==e[t]&&0!==e[t]).map(t=>[t,e[t]])),n={...s.config.HostConfig,...i};if(e.RestartPolicy?.Name){if(n.AutoRemove&&"no"!==e.RestartPolicy.Name)throw httpError(400,"Restart policy cannot be updated because AutoRemove is enabled for the container");n.RestartPolicy={Name:e.RestartPolicy.Name,MaximumRetryCount:e.RestartPolicy.MaximumRetryCount||0}}if(n.Memory>0&&n.MemoryReservation>n.Memory)throw httpError(400,"Minimum memory limit can not be less than memory reservation limit, see usage");if("running"===s.state&&Object.keys(i).length){await this.ready();const{devices:t,...e}=buildSpec({id:s.id,rootfs:"/",args:["x"],env:[],cwd:"/",user:{uid:0,gid:0},hostname:"x",mounts:[],hostConfig:{...n,Ulimits:[],Sysctls:{}}}).linux.resources,i=path.join(this.containerDir(s.id),"update.json");fs.writeFileSync(i,JSON.stringify(e));const{crun:r}=this.host.binaries(),o=await this.host.run([r,"update","--resources",this.host.guestPath(i),s.id]);if(fs.rmSync(i,{force:!0}),0!==o.code)throw httpError(500,`Cannot update container ${s.id}: ${(o.stderr||o.stdout).trim()}`)}return this._saveConfig(s.id,{...s.config,HostConfig:n}),this.emitEvent("container","update",s.id,this._attrs(s)),{Warnings:[]}}async commit(t,{repo:e,tag:s,comment:i="",author:n="",pause:r=!0,changes:o=[],config:a={}}={}){const{applyChanges:h}=require("./build/changes"),{COMMIT:c}=require("./build/builder"),u=this.findContainer(t);await this.ready();const l=this.store.getImage(u.image);if(!l)throw httpError(404,`No such image: ${u.config.imageRef}`);const d="running"===u.state&&r&&!u.runtime.paused;let m;d&&await this.pause(u.id);try{m=await(async()=>{const t=this.rootfs.root,e=`${t}/containers/${u.id}/commit`,s=path.join(this.paths.blobs,`commit-${u.id.slice(0,12)}-${Date.now()}.tmp`),{shim:i}=this.host.binaries();await this.host.sh('set -e; rm -rf "$2"; mkdir -p "$1" "$2"; cp -a "$1/." "$2/"; for f in /etc/hosts /etc/hostname /etc/resolv.conf /sbin/tnc-init; do [ -f "$2$f" ] && [ ! -s "$2$f" ] && rm -f "$2$f"; done; true',[`${t}/containers/${u.id}/upper`,e]);const n=u.runtime.userns;n&&await this.host.sh('"$1" idshift "$2" "$3" "$4" "$5" from',[i,e,String(n.uid),String(n.gid),String(n.size)]);try{const t=(await this.host.sh(c,[e,this.host.guestPath(s),i,"vm"===this.host.kind?1:0])).stdout.trim().split("\n").pop();if("EMPTY"===t)return null;const[n,r,o]=t.split(" ");return fs.renameSync(s,path.join(this.paths.blobs,n)),{digest:`sha256:${n}`,diffId:`sha256:${r}`,size:Number(o)}}finally{await this.host.sh('rm -rf "$1"',[e]).catch(()=>{})}})()}finally{d&&await this.unpause(u.id).catch(()=>{})}const f=l.config||{},g=h({...f.config||{},...stripEmpty(a)},o),p=(new Date).toISOString(),w={...f,created:p,...n?{author:n}:{},...i?{comment:i}:{},config:g,rootfs:{type:"layers",diff_ids:[...f.rootfs?.diff_ids||[],...m?[m.diffId]:[]]},history:[...f.history||[],{created:p,created_by:`thinknsh commit ${u.name}`,...i?{comment:i}:{},...m?{}:{empty_layer:!0}}]},y=JSON.stringify(w),v=`sha256:${crypto.createHash("sha256").update(y).digest("hex")}`;fs.writeFileSync(path.join(this.paths.blobs,v.slice(7)),y);const S=[...l.layers||[],...m?[{digest:m.digest,size:m.size,mediaType:"application/vnd.oci.image.layer.v1.tar+gzip"}]:[]],C=e?[parseReference(`${e}:${s||"latest"}`).familiar]:[];this.store.saveImage({id:v,repoTags:C,repoDigests:[],digest:null,size:S.reduce((t,e)=>t+e.size,0),layers:S,config:w,architecture:w.architecture,os:"linux",createdAt:Date.now()}),this.emitEvent("container","commit",u.id,this._attrs(u,{comment:i}));for(const t of C)this.emitEvent("image","tag",v,{name:t});return{Id:v}}async importImage(t,{repo:e,tag:s,changes:i=[],message:n="",platform:r}={}){const{applyChanges:o}=require("./build/changes"),a=parsePlatform(r),h=await archive.importRootfs(t,{blobsDir:this.paths.blobs,arch:a?.arch||this.linuxArch(),config:o({Env:["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"]},i),comment:n}),c=e?[parseReference(s?`${e}:${s}`:e).familiar]:[],u=this.store.saveImage({...h,repoTags:c});return this.emitEvent("image","import",u.id,c.length?{name:c[0]}:{}),u}saveSelection(t){if(!t.length)throw httpError(400,"no images given to save");const e=new Map;for(const s of t){const t=this.requireImage(s);let i=[],n=null;try{n=parseReference(s).familiar}catch(t){}n&&t.repoTags.includes(n)?i=[n]:/^(sha256:)?[0-9a-f]{12,64}$/.test(s)||(i=t.repoTags.filter(t=>n&&t.startsWith(n.replace(/:latest$/,"")+":")));const r=e.get(t.id)||{image:t,tags:[]};r.tags=[...new Set([...r.tags,...i])],e.set(t.id,r)}return[...e.values()]}async saveImages(t,e){const s=this.saveSelection(t);await archive.saveImages(s,{blobsDir:this.paths.blobs,sink:e});for(const{image:t}of s)this.emitEvent("image","save",t.id,{})}async loadImages(t,e){const s=path.join(this.paths.home,"build",`load-${crypto.randomBytes(6).toString("hex")}`);fs.mkdirSync(s,{recursive:!0});try{await archive.extractTo(t,s);const i=archive.readArchive(s,{blobsDir:this.paths.blobs,arch:this.linuxArch()});if(!i.length)throw httpError(400,"the archive holds no images");for(const t of i){const s=this.store.saveImage(t);if(this.emitEvent("image","load",s.id,{}),t.repoTags.length)for(const s of t.repoTags)e({stream:`Loaded image: ${s}\n`});else e({stream:`Loaded image ID: ${s.id}\n`})}}finally{fs.rmSync(s,{recursive:!0,force:!0})}}imageHistory(t){return archive.imageHistory(this.requireImage(t),t=>t.repoTags)}async _onExit(t,e,s=""){if(!this.exiting.has(t.id)){this.exiting.add(t.id);try{this.ports&&this.ports.stop(t.id),this.fsevents.untrack(t.id),this._stopHealth(t.id),this._host&&(await this.network.detachAll(t.id,t.runtime.networks),await this.host.isUp().catch(()=>!1)&&await this.rootfs.unmount(t.id).catch(()=>{}));const i="removing"===t.state;if(t=this.store.updateContainer(t.id,{state:i?"removing":"exited",runtime:{...t.runtime,exitCode:e,error:s,finishedAt:Date.now(),pid:0,ip:null,ports:{},networks:{},paused:!1}}),this._refreshHosts(Object.keys(t.config.networks?Object.fromEntries(t.config.networks.map(t=>[t.name,1])):{})),this.emitEvent("container","die",t.id,this._attrs(t,{exitCode:String(e)})),this._resolveWaiters(t.id,"exit",e),i)return;if(t.config.HostConfig.AutoRemove)return void await this._destroy(t,!0);this._maybeRestart(t,e)}finally{this.exiting.delete(t.id)}}}_maybeRestart(t,e){const s=t.config.HostConfig.RestartPolicy||{},i=s.Name||"no";if("no"===i||t.runtime.manualStop)return;if("on-failure"===i){if(0===e)return;if(s.MaximumRetryCount>0&&t.runtime.restartCount>=s.MaximumRetryCount)return}const n=Math.min(100*2**(t.runtime.restartCount||0),6e4);this.store.updateContainer(t.id,{runtime:{...t.runtime,restartCount:(t.runtime.restartCount||0)+1}}),setTimeout(()=>{const e=this.store.getContainer(t.id);e&&"exited"===e.state&&!e.runtime.manualStop&&this.start(t.id).catch(e=>this.log(`restart of ${t.name} failed: ${e.message}`))},n).unref()}_ensureReconciler(){this.timer||(this.timer=setInterval(()=>this.reconcile().catch(t=>this.log(`reconcile: ${t.message}`)),300),this.timer.unref())}async reconcile(){const t=this.store.listContainers().filter(t=>"running"===t.state||"removing"===t.state&&t.runtime.pid);if(0===t.length)return clearInterval(this.timer),void(this.timer=null);for(const e of t)if(fs.existsSync(this.exitFile(e.id))){const t=parseInt(fs.readFileSync(this.exitFile(e.id),"utf8"),10);await this._onExit(e,Number.isInteger(t)?t:255)}if(Date.now()-this.lastLiveness<3e3)return;this.lastLiveness=Date.now();const e=await this.host.isUp().catch(()=>!1);let s=null;if(e){const t=await this.host.run([this.host.binaries().crun,"list","-q"]);0===t.code&&(s=new Set(t.stdout.split("\n").map(t=>t.trim()).filter(Boolean)))}for(const i of t){const t=this.store.getContainer(i.id);t&&"running"===t.state&&!fs.existsSync(this.exitFile(i.id))&&(!e||s&&!s.has(i.id)&&Date.now()-(t.runtime.startedAt||0)>2e3)&&await this._onExit(t,255,"container runtime lost (the Linux host or MicroVM restarted)")}}async recover(){const t=this.store.listContainers().filter(t=>"running"===t.state);if(0!==t.length){this.host;for(const e of t)if(e.runtime.ip&&Object.keys(e.config.HostConfig.PortBindings||{}).length)try{await this.ports.start(e.id,e.config.HostConfig.PortBindings,e.runtime.ip)}catch(t){this.log(`could not restore ports for ${e.name}: ${t.message}`)}for(const e of t)this._startHealth(e);for(const e of t)this.fsevents.track(e.id,e.runtime.apiMounts);this._refreshDns().catch(t=>this.log(`dns: ${t.message}`)),this.lastLiveness=0,this._ensureReconciler()}}touch(){this.lastActivity=Date.now()}async track(t){this.streams=(this.streams||0)+1;try{return await t()}finally{this.streams-=1,this.touch()}}startResourceManager(){if(!["darwin","win32"].includes(process.platform)||this.resourceTimer)return;const t=6e4*Number(process.env.TNC_DOCK_IDLE_MINUTES??5);this.lastActivity=Date.now(),this.resourceTimer=setInterval(()=>this._manageResources(t).catch(t=>this.log(`resources: ${t.message}`)),1e4),this.resourceTimer.unref()}async _manageResources(t){if(this._host&&["vm","wsl"].includes(this.host.kind)&&!this._managing){this._managing=!0;try{if(!await this.host.isUp())return;if(!(this.store.listContainers().some(t=>"running"===t.state)||this.streams>0||this.execs.size>0&&[...this.execs.values()].some(t=>t.Running))&&t>0&&Date.now()-this.lastActivity>t)return this.log("idle: stopping the MicroVM (no containers, no activity)"),this.emitEvent("daemon","vm-sleep","microvm",{}),void await this.host.stop()}finally{this._managing=!1}}}shutdown(){this._dns&&(this._dns.proc.kill("SIGKILL"),this._dns=null),this.resourceTimer&&clearInterval(this.resourceTimer),this.resourceTimer=null;for(const t of[...this.healthTimers.keys()])this._stopHealth(t);this.timer&&clearInterval(this.timer),this.timer=null,this.ports&&this.ports.stopAll(),this.fsevents.stopAll(),this.hostfwd.stop(),this._overlayNames?.stop()}_healthConfig(t){const e={...this.store.getImage(t.image)?.config?.config?.Healthcheck||{},...t.config.Config.Healthcheck||{}};if(!e.Test||!e.Test.length||"NONE"===e.Test[0])return null;const s=(t,e)=>t>0?t/1e6:e;return{argv:"CMD-SHELL"===e.Test[0]?["/bin/sh","-c",e.Test.slice(1).join(" ")]:"CMD"===e.Test[0]?e.Test.slice(1):e.Test,interval:s(e.Interval,3e4),timeout:s(e.Timeout,3e4),startPeriod:s(e.StartPeriod,0),startInterval:s(e.StartInterval,5e3),retries:e.Retries>0?e.Retries:3}}_startHealth(t){const e=this._healthConfig(t);if(!e)return;const s=Date.now(),i={Status:"starting",FailingStreak:0,Log:[]};this._saveHealth(t.id,i);const n=async()=>{const r=this.store.getContainer(t.id);if(!r||"running"!==r.state)return;if(r.runtime.paused)return void this.healthTimers.set(t.id,setTimeout(n,e.interval));const o=Date.now(),{crun:a}=this.host.binaries(),h=this.host.spawn([a,"exec",t.id,...e.argv]);let c="";h.stdout.on("data",t=>{c.length<4096&&(c+=t)}),h.stderr.on("data",t=>{c.length<4096&&(c+=t)}),h.end();const u=setTimeout(()=>h.kill("SIGKILL"),e.timeout);let l=await h.exited;clearTimeout(u),Date.now()-o>=e.timeout&&(l=-1,c=`Health check exceeded timeout (${e.timeout}ms)`);const d=this.store.getContainer(t.id)?.runtime.health||i,m=Date.now()-s<e.startPeriod,f=d.Status;if(0===l?(d.Status="healthy",d.FailingStreak=0):m&&"healthy"!==d.Status||(d.FailingStreak+=1,d.FailingStreak>=e.retries&&(d.Status="unhealthy")),d.Log=[...d.Log,{Start:iso(o),End:iso(Date.now()),ExitCode:l,Output:c}].slice(-5),!this._saveHealth(t.id,d))return;d.Status!==f&&this.emitEvent("container",`health_status: ${d.Status}`,t.id,this._attrs(t));const g=Date.now()-s<e.startPeriod?e.startInterval:e.interval;this.healthTimers.set(t.id,setTimeout(n,g))};this.healthTimers.set(t.id,setTimeout(n,e.startPeriod>0?e.startInterval:e.interval))}_saveHealth(t,e){const s=this.store.getContainer(t);return!(!s||"running"!==s.state||(this.store.updateContainer(t,{runtime:{...s.runtime,health:e}}),0))}_stopHealth(t){clearTimeout(this.healthTimers.get(t)),this.healthTimers.delete(t)}createExec(t,e={}){const s=this.findContainer(t);if("running"!==s.state)throw httpError(409,`Container ${s.id} is not running`);if(s.runtime.paused)throw httpError(409,`Container ${s.id} is paused, unpause the container before exec`);if(!Array.isArray(e.Cmd)||0===e.Cmd.length)throw httpError(400,"No exec command specified");const i=crypto.randomBytes(32).toString("hex");return this.log(`exec create ${s.name}: ${JSON.stringify(redactExecBody(e))}`),this.execs.set(i,{ID:i,containerId:s.id,config:e,Running:!1,ExitCode:null,Pid:0}),this.emitEvent("container",`exec_create: ${e.Cmd.join(" ")}`,s.id,this._attrs(s,{execID:i})),{Id:i}}getExec(t){const e=this.execs.get(t);if(!e)throw httpError(404,`No such exec instance: ${t}`);return e}async startExec(t){const e=this.getExec(t);if(e.Running||null!==e.ExitCode)throw httpError(409,`exec ${t} has already run`);const s=this.findContainer(e.containerId);if("running"!==s.state)throw httpError(409,`Container ${s.id} is not running`);await this.ready();const{crun:i,shim:n}=this.host.binaries();let r=[i,"exec"];if(e.config.Tty){const s=`${this.rootfs.root}/exec-${t.slice(0,16)}.sock`;e.ctl=`${this.rootfs.root}/exec-${t.slice(0,16)}.ctl`,r=[n,"relay","--console",s,"--ctl",e.ctl,"--",i,"exec","--tty","--console-socket",s]}e.config.WorkingDir&&r.push("--cwd",e.config.WorkingDir);const o=this.store.getImage(s.image)?.config?.config?.Env||[],a=this._hostname(s);for(const t of mergeEnv(o,[`HOSTNAME=${a}`,...s.config.Config.Env||[],...e.config.Env||[]]))r.push("--env",t);if(e.config.User){const t=e.config.User.split(":").some(t=>!/^\d+$/.test(t))?resolveUser(e.config.User,await this.rootfs.readFile(s.id,"/etc/passwd"),await this.rootfs.readFile(s.id,"/etc/group")):resolveUser(e.config.User);r.push("--user",`${t.uid}:${t.gid}`)}r.push(s.id,...e.config.Cmd),e.Running=!0,this.emitEvent("container",`exec_start: ${e.config.Cmd.join(" ")}`,s.id,this._attrs(s,{execID:t}));const h=this.host.spawn(r);return h.exited.then(i=>{e.Running=!1,e.ExitCode=i,this.emitEvent("container","exec_die",s.id,this._attrs(s,{execID:t,exitCode:String(i)}))}),h}execInspect(t){const e=this.getExec(t);return{ID:e.ID,Running:e.Running,ExitCode:e.ExitCode,ProcessConfig:{tty:!!e.config.Tty,entrypoint:e.config.Cmd[0],arguments:e.config.Cmd.slice(1),privileged:!1,user:e.config.User||""},OpenStdin:!!e.config.AttachStdin,OpenStderr:!!e.config.AttachStderr,OpenStdout:!!e.config.AttachStdout,CanRemove:!1,ContainerID:e.containerId,DetachKeys:"",Pid:e.Pid}}async probe(t,e){const s=this.findContainer(t);if("running"!==s.state)return!1;const i=Object.values(s.runtime.networks||{}).find(t=>t.ip),n=i?.ip||(s.runtime.networks?.host?"127.0.0.1":null);return!!(n&&e>0)&&("function"==typeof this.host.probe?this.host.probe(n,e):new Promise(t=>{const s=this.host.connect(n,e),i=e=>{clearTimeout(r),s.destroy(),t(e)},r=setTimeout(()=>i(!1),1500);s.once("ready",()=>i(!0)),s.once("connect",()=>i(!0)),s.once("error",()=>i(!1))}))}async _ctl(t,e){await this.host.run(["timeout","2","sh","-c",'printf "%s\\n" "$1" > "$2"',"dock",e,t]).catch(()=>{})}async resize(t,e,s){const i=this.findContainer(t);if("running"!==i.state)throw httpError(409,`Container ${i.id} is not running`);i.config.Config.Tty&&e>0&&s>0&&await this._ctl(this.rootfs.ctlFifo(i.id),`resize ${e} ${s}`)}async resizeExec(t,e,s){const i=this.getExec(t);i.ctl&&i.Running&&e>0&&s>0&&await this._ctl(i.ctl,`resize ${e} ${s}`)}openStdin(t){const e=this.host.spawn(["sh","-c",'exec cat > "$1"',"dock",this.rootfs.stdinFifo(t.id)]);return e.stdout.resume(),e.stderr.resume(),{write:t=>e.write(t),end:async()=>{e.end(),await e.exited,t.config.Config.StdinOnce&&await this._ctl(this.rootfs.ctlFifo(t.id),"eof")}}}_status(t){const e=t.runtime;if("running"===t.state){if(e.paused)return`Up ${humanDuration(Date.now()-e.startedAt)} (Paused)`;const t=e.health?` (${"starting"===e.health.Status?"health: starting":e.health.Status})`:"";return`Up ${humanDuration(Date.now()-e.startedAt)}${t}`}return"created"===t.state?"Created":"removing"===t.state?"Removal In Progress":`Exited (${e.exitCode}) ${humanDuration(Date.now()-(e.finishedAt||Date.now()))} ago`}_portsList(t){const e=[],s=t.runtime.ports||{},i=this.store.getImage(t.image)?.config?.config?.ExposedPorts||{};for(const n of Object.keys({...i,...t.config.Config.ExposedPorts||{},...s})){const[t,i="tcp"]=n.split("/"),r=s[n];if(r?.length)for(const s of r)e.push({IP:s.HostIp,PrivatePort:Number(t),PublicPort:Number(s.HostPort),Type:i});else e.push({PrivatePort:Number(t),Type:i})}return e}_networks(t){const e={};for(const s of t.config.networks||[{name:"bridge",aliases:[]}]){const i=t.runtime.networks?.[s.name]||{};let n=null;try{n=this.network.resolve(s.name)}catch(t){}e[s.name]={IPAMConfig:null,Links:null,Aliases:s.aliases?.length?s.aliases:null,NetworkID:n?.id||s.name,EndpointID:i.ip?`${t.id.slice(0,32)}${(n?.id||"").slice(0,32)}`:"",Gateway:i.gateway||"",IPAddress:i.ip||"",IPPrefixLen:i.ip?i.prefixLen:0,IPv6Gateway:i.gateway6||"",GlobalIPv6Address:i.ip6||"",GlobalIPv6PrefixLen:i.ip6?64:0,MacAddress:i.mac||"",DNSNames:i.ip&&n?.dns?[...new Set([t.name,...s.aliases||[],t.id.slice(0,12)])]:null}}return e}async connectNetwork(t,e,s={}){this.host;const i=this.network.resolve(t);let n=this.findContainer(e);const r=n.config.networks||[];if(r.some(t=>t.name===i.name))throw httpError(403,`endpoint with name ${n.name} already exists in network ${i.name}`);if(["host","null"].includes(i.driver)||r.some(t=>["host","none"].includes(t.name)))throw httpError(400,"container cannot be connected to multiple networks with one of the networks in host or none mode");const o={name:i.name,aliases:s.Aliases||[]},a=[...r,o];if("running"===n.state){await this.ready();const t=Object.values(n.runtime.networks||{}).map(t=>t.idx??-1),e=Math.max(-1,...t)+1,s=await this.network.attachOne(n.id,i,e,o.aliases);n=this.store.updateContainer(n.id,{runtime:{...n.runtime,networks:{...n.runtime.networks,[i.name]:s}}})}this.store.db.prepare("UPDATE containers SET config = ? WHERE id = ?").run(JSON.stringify({...n.config,networks:a}),n.id),this.emitEvent("network","connect",i.id,{container:n.id,name:i.name,type:i.driver}),this._refreshHosts([i.name])}async disconnectNetwork(t,e){this.host;const s=this.network.resolve(t);let i=this.findContainer(e);const n=(i.config.networks||[]).filter(t=>t.name!==s.name);if(n.length===(i.config.networks||[]).length)throw httpError(404,`container ${i.id} is not connected to network ${s.name}`);const r=i.runtime.networks?.[s.name];if("running"===i.state&&r){await this.network.detachOne(i.id,r);const t={...i.runtime.networks};delete t[s.name],i=this.store.updateContainer(i.id,{runtime:{...i.runtime,networks:t}})}if(this.store.db.prepare("UPDATE containers SET config = ? WHERE id = ?").run(JSON.stringify({...i.config,networks:n}),i.id),this.emitEvent("network","disconnect",s.id,{container:i.id,name:s.name,type:s.driver}),this._refreshHosts([s.name]),"running"===i.state)try{fs.writeFileSync(path.join(this.containerDir(i.id),"hosts"),this._hostsContent(i))}catch(t){}}async removeNetwork(t){this.host;const e=this.network.resolve(t);if(this.store.listContainers().filter(t=>(t.config.networks||[]).some(t=>t.name===e.name)).length)throw httpError(403,`error while removing network: network ${e.name} id ${e.id} has active endpoints`);await this.network.remove(e),this.emitEvent("network","destroy",e.id,{name:e.name,type:e.driver})}networkInspect(t){const e={};for(const s of this.store.listContainers()){const i=s.runtime.networks?.[t.name];"running"===s.state&&i&&(e[s.id]={Name:s.name,EndpointID:s.id,MacAddress:i.mac||"",IPv4Address:i.ip?`${i.ip}/${i.prefixLen}`:"",IPv6Address:i.ip6?`${i.ip6}/64`:""})}return{Name:t.name,Id:t.id,Created:t.createdAt?iso(t.createdAt):"0001-01-01T00:00:00Z",Scope:"local",Driver:t.driver,EnableIPv6:!!t.subnet6,IPAM:{Driver:"default",Options:null,Config:[...t.subnet?[{Subnet:t.subnet,Gateway:t.gateway}]:[],...t.subnet6?[{Subnet:t.subnet6,Gateway:t.gateway6}]:[]]},Internal:!!t.internal,Attachable:"overlay"===t.driver,Ingress:!1,ConfigFrom:{Network:""},ConfigOnly:!1,Containers:e,Options:t.options||{},Labels:t.labels||{}}}containerSummary(t){const e=processArgs(t.config.Config,this.store.getImage(t.image)?.config?.config||{});return{Id:t.id,Names:[`/${t.name}`],Image:t.config.imageRef,ImageID:t.image,Command:e.join(" "),Created:Math.floor(t.createdAt/1e3),Ports:this._portsList(t),Labels:t.config.Config.Labels||{},State:"removing"===t.state?"removing":t.runtime.paused&&"running"===t.state?"paused":t.state,Status:this._status(t),HostConfig:{NetworkMode:t.config.HostConfig.NetworkMode||"bridge"},NetworkSettings:{Networks:this._networks(t)},Mounts:t.runtime.apiMounts||[]}}listContainers({all:t=!1,filters:e={}}={}){let s=this.store.listContainers();t||e.status||(s=s.filter(t=>"running"===t.state));const i=(t,i)=>{e[t]?.length&&(s=s.filter(s=>e[t].some(t=>i(s,t))))};return i("status",(t,e)=>"paused"===e?"running"===t.state&&!!t.runtime.paused:"running"===e?"running"===t.state&&!t.runtime.paused:t.state===e),i("name",(t,e)=>t.name.includes(e.replace(/^\//,""))),i("id",(t,e)=>t.id.startsWith(e)),e.label?.length&&(s=s.filter(t=>e.label.every(e=>labelMatch(t.config.Config.Labels,e)))),i("ancestor",(t,e)=>{const s=this.findImage(e);return s&&s.id===t.image}),s.map(t=>this.containerSummary(t))}inspect(t){const e=this.findContainer(t),s=e.runtime,i=this.store.getImage(e.image)?.config?.config||{},n=processArgs(e.config.Config,i),r="running"===e.state,o=Object.values(s.networks||{})[0]||{};return{Id:e.id,Created:iso(e.createdAt),Path:n[0]||"",Args:n.slice(1),State:{Status:"removing"===e.state?"removing":r&&s.paused?"paused":e.state,Running:r,Paused:!(!r||!s.paused),Restarting:!1,OOMKilled:!1,Dead:!1,Pid:r&&s.pid||0,ExitCode:s.exitCode||0,Error:s.error||"",StartedAt:iso(s.startedAt),...r&&s.health?{Health:s.health}:{},FinishedAt:iso(s.finishedAt)},Image:e.image,ResolvConfPath:path.join(this.containerDir(e.id),"resolv.conf"),HostnamePath:path.join(this.containerDir(e.id),"hostname"),HostsPath:path.join(this.containerDir(e.id),"hosts"),LogPath:this.logFile(e.id),Name:`/${e.name}`,RestartCount:s.restartCount||0,Driver:"overlayfs",Platform:"linux",MountLabel:"",ProcessLabel:"",AppArmorProfile:"",HostConfig:{NetworkMode:"default",RestartPolicy:{Name:"no",MaximumRetryCount:0},...e.config.HostConfig},GraphDriver:{Name:"overlayfs",Data:null},Mounts:s.apiMounts||[],Config:{Hostname:e.config.Config.Hostname||e.id.slice(0,12),Domainname:"",User:e.config.Config.User||i.User||"",AttachStdin:!!e.config.Config.AttachStdin,AttachStdout:!!e.config.Config.AttachStdout,AttachStderr:!!e.config.Config.AttachStderr,ExposedPorts:{...i.ExposedPorts||{},...e.config.Config.ExposedPorts||{}},Tty:!!e.config.Config.Tty,OpenStdin:!!e.config.Config.OpenStdin,StdinOnce:!!e.config.Config.StdinOnce,Env:mergeEnv(i.Env,e.config.Config.Env||[]),Cmd:e.config.Config.Cmd||i.Cmd||null,Image:e.config.imageRef,Volumes:i.Volumes||null,WorkingDir:e.config.Config.WorkingDir||i.WorkingDir||"",Entrypoint:e.config.Config.Entrypoint||i.Entrypoint||null,Labels:{...i.Labels||{},...e.config.Config.Labels||{}},StopSignal:e.config.Config.StopSignal||i.StopSignal,...i.Healthcheck||e.config.Config.Healthcheck?{Healthcheck:{...i.Healthcheck||{},...e.config.Config.Healthcheck||{}}}:{}},NetworkSettings:{Bridge:"",SandboxID:"",HairpinMode:!1,SandboxKey:s.network?.nsPath||"",Ports:s.ports||{},Gateway:o.gateway||"",IPAddress:o.ip||"",IPPrefixLen:o.ip?o.prefixLen:0,IPv6Gateway:o.gateway6||"",GlobalIPv6Address:o.ip6||"",GlobalIPv6PrefixLen:o.ip6?64:0,MacAddress:o.mac||"",Networks:this._networks(e)}}}}const COMPRESSED=[[31,139],[66,90,104],[253,55,122,88,90],[40,181,47,253]];function completeTar(t){const e=fs.openSync(t,"r+");try{const t=Buffer.alloc(6);if(fs.readSync(e,t,0,t.length,0),COMPRESSED.some(e=>e.every((e,s)=>t[s]===e)))return;const s=fs.fstatSync(e).size;fs.writeSync(e,Buffer.alloc((512-s%512)%512+1024),0,void 0,s)}finally{fs.closeSync(e)}}function joinedContainer(t){return"string"==typeof t&&t.startsWith("container:")&&t.length>10?t.slice(10):null}function sizeBytes(t){const e=/^(\d+(?:\.\d+)?)\s*([kmg]?)b?$/i.exec(String(t||"").trim());return e?Math.round(parseFloat(e[1])*{"":1,k:1024,m:1048576,g:1024**3}[e[2].toLowerCase()]):0}function logRotationArgs(t={}){const e=t?.Type||"json-file";if("json-file"!==e&&"local"!==e)throw httpError(400,`log driver "${e}" is not supported; use json-file`);const s=t?.Config||{},i=sizeBytes(s["max-size"]||process.env.TNC_DOCK_LOG_MAX_SIZE||"20m"),n=parseInt(s["max-file"]||process.env.TNC_DOCK_LOG_MAX_FILE||"5",10);return i>0?["--log-max-bytes",String(i),"--log-max-files",String(Math.max(1,n))]:[]}function stripEmpty(t={}){return Object.fromEntries(Object.entries(t||{}).filter(([,t])=>!(null==t||""===t||Array.isArray(t)&&!t.length||"object"==typeof t&&!Array.isArray(t)&&!Object.keys(t).length)))}function labelMatch(t={},e){const s=e.indexOf("=");return-1===s?Object.prototype.hasOwnProperty.call(t||{},e):(t||{})[e.slice(0,s)]===e.slice(s+1)}function signalArg(t){const e=String(t).trim().toUpperCase().replace(/^SIG/,"");if(/^\d+$/.test(e))return e;const s=/^RTM(IN|AX)(?:([+-])(\d+))?$/.exec(e);if(s){const t="IN"===s[1]?34:64,e=s[3]?Number(s[3])*("-"===s[2]?-1:1):0;return String(t+e)}return e}function humanDuration(t){const e=Math.round(t/1e3);if(e<1)return"Less than a second";if(1===e)return"1 second";if(e<60)return`${e} seconds`;const s=Math.round(e/60);if(1===s)return"About a minute";if(s<60)return`${s} minutes`;const i=Math.round(e/3600);if(1===i)return"About an hour";if(i<48)return`${i} hours`;const n=Math.round(i/24);return n<14?`${n} days`:n<60?`${Math.round(n/7)} weeks`:n<730?`${Math.round(n/30)} months`:`${Math.round(n/365)} years`}module.exports={Engine,humanDuration,labelMatch,redactExecBody,completeTar,signalArg};
7
+ "use strict";const fs=require("fs"),path=require("path"),crypto=require("crypto"),{EventEmitter}=require("events"),{parseReference}=require("./image/reference"),{pullImage,pushImage,pushIndex,describe:describeRemote,verifyLogin}=require("./image/registry"),{AuthStore,normalizeRegistry}=require("./image/auth"),{createLinuxHost}=require("./linux/host"),{Rootfs}=require("./runtime/rootfs"),{Network}=require("./runtime/network"),{PortProxies}=require("./runtime/ports"),{Volumes}=require("./runtime/volumes"),{buildSpec,mergeEnv,processArgs,resolveUser,additionalGids}=require("./runtime/spec"),{ContainerFs}=require("./runtime/fsops"),{Stats}=require("./runtime/stats"),{Emulation,parsePlatform}=require("./runtime/emulation"),{HostForward,HOST_IP,ENGINE_SOCK}=require("./runtime/hostfwd"),{FsEventRelay}=require("./runtime/fsevents"),archive=require("./image/archive"),{BUILD_FILE,HOST_ALIAS}=require("./api/wire"),NAME_RE=/^\/?[a-zA-Z0-9][a-zA-Z0-9_.-]+$/,CRUN_CREATE_ERROR=[/^crun: /,/^executable file `.*` not found/,/^open executable: /,/^exec(ve)? `.*`: /,/^chdir to `.*`: /],GO_ARCH={x64:"amd64",arm64:"arm64"},ADJECTIVES=["brave","calm","eager","fancy","gentle","happy","jolly","keen","lucid","merry","nifty","proud","quick","sharp","sunny","witty"],NOUNS=["aryabhata","bose","raman","kalam","sarabhai","ramanujan","bhabha","saha","chandra","khorana","visvesvaraya","dhawan","swaminathan","mahalanobis"];function httpError(t,e){const s=new Error(e);return s.statusCode=t,s}const shortId=t=>t.replace(/^sha256:/,"").slice(0,12),iso=t=>t?new Date(t).toISOString():"0001-01-01T00:00:00Z",sleep=t=>new Promise(e=>setTimeout(e,t));function redactEnv(t){return Array.isArray(t)?t.map(t=>String(t).replace(/=.*$/s,"=***")):t}function redactExecBody(t){return t&&t.Env?{...t,Env:redactEnv(t.Env)}:t}const LAYER_REMOVE='root="$1"; shift; for d in "$root"/layers "$root"/layers-*; do [ -d "$d" ] || continue; for h; do rm -rf "$d/$h" "$d/$h.done"; done; done; true',LAYER_GC='\nroot="$1"; keep=$(cat); n=0\nfor d in "$root"/layers "$root"/layers-*; do\n [ -d "$d" ] || continue\n for e in "$d"/*; do\n h=${e##*/}; h=${h%.done}\n case "$h" in empty|*.tmp.*|*.lock|*\'*\') continue;; esac\n printf \'%s\\n\' "$keep" | grep -qx "$h" && continue\n if [ -e "$d/$h" ] || [ -e "$d/$h.done" ]; then rm -rf "$d/$h" "$d/$h.done"; n=$((n+1)); fi\n done\ndone\necho "$n"\n';class Engine extends EventEmitter{constructor({store:t,paths:e,log:s=()=>{},hostFactory:i=createLinuxHost}){super(),this.store=t,this.paths=e,this.log=s,this.hostFactory=i,this._host=null,this.execs=new Map,this.waiters=new Map,this.exiting=new Set,this.timer=null,this.lastLiveness=0,this.healthTimers=new Map,this.starting=new Map,this.auth=new AuthStore(e.auth||path.join(e.home,"auth.json")),this.fs=new ContainerFs(this),this.stats=new Stats(this),this.emulation=new Emulation(this),this.hostfwd=new HostForward({engine:this,socketPath:e.socket,log:s}),this.fsevents=new FsEventRelay({engine:this,log:s});for(const t of[e.blobs,e.containers,e.volumes])fs.mkdirSync(t,{recursive:!0})}get host(){if(!this._host){try{this._host=this.hostFactory({log:this.log})}catch(t){throw httpError(501,t.message)}this.rootfs=new Rootfs({host:this._host,blobsDir:this.paths.blobs}),this.network=new Network({host:this._host,store:this.store}),this.ports=new PortProxies({host:this._host,log:this.log})}return this._host}get buildkit(){if(!this._buildkit){const{BuildKit:t}=require("./build/buildkit");this._buildkit=new t(this)}return this._buildkit}get volumes(){return this._volumes||(this._volumes=new Volumes({store:this.store,host:this.host,dir:this.paths.volumes,emit:(t,e,s,i)=>this.emitEvent(t,e,s,i),engineSocket:this.paths.socket,guestEngineSocket:ENGINE_SOCK})),this._volumes}async ready(){try{await this.host.ready()}catch(t){throw t.statusCode?t:httpError(500,t.message)}if(!this.kernelVersion){const t=await this.host.run(["uname","-r"]);0===t.code&&(this.kernelVersion=t.stdout.trim())}if(this._layersChecked||(this._layersChecked=!0,setImmediate(()=>this.gcLayers().catch(()=>{}))),"vm"===this.host.kind)await this.hostfwd.listen(),await this.hostfwd.ensureGuest();else if("wsl"===this.host.kind&&!this._wslGateway){const t=await this.host.run(["/bin/sh","-c","ip route show default | awk '{print $3; exit}'"]);this._wslGateway=t.stdout.trim()||null}}_hostGateway(t={}){return"vm"===this._host?.kind?HOST_IP:"wsl"===this._host?.kind?this._wslGateway:"rootless"===this._host?.kind?this._host.gateway:Object.values(t).find(t=>t.gateway)?.gateway||null}_hostIsolated(t){return"none"===t.config?.Config?.Labels?.["tnc.host-access"]}emitEvent(t,e,s,i={}){const n=this.store.recordEvent(t,e,s,i);this.emit("event",n)}_attrs(t,e={}){return{...this.store.getImage(t.image)?.config?.config?.Labels||{},...t.config.Config.Labels||{},image:t.config.imageRef,name:t.name,...e}}containerDir(t){return path.join(this.paths.containers,t)}logFile(t){return path.join(this.containerDir(t),"json.log")}exitFile(t){return path.join(this.containerDir(t),"exit")}linuxArch(){return GO_ARCH[process.arch]||process.arch}findImage(t){let e=null;try{e=parseReference(t).familiar}catch(t){}return this.store.findImage(t,e)}requireImage(t){const e=this.findImage(t);if(!e)throw httpError(404,`No such image: ${t}`);return e}_credentials(t,e){if(e&&e.username&&"string"==typeof e.password)return{username:e.username,password:e.password};const s=this.auth.get(t);if(s)return s;let i;try{i=normalizeRegistry(t)}catch(t){return null}const n=require("./image/credhelpers").lookup(i);return n?{username:n.username,password:n.password}:null}async pull(t,e=()=>{},{credentials:s,platform:i}={}){const n=parseReference(t),r="string"==typeof i?parsePlatform(i):i,{image:o,fresh:a}=await pullImage(n,{blobsDir:this.paths.blobs,arch:r?.arch||this.linuxArch(),variant:r?.variant,progress:e,credentials:this._credentials(n.domain,s)}),h=require("./image/sign");if(h.policyFor(n).length){const t=(o.repoDigests||[]).find(t=>t.startsWith(`${n.familiarName}@`));await h.enforcePolicy(this,n,n.digest||t?.split("@")[1]||null),e({status:`Signature verified for ${n.familiar}`})}const c=this.store.saveImage(o);return e({status:a?`Status: Downloaded newer image for ${n.familiar}`:`Status: Image is up to date for ${n.familiar}`}),this.emitEvent("image","pull",n.familiar,{name:n.familiar}),c}async push(t,e,s=()=>{},{credentials:i}={}){const n=parseReference(t);if(n.digest)throw httpError(400,`cannot push a digest reference: ${t}`);let r;if(e||t.lastIndexOf(":")>t.lastIndexOf("/"))r=[parseReference(e?`${n.familiarName}:${e}`:t)];else{const t=`${n.familiarName}:`;if(r=this.store.listImages().flatMap(t=>t.repoTags).filter(e=>e.startsWith(t)).map(t=>parseReference(t)),!r.length)throw httpError(404,`An image does not exist locally with the tag: ${n.familiarName}`)}const o=this._credentials(n.domain,i),a=[];for(const t of r){const e=this.findImage(t.familiar);if(!e)throw httpError(404,`An image does not exist locally with the tag: ${t.familiar}`);const i=(e.repoDigests||[]).map(t=>{try{return parseReference(t)}catch(t){return null}}).filter(e=>e&&e.domain===t.domain).map(t=>t.path),n=await pushImage(t,e,{blobsDir:this.paths.blobs,credentials:o,sources:i,progress:s}),r=`${t.familiarName}@${n.digest}`,h=(e.repoDigests||[]).filter(e=>!e.startsWith(`${t.familiarName}@`));this.store.saveImage({...e,repoTags:[],repoDigests:[...h,r]}),this.emitEvent("image","push",t.familiar,{name:t.familiar}),a.push({ref:t.familiar,...n})}return a}async pushIndex(t,e,s=()=>{},{credentials:i}={}){const n=parseReference(t);if(!n.tag||n.digest)throw httpError(400,`a multi-platform push needs a tag: ${t}`);if(!Array.isArray(e)||0===e.length)throw httpError(400,"no images to push");const r=e.map(t=>{const e=this.requireImage(t),s=e.config||{};return{image:e,platform:{os:e.os||s.os||"linux",architecture:e.architecture||s.architecture,variant:s.variant}}}),o=r.flatMap(t=>(t.image.repoDigests||[]).map(t=>{try{return parseReference(t)}catch(t){return null}})).filter(t=>t&&t.domain===n.domain).map(t=>t.path),a=await pushIndex(n,r,{blobsDir:this.paths.blobs,credentials:this._credentials(n.domain,i),sources:o,progress:s});return this.emitEvent("image","push",n.familiar,{name:n.familiar}),a}async distribution(t,{credentials:e}={}){const s=parseReference(t);return await describeRemote(s,{blobsDir:this.paths.blobs,credentials:this._credentials(s.domain,e)})}async search(t,{limit:e=25,filters:s={}}={}){if(!t)throw httpError(400,"a search term is required");const{DEFAULT_REGISTRY_SEARCH:i}=require("./api/wire"),n=new URL(i);let r;n.searchParams.set("q",t),n.searchParams.set("n",String(Math.min(100,Math.max(1,e))));try{r=await fetch(n,{headers:{"User-Agent":"tnc-dock"},signal:AbortSignal.timeout(2e4)})}catch(t){throw httpError(502,`image search failed: ${t.cause?.code||t.message}`)}if(!r.ok)throw await(r.body?.cancel()),httpError(502,`image search failed: HTTP ${r.status}`);let o=((await r.json().catch(()=>({}))).results||[]).map(t=>({name:t.name||t.repo_name,description:t.description||t.short_description||"",star_count:t.star_count||0,is_official:!!t.is_official,is_automated:!!t.is_automated}));const a=t=>(s[t]||[])[0];return void 0!==a("is-official")&&(o=o.filter(t=>String(t.is_official)===String(a("is-official")))),void 0!==a("is-automated")&&(o=o.filter(t=>String(t.is_automated)===String(a("is-automated")))),void 0!==a("stars")&&(o=o.filter(t=>t.star_count>=Number(a("stars")))),o.slice(0,e)}async login({username:t,password:e,serveraddress:s}={}){const i=normalizeRegistry(s);if(!t||"string"!=typeof e||!e)throw httpError(400,"username and password are required");return await verifyLogin(i,{username:t,password:e}),this.auth.set(i,{username:t,password:e}),this.emitEvent("registry","login",i,{name:i}),i}logout(t){const e=normalizeRegistry(t),s=this.auth.remove(e);return s&&this.emitEvent("registry","logout",e,{name:e}),{host:e,removed:s}}tagImage(t,e,s="latest"){const i=this.requireImage(t),n=parseReference(s?`${e}:${s}`:e);this.store.saveImage({...i,repoTags:[n.familiar]}),this.emitEvent("image","tag",i.id,{name:n.familiar})}async removeImage(t,{force:e=!1}={}){const s=this.requireImage(t);let i=null;try{i=parseReference(t).familiar}catch(t){}const n=this.store.listContainers().filter(t=>t.image===s.id);if(i&&s.repoTags.includes(i)&&s.repoTags.length>1)return this.store.setImageTags(s.id,s.repoTags.filter(t=>t!==i)),this.emitEvent("image","untag",s.id,{name:i}),[{Untagged:i}];if(n.length&&!e)throw httpError(409,`conflict: unable to remove repository reference "${t}" (must force) - container ${shortId(n[0].id)} is using its referenced image ${shortId(s.id)}`);if(n.length&&e)return this.store.setImageTags(s.id,[]),s.repoTags.map(t=>({Untagged:t}));this.store.deleteImage(s.id);const r=new Set;for(const t of this.store.listImages()){r.add(t.id);for(const e of t.layers||[])r.add(e.digest)}const o=[s.id,...(s.layers||[]).map(t=>t.digest)].filter(t=>!r.has(t));for(const t of o){const e=path.join(this.paths.blobs,t.replace(/^sha256:/,""));fs.rmSync(e,{force:!0}),fs.rmSync(`${e}.tar`,{force:!0})}if(this._host&&await this.host.isUp().catch(()=>!1)){const t=(s.layers||[]).map(t=>t.digest).filter(t=>!r.has(t)).map(t=>t.replace(/^sha256:/,""));t.length&&await this.host.sh(LAYER_REMOVE,[this.rootfs.root,...t]).catch(()=>{})}return this.emitEvent("image","delete",s.id,{}),[...s.repoTags.map(t=>({Untagged:t})),...(s.repoDigests||[]).map(t=>({Untagged:t})),{Deleted:s.id}]}async gcLayers(){if(this.building>0||!await this.host.isUp().catch(()=>!1))return 0;const t=new Set;for(const e of this.store.listImages())for(const s of e.layers||[])t.add(s.digest.replace(/^sha256:/,""));for(const e of this.store.db.prepare("SELECT digest FROM build_cache WHERE digest != ''").all())t.add(e.digest.replace(/^sha256:/,""));const e=await this.host.sh(LAYER_GC,[this.rootfs.root],{input:[...t].join("\n")+"\n"}).catch(()=>({stdout:""})),s=Number(e.stdout.trim().split("\n").pop())||0;return s&&this.log(`removed ${s} unpacked layer${1===s?"":"s"} nothing uses`),s}async pruneBuildCache(){const t=this.store.db.prepare("SELECT digest FROM build_cache").all();if(this.store.db.exec("DELETE FROM build_cache"),this.building>0)return{count:t.length,bytes:0};const e=new Set(this.store.listImages().flatMap(t=>[t.id,...(t.layers||[]).map(t=>t.digest)]));let s=0;for(const i of new Set(t.map(t=>t.digest).filter(Boolean))){if(e.has(i))continue;const t=path.join(this.paths.blobs,i.replace(/^sha256:/,""));try{s+=fs.statSync(t).size}catch(t){}fs.rmSync(t,{force:!0}),fs.rmSync(`${t}.tar`,{force:!0})}return await this.gcLayers().catch(()=>{}),{count:t.length,bytes:s}}imageSummary(t){return{Id:t.id,ParentId:"",RepoTags:t.repoTags,RepoDigests:t.repoDigests||[],Created:Math.floor(t.createdAt/1e3),Size:t.size,SharedSize:-1,VirtualSize:t.size,Labels:t.config?.config?.Labels||{},Containers:this.store.listContainers().filter(e=>e.image===t.id).length}}imageInspect(t){const e=t.config||{};return{Id:t.id,RepoTags:t.repoTags,RepoDigests:t.repoDigests||[],Parent:"",Comment:e.comment||"",Created:e.created||iso(t.createdAt),Author:e.author||"",Config:e.config||{},Architecture:t.architecture||e.architecture,Variant:e.variant,Os:t.os||e.os,Size:t.size,VirtualSize:t.size,GraphDriver:{Name:"overlayfs",Data:null},RootFS:{Type:"layers",Layers:e.rootfs?.diff_ids||[]},Metadata:{LastTagTime:"0001-01-01T00:00:00Z"}}}async build(t,e,s){const{Builder:i,normalizeTag:n}=require("./build/builder");await this.ready();const r=path.join(this.paths.home,"build",crypto.randomBytes(8).toString("hex")),o=path.join(r,"context");fs.mkdirSync(o,{recursive:!0});try{const{spawnSync:a}=require("child_process");completeTar(t);const h=a("tar",["-xf",t,"-C",o],{encoding:"utf8"});if(0!==h.status)throw httpError(400,`invalid build context: ${(h.stderr||"").trim()}`);const c=e.buildFile||BUILD_FILE,u=path.resolve(o,c);if(!u.startsWith(o+path.sep)||!fs.existsSync(u))throw httpError(400,`Cannot locate the build file: ${c}`);const l=(e.tags||[]).map(n);let d=null;const m=path.join(o,".tnc-build-secrets");fs.existsSync(m)&&(d=path.join(r,"secrets"),fs.renameSync(m,d));const f=new i(this,{...e,contextDir:o,buildDir:r,out:s,secretsDir:d,platform:parsePlatform(e.platform)});let g;this.building=(this.building||0)+1;try{g=await f.run(u)}finally{this.building--}const p=this.store.saveImage({...g,repoTags:l});if(s({aux:{ID:p.id}}),e.quiet)s({stream:`${p.id}\n`});else{s({stream:`Successfully built ${shortId(p.id)}\n`});for(const t of l)s({stream:`Successfully tagged ${t}\n`})}for(const t of l)this.emitEvent("image","tag",p.id,{name:t});return p}finally{fs.rmSync(r,{recursive:!0,force:!0})}}findContainer(t){const e=this.store.findContainer(t);if(!e)throw httpError(404,`No such container: ${t}`);return e}_generateName(){for(let t=0;t<50;t++){const t=`${ADJECTIVES[crypto.randomInt(ADJECTIVES.length)]}_${NOUNS[crypto.randomInt(NOUNS.length)]}`;if(!this.store.findContainer(t))return t}return`dock_${crypto.randomBytes(4).toString("hex")}`}create(t,e={},{platform:s}={}){if(t&&!NAME_RE.test(t))throw httpError(400,`Invalid container name (${t}), only [a-zA-Z0-9][a-zA-Z0-9_.-] are allowed`);const i=t?t.replace(/^\//,""):this._generateName(),n=this.store.findContainer(i);if(n&&n.name===i)throw httpError(409,`Conflict. The container name "/${i}" is already in use by container "${n.id}". You have to remove (or rename) that container to be able to reuse that name.`);if(!e.Image)throw httpError(400,"Config cannot be empty in order to create a container");const r=this.requireImage(e.Image),o=parsePlatform(s);if(o&&r.architecture&&r.architecture!==o.arch)throw httpError(404,`No such image: ${e.Image} for linux/${o.arch} (the local one is linux/${r.architecture})`);if(require("./runtime/cdi").requested(e.HostConfig||{}).requests.length&&"darwin"===process.platform)throw httpError(400,"GPUs and CDI devices cannot reach containers on macOS: Apple's Virtualization framework has no device passthrough");const{HostConfig:a={},NetworkingConfig:h,...c}=e,u=require("./config").containerProxyEnv(void 0,"host.tnc.internal"),l=new Set([...c.Env||[],...r.config?.config?.Env||[]].map(t=>String(t).split("=")[0])),d=Object.entries(u).filter(([t])=>!l.has(t)).map(([t,e])=>`${t}=${e}`);d.length&&(c.Env=[...c.Env||[],...d]);const m=joinedContainer(a.NetworkMode);if(m&&(this.findContainer(m),Object.keys(a.PortBindings||{}).length))throw httpError(400,"conflicting options: port publishing and the container network mode");const f=m?[]:this._wantedNetworks(a.NetworkMode,h?.EndpointsConfig);if(0===processArgs(c,r.config?.config||{}).length)throw httpError(400,"No command specified");const g=this._containerUserns(a),p=crypto.randomBytes(32).toString("hex"),w=this.volumes.resolve(a,r.config?.config||{});this._volumesFrom(a.VolumesFrom,w);const y=this.containerDir(p);fs.mkdirSync(y,{recursive:!0});const v=this.store.insertContainer({id:p,name:i,image:r.id,state:"created",config:{Config:c,HostConfig:a,imageRef:e.Image,networks:f},runtime:{restartCount:0,volumes:w.volumes,anonymousVolumes:w.anonymous,ociMounts:w.ociMounts,apiMounts:w.apiMounts,...g?{userns:g}:{}}});return this.emitEvent("container","create",p,{...r.config?.config?.Labels||{},...c.Labels||{},image:e.Image,name:i}),{Id:v.id,Warnings:[]}}usernsRange(){if(void 0!==this._userns)return this._userns;const{remapSetting:t,resolveRemap:e}=require("./runtime/userns"),s=t();return this._userns=null,s&&"off"!==s?"local"!==this.host.kind?(this.log(`userns-remap is for dock on Linux as root; not used here (${this.host.kind})`),null):(this._userns=e(s),this._userns):null}async _shimSupports(t,e){if(this._shimModes=this._shimModes||new Map,!this._shimModes.has(t)){const{shim:s}=this.host.binaries(),i=await this.host.run([s,t,...e]).catch(()=>({code:2,stderr:"usage: tnc-shim --log"}));this._shimModes.set(t,!(2===i.code&&/usage: tnc-shim --log/.test(i.stderr||"")))}return this._shimModes.get(t)}_containerUserns(t={}){const e=t.UsernsMode||"";if(e&&"host"!==e)throw httpError(400,`--userns=${e} is not supported (use host, or set dock config --userns-remap)`);const s=this.usernsRange();if(!s||"host"===e)return null;if(t.Privileged)throw httpError(400,"privileged mode is incompatible with user namespaces; run it in the host user namespace: --privileged --userns=host");for(const[e,s]of[["NetworkMode","--network"],["PidMode","--pid"],["IpcMode","--ipc"],["UTSMode","--uts"]])if("host"===t[e])throw httpError(400,`${s}=host is incompatible with user namespaces; add --userns=host`);return{uid:s.uid,gid:s.gid,size:s.size}}_chownForUserns(t,e){const s=t.runtime.userns;if(s){for(const t of["hostname","hosts","resolv.conf"])try{fs.chownSync(path.join(e,t),s.uid,s.gid)}catch(t){}for(const e of t.runtime.volumes||[]){const t=this.store.getVolume(e);if(t)try{0===fs.statSync(t.mountpoint).uid&&0===fs.readdirSync(t.mountpoint).length&&fs.chownSync(t.mountpoint,s.uid,s.gid)}catch(t){}}}}_volumesFrom(t=[],e){const s=new Set(e.ociMounts.map(t=>t.destination));for(const i of t||[]){const[t,n]=String(i).split(":"),r=this.findContainer(t),o="ro"===n;for(const t of r.runtime.ociMounts||[]){if("bind"!==t.type||s.has(t.destination))continue;const i=t.options.filter(t=>"ro"!==t&&"rw"!==t);e.ociMounts.push({...t,options:[...i,o||t.options.includes("ro")&&"rw"!==n?"ro":"rw"]});const a=(r.runtime.apiMounts||[]).find(e=>e.Destination===t.destination);a&&e.apiMounts.push({...a,RW:!o&&a.RW,Mode:o?"ro":a.Mode}),s.add(t.destination)}for(const t of r.runtime.volumes||[])e.volumes.includes(t)||e.volumes.push(t)}}_wantedNetworks(t,e={}){this.host;const s=this.network.resolve(t),i=[{name:s.name,aliases:(n=s,e?.[n.name]?.Aliases||e?.[t]?.Aliases||[])}];var n;for(const t of Object.keys(e||{})){const n=this.network.resolve(t);if(!i.some(t=>t.name===n.name)){if(["host","null"].includes(s.driver)||["host","null"].includes(n.driver))throw httpError(400,"container cannot be connected to multiple networks with one of the networks in host or none mode");i.push({name:n.name,aliases:e[t].Aliases||[]})}}return i}_hostname(t){return t.config.Config.Hostname||t.id.slice(0,12)}_hostsContent(t){const e=["127.0.0.1\tlocalhost","::1\tlocalhost ip6-localhost ip6-loopback"],s=t.runtime.networks||{},i=Object.values(s).find(t=>t.ip);i&&e.push(`${i.ip}\t${this._hostname(t)}`),i?.ip6&&e.push(`${i.ip6}\t${this._hostname(t)}`);const n=new Set;for(const[i,r]of Object.entries(s)){let s;try{s=this.network.resolve(i)}catch(t){continue}if(s.dns&&r.ip)for(const s of this.store.listContainers()){if(s.id===t.id||"running"!==s.state)continue;const r=s.runtime.networks?.[i];if(!r?.ip||n.has(`${i}:${s.id}`))continue;n.add(`${i}:${s.id}`);const o=[...new Set([s.name,...r.aliases||[],this._hostname(s),s.id.slice(0,12)])];e.push(`${r.ip}\t${o.join(" ")}`),r.ip6&&e.push(`${r.ip6}\t${o.join(" ")}`)}}const r=this._hostGateway(s),o=(t.config.HostConfig.ExtraHosts||[]).map(t=>{const e=t.search(/[:=]/);return[t.slice(0,e),t.slice(e+1)]});r&&!o.some(([t])=>t===HOST_ALIAS)&&e.push(`${r}\t${HOST_ALIAS} host.tnc.internal`);for(const[t,s]of o){const i="host-gateway"===s?r:s;i&&e.push(`${i}\t${t}`)}return e.join("\n")+"\n"}_refreshHosts(t){const e=new Set(t);for(const t of this.store.listContainers())if("running"===t.state&&Object.keys(t.runtime.networks||{}).some(t=>e.has(t)))try{fs.writeFileSync(path.join(this.containerDir(t.id),"hosts"),this._hostsContent(t))}catch(t){}this._refreshDns().catch(t=>this.log(`dns: ${t.message}`))}_dnsRecords(t=null){const e=[],s=this.store.listContainers().filter(e=>"running"===e.state&&e.id!==t?.c.id).map(t=>[t,t.runtime.networks||{}]);t&&s.push([t.c,t.endpoints]);for(const[t,i]of s){const s=t.id.slice(0,12);let n=!1;for(const[r,o]of Object.entries(i)){let i;try{i=this.network.resolve(r)}catch(t){continue}if(!i.dns||!o.ip)continue;const a=i.id.slice(0,12);n||(e.push(`s /var/run/netns/${this.network.names(t.id,0).ns} ${s}`),n=!0),e.push(`n ${s} ${a}`);for(const i of new Set([t.name,...o.aliases||[],this._hostname(t),s]))/^[A-Za-z0-9_][A-Za-z0-9_.-]*$/.test(i)&&(e.push(`a ${a} ${i} ${o.ip}`),o.ip6&&e.push(`a ${a} ${i} ${o.ip6}`))}}return this._overlayNames&&e.push(...this._overlayNames.dnsLines()),e.join("\n")+"\n"}async _refreshDns(t=null){if("0"===process.env.TNC_DOCK_DNS||!this._host||!1===this._dnsSupported)return;"local"===this.host.kind&&(this._overlayNames||(this._overlayNames=new(require("./runtime/overlaydns").OverlayNames)(this)),this._overlayNames.sync());const e=this._dnsRecords(t),s=path.join(this.paths.home,"dns","records");fs.mkdirSync(path.dirname(s),{recursive:!0});const i=`${s}.${process.pid}.tmp`;if(fs.writeFileSync(i,e),fs.renameSync(i,s),this._dns&&!this._dns.dead)return void this._dns.proc.kill("SIGHUP");if(!/^s /m.test(e))return;const n=((await this._resolvConf()).match(/^nameserver\s+\S+/gm)||[]).map(t=>t.split(/\s+/)[1]).join(",")||"1.1.1.1",r=this.host.spawn([this.host.binaries().shim,"dns",this.host.guestPath(s),n]),o={proc:r,dead:!1};r.stdout.resume(),r.stderr.on("data",t=>this.log(`dns: ${String(t).trim()}`)),r.exited.then(t=>{o.dead=!0,this._dns===o&&(this._dns=null,this.log(`dns resolver exited (${t})`))}),this._dns=o}async _usesEmbeddedDns(t,e){if("0"===process.env.TNC_DOCK_DNS||t.Dns?.length)return!1;const[s,i]=Object.entries(e||{})[0]||[];if(!s||!i?.ip)return!1;try{if(!this.network.resolve(s).dns)return!1}catch(t){return!1}if(void 0===this._dnsSupported){const t=await this.host.run([this.host.binaries().shim,"dns"]).catch(()=>({stderr:""}));this._dnsSupported=/tnc-shim dns/.test(t.stderr||""),this._dnsSupported||this.log("dns: this runtime's tnc-shim has no resolver; user networks use /etc/hosts only (update: thinknsh dock setup)")}return this._dnsSupported}async _resolvConf(){if(this._resolv)return this._resolv;if(this.host.resolvConf)return this._resolv=this.host.resolvConf();let t=await this.host.run(["cat","/run/systemd/resolve/resolv.conf"]);0!==t.code&&(t=await this.host.run(["cat","/etc/resolv.conf"]));const e=(t.stdout.match(/^nameserver\s+(\S+)/gm)||[]).map(t=>t.split(/\s+/)[1]).filter(t=>!t.startsWith("127."));return this._resolv=(e.length?e:["1.1.1.1","8.8.8.8"]).map(t=>`nameserver ${t}`).join("\n")+"\n",this._resolv}async _containerResolv(t,e,s=!1){if(s){const e=await this._resolvConf();return["nameserver 127.0.0.11",...t.DnsSearch?.length?[`search ${t.DnsSearch.join(" ")}`]:e.split("\n").filter(t=>/^search\s/.test(t)),`options ${(t.DnsOptions?.length?t.DnsOptions:["ndots:0"]).join(" ")}`].join("\n")+"\n"}const i=e&&!this.host.resolvConf?(await this.host.run(["cat","/etc/resolv.conf"])).stdout:await this._resolvConf();return[...t.Dns?.length?t.Dns.map(t=>`nameserver ${t}`):i.split("\n").filter(t=>/^nameserver\s/.test(t)),...t.DnsSearch?.length?[`search ${t.DnsSearch.join(" ")}`]:i.split("\n").filter(t=>/^search\s/.test(t)),...t.DnsOptions?.length?[`options ${t.DnsOptions.join(" ")}`]:i.split("\n").filter(t=>/^options\s/.test(t))].join("\n")+"\n"}async _devices(t=[]){const e=[];for(const s of t||[]){const t=await this.host.run(["stat","-L","-c","%t %T %F %u %g %a",s.PathOnHost]);if(0!==t.code)throw httpError(400,`error gathering device information while adding custom device "${s.PathOnHost}": no such file or directory`);const[i,n,...r]=t.stdout.trim().split(" "),o=r.slice(0,-3).join(" "),[a,h,c]=r.slice(-3);if(!/character|block/.test(o))throw httpError(400,`${s.PathOnHost} is not a device`);e.push({path:s.PathInContainer||s.PathOnHost,type:/block/.test(o)?"b":"c",major:parseInt(i,16),minor:parseInt(n,16),fileMode:parseInt(c,8)|(/block/.test(o)?24576:8192),uid:Number(a),gid:Number(h),CgroupPermissions:s.CgroupPermissions||"rwm"})}return e}async start(t){const e=this.findContainer(t).id,s=this.starting.get(e);if(s)return await s.catch(()=>{}),"running"===this.store.getContainer(e)?.state?"notModified":this.start(e);const i=this._start(e);this.starting.set(e,i);try{return await i}finally{this.starting.delete(e)}}async _start(t){let e=this.findContainer(t);if("running"===e.state){if(await this._stillRunning(e))return"notModified";await this._onExit(e,255,"container runtime lost (the Linux host or MicroVM restarted)"),e=this.findContainer(e.id)}if("removing"===e.state)throw httpError(409,`container ${e.id} is marked for removal and cannot be started`);await this.ready();for(let t=0;t<600&&!this.fs.idle(e.id);t++)await sleep(50);const s=this.store.getImage(e.image);if(!s)throw httpError(404,`No such image: ${e.config.imageRef}`);s.architecture&&s.architecture!==this.linuxArch()&&await this.emulation.ensure(s.architecture);const i=s.config?.config||{},{Config:n,HostConfig:r}=e.config,o=e.id,a=this.containerDir(o),h=t=>this.host.guestPath(t),c=n.WorkingDir||i.WorkingDir||"/",u=n.Hostname||o.slice(0,12);let l=null,d={};try{if(this.volumes.linuxSide&&e.runtime.volumes?.length&&await this.host.sh('for d; do mkdir -p "$d"; done',e.runtime.volumes.map(t=>this.volumes.linuxDir(t))),e.runtime.userns&&!await this._shimSupports("idshift",["/proc/self/no-such-dir","0","0","1","to"]))throw httpError(501,"userns-remap needs dock runtime 0.2.3 or newer: run thinknsh dock setup (or start this container with --userns host)");const t=await this.rootfs.prepare(o,s,c,{userns:e.runtime.userns}),m=n.User||i.User||"",f=m&&m.split(":").some(t=>!/^\d+$/.test(t))?resolveUser(m,await this.rootfs.readFile(o,"/etc/passwd"),await this.rootfs.readFile(o,"/etc/group")):resolveUser(m);if(r.GroupAdd?.length){const t=r.GroupAdd.some(t=>!/^\d+$/.test(String(t)));f.additionalGids=additionalGids(r.GroupAdd,t?await this.rootfs.readFile(o,"/etc/group"):"")}const g=require("./runtime/cdi"),{requests:p,devices:w}=g.requested(r),y=await this._devices(w);if(r.Privileged){const t=new Set(y.map(t=>t.path));for(const e of await this._hostDevices())t.has(e.path)||y.push(e)}let v=null;if(p.length){const t=await this.host.run(["/bin/sh","-c",g.LIST_SCRIPT]);v=g.resolve(p,g.parseSpecs(t.stdout));for(const t of v.deviceNodes)y.push(...await this._devices([{PathOnHost:t.hostPath||t.path,PathInContainer:t.path,CgroupPermissions:t.permissions||"rwm"}]))}const S=joinedContainer(r.NetworkMode);let C=null;if(S){if(C=this.findContainer(S),"running"!==C.state||!C.runtime.network)throw httpError(409,`cannot join the network of ${C.name}: it is not running`);l={nsPath:C.runtime.network.nsPath,endpoints:{}}}else l=await this.network.attachAll(o,e.config.networks||this._wantedNetworks(r.NetworkMode));this._hostIsolated(e)&&l.nsPath&&await this.network.isolateHost(o,[this._hostGateway(l.endpoints),...Object.values(l.endpoints).map(t=>t.gateway)]);const _=Object.values(l.endpoints)[0]||{};if(fs.writeFileSync(path.join(a,"hostname"),u+"\n"),C)for(const t of["hosts","resolv.conf"])fs.copyFileSync(path.join(this.containerDir(C.id),t),path.join(a,t));else{fs.writeFileSync(path.join(a,"hosts"),this._hostsContent({...e,runtime:{...e.runtime,networks:l.endpoints}}));const t=await this._usesEmbeddedDns(r,l.endpoints);fs.writeFileSync(path.join(a,"resolv.conf"),await this._containerResolv(r,null===l.nsPath,t)),t&&await this._refreshDns({c:e,endpoints:l.endpoints}).catch(t=>this.log(`dns: ${t.message}`))}this._chownForUserns(e,a);const E=mergeEnv(i.Env,[`HOSTNAME=${u}`,...v?.env||[],...n.Env||[]]),{shim:b}=this.host.binaries(),$=processArgs(n,i),k=buildSpec({id:o,rootfs:t,cwd:c,user:f,hostname:u,nsPath:l.nsPath,hostConfig:r,terminal:!!n.Tty,devices:y,cgroupParent:this.host.cgroupParent?.(),userns:e.runtime.userns,args:r.Init?["/sbin/tnc-init","init","--",...$]:$,env:E,mounts:[...r.Init?[{destination:"/sbin/tnc-init",type:"bind",source:b,options:["rbind","ro"]}]:[],{destination:"/etc/hostname",type:"bind",source:h(path.join(a,"hostname")),options:["rbind","rw"]},{destination:"/etc/hosts",type:"bind",source:h(path.join(a,"hosts")),options:["rbind","rw"]},{destination:"/etc/resolv.conf",type:"bind",source:h(path.join(a,"resolv.conf")),options:["rbind","rw"]},...e.runtime.ociMounts||[],...v?g.ociMounts(v):[]]});v&&Object.keys(v.hooks).length&&(k.hooks=v.hooks),fs.writeFileSync(path.join(a,"config.json"),JSON.stringify(k,null,2)),fs.rmSync(this.exitFile(o),{force:!0}),_.ip&&(d=await this.ports.start(o,r.PortBindings,_.ip));const{shim:I,crun:x}=this.host.binaries(),A=fs.existsSync(this.logFile(o))?fs.statSync(this.logFile(o)).size:0,D=["--log",h(this.logFile(o)),"--exit",h(this.exitFile(o)),"--ctl",this.rootfs.ctlFifo(o),...logRotationArgs(r.LogConfig),...e.runtime.userns?["--stdio-owner",`${e.runtime.userns.uid}:${e.runtime.userns.gid}`]:[]],P=[x,`--cgroup-manager=${this.host.cgroupManager?.()||"cgroupfs"}`,"run","--bundle",h(a)];n.OpenStdin&&D.push("--stdin",this.rootfs.stdinFifo(o)),n.Tty&&(D.push("--console",this.rootfs.consoleSock(o)),P.push("--console-socket",this.rootfs.consoleSock(o))),await this.host.run([x,"delete","-f",o]),await this.host.check([I,...D,"--",...P,o]);const R=await this._waitForStart(o,A);return e=this.store.updateContainer(o,{state:"running",runtime:{...e.runtime,pid:R,health:null,networks:l.endpoints,network:{nsPath:l.nsPath},ip:_.ip||null,ports:d,startedAt:Date.now(),finishedAt:null,exitCode:0,error:"",manualStop:!1,paused:!1,cgroupPath:k.linux.cgroupsPath}}),this._refreshHosts(Object.keys(l.endpoints)),this.fsevents.track(o,e.runtime.apiMounts),this.emitEvent("container","start",o,this._attrs(e)),this._ensureReconciler(),this._startHealth(e),"started"}catch(t){this.ports&&this.ports.stop(o),l&&await this.network.detachAll(o,l.endpoints),await this.rootfs.unmount(o).catch(()=>{});const s=t.exitCode??128;throw this.store.updateContainer(o,{state:"created"===e.state?"created":"exited",runtime:{...e.runtime,error:t.message,exitCode:s,finishedAt:Date.now()}}),t.statusCode?t:httpError(500,t.message)}}async _waitForStart(t,e){const{crun:s}=this.host.binaries();for(let i=0;i<40;i++){if(fs.existsSync(this.exitFile(t))){const s=parseInt(fs.readFileSync(this.exitFile(t),"utf8"),10),i=this._crunErrorSince(t,e);if(i){fs.rmSync(this.exitFile(t),{force:!0});const e=httpError(400,`failed to create task for container: OCI runtime create failed: ${i}`);throw e.exitCode=/not found|No such file/.test(i)?127:/ermission denied/.test(i)?126:s||127,e}return 0}const i=await this.host.run([s,"state",t]);if(0===i.code)try{const t=JSON.parse(i.stdout);if("running"===t.status)return t.pid||0}catch(t){}await sleep(50)}return 0}_crunErrorSince(t,e){try{const s=fs.openSync(this.logFile(t),"r"),i=fs.fstatSync(s).size,n=Buffer.alloc(Math.max(0,i-e));fs.readSync(s,n,0,n.length,e),fs.closeSync(s);for(const t of n.toString("utf8").split("\n")){if(!t)continue;const e=JSON.parse(t);if("stderr"===e.stream&&CRUN_CREATE_ERROR.some(t=>t.test(e.log)))return e.log.replace(/^crun: /,"").trim()}}catch(t){}return null}_waitFor(t,e){return new Promise(s=>{this.waiters.has(t)||this.waiters.set(t,[]),this.waiters.get(t).push({condition:e,resolve:s})})}_resolveWaiters(t,e,s){const i=this.waiters.get(t)||[],n=[];for(const t of i)"removed"===e||"removed"!==t.condition?t.resolve({StatusCode:s,Error:null}):n.push(t);n.length?this.waiters.set(t,n):this.waiters.delete(t)}wait(t,e="not-running"){const s=this.findContainer(t);if(!["not-running","next-exit","removed"].includes(e))throw httpError(400,`invalid condition: "${e}"`);return"not-running"===e&&"running"!==s.state?Promise.resolve({StatusCode:s.runtime.exitCode||0,Error:null}):this._waitFor(s.id,e)}async _signal(t,e){const{crun:s}=this.host.binaries();return 0===(await this.host.run([s,"kill",t.id,signalArg(e)])).code}async _hostDevices(){const t="vm"===this.host.kind||"wsl"===this.host.kind,e=(await this.host.run(["/bin/sh","-c","find /dev -xdev \\( -type c -o -type b \\) ! -path '/dev/pts/*' ! -path '/dev/shm/*' ! -path '/dev/mqueue/*' 2>/dev/null"])).stdout.split("\n").filter(Boolean).filter(t=>!["/dev/console","/dev/ptmx","/dev/tty"].includes(t)).filter(e=>!(t&&/^\/dev\/(vd|sd|nvme|xvd|hd|dm-|mapper\/)/.test(e))),s=[];for(const t of e)try{s.push(...await this._devices([{PathOnHost:t,PathInContainer:t,CgroupPermissions:"rwm"}]))}catch(t){}return s}async stop(t,e){const s=this.findContainer(t);if("running"!==s.state)return"notModified";await this.ready(),await this._thaw(s),this.store.updateContainer(s.id,{runtime:{...s.runtime,manualStop:!0}});const i=this.store.getImage(s.image)?.config?.config||{},n=s.config.Config.StopSignal||i.StopSignal||"SIGTERM",r=Number.isFinite(e)?e:s.config.Config.StopTimeout??10,o=this._waitFor(s.id,"next-exit");return await this._signal(s,n),await Promise.race([o,sleep(1e3*r).then(()=>null)])||(await this._signal(s,"SIGKILL"),await Promise.race([o,sleep(1e4)])),"stopped"}async kill(t,e="SIGKILL"){const s=this.findContainer(t);if("running"!==s.state)throw httpError(409,`Cannot kill container: ${t}: Container ${s.id} is not running`);if(await this.ready(),"KILL"!==String(e).replace(/^SIG/,"")&&9!==e&&"9"!==e||await this._thaw(s),this.store.updateContainer(s.id,{runtime:{...s.runtime,manualStop:!0}}),!await this._signal(s,e))throw httpError(500,`Cannot kill container ${t}: crun kill failed`)}async restart(t,e){await this.stop(t,e),await this.start(t)}async remove(t,{force:e=!1,volumes:s=!1}={}){let i=this.findContainer(t);if("running"===i.state&&!e)throw httpError(409,`You cannot remove a running container ${i.id}. Stop the container before attempting removal or force remove`);if("removing"===i.state)return;const n="running"===i.state;if(i=this.store.updateContainer(i.id,{state:"removing",runtime:{...i.runtime,manualStop:!0}}),n){await this.ready(),await this._thaw(i);const t=this._waitFor(i.id,"next-exit");await this._signal(i,"SIGKILL"),await Promise.race([t,sleep(1e4)]),i=this.store.getContainer(i.id)}await this._destroy(i,s)}async _destroy(t,e){if(this._host&&await this.host.isUp().catch(()=>!1)&&await this.rootfs.remove(t.id).catch(e=>this.log(`rootfs cleanup for ${t.id}: ${e.message}`)),fs.rmSync(this.containerDir(t.id),{recursive:!0,force:!0}),this.store.deleteContainer(t.id),e)for(const e of t.runtime.anonymousVolumes||[])try{this.volumes.remove(e)}catch(t){}this.emitEvent("container","destroy",t.id,this._attrs(t)),this._resolveWaiters(t.id,"removed",t.runtime.exitCode||0)}async pause(t){const e=this.findContainer(t);if("running"!==e.state)throw httpError(409,`Container ${e.id} is not running`);if(e.runtime.paused)throw httpError(409,`Container ${e.id} is already paused`);if(await this.ready(),"disabled"===this.host.cgroupManager?.())throw httpError(501,"pause needs cgroups, which rootless mode does not have; stop the container instead");const{crun:s}=this.host.binaries(),i=await this.host.run([s,"pause",e.id]);if(0!==i.code)throw httpError(500,`Cannot pause container ${e.id}: ${(i.stderr||i.stdout).trim()}`);const n=this.store.getContainer(e.id);this.store.updateContainer(e.id,{runtime:{...n.runtime,paused:!0}}),this.emitEvent("container","pause",e.id,this._attrs(e))}async unpause(t){const e=this.findContainer(t);if(!e.runtime.paused||"running"!==e.state)throw httpError(409,`Container ${e.id} is not paused`);await this.ready(),await this._thaw(e,!0),this.emitEvent("container","unpause",e.id,this._attrs(e))}async _thaw(t,e=!1){const s=this.store.getContainer(t.id);if(!s?.runtime.paused)return;const{crun:i}=this.host.binaries(),n=await this.host.run([i,"resume",t.id]);if(0!==n.code&&e)throw httpError(500,`Cannot unpause container ${t.id}: ${(n.stderr||n.stdout).trim()}`);this.store.updateContainer(t.id,{runtime:{...this.store.getContainer(t.id).runtime,paused:!1}})}rename(t,e){const s=this.findContainer(t);if(!e||!NAME_RE.test(e))throw httpError(400,`Invalid container name (${e}), only [a-zA-Z0-9][a-zA-Z0-9_.-] are allowed`);const i=e.replace(/^\//,"");if(i===s.name)throw httpError(409,"Renaming a container with the same name as its current name");const n=this.store.findContainer(i);if(n&&n.name===i)throw httpError(409,`Conflict. The container name "/${i}" is already in use by container "${n.id}". You have to remove (or rename) that container to be able to reuse that name.`);this.store.updateContainer(s.id,{name:i}),this._refreshHosts(Object.keys(s.runtime.networks||{})),this.emitEvent("container","rename",s.id,this._attrs({...s,name:i},{oldName:`/${s.name}`}))}_saveConfig(t,e){return this.store.db.prepare("UPDATE containers SET config = ? WHERE id = ?").run(JSON.stringify(e),t),this.store.getContainer(t)}async update(t,e={}){const s=this.findContainer(t),i=Object.fromEntries(["Memory","MemorySwap","MemoryReservation","NanoCpus","CpuShares","CpuQuota","CpuPeriod","CpusetCpus","CpusetMems","PidsLimit","BlkioWeight"].filter(t=>void 0!==e[t]&&null!==e[t]&&""!==e[t]&&0!==e[t]).map(t=>[t,e[t]])),n={...s.config.HostConfig,...i};if(e.RestartPolicy?.Name){if(n.AutoRemove&&"no"!==e.RestartPolicy.Name)throw httpError(400,"Restart policy cannot be updated because AutoRemove is enabled for the container");n.RestartPolicy={Name:e.RestartPolicy.Name,MaximumRetryCount:e.RestartPolicy.MaximumRetryCount||0}}if(n.Memory>0&&n.MemoryReservation>n.Memory)throw httpError(400,"Minimum memory limit can not be less than memory reservation limit, see usage");if("running"===s.state&&Object.keys(i).length){await this.ready();const{devices:t,...e}=buildSpec({id:s.id,rootfs:"/",args:["x"],env:[],cwd:"/",user:{uid:0,gid:0},hostname:"x",mounts:[],hostConfig:{...n,Ulimits:[],Sysctls:{}}}).linux.resources,i=path.join(this.containerDir(s.id),"update.json");fs.writeFileSync(i,JSON.stringify(e));const{crun:r}=this.host.binaries(),o=await this.host.run([r,"update","--resources",this.host.guestPath(i),s.id]);if(fs.rmSync(i,{force:!0}),0!==o.code)throw httpError(500,`Cannot update container ${s.id}: ${(o.stderr||o.stdout).trim()}`)}return this._saveConfig(s.id,{...s.config,HostConfig:n}),this.emitEvent("container","update",s.id,this._attrs(s)),{Warnings:[]}}async commit(t,{repo:e,tag:s,comment:i="",author:n="",pause:r=!0,changes:o=[],config:a={}}={}){const{applyChanges:h}=require("./build/changes"),{COMMIT:c}=require("./build/builder"),u=this.findContainer(t);await this.ready();const l=this.store.getImage(u.image);if(!l)throw httpError(404,`No such image: ${u.config.imageRef}`);const d="running"===u.state&&r&&!u.runtime.paused;let m;d&&await this.pause(u.id);try{m=await(async()=>{const t=this.rootfs.root,e=`${t}/containers/${u.id}/commit`,s=path.join(this.paths.blobs,`commit-${u.id.slice(0,12)}-${Date.now()}.tmp`),{shim:i}=this.host.binaries();await this.host.sh('set -e; rm -rf "$2"; mkdir -p "$1" "$2"; cp -a "$1/." "$2/"; for f in /etc/hosts /etc/hostname /etc/resolv.conf /sbin/tnc-init; do [ -f "$2$f" ] && [ ! -s "$2$f" ] && rm -f "$2$f"; done; true',[`${t}/containers/${u.id}/upper`,e]);const n=u.runtime.userns;n&&await this.host.sh('"$1" idshift "$2" "$3" "$4" "$5" from',[i,e,String(n.uid),String(n.gid),String(n.size)]);try{const t=(await this.host.sh(c,[e,this.host.guestPath(s),i,"vm"===this.host.kind?1:0])).stdout.trim().split("\n").pop();if("EMPTY"===t)return null;const[n,r,o]=t.split(" ");return fs.renameSync(s,path.join(this.paths.blobs,n)),{digest:`sha256:${n}`,diffId:`sha256:${r}`,size:Number(o)}}finally{await this.host.sh('rm -rf "$1"',[e]).catch(()=>{})}})()}finally{d&&await this.unpause(u.id).catch(()=>{})}const f=l.config||{},g=h({...f.config||{},...stripEmpty(a)},o),p=(new Date).toISOString(),w={...f,created:p,...n?{author:n}:{},...i?{comment:i}:{},config:g,rootfs:{type:"layers",diff_ids:[...f.rootfs?.diff_ids||[],...m?[m.diffId]:[]]},history:[...f.history||[],{created:p,created_by:`thinknsh commit ${u.name}`,...i?{comment:i}:{},...m?{}:{empty_layer:!0}}]},y=JSON.stringify(w),v=`sha256:${crypto.createHash("sha256").update(y).digest("hex")}`;fs.writeFileSync(path.join(this.paths.blobs,v.slice(7)),y);const S=[...l.layers||[],...m?[{digest:m.digest,size:m.size,mediaType:"application/vnd.oci.image.layer.v1.tar+gzip"}]:[]],C=e?[parseReference(`${e}:${s||"latest"}`).familiar]:[];this.store.saveImage({id:v,repoTags:C,repoDigests:[],digest:null,size:S.reduce((t,e)=>t+e.size,0),layers:S,config:w,architecture:w.architecture,os:"linux",createdAt:Date.now()}),this.emitEvent("container","commit",u.id,this._attrs(u,{comment:i}));for(const t of C)this.emitEvent("image","tag",v,{name:t});return{Id:v}}async importImage(t,{repo:e,tag:s,changes:i=[],message:n="",platform:r}={}){const{applyChanges:o}=require("./build/changes"),a=parsePlatform(r),h=await archive.importRootfs(t,{blobsDir:this.paths.blobs,arch:a?.arch||this.linuxArch(),config:o({Env:["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"]},i),comment:n}),c=e?[parseReference(s?`${e}:${s}`:e).familiar]:[],u=this.store.saveImage({...h,repoTags:c});return this.emitEvent("image","import",u.id,c.length?{name:c[0]}:{}),u}saveSelection(t){if(!t.length)throw httpError(400,"no images given to save");const e=new Map;for(const s of t){const t=this.requireImage(s);let i=[],n=null;try{n=parseReference(s).familiar}catch(t){}n&&t.repoTags.includes(n)?i=[n]:/^(sha256:)?[0-9a-f]{12,64}$/.test(s)||(i=t.repoTags.filter(t=>n&&t.startsWith(n.replace(/:latest$/,"")+":")));const r=e.get(t.id)||{image:t,tags:[]};r.tags=[...new Set([...r.tags,...i])],e.set(t.id,r)}return[...e.values()]}async saveImages(t,e){const s=this.saveSelection(t);await archive.saveImages(s,{blobsDir:this.paths.blobs,sink:e});for(const{image:t}of s)this.emitEvent("image","save",t.id,{})}async loadImages(t,e){const s=path.join(this.paths.home,"build",`load-${crypto.randomBytes(6).toString("hex")}`);fs.mkdirSync(s,{recursive:!0});try{await archive.extractTo(t,s);const i=archive.readArchive(s,{blobsDir:this.paths.blobs,arch:this.linuxArch()});if(!i.length)throw httpError(400,"the archive holds no images");for(const t of i){const s=this.store.saveImage(t);if(this.emitEvent("image","load",s.id,{}),t.repoTags.length)for(const s of t.repoTags)e({stream:`Loaded image: ${s}\n`});else e({stream:`Loaded image ID: ${s.id}\n`})}}finally{fs.rmSync(s,{recursive:!0,force:!0})}}imageHistory(t){return archive.imageHistory(this.requireImage(t),t=>t.repoTags)}async _onExit(t,e,s=""){if(!this.exiting.has(t.id)){this.exiting.add(t.id);try{this.ports&&this.ports.stop(t.id),this.fsevents.untrack(t.id),this._stopHealth(t.id),this._host&&(await this.network.detachAll(t.id,t.runtime.networks),await this.host.isUp().catch(()=>!1)&&await this.rootfs.unmount(t.id).catch(()=>{}));const i="removing"===t.state;if(t=this.store.updateContainer(t.id,{state:i?"removing":"exited",runtime:{...t.runtime,exitCode:e,error:s,finishedAt:Date.now(),pid:0,ip:null,ports:{},networks:{},paused:!1}}),this._refreshHosts(Object.keys(t.config.networks?Object.fromEntries(t.config.networks.map(t=>[t.name,1])):{})),this.emitEvent("container","die",t.id,this._attrs(t,{exitCode:String(e)})),this._resolveWaiters(t.id,"exit",e),i)return;if(t.config.HostConfig.AutoRemove)return void await this._destroy(t,!0);this._maybeRestart(t,e)}finally{this.exiting.delete(t.id)}}}_maybeRestart(t,e){const s=t.config.HostConfig.RestartPolicy||{},i=s.Name||"no";if("no"===i||t.runtime.manualStop)return;if("on-failure"===i){if(0===e)return;if(s.MaximumRetryCount>0&&t.runtime.restartCount>=s.MaximumRetryCount)return}const n=Math.min(100*2**(t.runtime.restartCount||0),6e4);this.store.updateContainer(t.id,{runtime:{...t.runtime,restartCount:(t.runtime.restartCount||0)+1}}),setTimeout(()=>{const e=this.store.getContainer(t.id);e&&"exited"===e.state&&!e.runtime.manualStop&&this.start(t.id).catch(e=>this.log(`restart of ${t.name} failed: ${e.message}`))},n).unref()}_ensureReconciler(){this.timer||(this.timer=setInterval(()=>this.reconcile().catch(t=>this.log(`reconcile: ${t.message}`)),300),this.timer.unref())}async reconcile(){const t=this.store.listContainers().filter(t=>"running"===t.state||"removing"===t.state&&t.runtime.pid);if(0===t.length)return clearInterval(this.timer),void(this.timer=null);for(const e of t)if(fs.existsSync(this.exitFile(e.id))){const t=parseInt(fs.readFileSync(this.exitFile(e.id),"utf8"),10);await this._onExit(e,Number.isInteger(t)?t:255)}if(Date.now()-this.lastLiveness<3e3)return;this.lastLiveness=Date.now();const e=await this.host.isUp().catch(()=>!1);let s=null;if(e){const t=await this.host.run([this.host.binaries().crun,"list","-q"]);0===t.code&&(s=new Set(t.stdout.split("\n").map(t=>t.trim()).filter(Boolean)))}for(const i of t){const t=this.store.getContainer(i.id);t&&"running"===t.state&&!fs.existsSync(this.exitFile(i.id))&&(!e||s&&!s.has(i.id)&&Date.now()-(t.runtime.startedAt||0)>2e3)&&await this._onExit(t,255,"container runtime lost (the Linux host or MicroVM restarted)")}}async _stillRunning(t){if(!await this.host.isUp().catch(()=>!1))return!1;const e=await this.host.run([this.host.binaries().crun,"list","-q"]).catch(()=>null);return!e||0!==e.code||e.stdout.split("\n").map(t=>t.trim()).includes(t.id)||Date.now()-(t.runtime.startedAt||0)<2e3}async recover(){const t=this.store.listContainers().filter(t=>"running"===t.state);if(0!==t.length){this.host;for(const e of t)if(e.runtime.ip&&Object.keys(e.config.HostConfig.PortBindings||{}).length)try{await this.ports.start(e.id,e.config.HostConfig.PortBindings,e.runtime.ip)}catch(t){this.log(`could not restore ports for ${e.name}: ${t.message}`)}for(const e of t)this._startHealth(e);for(const e of t)this.fsevents.track(e.id,e.runtime.apiMounts);this._refreshDns().catch(t=>this.log(`dns: ${t.message}`)),this.lastLiveness=0,this._ensureReconciler()}}touch(){this.lastActivity=Date.now()}async track(t){this.streams=(this.streams||0)+1;try{return await t()}finally{this.streams-=1,this.touch()}}startResourceManager(){if(!["darwin","win32"].includes(process.platform)||this.resourceTimer)return;const t=6e4*Number(process.env.TNC_DOCK_IDLE_MINUTES??5);this.lastActivity=Date.now(),this.resourceTimer=setInterval(()=>this._manageResources(t).catch(t=>this.log(`resources: ${t.message}`)),1e4),this.resourceTimer.unref()}async _manageResources(t){if(this._host&&["vm","wsl"].includes(this.host.kind)&&!this._managing){this._managing=!0;try{if(!await this.host.isUp())return;if(!(this.store.listContainers().some(t=>"running"===t.state)||this.streams>0||this.execs.size>0&&[...this.execs.values()].some(t=>t.Running))&&t>0&&Date.now()-this.lastActivity>t)return this.log("idle: stopping the MicroVM (no containers, no activity)"),this.emitEvent("daemon","vm-sleep","microvm",{}),void await this.host.stop()}finally{this._managing=!1}}}shutdown(){this._dns&&(this._dns.proc.kill("SIGKILL"),this._dns=null),this.resourceTimer&&clearInterval(this.resourceTimer),this.resourceTimer=null;for(const t of[...this.healthTimers.keys()])this._stopHealth(t);this.timer&&clearInterval(this.timer),this.timer=null,this.ports&&this.ports.stopAll(),this.fsevents.stopAll(),this.hostfwd.stop(),this._overlayNames?.stop()}_healthConfig(t){const e={...this.store.getImage(t.image)?.config?.config?.Healthcheck||{},...t.config.Config.Healthcheck||{}};if(!e.Test||!e.Test.length||"NONE"===e.Test[0])return null;const s=(t,e)=>t>0?t/1e6:e;return{argv:"CMD-SHELL"===e.Test[0]?["/bin/sh","-c",e.Test.slice(1).join(" ")]:"CMD"===e.Test[0]?e.Test.slice(1):e.Test,interval:s(e.Interval,3e4),timeout:s(e.Timeout,3e4),startPeriod:s(e.StartPeriod,0),startInterval:s(e.StartInterval,5e3),retries:e.Retries>0?e.Retries:3}}_startHealth(t){const e=this._healthConfig(t);if(!e)return;const s=Date.now(),i={Status:"starting",FailingStreak:0,Log:[]};this._saveHealth(t.id,i);const n=async()=>{const r=this.store.getContainer(t.id);if(!r||"running"!==r.state)return;if(r.runtime.paused)return void this.healthTimers.set(t.id,setTimeout(n,e.interval));const o=Date.now(),{crun:a}=this.host.binaries(),h=this.host.spawn([a,"exec",t.id,...e.argv]);let c="";h.stdout.on("data",t=>{c.length<4096&&(c+=t)}),h.stderr.on("data",t=>{c.length<4096&&(c+=t)}),h.end();const u=setTimeout(()=>h.kill("SIGKILL"),e.timeout);let l=await h.exited;clearTimeout(u),Date.now()-o>=e.timeout&&(l=-1,c=`Health check exceeded timeout (${e.timeout}ms)`);const d=this.store.getContainer(t.id)?.runtime.health||i,m=Date.now()-s<e.startPeriod,f=d.Status;if(0===l?(d.Status="healthy",d.FailingStreak=0):m&&"healthy"!==d.Status||(d.FailingStreak+=1,d.FailingStreak>=e.retries&&(d.Status="unhealthy")),d.Log=[...d.Log,{Start:iso(o),End:iso(Date.now()),ExitCode:l,Output:c}].slice(-5),!this._saveHealth(t.id,d))return;d.Status!==f&&this.emitEvent("container",`health_status: ${d.Status}`,t.id,this._attrs(t));const g=Date.now()-s<e.startPeriod?e.startInterval:e.interval;this.healthTimers.set(t.id,setTimeout(n,g))};this.healthTimers.set(t.id,setTimeout(n,e.startPeriod>0?e.startInterval:e.interval))}_saveHealth(t,e){const s=this.store.getContainer(t);return!(!s||"running"!==s.state||(this.store.updateContainer(t,{runtime:{...s.runtime,health:e}}),0))}_stopHealth(t){clearTimeout(this.healthTimers.get(t)),this.healthTimers.delete(t)}createExec(t,e={}){const s=this.findContainer(t);if("running"!==s.state)throw httpError(409,`Container ${s.id} is not running`);if(s.runtime.paused)throw httpError(409,`Container ${s.id} is paused, unpause the container before exec`);if(!Array.isArray(e.Cmd)||0===e.Cmd.length)throw httpError(400,"No exec command specified");const i=crypto.randomBytes(32).toString("hex");return this.log(`exec create ${s.name}: ${JSON.stringify(redactExecBody(e))}`),this.execs.set(i,{ID:i,containerId:s.id,config:e,Running:!1,ExitCode:null,Pid:0}),this.emitEvent("container",`exec_create: ${e.Cmd.join(" ")}`,s.id,this._attrs(s,{execID:i})),{Id:i}}getExec(t){const e=this.execs.get(t);if(!e)throw httpError(404,`No such exec instance: ${t}`);return e}async startExec(t){const e=this.getExec(t);if(e.Running||null!==e.ExitCode)throw httpError(409,`exec ${t} has already run`);const s=this.findContainer(e.containerId);if("running"!==s.state)throw httpError(409,`Container ${s.id} is not running`);await this.ready();const{crun:i,shim:n}=this.host.binaries();let r=[i,"exec"];if(e.config.Tty){const s=`${this.rootfs.root}/exec-${t.slice(0,16)}.sock`;e.ctl=`${this.rootfs.root}/exec-${t.slice(0,16)}.ctl`,r=[n,"relay","--console",s,"--ctl",e.ctl,"--",i,"exec","--tty","--console-socket",s]}e.config.WorkingDir&&r.push("--cwd",e.config.WorkingDir);const o=this.store.getImage(s.image)?.config?.config?.Env||[],a=this._hostname(s);for(const t of mergeEnv(o,[`HOSTNAME=${a}`,...s.config.Config.Env||[],...e.config.Env||[]]))r.push("--env",t);if(e.config.User){const t=e.config.User.split(":").some(t=>!/^\d+$/.test(t))?resolveUser(e.config.User,await this.rootfs.readFile(s.id,"/etc/passwd"),await this.rootfs.readFile(s.id,"/etc/group")):resolveUser(e.config.User);r.push("--user",`${t.uid}:${t.gid}`)}r.push(s.id,...e.config.Cmd),e.Running=!0,this.emitEvent("container",`exec_start: ${e.config.Cmd.join(" ")}`,s.id,this._attrs(s,{execID:t}));const h=this.host.spawn(r);return h.exited.then(i=>{e.Running=!1,e.ExitCode=i,this.emitEvent("container","exec_die",s.id,this._attrs(s,{execID:t,exitCode:String(i)}))}),h}execInspect(t){const e=this.getExec(t);return{ID:e.ID,Running:e.Running,ExitCode:e.ExitCode,ProcessConfig:{tty:!!e.config.Tty,entrypoint:e.config.Cmd[0],arguments:e.config.Cmd.slice(1),privileged:!1,user:e.config.User||""},OpenStdin:!!e.config.AttachStdin,OpenStderr:!!e.config.AttachStderr,OpenStdout:!!e.config.AttachStdout,CanRemove:!1,ContainerID:e.containerId,DetachKeys:"",Pid:e.Pid}}async probe(t,e){const s=this.findContainer(t);if("running"!==s.state)return!1;const i=Object.values(s.runtime.networks||{}).find(t=>t.ip),n=i?.ip||(s.runtime.networks?.host?"127.0.0.1":null);return!!(n&&e>0)&&("function"==typeof this.host.probe?this.host.probe(n,e):new Promise(t=>{const s=this.host.connect(n,e),i=e=>{clearTimeout(r),s.destroy(),t(e)},r=setTimeout(()=>i(!1),1500);s.once("ready",()=>i(!0)),s.once("connect",()=>i(!0)),s.once("error",()=>i(!1))}))}async _ctl(t,e){await this.host.run(["timeout","2","sh","-c",'printf "%s\\n" "$1" > "$2"',"dock",e,t]).catch(()=>{})}async resize(t,e,s){const i=this.findContainer(t);if("running"!==i.state)throw httpError(409,`Container ${i.id} is not running`);i.config.Config.Tty&&e>0&&s>0&&await this._ctl(this.rootfs.ctlFifo(i.id),`resize ${e} ${s}`)}async resizeExec(t,e,s){const i=this.getExec(t);i.ctl&&i.Running&&e>0&&s>0&&await this._ctl(i.ctl,`resize ${e} ${s}`)}openStdin(t){const e=this.host.spawn(["sh","-c",'exec cat > "$1"',"dock",this.rootfs.stdinFifo(t.id)]);return e.stdout.resume(),e.stderr.resume(),{write:t=>e.write(t),end:async()=>{e.end(),await e.exited,t.config.Config.StdinOnce&&await this._ctl(this.rootfs.ctlFifo(t.id),"eof")}}}_status(t){const e=t.runtime;if("running"===t.state){if(e.paused)return`Up ${humanDuration(Date.now()-e.startedAt)} (Paused)`;const t=e.health?` (${"starting"===e.health.Status?"health: starting":e.health.Status})`:"";return`Up ${humanDuration(Date.now()-e.startedAt)}${t}`}return"created"===t.state?"Created":"removing"===t.state?"Removal In Progress":`Exited (${e.exitCode}) ${humanDuration(Date.now()-(e.finishedAt||Date.now()))} ago`}_portsList(t){const e=[],s=t.runtime.ports||{},i=this.store.getImage(t.image)?.config?.config?.ExposedPorts||{};for(const n of Object.keys({...i,...t.config.Config.ExposedPorts||{},...s})){const[t,i="tcp"]=n.split("/"),r=s[n];if(r?.length)for(const s of r)e.push({IP:s.HostIp,PrivatePort:Number(t),PublicPort:Number(s.HostPort),Type:i});else e.push({PrivatePort:Number(t),Type:i})}return e}_networks(t){const e={};for(const s of t.config.networks||[{name:"bridge",aliases:[]}]){const i=t.runtime.networks?.[s.name]||{};let n=null;try{n=this.network.resolve(s.name)}catch(t){}e[s.name]={IPAMConfig:null,Links:null,Aliases:s.aliases?.length?s.aliases:null,NetworkID:n?.id||s.name,EndpointID:i.ip?`${t.id.slice(0,32)}${(n?.id||"").slice(0,32)}`:"",Gateway:i.gateway||"",IPAddress:i.ip||"",IPPrefixLen:i.ip?i.prefixLen:0,IPv6Gateway:i.gateway6||"",GlobalIPv6Address:i.ip6||"",GlobalIPv6PrefixLen:i.ip6?64:0,MacAddress:i.mac||"",DNSNames:i.ip&&n?.dns?[...new Set([t.name,...s.aliases||[],t.id.slice(0,12)])]:null}}return e}async connectNetwork(t,e,s={}){this.host;const i=this.network.resolve(t);let n=this.findContainer(e);const r=n.config.networks||[];if(r.some(t=>t.name===i.name))throw httpError(403,`endpoint with name ${n.name} already exists in network ${i.name}`);if(["host","null"].includes(i.driver)||r.some(t=>["host","none"].includes(t.name)))throw httpError(400,"container cannot be connected to multiple networks with one of the networks in host or none mode");const o={name:i.name,aliases:s.Aliases||[]},a=[...r,o];if("running"===n.state){await this.ready();const t=Object.values(n.runtime.networks||{}).map(t=>t.idx??-1),e=Math.max(-1,...t)+1,s=await this.network.attachOne(n.id,i,e,o.aliases);this._hostIsolated(n)&&await this.network.isolateHost(n.id,[s.gateway]),n=this.store.updateContainer(n.id,{runtime:{...n.runtime,networks:{...n.runtime.networks,[i.name]:s}}})}this.store.db.prepare("UPDATE containers SET config = ? WHERE id = ?").run(JSON.stringify({...n.config,networks:a}),n.id),this.emitEvent("network","connect",i.id,{container:n.id,name:i.name,type:i.driver}),this._refreshHosts([i.name])}async disconnectNetwork(t,e){this.host;const s=this.network.resolve(t);let i=this.findContainer(e);const n=(i.config.networks||[]).filter(t=>t.name!==s.name);if(n.length===(i.config.networks||[]).length)throw httpError(404,`container ${i.id} is not connected to network ${s.name}`);const r=i.runtime.networks?.[s.name];if("running"===i.state&&r){await this.network.detachOne(i.id,r);const t={...i.runtime.networks};delete t[s.name],i=this.store.updateContainer(i.id,{runtime:{...i.runtime,networks:t}})}if(this.store.db.prepare("UPDATE containers SET config = ? WHERE id = ?").run(JSON.stringify({...i.config,networks:n}),i.id),this.emitEvent("network","disconnect",s.id,{container:i.id,name:s.name,type:s.driver}),this._refreshHosts([s.name]),"running"===i.state)try{fs.writeFileSync(path.join(this.containerDir(i.id),"hosts"),this._hostsContent(i))}catch(t){}}async removeNetwork(t){this.host;const e=this.network.resolve(t);if(this.store.listContainers().filter(t=>(t.config.networks||[]).some(t=>t.name===e.name)).length)throw httpError(403,`error while removing network: network ${e.name} id ${e.id} has active endpoints`);await this.network.remove(e),this.emitEvent("network","destroy",e.id,{name:e.name,type:e.driver})}networkInspect(t){const e={};for(const s of this.store.listContainers()){const i=s.runtime.networks?.[t.name];"running"===s.state&&i&&(e[s.id]={Name:s.name,EndpointID:s.id,MacAddress:i.mac||"",IPv4Address:i.ip?`${i.ip}/${i.prefixLen}`:"",IPv6Address:i.ip6?`${i.ip6}/64`:""})}return{Name:t.name,Id:t.id,Created:t.createdAt?iso(t.createdAt):"0001-01-01T00:00:00Z",Scope:"local",Driver:t.driver,EnableIPv6:!!t.subnet6,IPAM:{Driver:"default",Options:null,Config:[...t.subnet?[{Subnet:t.subnet,Gateway:t.gateway}]:[],...t.subnet6?[{Subnet:t.subnet6,Gateway:t.gateway6}]:[]]},Internal:!!t.internal,Attachable:"overlay"===t.driver,Ingress:!1,ConfigFrom:{Network:""},ConfigOnly:!1,Containers:e,Options:t.options||{},Labels:t.labels||{}}}containerSummary(t){const e=processArgs(t.config.Config,this.store.getImage(t.image)?.config?.config||{});return{Id:t.id,Names:[`/${t.name}`],Image:t.config.imageRef,ImageID:t.image,Command:e.join(" "),Created:Math.floor(t.createdAt/1e3),Ports:this._portsList(t),Labels:t.config.Config.Labels||{},State:"removing"===t.state?"removing":t.runtime.paused&&"running"===t.state?"paused":t.state,Status:this._status(t),HostConfig:{NetworkMode:t.config.HostConfig.NetworkMode||"bridge"},NetworkSettings:{Networks:this._networks(t)},Mounts:t.runtime.apiMounts||[]}}listContainers({all:t=!1,filters:e={}}={}){let s=this.store.listContainers();t||e.status||(s=s.filter(t=>"running"===t.state));const i=(t,i)=>{e[t]?.length&&(s=s.filter(s=>e[t].some(t=>i(s,t))))};return i("status",(t,e)=>"paused"===e?"running"===t.state&&!!t.runtime.paused:"running"===e?"running"===t.state&&!t.runtime.paused:t.state===e),i("name",(t,e)=>t.name.includes(e.replace(/^\//,""))),i("id",(t,e)=>t.id.startsWith(e)),e.label?.length&&(s=s.filter(t=>e.label.every(e=>labelMatch(t.config.Config.Labels,e)))),i("ancestor",(t,e)=>{const s=this.findImage(e);return s&&s.id===t.image}),s.map(t=>this.containerSummary(t))}inspect(t){const e=this.findContainer(t),s=e.runtime,i=this.store.getImage(e.image)?.config?.config||{},n=processArgs(e.config.Config,i),r="running"===e.state,o=Object.values(s.networks||{})[0]||{};return{Id:e.id,Created:iso(e.createdAt),Path:n[0]||"",Args:n.slice(1),State:{Status:"removing"===e.state?"removing":r&&s.paused?"paused":e.state,Running:r,Paused:!(!r||!s.paused),Restarting:!1,OOMKilled:!1,Dead:!1,Pid:r&&s.pid||0,ExitCode:s.exitCode||0,Error:s.error||"",StartedAt:iso(s.startedAt),...r&&s.health?{Health:s.health}:{},FinishedAt:iso(s.finishedAt)},Image:e.image,ResolvConfPath:path.join(this.containerDir(e.id),"resolv.conf"),HostnamePath:path.join(this.containerDir(e.id),"hostname"),HostsPath:path.join(this.containerDir(e.id),"hosts"),LogPath:this.logFile(e.id),Name:`/${e.name}`,RestartCount:s.restartCount||0,Driver:"overlayfs",Platform:"linux",MountLabel:"",ProcessLabel:"",AppArmorProfile:"",HostConfig:{NetworkMode:"default",RestartPolicy:{Name:"no",MaximumRetryCount:0},...e.config.HostConfig},GraphDriver:{Name:"overlayfs",Data:null},Mounts:s.apiMounts||[],Config:{Hostname:e.config.Config.Hostname||e.id.slice(0,12),Domainname:"",User:e.config.Config.User||i.User||"",AttachStdin:!!e.config.Config.AttachStdin,AttachStdout:!!e.config.Config.AttachStdout,AttachStderr:!!e.config.Config.AttachStderr,ExposedPorts:{...i.ExposedPorts||{},...e.config.Config.ExposedPorts||{}},Tty:!!e.config.Config.Tty,OpenStdin:!!e.config.Config.OpenStdin,StdinOnce:!!e.config.Config.StdinOnce,Env:mergeEnv(i.Env,e.config.Config.Env||[]),Cmd:e.config.Config.Cmd||i.Cmd||null,Image:e.config.imageRef,Volumes:i.Volumes||null,WorkingDir:e.config.Config.WorkingDir||i.WorkingDir||"",Entrypoint:e.config.Config.Entrypoint||i.Entrypoint||null,Labels:{...i.Labels||{},...e.config.Config.Labels||{}},StopSignal:e.config.Config.StopSignal||i.StopSignal,...i.Healthcheck||e.config.Config.Healthcheck?{Healthcheck:{...i.Healthcheck||{},...e.config.Config.Healthcheck||{}}}:{}},NetworkSettings:{Bridge:"",SandboxID:"",HairpinMode:!1,SandboxKey:s.network?.nsPath||"",Ports:s.ports||{},Gateway:o.gateway||"",IPAddress:o.ip||"",IPPrefixLen:o.ip?o.prefixLen:0,IPv6Gateway:o.gateway6||"",GlobalIPv6Address:o.ip6||"",GlobalIPv6PrefixLen:o.ip6?64:0,MacAddress:o.mac||"",Networks:this._networks(e)}}}}const COMPRESSED=[[31,139],[66,90,104],[253,55,122,88,90],[40,181,47,253]];function completeTar(t){const e=fs.openSync(t,"r+");try{const t=Buffer.alloc(6);if(fs.readSync(e,t,0,t.length,0),COMPRESSED.some(e=>e.every((e,s)=>t[s]===e)))return;const s=fs.fstatSync(e).size;fs.writeSync(e,Buffer.alloc((512-s%512)%512+1024),0,void 0,s)}finally{fs.closeSync(e)}}function joinedContainer(t){return"string"==typeof t&&t.startsWith("container:")&&t.length>10?t.slice(10):null}function sizeBytes(t){const e=/^(\d+(?:\.\d+)?)\s*([kmg]?)b?$/i.exec(String(t||"").trim());return e?Math.round(parseFloat(e[1])*{"":1,k:1024,m:1048576,g:1024**3}[e[2].toLowerCase()]):0}function logRotationArgs(t={}){const e=t?.Type||"json-file";if("json-file"!==e&&"local"!==e)throw httpError(400,`log driver "${e}" is not supported; use json-file`);const s=t?.Config||{},i=sizeBytes(s["max-size"]||process.env.TNC_DOCK_LOG_MAX_SIZE||"20m"),n=parseInt(s["max-file"]||process.env.TNC_DOCK_LOG_MAX_FILE||"5",10);return i>0?["--log-max-bytes",String(i),"--log-max-files",String(Math.max(1,n))]:[]}function stripEmpty(t={}){return Object.fromEntries(Object.entries(t||{}).filter(([,t])=>!(null==t||""===t||Array.isArray(t)&&!t.length||"object"==typeof t&&!Array.isArray(t)&&!Object.keys(t).length)))}function labelMatch(t={},e){const s=e.indexOf("=");return-1===s?Object.prototype.hasOwnProperty.call(t||{},e):(t||{})[e.slice(0,s)]===e.slice(s+1)}function signalArg(t){const e=String(t).trim().toUpperCase().replace(/^SIG/,"");if(/^\d+$/.test(e))return e;const s=/^RTM(IN|AX)(?:([+-])(\d+))?$/.exec(e);if(s){const t="IN"===s[1]?34:64,e=s[3]?Number(s[3])*("-"===s[2]?-1:1):0;return String(t+e)}return e}function humanDuration(t){const e=Math.round(t/1e3);if(e<1)return"Less than a second";if(1===e)return"1 second";if(e<60)return`${e} seconds`;const s=Math.round(e/60);if(1===s)return"About a minute";if(s<60)return`${s} minutes`;const i=Math.round(e/3600);if(1===i)return"About an hour";if(i<48)return`${i} hours`;const n=Math.round(i/24);return n<14?`${n} days`:n<60?`${Math.round(n/7)} weeks`:n<730?`${Math.round(n/30)} months`:`${Math.round(n/365)} years`}module.exports={Engine,humanDuration,labelMatch,redactExecBody,completeTar,signalArg};
@@ -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"),path=require("path"),https=require("https"),tls=require("tls"),crypto=require("crypto"),{Readable}=require("stream"),{dockHome}=require("../paths"),certsDir=(e=dockHome())=>path.join(e,"certs.d"),safeHost=e=>/^[A-Za-z0-9.-]+(:\d{1,5})?$|^\[[0-9a-f:]+\](:\d{1,5})?$/i.test(e);function caFor(e,t=dockHome()){if(!safeHost(e))return null;try{return fs.readFileSync(path.join(certsDir(t),e,"ca.crt"),"utf8")}catch(e){return null}}const fingerprintOf=e=>new crypto.X509Certificate(e).fingerprint256;function fetchCertificate(e,t,{timeout:r=1e4}={}){return new Promise((o,n)=>{const s=tls.connect({host:e,port:t,servername:require("net").isIP(e)?void 0:e,rejectUnauthorized:!1,timeout:r},()=>{const r=s.getPeerCertificate(!0)?.raw;if(s.end(),!r)return n(new Error(`${e}:${t} sent no certificate`));o(`-----BEGIN CERTIFICATE-----\n${r.toString("base64").match(/.{1,64}/g).join("\n")}\n-----END CERTIFICATE-----\n`)});s.on("timeout",()=>s.destroy(new Error(`${e}:${t} did not answer`))),s.on("error",n)})}function trust(e,t,r=dockHome()){if(!safeHost(e))throw new Error(`invalid registry address ${e}`);const o=path.join(certsDir(r),e);fs.mkdirSync(o,{recursive:!0}),fs.writeFileSync(path.join(o,"ca.crt"),t)}function tlsFetch(e,t={},{home:r=dockHome(),redirects:o=10}={}){const n=new URL(e),s="https:"===n.protocol?caFor(n.host,r):null;return s?new Promise((e,i)=>{const c={...t.headers||{}},a=https.request(n,{method:t.method||"GET",headers:c,ca:s,signal:t.signal},s=>{const c=s.statusCode;if([301,302,303,307,308].includes(c)&&s.headers.location&&"manual"!==t.redirect){if(s.resume(),o<=0)return i(new Error(`too many redirects from ${n.host}`));const a=new URL(s.headers.location,n),d=303===c?{...t,method:"GET",body:void 0}:t;return a.host!==n.host&&d.headers&&(d.headers=Object.fromEntries(Object.entries(d.headers).filter(([e])=>"authorization"!==e.toLowerCase()))),e(tlsFetch(a.href,d,{home:r,redirects:o-1}))}const a=new Headers;for(const[e,t]of Object.entries(s.headers))for(const r of[].concat(t))a.append(e,r);const d=204===c||304===c||"HEAD"===(t.method||"GET").toUpperCase();e(new Response(d?null:Readable.toWeb(s),{status:c,headers:a}))});a.on("error",i);const d=t.body;return null==d?a.end():"string"==typeof d||Buffer.isBuffer(d)||d instanceof Uint8Array?a.end(d):void(async()=>{for await(const e of d)a.write(e)||await new Promise(e=>a.once("drain",e));a.end()})().catch(e=>a.destroy(e))}):fetch(e,t)}module.exports={tlsFetch,caFor,trust,fetchCertificate,fingerprintOf,certsDir};
@@ -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"),{Readable}=require("stream"),{MANIFEST_LIST_V2,MANIFEST_V2,DEFAULT_REGISTRY_HOST,DEFAULT_REGISTRY_API}=require("../api/wire"),MEDIA={OCI_INDEX:"application/vnd.oci.image.index.v1+json",OCI_MANIFEST:"application/vnd.oci.image.manifest.v1+json",OCI_CONFIG:"application/vnd.oci.image.config.v1+json",OCI_LAYER:"application/vnd.oci.image.layer.v1.tar",OCI_LAYER_GZIP:"application/vnd.oci.image.layer.v1.tar+gzip",OCI_LAYER_ZSTD:"application/vnd.oci.image.layer.v1.tar+zstd",V2_LIST:MANIFEST_LIST_V2,V2_MANIFEST:MANIFEST_V2},UPLOAD_RETRIES=5,DOWNLOAD_RETRIES=5,sleep=t=>new Promise(e=>setTimeout(e,t)),uploadChunkSize=()=>Math.max(65536,Number(process.env.TNC_DOCK_UPLOAD_CHUNK)||16<<20),ACCEPT=[MEDIA.OCI_INDEX,MEDIA.V2_LIST,MEDIA.OCI_MANIFEST,MEDIA.V2_MANIFEST].join(", ");function httpError(t,e){const s=new Error(e);return s.statusCode=t,s}function parseAuthenticate(t){const e=/^\s*(Bearer|Basic)\b\s*(.*)$/i.exec(t||"");if(!e)return null;const s={scheme:e[1].toLowerCase()};for(const[,t,a,i]of e[2].matchAll(/([\w-]+)\s*=\s*(?:"((?:[^"\\]|\\.)*)"|([^\s,]+))/g))s[t.toLowerCase()]=void 0!==a?a.replace(/\\(.)/g,"$1"):i;return"bearer"!==s.scheme||s.realm?s:null}function parseChallenge(t){const e=parseAuthenticate(t);return e&&"bearer"===e.scheme?e:null}function registryScheme(t,e=process.env,s=null){const a=t.replace(/:\d+$/,"").replace(/^\[|\]$/g,"");if("localhost"===a||a.endsWith(".localhost")||"::1"===a||/^127\./.test(a))return"http";const i=(e.TNC_DOCK_INSECURE_REGISTRIES||"").split(",").map(t=>t.trim().toLowerCase()).filter(Boolean),r=s||require("../config").readConfig();i.push(...(r.insecureRegistries||[]).map(t=>String(t).toLowerCase()));for(const t of r.registryMirrors||[])/^http:\/\//i.test(t)&&i.push(new URL(t).host.toLowerCase());return i.includes(t.toLowerCase())||i.includes(a.toLowerCase())?"http":"https"}function mirrorsFor(t,e=require("../config").readConfig()){return t.domain!==DEFAULT_REGISTRY_HOST?[]:(e.registryMirrors||[]).map(t=>{try{return new URL(/^[a-z]+:\/\//i.test(t)?t:`https://${t}`).host}catch(t){return null}}).filter(Boolean)}const basic=t=>"Basic "+Buffer.from(`${t.username}:${t.password}`).toString("base64");class RegistryClient{constructor(t,{userAgent:e="tnc-dock",credentials:s=null,actions:a=["pull"],mountFrom:i=[]}={}){this.ref=t,this.origin=`${registryScheme(t.apiHost)}://${t.apiHost}`,this.base=`${this.origin}/v2/${t.path}`,this.userAgent=e,this.credentials=s,this.scopes=t.path?[`repository:${t.path}:${a.join(",")}`,...i.map(t=>`repository:${t}:pull`)]:[],this.authorization=null,this.token=null}get name(){return this.ref.familiarName||this.ref.apiHost}async _authenticate(t){if("basic"===t.scheme){if(!this.credentials)throw httpError(401,`${this.ref.apiHost} requires a login: run thinknsh login ${this.ref.domain||this.ref.apiHost}`);return void(this.authorization=basic(this.credentials))}const e=new URL(t.realm);t.service&&e.searchParams.set("service",t.service);const s=new Set(this.scopes);for(const e of(t.scope||"").split(" ").filter(Boolean))s.add(e);for(const t of s)e.searchParams.append("scope",t);const a={"User-Agent":this.userAgent};this.credentials&&(a.Authorization=basic(this.credentials));const i=await fetch(e,{headers:a});if(401===i.status||403===i.status)throw await(i.body?.cancel()),httpError(401,this.credentials?`unauthorized: incorrect username or password for ${this.ref.apiHost}`:`${this.name} needs a login (HTTP ${i.status}): run thinknsh login ${this.ref.domain||this.ref.apiHost}`);if(!i.ok)throw await(i.body?.cancel()),httpError(502,`registry auth failed for ${this.name}: HTTP ${i.status}`);const r=await i.json().catch(()=>({}));if(this.token=r.token||r.access_token,!this.token)throw httpError(502,`registry auth for ${this.name} returned no token`);this.authorization=`Bearer ${this.token}`}async request(t,e,{headers:s={},body:a,retried:i=!1}={}){const r={method:t,headers:{"User-Agent":this.userAgent,...s,...this.authorization?{Authorization:this.authorization}:{}},redirect:"follow"};void 0!==a&&(r.body="function"==typeof a?a():a,r.body&&"function"==typeof r.body[Symbol.asyncIterator]&&(r.duplex="half"));const o=await fetch(e,r);if(401===o.status&&!i){const i=parseAuthenticate(o.headers.get("www-authenticate"));return await(o.body?.cancel()),i?(await this._authenticate(i),this.request(t,e,{headers:s,body:a,retried:!0})):o}return o}async _get(t,e={}){const s=()=>httpError(404,`pull access denied for ${this.name}, repository does not exist or may require authentication`+(this.credentials?"":`: run thinknsh login ${this.ref.domain}`));let a;try{a=await this.request("GET",t,{headers:e})}catch(t){if(401===t.statusCode&&!this.credentials)throw s();throw t}if(401===a.status||403===a.status||404===a.status)throw await(a.body?.cancel()),s();if(!a.ok)throw await(a.body?.cancel()),httpError(502,`registry error for ${this.name}: HTTP ${a.status}`);return a}async manifest(t){const e=await this._get(`${this.base}/manifests/${t}`,{Accept:ACCEPT}),s=Buffer.from(await e.arrayBuffer()),a="sha256:"+crypto.createHash("sha256").update(s).digest("hex");if(t.startsWith("sha256:")&&a!==t)throw httpError(502,`manifest digest mismatch for ${this.ref.familiarName}: expected ${t}, got ${a}`);const i=JSON.parse(s.toString("utf8"));return{body:i,digest:a,size:s.length,mediaType:i.mediaType||e.headers.get("content-type")}}async blob(t,e,s,a){if(fs.existsSync(e)&&fs.statSync(e).size===s)return!1;const i=`${e}.${crypto.randomBytes(6).toString("hex")}.part`;let r=crypto.createHash("sha256"),o=fs.createWriteStream(i),n=0,c=0;try{for(let e=0;;e++)try{const e=await this._get(`${this.base}/blobs/${t}`,n?{Range:`bytes=${n}-`}:{});n&&206!==e.status&&(await new Promise(t=>o.end(t)),o=fs.createWriteStream(i),r=crypto.createHash("sha256"),n=0);for await(const t of Readable.fromWeb(e.body))r.update(t),n+=t.length,o.write(t)||await new Promise(t=>o.once("drain",t)),a&&Date.now()-c>100&&(c=Date.now(),a(n,s));break}catch(t){if(t.statusCode||e>=5||s&&n>=s)throw t;await sleep(500*2**e)}await new Promise((t,e)=>o.end(s=>s?e(s):t()));const l="sha256:"+r.digest("hex");if(l!==t)throw httpError(502,`blob digest mismatch: expected ${t}, got ${l}`);return fs.renameSync(i,e),a?.(n,s),!0}catch(t){throw o.destroy(),fs.rmSync(i,{force:!0}),t}}_pushDenied(t){return httpError(404===t.status?404:403,`denied: requested access to ${this.name} is denied (HTTP ${t.status})`+(this.credentials?"":`; log in first: thinknsh login ${this.ref.domain}`))}async _expect(t,e,s){if(e.includes(t.status))return t;let a="";try{const e=await t.json();a=e?.errors?.[0]?.message?`: ${e.errors[0].message}`:""}catch(t){}if(401===t.status||403===t.status)throw this._pushDenied(t);throw httpError(502,`${s} failed for ${this.name}: HTTP ${t.status}${a}`)}async hasBlob(t){const e=await this.request("HEAD",`${this.base}/blobs/${t}`);return await(e.body?.cancel()),200===e.status||(404===e.status||await this._expect(e,[200,404],"blob check"),!1)}async mountBlob(t,e){const s=await this.request("POST",`${this.base}/blobs/uploads/?mount=${encodeURIComponent(t)}&from=${encodeURIComponent(e)}`);return await(s.body?.cancel()),201===s.status||(202===s.status&&s.headers.get("location")?new URL(s.headers.get("location"),this.origin):null)}async startUpload(){const t=await this._expect(await this.request("POST",`${this.base}/blobs/uploads/`),[202],"upload start");await(t.body?.cancel());const e=t.headers.get("location");if(!e)throw httpError(502,`registry ${this.ref.apiHost} gave no upload location`);return new URL(e,this.origin)}async uploadBlob(t,e,s,a,i){const r=uploadChunkSize();if(a>r)try{return await this._uploadChunked(t,e,s,a,r,i)}catch(e){if(!e.chunksRefused)throw e;t=await this.startUpload()}for(let r=0;;r++)try{return await this._uploadWhole(t,e,s,a,i),t}catch(e){if(e.statusCode||r>=5)throw e;await sleep(500*2**r),t=await this.startUpload()}}async _uploadWhole(t,e,s,a,i){const r=new URL(t);r.searchParams.set("digest",e);const o=await this.request("PUT",r,{headers:{"Content-Type":"application/octet-stream","Content-Length":String(a)},body:()=>async function*(){let t=0,e=0;for await(const r of fs.createReadStream(s,{highWaterMark:1<<20}))t+=r.length,i&&Date.now()-e>100&&(e=Date.now(),i(t,a)),yield r}()});await this._expect(o,[201,204],"blob upload"),await(o.body?.cancel()),i?.(a,a)}async _uploadChunked(t,e,s,a,i,r){let o=new URL(t),n=0,c=0;const l=fs.openSync(s,"r");try{for(;n<a;){const t=Math.min(n+i,a),e=Buffer.alloc(t-n);let s;fs.readSync(l,e,0,e.length,n);try{s=await this.request("PATCH",o,{headers:{"Content-Type":"application/octet-stream","Content-Length":String(e.length),"Content-Range":`${n}-${t-1}`},body:e})}catch(t){if(t.statusCode||++c>5)throw t;await sleep(500*2**(c-1)),({url:o,offset:n}=await this._resumePoint(o));continue}if(202===s.status){await(s.body?.cancel()),s.headers.get("location")&&(o=new URL(s.headers.get("location"),this.origin));const e=/^(?:bytes=)?0-(\d+)$/.exec(s.headers.get("range")||"");n=e?Number(e[1])+1:t,r?.(n,a),c=0;continue}if(0===n&&[400,405,413,416,501].includes(s.status)){await(s.body?.cancel());const t=new Error("chunked uploads refused");throw t.chunksRefused=!0,t}s.status>=500&&++c<=5?(await(s.body?.cancel()),await sleep(500*2**(c-1)),({url:o,offset:n}=await this._resumePoint(o))):await this._expect(s,[202],"blob upload")}}finally{fs.closeSync(l)}const h=new URL(o);h.searchParams.set("digest",e);const d=await this.request("PUT",h,{headers:{"Content-Length":"0"},body:Buffer.alloc(0)});return await this._expect(d,[201,204],"blob upload"),await(d.body?.cancel()),r?.(a,a),o}async _resumePoint(t){const e=await this.request("GET",t);if(await(e.body?.cancel()),404===e.status)throw httpError(502,`the registry dropped the upload session for ${this.name}; push again`);await this._expect(e,[200,204],"upload status");const s=/^(?:bytes=)?0-(\d+)$/.exec(e.headers.get("range")||"");return{url:e.headers.get("location")?new URL(e.headers.get("location"),this.origin):t,offset:s?Number(s[1])+1:0}}async putManifest(t,e,s){const a=await this.request("PUT",`${this.base}/manifests/${t}`,{headers:{"Content-Type":s},body:e});await this._expect(a,[200,201],"manifest upload"),await(a.body?.cancel())}async verify(){const t=await this.request("GET",`${this.origin}/v2/`);if(await(t.body?.cancel()),401===t.status||403===t.status)throw httpError(401,`unauthorized: incorrect username or password for ${this.ref.apiHost}`);if(!t.ok)throw httpError(502,`login to ${this.ref.apiHost} failed: HTTP ${t.status}`)}}const shortId=t=>t.replace(/^sha256:/,"").slice(0,12);function pickPlatform(t,e,s,a){const i=(t.manifests||[]).filter(t=>t.platform&&"linux"===t.platform.os&&t.platform.architecture===e&&(a?(t.platform.variant||("arm64"===e?"v8":""))===a:"arm64"!==e||!t.platform.variant||"v8"===t.platform.variant));if(0===i.length){const i=["linux",e,a].filter(Boolean).join("/"),r=(t.manifests||[]).filter(t=>t.platform&&"unknown"!==t.platform.architecture).map(t=>[t.platform.os,t.platform.architecture,t.platform.variant].filter(Boolean).join("/")).join(", ");throw httpError(404,`no matching manifest for ${i} in the manifest list entries for ${s.familiar} (available: ${r})`)}return i[0]}async function pullImage(t,{blobsDir:e,arch:s,variant:a,credentials:i=null,progress:r=()=>{}}){fs.mkdirSync(e,{recursive:!0});let o=new RegistryClient(t,{credentials:i});r({status:`Pulling from ${t.familiarName}`,id:t.tag||t.digest});let n=null;for(const e of mirrorsFor(t)){const s=new RegistryClient({...t,apiHost:e},{});try{n=await s.manifest(t.digest||t.tag),o=s;break}catch(t){}}n||(n=await o.manifest(t.digest||t.tag));const c=`${t.familiarName}@${n.digest}`;let l=n;if(n.body.manifests){const e=pickPlatform(n.body,s,t,a);l=await o.manifest(e.digest)}if(!l.body.config||!Array.isArray(l.body.layers))throw httpError(502,`unsupported manifest type ${l.mediaType} for ${t.familiar}`);const h=t=>path.join(e,t.replace(/^sha256:/,"")),d=l.body.config;await o.blob(d.digest,h(d.digest),d.size);const u=JSON.parse(fs.readFileSync(h(d.digest),"utf8"));!n.body.manifests&&u.architecture&&s&&u.architecture!==s&&r({status:`Warning: ${t.familiar} is a linux/${u.architecture} image (requested linux/${s})`});const p=l.body.layers.map(t=>({digest:t.digest,size:t.size,mediaType:t.mediaType}));let f=!1;for(const t of p)r({status:"Pulling fs layer",progressDetail:{},id:shortId(t.digest)});for(const t of p){const e=shortId(t.digest),s=await o.blob(t.digest,h(t.digest),t.size,(t,s)=>r({status:"Downloading",progressDetail:{current:t,total:s},id:e}));s&&(f=!0),r({status:s?"Download complete":"Already exists",progressDetail:{},id:e}),s&&r({status:"Pull complete",progressDetail:{},id:e})}return r({status:`Digest: ${n.digest}`}),{fresh:f,image:{id:d.digest,repoTags:t.tagged?[t.tagged]:[],repoDigests:[c],digest:l.digest,size:p.reduce((t,e)=>t+e.size,0),layers:p,config:u,architecture:u.architecture,os:u.os,createdAt:Date.parse(u.created)||Date.now()}}}function ociLayerType(t=""){return/zstd$/.test(t)?MEDIA.OCI_LAYER_ZSTD:/gzip$/.test(t)?MEDIA.OCI_LAYER_GZIP:/\.tar$|layer\.v1\.tar$/.test(t)?MEDIA.OCI_LAYER:t||MEDIA.OCI_LAYER_GZIP}async function sendBlob(t,e,s,a,{mountFrom:i=[],progress:r=()=>{},id:o=null}={}){if(await t.hasBlob(e))return void(o&&r({status:"Layer already exists",progressDetail:{},id:o}));let n=null;for(const s of i){const a=await t.mountBlob(e,s);if(!0===a)return void(o&&r({status:`Mounted from ${s}`,progressDetail:{},id:o}));if(a){n=a;break}}n=n||await t.startUpload(),await t.uploadBlob(n,e,s,a,o?(t,e)=>r({status:"Pushing",progressDetail:{current:t,total:e},id:o}):null),o&&r({status:"Pushed",progressDetail:{},id:o})}async function pushArtifact(t,{config:e,layers:s,annotations:a,credentials:i=null,tag:r=t.tag,progress:o=()=>{}}){const n=new RegistryClient(t,{credentials:i,actions:["pull","push"]});for(const t of s)await sendBlob(n,t.digest,t.file,t.size,{progress:o});const c=Buffer.from(e.raw),l="sha256:"+crypto.createHash("sha256").update(c).digest("hex");if(!await n.hasBlob(l)){const t=path.join(require("os").tmpdir(),`tnc-artifact-${process.pid}-${crypto.randomBytes(4).toString("hex")}`);fs.writeFileSync(t,c);try{await n.uploadBlob(await n.startUpload(),l,t,c.length)}finally{fs.rmSync(t,{force:!0})}}const h={schemaVersion:2,mediaType:MEDIA.OCI_MANIFEST,config:{mediaType:e.mediaType,digest:l,size:c.length},layers:s.map(t=>({mediaType:t.mediaType,digest:t.digest,size:t.size,...t.annotations?{annotations:t.annotations}:{}})),...a?{annotations:a}:{}},d=Buffer.from(JSON.stringify(h)),u="sha256:"+crypto.createHash("sha256").update(d).digest("hex");return await n.putManifest(r,d,MEDIA.OCI_MANIFEST),o({status:`${r}: digest: ${u} size: ${d.length}`}),{digest:u,size:d.length}}async function pushImage(t,e,{blobsDir:s,credentials:a=null,sources:i=[],progress:r=()=>{},byDigest:o=!1}){if(!t.tag&&!o)throw httpError(400,`push needs a tag: ${t.familiar}`);const n=[...new Set(i)].filter(e=>e&&e!==t.path),c=new RegistryClient(t,{credentials:a,actions:["pull","push"],mountFrom:n}),l=t=>path.join(s,t.replace(/^sha256:/,"")),h=t=>{try{return fs.statSync(l(t)).size}catch(t){return-1}};r({status:`The push refers to repository [${t.domain}/${t.path}]`});const d=h(e.id);if(d<0)throw httpError(404,`image config ${shortId(e.id)} is missing from the local store; rebuild or pull ${t.familiarName}`);const u=(e.layers||[]).map(t=>({mediaType:ociLayerType(t.mediaType),digest:t.digest,size:t.size}));for(const t of u){const e=h(t.digest);if(e<0)throw httpError(404,`layer ${shortId(t.digest)} is missing from the local store; pull or rebuild the image`);t.size=e}const p=(t,e,s)=>sendBlob(c,t,l(t),e,{mountFrom:n,progress:r,id:s}),f=[...u].reverse();for(const t of f)r({status:"Preparing",progressDetail:{},id:shortId(t.digest)});for(const t of f)await p(t.digest,t.size,shortId(t.digest));await p(e.id,d,null);const g={schemaVersion:2,mediaType:MEDIA.OCI_MANIFEST,config:{mediaType:MEDIA.OCI_CONFIG,digest:e.id,size:d},layers:u},m=Buffer.from(JSON.stringify(g)),y="sha256:"+crypto.createHash("sha256").update(m).digest("hex");return await c.putManifest(o?y:t.tag,m,MEDIA.OCI_MANIFEST),o||(r({status:`${t.tag}: digest: ${y} size: ${m.length}`}),r({aux:{Tag:t.tag,Digest:y,Size:m.length}})),{digest:y,size:m.length}}async function pushIndex(t,e,{blobsDir:s,credentials:a=null,sources:i=[],progress:r=()=>{}}){if(!t.tag)throw httpError(400,`push needs a tag: ${t.familiar}`);const o=new Set,n=[];for(const{image:c,platform:l}of e){const e=[l.os,l.architecture,l.variant].filter(Boolean).join("/");if(o.has(e))throw httpError(400,`two images for ${e} in one index`);o.add(e),r({status:`Pushing ${e}`});const h=await pushImage(t,c,{blobsDir:s,credentials:a,sources:i,progress:r,byDigest:!0});n.push({mediaType:MEDIA.OCI_MANIFEST,digest:h.digest,size:h.size,platform:{architecture:l.architecture,os:l.os||"linux",...l.variant?{variant:l.variant}:{}}})}const c={schemaVersion:2,mediaType:MEDIA.OCI_INDEX,manifests:n},l=Buffer.from(JSON.stringify(c)),h="sha256:"+crypto.createHash("sha256").update(l).digest("hex"),d=new RegistryClient(t,{credentials:a,actions:["pull","push"]});return await d.putManifest(t.tag,l,MEDIA.OCI_INDEX),r({status:`${t.tag}: digest: ${h} size: ${l.length} (${n.length} platforms)`}),r({aux:{Tag:t.tag,Digest:h,Size:l.length}}),{digest:h,size:l.length,manifests:n}}async function describe(t,{blobsDir:e,credentials:s=null}={}){const a=new RegistryClient(t,{credentials:s}),i=await a.manifest(t.digest||t.tag),r=i.size;let o;if(i.body.manifests)o=i.body.manifests.filter(t=>t.platform&&"unknown"!==t.platform.architecture).map(t=>t.platform);else{const t=i.body.config,s=path.join(e,t.digest.replace(/^sha256:/,""));fs.mkdirSync(e,{recursive:!0}),await a.blob(t.digest,s,t.size);const r=JSON.parse(fs.readFileSync(s,"utf8"));o=[{architecture:r.architecture,os:r.os,...r.variant?{variant:r.variant}:{}}]}return{descriptor:{mediaType:i.mediaType,digest:i.digest,size:r},platforms:o,manifest:i.body}}async function verifyLogin(t,e){const s=t===DEFAULT_REGISTRY_HOST?DEFAULT_REGISTRY_API:t;await new RegistryClient({apiHost:s,domain:t,path:"",familiarName:t},{credentials:e}).verify()}module.exports={pullImage,pushImage,pushIndex,pushArtifact,sendBlob,describe,verifyLogin,RegistryClient,MEDIA,shortId,parseAuthenticate,parseChallenge,registryScheme,mirrorsFor,ociLayerType};
7
+ "use strict";const{tlsFetch,caFor}=require("./certs"),fs=require("fs"),path=require("path"),crypto=require("crypto"),{Readable}=require("stream"),{MANIFEST_LIST_V2,MANIFEST_V2,DEFAULT_REGISTRY_HOST,DEFAULT_REGISTRY_API}=require("../api/wire"),MEDIA={OCI_INDEX:"application/vnd.oci.image.index.v1+json",OCI_MANIFEST:"application/vnd.oci.image.manifest.v1+json",OCI_CONFIG:"application/vnd.oci.image.config.v1+json",OCI_LAYER:"application/vnd.oci.image.layer.v1.tar",OCI_LAYER_GZIP:"application/vnd.oci.image.layer.v1.tar+gzip",OCI_LAYER_ZSTD:"application/vnd.oci.image.layer.v1.tar+zstd",V2_LIST:MANIFEST_LIST_V2,V2_MANIFEST:MANIFEST_V2},UPLOAD_RETRIES=5,DOWNLOAD_RETRIES=5,sleep=t=>new Promise(e=>setTimeout(e,t)),uploadChunkSize=()=>Math.max(65536,Number(process.env.TNC_DOCK_UPLOAD_CHUNK)||16<<20),ACCEPT=[MEDIA.OCI_INDEX,MEDIA.V2_LIST,MEDIA.OCI_MANIFEST,MEDIA.V2_MANIFEST].join(", ");function httpError(t,e){const s=new Error(e);return s.statusCode=t,s}function parseAuthenticate(t){const e=/^\s*(Bearer|Basic)\b\s*(.*)$/i.exec(t||"");if(!e)return null;const s={scheme:e[1].toLowerCase()};for(const[,t,a,i]of e[2].matchAll(/([\w-]+)\s*=\s*(?:"((?:[^"\\]|\\.)*)"|([^\s,]+))/g))s[t.toLowerCase()]=void 0!==a?a.replace(/\\(.)/g,"$1"):i;return"bearer"!==s.scheme||s.realm?s:null}function parseChallenge(t){const e=parseAuthenticate(t);return e&&"bearer"===e.scheme?e:null}function registryScheme(t,e=process.env,s=null){if(caFor(t))return"https";const a=t.replace(/:\d+$/,"").replace(/^\[|\]$/g,"");if("localhost"===a||a.endsWith(".localhost")||"::1"===a||/^127\./.test(a))return"http";const i=(e.TNC_DOCK_INSECURE_REGISTRIES||"").split(",").map(t=>t.trim().toLowerCase()).filter(Boolean),r=s||require("../config").readConfig();i.push(...(r.insecureRegistries||[]).map(t=>String(t).toLowerCase()));for(const t of r.registryMirrors||[])/^http:\/\//i.test(t)&&i.push(new URL(t).host.toLowerCase());return i.includes(t.toLowerCase())||i.includes(a.toLowerCase())?"http":"https"}function mirrorsFor(t,e=require("../config").readConfig()){return t.domain!==DEFAULT_REGISTRY_HOST?[]:(e.registryMirrors||[]).map(t=>{try{return new URL(/^[a-z]+:\/\//i.test(t)?t:`https://${t}`).host}catch(t){return null}}).filter(Boolean)}const basic=t=>"Basic "+Buffer.from(`${t.username}:${t.password}`).toString("base64");class RegistryClient{constructor(t,{userAgent:e="tnc-dock",credentials:s=null,actions:a=["pull"],mountFrom:i=[]}={}){this.ref=t,this.origin=`${registryScheme(t.apiHost)}://${t.apiHost}`,this.base=`${this.origin}/v2/${t.path}`,this.userAgent=e,this.credentials=s,this.scopes=t.path?[`repository:${t.path}:${a.join(",")}`,...i.map(t=>`repository:${t}:pull`)]:[],this.authorization=null,this.token=null}get name(){return this.ref.familiarName||this.ref.apiHost}async _authenticate(t){if("basic"===t.scheme){if(!this.credentials)throw httpError(401,`${this.ref.apiHost} requires a login: run thinknsh login ${this.ref.domain||this.ref.apiHost}`);return void(this.authorization=basic(this.credentials))}const e=new URL(t.realm);t.service&&e.searchParams.set("service",t.service);const s=new Set(this.scopes);for(const e of(t.scope||"").split(" ").filter(Boolean))s.add(e);for(const t of s)e.searchParams.append("scope",t);const a={"User-Agent":this.userAgent};this.credentials&&(a.Authorization=basic(this.credentials));const i=await tlsFetch(e,{headers:a});if(401===i.status||403===i.status)throw await(i.body?.cancel()),httpError(401,this.credentials?`unauthorized: incorrect username or password for ${this.ref.apiHost}`:`${this.name} needs a login (HTTP ${i.status}): run thinknsh login ${this.ref.domain||this.ref.apiHost}`);if(!i.ok)throw await(i.body?.cancel()),httpError(502,`registry auth failed for ${this.name}: HTTP ${i.status}`);const r=await i.json().catch(()=>({}));if(this.token=r.token||r.access_token,!this.token)throw httpError(502,`registry auth for ${this.name} returned no token`);this.authorization=`Bearer ${this.token}`}async request(t,e,{headers:s={},body:a,retried:i=!1}={}){const r={method:t,headers:{"User-Agent":this.userAgent,...s,...this.authorization?{Authorization:this.authorization}:{}},redirect:"follow"};void 0!==a&&(r.body="function"==typeof a?a():a,r.body&&"function"==typeof r.body[Symbol.asyncIterator]&&(r.duplex="half"));const o=await tlsFetch(e,r);if(401===o.status&&!i){const i=parseAuthenticate(o.headers.get("www-authenticate"));return await(o.body?.cancel()),i?(await this._authenticate(i),this.request(t,e,{headers:s,body:a,retried:!0})):o}return o}async _get(t,e={}){const s=()=>httpError(404,`pull access denied for ${this.name}, repository does not exist or may require authentication`+(this.credentials?"":`: run thinknsh login ${this.ref.domain}`));let a;try{a=await this.request("GET",t,{headers:e})}catch(t){if(401===t.statusCode&&!this.credentials)throw s();throw t}if(401===a.status||403===a.status||404===a.status)throw await(a.body?.cancel()),s();if(!a.ok)throw await(a.body?.cancel()),httpError(502,`registry error for ${this.name}: HTTP ${a.status}`);return a}async manifest(t){const e=await this._get(`${this.base}/manifests/${t}`,{Accept:ACCEPT}),s=Buffer.from(await e.arrayBuffer()),a="sha256:"+crypto.createHash("sha256").update(s).digest("hex");if(t.startsWith("sha256:")&&a!==t)throw httpError(502,`manifest digest mismatch for ${this.ref.familiarName}: expected ${t}, got ${a}`);const i=JSON.parse(s.toString("utf8"));return{body:i,digest:a,size:s.length,mediaType:i.mediaType||e.headers.get("content-type")}}async blob(t,e,s,a){if(fs.existsSync(e)&&fs.statSync(e).size===s)return!1;const i=`${e}.${crypto.randomBytes(6).toString("hex")}.part`;let r=crypto.createHash("sha256"),o=fs.createWriteStream(i),n=0,c=0;try{for(let e=0;;e++)try{const e=await this._get(`${this.base}/blobs/${t}`,n?{Range:`bytes=${n}-`}:{});n&&206!==e.status&&(await new Promise(t=>o.end(t)),o=fs.createWriteStream(i),r=crypto.createHash("sha256"),n=0);for await(const t of Readable.fromWeb(e.body))r.update(t),n+=t.length,o.write(t)||await new Promise(t=>o.once("drain",t)),a&&Date.now()-c>100&&(c=Date.now(),a(n,s));break}catch(t){if(t.statusCode||e>=5||s&&n>=s)throw t;await sleep(500*2**e)}await new Promise((t,e)=>o.end(s=>s?e(s):t()));const l="sha256:"+r.digest("hex");if(l!==t)throw httpError(502,`blob digest mismatch: expected ${t}, got ${l}`);return fs.renameSync(i,e),a?.(n,s),!0}catch(t){throw o.destroy(),fs.rmSync(i,{force:!0}),t}}_pushDenied(t){return httpError(404===t.status?404:403,`denied: requested access to ${this.name} is denied (HTTP ${t.status})`+(this.credentials?"":`; log in first: thinknsh login ${this.ref.domain}`))}async _expect(t,e,s){if(e.includes(t.status))return t;let a="";try{const e=await t.json();a=e?.errors?.[0]?.message?`: ${e.errors[0].message}`:""}catch(t){}if(401===t.status||403===t.status)throw this._pushDenied(t);throw httpError(502,`${s} failed for ${this.name}: HTTP ${t.status}${a}`)}async hasBlob(t){const e=await this.request("HEAD",`${this.base}/blobs/${t}`);return await(e.body?.cancel()),200===e.status||(404===e.status||await this._expect(e,[200,404],"blob check"),!1)}async mountBlob(t,e){const s=await this.request("POST",`${this.base}/blobs/uploads/?mount=${encodeURIComponent(t)}&from=${encodeURIComponent(e)}`);return await(s.body?.cancel()),201===s.status||(202===s.status&&s.headers.get("location")?new URL(s.headers.get("location"),this.origin):null)}async startUpload(){const t=await this._expect(await this.request("POST",`${this.base}/blobs/uploads/`),[202],"upload start");await(t.body?.cancel());const e=t.headers.get("location");if(!e)throw httpError(502,`registry ${this.ref.apiHost} gave no upload location`);return new URL(e,this.origin)}async uploadBlob(t,e,s,a,i){const r=uploadChunkSize();if(a>r)try{return await this._uploadChunked(t,e,s,a,r,i)}catch(e){if(!e.chunksRefused)throw e;t=await this.startUpload()}for(let r=0;;r++)try{return await this._uploadWhole(t,e,s,a,i),t}catch(e){if(e.statusCode||r>=5)throw e;await sleep(500*2**r),t=await this.startUpload()}}async _uploadWhole(t,e,s,a,i){const r=new URL(t);r.searchParams.set("digest",e);const o=await this.request("PUT",r,{headers:{"Content-Type":"application/octet-stream","Content-Length":String(a)},body:()=>async function*(){let t=0,e=0;for await(const r of fs.createReadStream(s,{highWaterMark:1<<20}))t+=r.length,i&&Date.now()-e>100&&(e=Date.now(),i(t,a)),yield r}()});await this._expect(o,[201,204],"blob upload"),await(o.body?.cancel()),i?.(a,a)}async _uploadChunked(t,e,s,a,i,r){let o=new URL(t),n=0,c=0;const l=fs.openSync(s,"r");try{for(;n<a;){const t=Math.min(n+i,a),e=Buffer.alloc(t-n);let s;fs.readSync(l,e,0,e.length,n);try{s=await this.request("PATCH",o,{headers:{"Content-Type":"application/octet-stream","Content-Length":String(e.length),"Content-Range":`${n}-${t-1}`},body:e})}catch(t){if(t.statusCode||++c>5)throw t;await sleep(500*2**(c-1)),({url:o,offset:n}=await this._resumePoint(o));continue}if(202===s.status){await(s.body?.cancel()),s.headers.get("location")&&(o=new URL(s.headers.get("location"),this.origin));const e=/^(?:bytes=)?0-(\d+)$/.exec(s.headers.get("range")||"");n=e?Number(e[1])+1:t,r?.(n,a),c=0;continue}if(0===n&&[400,405,413,416,501].includes(s.status)){await(s.body?.cancel());const t=new Error("chunked uploads refused");throw t.chunksRefused=!0,t}s.status>=500&&++c<=5?(await(s.body?.cancel()),await sleep(500*2**(c-1)),({url:o,offset:n}=await this._resumePoint(o))):await this._expect(s,[202],"blob upload")}}finally{fs.closeSync(l)}const h=new URL(o);h.searchParams.set("digest",e);const d=await this.request("PUT",h,{headers:{"Content-Length":"0"},body:Buffer.alloc(0)});return await this._expect(d,[201,204],"blob upload"),await(d.body?.cancel()),r?.(a,a),o}async _resumePoint(t){const e=await this.request("GET",t);if(await(e.body?.cancel()),404===e.status)throw httpError(502,`the registry dropped the upload session for ${this.name}; push again`);await this._expect(e,[200,204],"upload status");const s=/^(?:bytes=)?0-(\d+)$/.exec(e.headers.get("range")||"");return{url:e.headers.get("location")?new URL(e.headers.get("location"),this.origin):t,offset:s?Number(s[1])+1:0}}async putManifest(t,e,s){const a=await this.request("PUT",`${this.base}/manifests/${t}`,{headers:{"Content-Type":s},body:e});await this._expect(a,[200,201],"manifest upload"),await(a.body?.cancel())}async verify(){const t=await this.request("GET",`${this.origin}/v2/`);if(await(t.body?.cancel()),401===t.status||403===t.status)throw httpError(401,`unauthorized: incorrect username or password for ${this.ref.apiHost}`);if(!t.ok)throw httpError(502,`login to ${this.ref.apiHost} failed: HTTP ${t.status}`)}}const shortId=t=>t.replace(/^sha256:/,"").slice(0,12);function pickPlatform(t,e,s,a){const i=(t.manifests||[]).filter(t=>t.platform&&"linux"===t.platform.os&&t.platform.architecture===e&&(a?(t.platform.variant||("arm64"===e?"v8":""))===a:"arm64"!==e||!t.platform.variant||"v8"===t.platform.variant));if(0===i.length){const i=["linux",e,a].filter(Boolean).join("/"),r=(t.manifests||[]).filter(t=>t.platform&&"unknown"!==t.platform.architecture).map(t=>[t.platform.os,t.platform.architecture,t.platform.variant].filter(Boolean).join("/")).join(", ");throw httpError(404,`no matching manifest for ${i} in the manifest list entries for ${s.familiar} (available: ${r})`)}return i[0]}async function pullImage(t,{blobsDir:e,arch:s,variant:a,credentials:i=null,progress:r=()=>{}}){fs.mkdirSync(e,{recursive:!0});let o=new RegistryClient(t,{credentials:i});r({status:`Pulling from ${t.familiarName}`,id:t.tag||t.digest});let n=null;for(const e of mirrorsFor(t)){const s=new RegistryClient({...t,apiHost:e},{});try{n=await s.manifest(t.digest||t.tag),o=s;break}catch(t){}}n||(n=await o.manifest(t.digest||t.tag));const c=`${t.familiarName}@${n.digest}`;let l=n;if(n.body.manifests){const e=pickPlatform(n.body,s,t,a);l=await o.manifest(e.digest)}if(!l.body.config||!Array.isArray(l.body.layers))throw httpError(502,`unsupported manifest type ${l.mediaType} for ${t.familiar}`);const h=t=>path.join(e,t.replace(/^sha256:/,"")),d=l.body.config;await o.blob(d.digest,h(d.digest),d.size);const u=JSON.parse(fs.readFileSync(h(d.digest),"utf8"));!n.body.manifests&&u.architecture&&s&&u.architecture!==s&&r({status:`Warning: ${t.familiar} is a linux/${u.architecture} image (requested linux/${s})`});const p=l.body.layers.map(t=>({digest:t.digest,size:t.size,mediaType:t.mediaType}));let f=!1;for(const t of p)r({status:"Pulling fs layer",progressDetail:{},id:shortId(t.digest)});for(const t of p){const e=shortId(t.digest),s=await o.blob(t.digest,h(t.digest),t.size,(t,s)=>r({status:"Downloading",progressDetail:{current:t,total:s},id:e}));s&&(f=!0),r({status:s?"Download complete":"Already exists",progressDetail:{},id:e}),s&&r({status:"Pull complete",progressDetail:{},id:e})}return r({status:`Digest: ${n.digest}`}),{fresh:f,image:{id:d.digest,repoTags:t.tagged?[t.tagged]:[],repoDigests:[c],digest:l.digest,size:p.reduce((t,e)=>t+e.size,0),layers:p,config:u,architecture:u.architecture,os:u.os,createdAt:Date.parse(u.created)||Date.now()}}}function ociLayerType(t=""){return/zstd$/.test(t)?MEDIA.OCI_LAYER_ZSTD:/gzip$/.test(t)?MEDIA.OCI_LAYER_GZIP:/\.tar$|layer\.v1\.tar$/.test(t)?MEDIA.OCI_LAYER:t||MEDIA.OCI_LAYER_GZIP}async function sendBlob(t,e,s,a,{mountFrom:i=[],progress:r=()=>{},id:o=null}={}){if(await t.hasBlob(e))return void(o&&r({status:"Layer already exists",progressDetail:{},id:o}));let n=null;for(const s of i){const a=await t.mountBlob(e,s);if(!0===a)return void(o&&r({status:`Mounted from ${s}`,progressDetail:{},id:o}));if(a){n=a;break}}n=n||await t.startUpload(),await t.uploadBlob(n,e,s,a,o?(t,e)=>r({status:"Pushing",progressDetail:{current:t,total:e},id:o}):null),o&&r({status:"Pushed",progressDetail:{},id:o})}async function pushArtifact(t,{config:e,layers:s,annotations:a,credentials:i=null,tag:r=t.tag,progress:o=()=>{}}){const n=new RegistryClient(t,{credentials:i,actions:["pull","push"]});for(const t of s)await sendBlob(n,t.digest,t.file,t.size,{progress:o});const c=Buffer.from(e.raw),l="sha256:"+crypto.createHash("sha256").update(c).digest("hex");if(!await n.hasBlob(l)){const t=path.join(require("os").tmpdir(),`tnc-artifact-${process.pid}-${crypto.randomBytes(4).toString("hex")}`);fs.writeFileSync(t,c);try{await n.uploadBlob(await n.startUpload(),l,t,c.length)}finally{fs.rmSync(t,{force:!0})}}const h={schemaVersion:2,mediaType:MEDIA.OCI_MANIFEST,config:{mediaType:e.mediaType,digest:l,size:c.length},layers:s.map(t=>({mediaType:t.mediaType,digest:t.digest,size:t.size,...t.annotations?{annotations:t.annotations}:{}})),...a?{annotations:a}:{}},d=Buffer.from(JSON.stringify(h)),u="sha256:"+crypto.createHash("sha256").update(d).digest("hex");return await n.putManifest(r,d,MEDIA.OCI_MANIFEST),o({status:`${r}: digest: ${u} size: ${d.length}`}),{digest:u,size:d.length}}async function pushImage(t,e,{blobsDir:s,credentials:a=null,sources:i=[],progress:r=()=>{},byDigest:o=!1}){if(!t.tag&&!o)throw httpError(400,`push needs a tag: ${t.familiar}`);const n=[...new Set(i)].filter(e=>e&&e!==t.path),c=new RegistryClient(t,{credentials:a,actions:["pull","push"],mountFrom:n}),l=t=>path.join(s,t.replace(/^sha256:/,"")),h=t=>{try{return fs.statSync(l(t)).size}catch(t){return-1}};r({status:`The push refers to repository [${t.domain}/${t.path}]`});const d=h(e.id);if(d<0)throw httpError(404,`image config ${shortId(e.id)} is missing from the local store; rebuild or pull ${t.familiarName}`);const u=(e.layers||[]).map(t=>({mediaType:ociLayerType(t.mediaType),digest:t.digest,size:t.size}));for(const t of u){const e=h(t.digest);if(e<0)throw httpError(404,`layer ${shortId(t.digest)} is missing from the local store; pull or rebuild the image`);t.size=e}const p=(t,e,s)=>sendBlob(c,t,l(t),e,{mountFrom:n,progress:r,id:s}),f=[...u].reverse();for(const t of f)r({status:"Preparing",progressDetail:{},id:shortId(t.digest)});for(const t of f)await p(t.digest,t.size,shortId(t.digest));await p(e.id,d,null);const g={schemaVersion:2,mediaType:MEDIA.OCI_MANIFEST,config:{mediaType:MEDIA.OCI_CONFIG,digest:e.id,size:d},layers:u},m=Buffer.from(JSON.stringify(g)),y="sha256:"+crypto.createHash("sha256").update(m).digest("hex");return await c.putManifest(o?y:t.tag,m,MEDIA.OCI_MANIFEST),o||(r({status:`${t.tag}: digest: ${y} size: ${m.length}`}),r({aux:{Tag:t.tag,Digest:y,Size:m.length}})),{digest:y,size:m.length}}async function pushIndex(t,e,{blobsDir:s,credentials:a=null,sources:i=[],progress:r=()=>{}}){if(!t.tag)throw httpError(400,`push needs a tag: ${t.familiar}`);const o=new Set,n=[];for(const{image:c,platform:l}of e){const e=[l.os,l.architecture,l.variant].filter(Boolean).join("/");if(o.has(e))throw httpError(400,`two images for ${e} in one index`);o.add(e),r({status:`Pushing ${e}`});const h=await pushImage(t,c,{blobsDir:s,credentials:a,sources:i,progress:r,byDigest:!0});n.push({mediaType:MEDIA.OCI_MANIFEST,digest:h.digest,size:h.size,platform:{architecture:l.architecture,os:l.os||"linux",...l.variant?{variant:l.variant}:{}}})}const c={schemaVersion:2,mediaType:MEDIA.OCI_INDEX,manifests:n},l=Buffer.from(JSON.stringify(c)),h="sha256:"+crypto.createHash("sha256").update(l).digest("hex"),d=new RegistryClient(t,{credentials:a,actions:["pull","push"]});return await d.putManifest(t.tag,l,MEDIA.OCI_INDEX),r({status:`${t.tag}: digest: ${h} size: ${l.length} (${n.length} platforms)`}),r({aux:{Tag:t.tag,Digest:h,Size:l.length}}),{digest:h,size:l.length,manifests:n}}async function describe(t,{blobsDir:e,credentials:s=null}={}){const a=new RegistryClient(t,{credentials:s}),i=await a.manifest(t.digest||t.tag),r=i.size;let o;if(i.body.manifests)o=i.body.manifests.filter(t=>t.platform&&"unknown"!==t.platform.architecture).map(t=>t.platform);else{const t=i.body.config,s=path.join(e,t.digest.replace(/^sha256:/,""));fs.mkdirSync(e,{recursive:!0}),await a.blob(t.digest,s,t.size);const r=JSON.parse(fs.readFileSync(s,"utf8"));o=[{architecture:r.architecture,os:r.os,...r.variant?{variant:r.variant}:{}}]}return{descriptor:{mediaType:i.mediaType,digest:i.digest,size:r},platforms:o,manifest:i.body}}async function verifyLogin(t,e){const s=t===DEFAULT_REGISTRY_HOST?DEFAULT_REGISTRY_API:t;await new RegistryClient({apiHost:s,domain:t,path:"",familiarName:t},{credentials:e}).verify()}module.exports={pullImage,pushImage,pushIndex,pushArtifact,sendBlob,describe,verifyLogin,RegistryClient,MEDIA,shortId,parseAuthenticate,parseChallenge,registryScheme,mirrorsFor,ociLayerType};
@@ -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"),path=require("path"),{DockApi,demuxer,enc}=require("./cli/api"),DUMPS={postgres:{file:"postgres.sql",dump:["pg_dumpall","-U","postgres","--clean","--if-exists"],load:["psql","-U","postgres","-q","-v","ON_ERROR_STOP=0","-d","postgres","-f","-"]},mysql:{file:"mysql.sql",dump:["sh","-c","mysqldump -uroot --all-databases --add-drop-database --routines --events --single-transaction 2>/dev/null"],load:["mysql","-uroot"]},mongo:{file:"mongo.archive",dump:["mongodump","--archive","--quiet"],load:["mongorestore","--archive","--drop","--quiet"]},redis:{file:"redis.rdb",dump:["sh","-c","redis-cli --rdb /tmp/tnc-share.rdb >/dev/null 2>&1 && cat /tmp/tnc-share.rdb && rm -f /tmp/tnc-share.rdb"],load:["sh","-c","cat > /data/tnc-incoming.rdb && mv /data/tnc-incoming.rdb /data/dump.rdb && redis-cli shutdown nosave >/dev/null 2>&1; true"],restarts:!0}},containerName=e=>`tnc-svc-${e}`;async function execIn(e,t,r,{stdinFile:n=null,outFile:s=null,user:i=null}={}){const{Id:a}=await e.json("POST",`/containers/${enc(t)}/exec`,{Cmd:r,AttachStdin:!!n,AttachStdout:!0,AttachStderr:!0,Tty:!1,...i?{User:i}:{}}),{socket:o}=await e.hijack(`/exec/${a}/start`,{Detach:!1,Tty:!1}),c=s?fs.createWriteStream(s):null;let d="";const u=demuxer(e=>{c&&c.write(e)},e=>{d.length<8192&&(d+=e)});return await new Promise((e,t)=>{if(o.on("data",u),o.on("end",e),o.on("close",e),o.on("error",t),n){const e=fs.createReadStream(n);e.on("error",t),e.on("data",e=>o.write(e)),e.on("end",()=>o.end())}}),c&&await new Promise(e=>c.end(e)),{code:(await e.json("GET",`/exec/${a}/json`)).ExitCode??0,stderr:d.trim()}}async function inspectOrNull(e,t){try{return await e.json("GET",`/containers/${enc(t)}/json`)}catch(e){return null}}async function captureServices(e,{api:t=new DockApi,log:r=()=>{}}={}){try{await t.ensureRunning({quiet:!0})}catch(e){return[]}const n=path.join(e,"data","dock"),s=[];for(const[e,i]of Object.entries(DUMPS)){const a=await inspectOrNull(t,containerName(e));if(!a?.State?.Running)continue;fs.mkdirSync(n,{recursive:!0});const o=path.join(n,i.file);r(`dumping ${e} (${containerName(e)})`);const{code:c,stderr:d}=await execIn(t,containerName(e),i.dump,{outFile:o});0===c&&fs.statSync(o).size?s.push(e):(fs.rmSync(o,{force:!0}),r(`${e} skipped: ${d||`exit ${c}`}`))}return s}async function restoreServices(e,{api:t=new DockApi,log:r=()=>{}}={}){const n=path.join(e,"data","dock");if(!fs.existsSync(n))return[];await t.ensureRunning({quiet:!0});const{service:s}=require("./cli/services"),i={write(){}},a=[];for(const[e,o]of Object.entries(DUMPS)){const c=path.join(n,o.file);if(!fs.existsSync(c))continue;r(`starting ${e}`),await s(["start",e],{api:t,out:i,err:i}),r(`loading ${e}`);const{code:d,stderr:u}=await execIn(t,containerName(e),o.load,{stdinFile:c});if(0!==d&&!o.restarts)throw new Error(`${e} restore failed: ${u||`exit ${d}`}`);for(let r=0;o.restarts;r++){await new Promise(e=>setTimeout(e,1e3));try{await s(["start",e],{api:t,out:i,err:i});break}catch(e){if(r>=4)throw e}}a.push(e)}return a}module.exports={captureServices,restoreServices,execIn,DUMPS};