thinkncollab-shell 2.0.0-beta.2 → 2.0.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Readme.md +41 -10
- package/dist/native/tnc-tray/build-macos.sh +54 -0
- package/dist/native/tnc-tray/main.swift +201 -0
- package/dist/native/tnc-tray/tray.ps1 +136 -0
- package/dist/src/commands/dock/containers.js +1 -1
- package/dist/src/commands/dock/dock.js +1 -1
- package/dist/src/dock/api/routes.js +1 -1
- package/dist/src/dock/api/server.js +1 -1
- package/dist/src/dock/api/websocket.js +7 -0
- package/dist/src/dock/api/wire.js +1 -1
- package/dist/src/dock/build/builder.js +1 -1
- package/dist/src/dock/build/buildkit.js +1 -1
- package/dist/src/dock/build/cache.js +7 -0
- package/dist/src/dock/build/sshagent.js +7 -0
- package/dist/src/dock/cli/api.js +1 -1
- package/dist/src/dock/cli/commands.js +1 -1
- package/dist/src/dock/cli/compose.js +1 -1
- package/dist/src/dock/cli/trust.js +7 -0
- package/dist/src/dock/compat/cases.js +1 -1
- package/dist/src/dock/config.js +1 -1
- package/dist/src/dock/daemon-main.js +1 -1
- package/dist/src/dock/daemon.js +1 -1
- package/dist/src/dock/engine.js +1 -1
- package/dist/src/dock/image/auth.js +1 -1
- package/dist/src/dock/image/credhelpers.js +7 -0
- package/dist/src/dock/image/keychain.js +7 -0
- package/dist/src/dock/image/registry.js +1 -1
- package/dist/src/dock/image/scan.js +7 -0
- package/dist/src/dock/image/sign.js +7 -0
- package/dist/src/dock/linux/host.js +1 -1
- package/dist/src/dock/paths.js +1 -1
- package/dist/src/dock/runtime/cdi.js +7 -0
- package/dist/src/dock/runtime/fsops.js +1 -1
- package/dist/src/dock/runtime/network.js +1 -1
- package/dist/src/dock/runtime/overlaydns.js +7 -0
- package/dist/src/dock/runtime/ports.js +1 -1
- package/dist/src/dock/runtime/rootfs.js +1 -1
- package/dist/src/dock/runtime/spec.js +1 -1
- package/dist/src/dock/runtime/stats.js +1 -1
- package/dist/src/dock/runtime/userns.js +7 -0
- package/dist/src/dock/runtime/volumes.js +1 -1
- package/dist/src/dock/setup/autostart.js +1 -1
- package/dist/src/dock/setup/diagnose.js +7 -0
- package/dist/src/dock/setup/index.js +1 -1
- package/dist/src/dock/tray/agent.js +7 -0
- package/dist/src/dock/tray/install.js +7 -0
- package/dist/src/dock/ui/page.js +228 -15
- package/dist/src/dock/ui/server.js +1 -1
- package/package.json +1 -1
- package/dist/src/dock/setup/app.js +0 -7
|
@@ -4,4 +4,4 @@
|
|
|
4
4
|
* PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
|
|
5
5
|
* UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
|
|
6
6
|
*/
|
|
7
|
-
"use strict";const{PATH_STAT_HEADER,BUILD_FILE,HOST_ALIAS,ENGINE_SOCKET,ENGINE_CLI,ENGINE_CLI_IMAGE}=require("../api/wire"),isJson=e=>e.body&&"object"==typeof e.body,status=e=>t=>t.status===e||`HTTP ${t.status}, expected ${e}`,API_CASES=[{id:"ping",phase:"P0",area:"system",req:["GET","/_ping"],check:e=>200===e.status&&"OK"===e.body&&void 0!==e.headers["api-version"]||"ping must return OK with Api-Version header"},{id:"ping-head",phase:"P0",area:"system",req:["HEAD","/_ping"],check:status(200)},{id:"version",phase:"P0",area:"system",req:["GET","/version"],check:e=>200===e.status&&isJson(e)&&e.body.ApiVersion&&"linux"===e.body.Os||"version body incomplete"},{id:"version-prefixed",phase:"P0",area:"system",req:["GET","/v1.43/version"],check:status(200)},{id:"version-too-new",phase:"P0",area:"system",req:["GET","/v9.99/version"],check:e=>400===e.status&&/too new/.test(e.body?.message||"")||"too-new client must get 400"},{id:"info",phase:"P0",area:"system",req:["GET","/info"],check:e=>200===e.status&&Number.isInteger(e.body?.Containers)&&!!e.body?.Swarm||"info body incomplete"},{id:"unimplemented-501",phase:"P0",area:"system",req:["GET","/swarm"],check:e=>501===e.status&&"string"==typeof e.body?.message||"unimplemented endpoints must return 501 + JSON message"},{id:"containers-list",phase:"P1",area:"container",req:["GET","/containers/json?all=1"],check:status(200)},{id:"images-list",phase:"P1",area:"image",req:["GET","/images/json"],check:status(200)},{id:"image-pull",phase:"P1",area:"image",req:["POST","/images/create?fromImage=alpine&tag=3.20"],check:status(200)},{id:"container-create",phase:"P1",area:"container",req:["POST","/containers/create?name=dock-compat",{Image:"alpine:3.20",Cmd:["echo","hi"]}],check:status(201)},{id:"container-start",phase:"P1",area:"container",req:["POST","/containers/dock-compat/start"],check:status(204)},{id:"container-wait",phase:"P1",area:"container",req:["POST","/containers/dock-compat/wait"],check:status(200)},{id:"container-logs",phase:"P1",area:"container",req:["GET","/containers/dock-compat/logs?stdout=1"],check:status(200)},{id:"archive-stat",phase:"P7",area:"container",req:["HEAD","/containers/dock-compat/archive?path=/etc/alpine-release"],check:e=>200===e.status&&!!e.headers[PATH_STAT_HEADER.toLowerCase()]||`HTTP ${e.status}, expected 200 with a path-stat header`},{id:"archive-get",phase:"P7",area:"container",req:["GET","/containers/dock-compat/archive?path=/etc/alpine-release"],check:e=>200===e.status&&/x-tar/.test(e.headers["content-type"]||"")&&/alpine-release/.test(e.body)||`HTTP ${e.status}, expected a tar`},{id:"archive-missing",phase:"P7",area:"container",req:["GET","/containers/dock-compat/archive?path=/no/such/file"],check:status(404)},{id:"container-changes",phase:"P7",area:"container",req:["GET","/containers/dock-compat/changes"],check:status(200)},{id:"container-export",phase:"P7",area:"container",req:["GET","/containers/dock-compat/export"],check:e=>200===e.status&&/etc\/alpine-release/.test(e.body)||`HTTP ${e.status}, expected the filesystem tar`},{id:"container-stats",phase:"P7",area:"container",req:["GET","/containers/dock-compat/stats?stream=0"],check:e=>200===e.status&&/"memory_stats"/.test("string"==typeof e.body?e.body:JSON.stringify(e.body))||`HTTP ${e.status}`},{id:"commit",phase:"P7",area:"image",req:["POST","/commit?container=dock-compat&repo=dock-compat-commit&tag=api",{}],check:e=>201===e.status&&/^sha256:/.test(e.body?.Id||"")||`HTTP ${e.status}, expected 201 with an image Id`},{id:"image-history",phase:"P7",area:"image",req:["GET","/images/alpine:3.20/history"],check:e=>200===e.status&&Array.isArray(e.body)&&e.body.length>0||`HTTP ${e.status}`},{id:"image-save",phase:"P7",area:"image",req:["GET","/images/get?names=alpine:3.20"],check:e=>200===e.status&&/manifest\.json/.test(e.body)&&/oci-layout/.test(e.body)||`HTTP ${e.status}, expected an image archive`},{id:"container-remove",phase:"P1",area:"container",req:["DELETE","/containers/dock-compat?force=1"],check:status(204)},{id:"volumes-list",phase:"P1",area:"volume",req:["GET","/volumes"],check:status(200)},{id:"networks-list",phase:"P1",area:"network",req:["GET","/networks"],check:status(200)},{id:"events",phase:"P1",area:"system",req:["GET","/events?until=0"],check:status(200)},{id:"network-create",phase:"P2",area:"network",req:["POST","/networks/create",{Name:"dock-compat-net",Labels:{"tnc.compose.project":"compat"}}],check:status(201)},{id:"network-remove",phase:"P2",area:"network",req:["DELETE","/networks/dock-compat-net"],check:status(204)},{id:"system-df",phase:"P2",area:"system",req:["GET","/system/df"],check:status(200)},{id:"containers-prune",phase:"P2",area:"container",req:["POST",`/containers/prune?filters=${encodeURIComponent('{"label":["tnc.compat=1"]}')}`],check:status(200)},{id:"volumes-prune",phase:"P2",area:"volume",req:["POST",`/volumes/prune?filters=${encodeURIComponent('{"label":["tnc.compat=1"]}')}`],check:status(200)},{id:"auth-missing",phase:"P2",area:"registry",req:["POST","/auth",{serveraddress:"localhost:1"}],check:e=>400===e.status&&/required/.test(e.body?.message||"")||"login without credentials must get 400"},{id:"image-push-missing",phase:"P2",area:"registry",req:["POST","/images/localhost:1/dock-compat-none/push?tag=1"],check:status(404)}],CLI_CASES=[{id:"cli-info",phase:"P0",argv:["system","info"],expect:/dock \d/},{id:"cli-ps",phase:"P1",argv:["ps","-a"],expect:/CONTAINER ID/},{id:"cli-run",phase:"P1",argv:["run","--rm","alpine:3.20","echo","dock-ok"],expect:/dock-ok/},{id:"cli-exit",phase:"P1",argv:["run","--rm","alpine:3.20","sh","-c","exit 3"],status:3,expect:/^/},{id:"cli-port",phase:"P1",argv:["run","-d","--name","dock-web","-p","18081:80","nginx:alpine"],expect:/^[0-9a-f]{12,}/m},{id:"cli-exec",phase:"P1",argv:["exec","dock-web","nginx","-v"],expect:/nginx version/},{id:"cli-logs",phase:"P1",argv:["logs","dock-web"],expect:/^/},{id:"cli-build",phase:"P2",argv:["build","-q","-t","dock-compat-img:1","{dir}"],files:{[BUILD_FILE]:"FROM alpine:3.20 AS a\nRUN echo hi > /x\nFROM alpine:3.20\nCOPY --from=a /x /x\n"},expect:/sha256:/,teardown:[["rmi","-f","dock-compat-img:1"]]},{id:"cli-push",phase:"P2",argv:["push","localhost:15001/dock-compat/app:1"],expect:/Pushed|Layer already exists/,setup:[["rm","-f","dock-compat-registry"],["run","-d","--name","dock-compat-registry","-p","15001:5000","registry:2"],["run","--rm","alpine:3.20","sleep","2"],["tag","alpine:3.20","localhost:15001/dock-compat/app:1"]],teardown:[["rmi","localhost:15001/dock-compat/app:1"],["rm","-f","dock-compat-registry"]]},{id:"cli-net-dns",phase:"P2",argv:["run","--rm","--network","dock-compat-net","alpine:3.20","sh","-c","cat /etc/hosts | grep -c localhost"],expect:/^2$/m,setup:[["network","create","dock-compat-net"]],teardown:[["network","rm","dock-compat-net"]]},{id:"cli-run-i",phase:"P2",argv:["run","--rm","-i","alpine:3.20","cat"],stdin:"stdin-ok\n",expect:/stdin-ok/},{id:"cli-health",phase:"P2",argv:["run","--rm","--health-cmd","true","--health-interval","1s","alpine:3.20","sh","-c","echo health-ok"],expect:/health-ok/},{id:"cli-limits",phase:"P3",argv:["run","--rm","-m","64m","--cpus","0.5","--pids-limit","50","alpine:3.20","cat","/sys/fs/cgroup/memory.max","/sys/fs/cgroup/cpu.max","/sys/fs/cgroup/pids.max"],expect:/^67108864\s+50000 100000\s+50\s*$/m},{id:"cli-oom",phase:"P3",argv:["run","--rm","-m","32m","alpine:3.20","sh","-c","tail /dev/zero"],status:137,expect:/^/},{id:"cli-compose",phase:"P3",argv:["compose","-f","{dir}/compose.yaml","-p","dockcompat","up","-d","--wait"],files:{"compose.yaml":'services:\n web:\n image: nginx:alpine\n healthcheck: { test: ["CMD-SHELL", "wget -qO- http://127.0.0.1/ >/dev/null"], interval: 1s, retries: 30 }\n probe:\n image: alpine:3.20\n command: ["sh", "-c", "wget -qO- http://web/ | grep -q nginx && sleep 300"]\n depends_on: { web: { condition: service_healthy } }\n'},expect:/healthy/,teardown:[["compose","-f","{dir}/compose.yaml","-p","dockcompat","down","-v"]]},{id:"cli-cp-in",phase:"P7",argv:["exec","dock-cp","cat","/tmp/hello.txt"],files:{"hello.txt":"cp-in-ok\n"},expect:/cp-in-ok/,setup:[["rm","-f","dock-cp"],["run","-d","--name","dock-cp","alpine:3.20","sleep","300"],["cp","{dir}/hello.txt","dock-cp:/tmp/"]]},{id:"cli-cp-out",phase:"P7",argv:["cp","dock-cp:/tmp/hello.txt","-"],expect:/cp-in-ok/,teardown:[["rm","-f","dock-cp"]]},{id:"cli-stats",phase:"P7",argv:["stats","--no-stream","dock-stats"],expect:/dock-stats\s+\d+\.\d+%/,setup:[["rm","-f","dock-stats"],["run","-d","--name","dock-stats","alpine:3.20","sleep","300"]]},{id:"cli-top",phase:"P7",argv:["top","dock-stats"],expect:/sleep 300/,teardown:[["rm","-f","dock-stats"]]},{id:"cli-pause",phase:"P7",argv:["ps","-f","status=paused"],expect:/dock-pause/,setup:[["rm","-f","dock-pause"],["run","-d","--name","dock-pause","alpine:3.20","sleep","300"],["pause","dock-pause"]],teardown:[["rm","-f","dock-pause"]]},{id:"cli-rename",phase:"P7",argv:["ps","-a","-f","name=dock-ren2"],expect:/dock-ren2/,setup:[["rm","-f","dock-ren1","dock-ren2"],["create","--name","dock-ren1","alpine:3.20","true"],["rename","dock-ren1","dock-ren2"]],teardown:[["rm","-f","dock-ren2"]]},{id:"cli-update",phase:"P7",argv:["stats","--no-stream","dock-upd"],expect:/\/ 67\.1MB/,setup:[["rm","-f","dock-upd"],["run","-d","--name","dock-upd","alpine:3.20","sleep","300"],["update","--memory","64m","dock-upd"]],teardown:[["rm","-f","dock-upd"]]},{id:"cli-init",phase:"P7",argv:["run","--rm","--init","alpine:3.20","sh","-c",'tr "\\0" " " < /proc/1/cmdline'],expect:/tnc-init init/},{id:"cli-ulimit",phase:"P7",argv:["run","--rm","--ulimit","nofile=1024:2048","alpine:3.20","sh","-c","ulimit -n"],expect:/^1024$/m},{id:"cli-dns",phase:"P7",argv:["run","--rm","--dns","9.9.9.9","--dns-search","example.test","alpine:3.20","cat","/etc/resolv.conf"],expect:/nameserver 9\.9\.9\.9[\s\S]*search example\.test/},{id:"cli-mount",phase:"P7",argv:["run","--rm","--mount","type=tmpfs,target=/scratch","alpine:3.20","sh","-c",'grep " /scratch " /proc/mounts'],expect:/tmpfs/},{id:"cli-build-mounts",phase:"P7",argv:["build","--no-cache","-t","dock-compat-mounts:1","--secret","id=tok,src={dir}/tok.txt","{dir}"],files:{"tok.txt":"s3cret","ctx.txt":"bind-ok",[BUILD_FILE]:'FROM alpine:3.20\nRUN --mount=type=cache,target=/cache echo x >> /cache/f && test -s /cache/f\nRUN --mount=type=secret,id=tok test "$(cat /run/secrets/tok)" = s3cret && echo secret-ok\nRUN --mount=type=bind,source=ctx.txt,target=/mnt/ctx.txt cat /mnt/ctx.txt\nRUN <<EOF\necho heredoc-ok > /hd\nEOF\nCOPY <<EOF /greeting\nhello-heredoc\nEOF\nRUN test ! -e /run/secrets/tok && test ! -e /cache/f && cat /hd /greeting\n'},expect:/secret-ok[\s\S]*bind-ok[\s\S]*heredoc-ok\s+hello-heredoc/,teardown:[["rmi","-f","dock-compat-mounts:1"]]},{id:"cli-save-load",phase:"P7",argv:["load","-i","{dir}/a.tar"],files:{".keep":""},expect:/Loaded image: alpine:3\.20/,setup:[["save","-o","{dir}/a.tar","alpine:3.20"]]},{id:"cli-commit",phase:"P7",argv:["run","--rm","dock-compat-commit:1","cat","/c.txt"],expect:/committed/,setup:[["rm","-f","dock-cm"],["run","--name","dock-cm","alpine:3.20","sh","-c","echo committed > /c.txt"],["commit","-c","ENV X=1","dock-cm","dock-compat-commit:1"]],teardown:[["rm","-f","dock-cm"],["rmi","-f","dock-compat-commit:1"]]},{id:"cli-diff",phase:"P7",argv:["diff","dock-df"],expect:/^A \/newfile$/m,setup:[["rm","-f","dock-df"],["run","--name","dock-df","alpine:3.20","touch","/newfile"]],teardown:[["rm","-f","dock-df"]]},{id:"cli-history",phase:"P7",argv:["image","history","alpine:3.20"],expect:/CREATED BY/},{id:"cli-import",phase:"P7",argv:["run","--rm","dock-compat-import:1","cat","/etc/alpine-release"],files:{".keep":""},expect:/^3\.20/m,setup:[["rm","-f","dock-ex"],["create","--name","dock-ex","alpine:3.20","true"],["container","export","-o","{dir}/fs.tar","dock-ex"],["import","-c",'CMD ["sh"]',"{dir}/fs.tar","dock-compat-import:1"]],teardown:[["rm","-f","dock-ex"],["rmi","-f","dock-compat-import:1"]]},{id:"cli-compose-extends",phase:"P7",argv:["compose","-f","{dir}/compose.yaml","-p","dockcompat2","exec","-T","app","sh","-c","cat /run/secrets/tok /cfg; echo; echo $FROM_BASE"],files:{"compose.yaml":'include: [inc.yaml]\nservices:\n base:\n image: alpine:3.20\n command: ["sleep", "300"]\n environment: { FROM_BASE: "from-base" }\n app:\n extends: base\n secrets: [tok]\n configs: [{ source: cfg, target: /cfg }]\nsecrets:\n tok: { file: ./tok.txt }\nconfigs:\n cfg: { content: "config-ok" }\n',"inc.yaml":'services:\n extra:\n image: alpine:3.20\n command: ["sleep", "300"]\n',"tok.txt":"tok-ok\n"},expect:/tok-ok[\s\S]*config-ok[\s\S]*from-base/,setup:[["compose","-f","{dir}/compose.yaml","-p","dockcompat2","up","-d"]],teardown:[["compose","-f","{dir}/compose.yaml","-p","dockcompat2","down"]]},{id:"cli-host-internal",phase:"P7",argv:["run","--rm","alpine:3.20","getent","hosts",HOST_ALIAS],expect:new RegExp(HOST_ALIAS.replace(/\./g,"\\."))},{id:"cli-engine-socket",phase:"P7",argv:["run","--rm","-v",`${ENGINE_SOCKET}:${ENGINE_SOCKET}`,ENGINE_CLI_IMAGE,ENGINE_CLI,"version","--format","server={{.Server.APIVersion}}"],expect:/server=1\.\d+/},{id:"cli-bind-tmpdir",phase:"P7",argv:["run","--rm","-v","{dir}:/d","alpine:3.20","cat","/d/hello.txt"],files:{"hello.txt":"tmpdir-share-ok\n"},expect:/tmpdir-share-ok/},{id:"cli-platform",phase:"P8",argv:["run","--rm","--platform","linux/amd64","busybox:1.36","uname","-m"],expect:/x86_64/,teardown:[["rmi","-f","busybox:1.36"]]}],CLEANUP=[["DELETE","/containers/dock-compat?force=1"],["DELETE","/containers/dock-web?force=1"],["DELETE","/networks/dock-compat-net"],["DELETE","/containers/dock-compat-registry?force=1"],["DELETE","/images/dock-compat-commit:api?force=1"],...["dock-cp","dock-stats","dock-pause","dock-ren1","dock-ren2","dock-upd","dock-cm","dock-df","dock-ex"].map(e=>["DELETE",`/containers/${e}?force=1`])];module.exports={API_CASES,CLI_CASES,CLEANUP};
|
|
7
|
+
"use strict";const{PATH_STAT_HEADER,BUILD_FILE,HOST_ALIAS,ENGINE_SOCKET,ENGINE_CLI,ENGINE_CLI_IMAGE}=require("../api/wire"),isJson=e=>e.body&&"object"==typeof e.body,status=e=>t=>t.status===e||`HTTP ${t.status}, expected ${e}`,API_CASES=[{id:"ping",phase:"P0",area:"system",req:["GET","/_ping"],check:e=>200===e.status&&"OK"===e.body&&void 0!==e.headers["api-version"]||"ping must return OK with Api-Version header"},{id:"ping-head",phase:"P0",area:"system",req:["HEAD","/_ping"],check:status(200)},{id:"version",phase:"P0",area:"system",req:["GET","/version"],check:e=>200===e.status&&isJson(e)&&e.body.ApiVersion&&"linux"===e.body.Os||"version body incomplete"},{id:"version-prefixed",phase:"P0",area:"system",req:["GET","/v1.43/version"],check:status(200)},{id:"version-too-new",phase:"P0",area:"system",req:["GET","/v9.99/version"],check:e=>400===e.status&&/too new/.test(e.body?.message||"")||"too-new client must get 400"},{id:"info",phase:"P0",area:"system",req:["GET","/info"],check:e=>200===e.status&&Number.isInteger(e.body?.Containers)&&!!e.body?.Swarm||"info body incomplete"},{id:"unimplemented-501",phase:"P0",area:"system",req:["GET","/swarm"],check:e=>501===e.status&&"string"==typeof e.body?.message||"unimplemented endpoints must return 501 + JSON message"},{id:"containers-list",phase:"P1",area:"container",req:["GET","/containers/json?all=1"],check:status(200)},{id:"images-list",phase:"P1",area:"image",req:["GET","/images/json"],check:status(200)},{id:"image-pull",phase:"P1",area:"image",req:["POST","/images/create?fromImage=alpine&tag=3.20"],check:status(200)},{id:"container-create",phase:"P1",area:"container",req:["POST","/containers/create?name=dock-compat",{Image:"alpine:3.20",Cmd:["echo","hi"]}],check:status(201)},{id:"container-start",phase:"P1",area:"container",req:["POST","/containers/dock-compat/start"],check:status(204)},{id:"container-wait",phase:"P1",area:"container",req:["POST","/containers/dock-compat/wait"],check:status(200)},{id:"container-logs",phase:"P1",area:"container",req:["GET","/containers/dock-compat/logs?stdout=1"],check:status(200)},{id:"archive-stat",phase:"P7",area:"container",req:["HEAD","/containers/dock-compat/archive?path=/etc/alpine-release"],check:e=>200===e.status&&!!e.headers[PATH_STAT_HEADER.toLowerCase()]||`HTTP ${e.status}, expected 200 with a path-stat header`},{id:"archive-get",phase:"P7",area:"container",req:["GET","/containers/dock-compat/archive?path=/etc/alpine-release"],check:e=>200===e.status&&/x-tar/.test(e.headers["content-type"]||"")&&/alpine-release/.test(e.body)||`HTTP ${e.status}, expected a tar`},{id:"archive-missing",phase:"P7",area:"container",req:["GET","/containers/dock-compat/archive?path=/no/such/file"],check:status(404)},{id:"container-changes",phase:"P7",area:"container",req:["GET","/containers/dock-compat/changes"],check:status(200)},{id:"container-export",phase:"P7",area:"container",req:["GET","/containers/dock-compat/export"],check:e=>200===e.status&&/etc\/alpine-release/.test(e.body)||`HTTP ${e.status}, expected the filesystem tar`},{id:"container-stats",phase:"P7",area:"container",req:["GET","/containers/dock-compat/stats?stream=0"],check:e=>200===e.status&&/"memory_stats"/.test("string"==typeof e.body?e.body:JSON.stringify(e.body))||`HTTP ${e.status}`},{id:"commit",phase:"P7",area:"image",req:["POST","/commit?container=dock-compat&repo=dock-compat-commit&tag=api",{}],check:e=>201===e.status&&/^sha256:/.test(e.body?.Id||"")||`HTTP ${e.status}, expected 201 with an image Id`},{id:"image-history",phase:"P7",area:"image",req:["GET","/images/alpine:3.20/history"],check:e=>200===e.status&&Array.isArray(e.body)&&e.body.length>0||`HTTP ${e.status}`},{id:"image-save",phase:"P7",area:"image",req:["GET","/images/get?names=alpine:3.20"],check:e=>200===e.status&&/manifest\.json/.test(e.body)&&/oci-layout/.test(e.body)||`HTTP ${e.status}, expected an image archive`},{id:"metrics",phase:"P7",area:"system",req:["GET","/metrics"],check:e=>200===e.status&&/^dock_containers\{state="running"\} \d+$/m.test(e.body)&&/# TYPE dock_api_requests_total counter/.test(e.body)||`HTTP ${e.status}, expected Prometheus text`},{id:"attach-ws-plain",phase:"P7",area:"container",req:["GET","/containers/dock-compat/attach/ws"],check:e=>400===e.status&&/WebSocket/.test(e.body?.message||"")||`HTTP ${e.status}, expected 400 without an upgrade`},{id:"distribution",phase:"P7",area:"registry",req:["GET","/distribution/alpine:3.20/json"],check:e=>200===e.status&&/^sha256:/.test(e.body?.Descriptor?.digest||"")&&Array.isArray(e.body?.Platforms)&&e.body.Platforms.some(e=>"arm64"===e.architecture)||`HTTP ${e.status}, expected a descriptor and platforms`},{id:"images-search",phase:"P7",area:"registry",req:["GET","/images/search?term=alpine&limit=3"],check:e=>200===e.status&&Array.isArray(e.body)&&e.body.length>0&&"string"==typeof e.body[0].name||`HTTP ${e.status}, expected search results`},{id:"manifest-push-missing",phase:"P7",area:"registry",req:["POST","/tnc/manifests/localhost:1/dock-compat-none:1/push",{Images:["dock-compat-none:1"]}],check:status(404)},{id:"container-remove",phase:"P1",area:"container",req:["DELETE","/containers/dock-compat?force=1"],check:status(204)},{id:"volumes-list",phase:"P1",area:"volume",req:["GET","/volumes"],check:status(200)},{id:"networks-list",phase:"P1",area:"network",req:["GET","/networks"],check:status(200)},{id:"events",phase:"P1",area:"system",req:["GET","/events?until=0"],check:status(200)},{id:"network-create",phase:"P2",area:"network",req:["POST","/networks/create",{Name:"dock-compat-net",Labels:{"tnc.compose.project":"compat"}}],check:status(201)},{id:"network-remove",phase:"P2",area:"network",req:["DELETE","/networks/dock-compat-net"],check:status(204)},{id:"system-df",phase:"P2",area:"system",req:["GET","/system/df"],check:status(200)},{id:"containers-prune",phase:"P2",area:"container",req:["POST",`/containers/prune?filters=${encodeURIComponent('{"label":["tnc.compat=1"]}')}`],check:status(200)},{id:"volumes-prune",phase:"P2",area:"volume",req:["POST",`/volumes/prune?filters=${encodeURIComponent('{"label":["tnc.compat=1"]}')}`],check:status(200)},{id:"auth-missing",phase:"P2",area:"registry",req:["POST","/auth",{serveraddress:"localhost:1"}],check:e=>400===e.status&&/required/.test(e.body?.message||"")||"login without credentials must get 400"},{id:"image-push-missing",phase:"P2",area:"registry",req:["POST","/images/localhost:1/dock-compat-none/push?tag=1"],check:status(404)}],CLI_CASES=[{id:"cli-info",phase:"P0",argv:["system","info"],expect:/dock \d/},{id:"cli-ps",phase:"P1",argv:["ps","-a"],expect:/CONTAINER ID/},{id:"cli-run",phase:"P1",argv:["run","--rm","alpine:3.20","echo","dock-ok"],expect:/dock-ok/},{id:"cli-exit",phase:"P1",argv:["run","--rm","alpine:3.20","sh","-c","exit 3"],status:3,expect:/^/},{id:"cli-port",phase:"P1",argv:["run","-d","--name","dock-web","-p","18081:80","nginx:alpine"],expect:/^[0-9a-f]{12,}/m},{id:"cli-exec",phase:"P1",argv:["exec","dock-web","nginx","-v"],expect:/nginx version/},{id:"cli-logs",phase:"P1",argv:["logs","dock-web"],expect:/^/},{id:"cli-build",phase:"P2",argv:["build","-q","-t","dock-compat-img:1","{dir}"],files:{[BUILD_FILE]:"FROM alpine:3.20 AS a\nRUN echo hi > /x\nFROM alpine:3.20\nCOPY --from=a /x /x\n"},expect:/sha256:/,teardown:[["rmi","-f","dock-compat-img:1"]]},{id:"cli-push",phase:"P2",argv:["push","localhost:15001/dock-compat/app:1"],expect:/Pushed|Layer already exists/,setup:[["rm","-f","dock-compat-registry"],["run","-d","--name","dock-compat-registry","-p","15001:5000","registry:2"],["run","--rm","alpine:3.20","sleep","2"],["tag","alpine:3.20","localhost:15001/dock-compat/app:1"]],teardown:[["rmi","localhost:15001/dock-compat/app:1"],["rm","-f","dock-compat-registry"]]},{id:"cli-net-dns",phase:"P2",argv:["run","--rm","--network","dock-compat-net","alpine:3.20","sh","-c","cat /etc/hosts | grep -c localhost"],expect:/^2$/m,setup:[["network","create","dock-compat-net"]],teardown:[["network","rm","dock-compat-net"]]},{id:"cli-run-i",phase:"P2",argv:["run","--rm","-i","alpine:3.20","cat"],stdin:"stdin-ok\n",expect:/stdin-ok/},{id:"cli-health",phase:"P2",argv:["run","--rm","--health-cmd","true","--health-interval","1s","alpine:3.20","sh","-c","echo health-ok"],expect:/health-ok/},{id:"cli-limits",phase:"P3",argv:["run","--rm","-m","64m","--cpus","0.5","--pids-limit","50","alpine:3.20","cat","/sys/fs/cgroup/memory.max","/sys/fs/cgroup/cpu.max","/sys/fs/cgroup/pids.max"],expect:/^67108864\s+50000 100000\s+50\s*$/m},{id:"cli-oom",phase:"P3",argv:["run","--rm","-m","32m","alpine:3.20","sh","-c","tail /dev/zero"],status:137,expect:/^/},{id:"cli-compose",phase:"P3",argv:["compose","-f","{dir}/compose.yaml","-p","dockcompat","up","-d","--wait"],files:{"compose.yaml":'services:\n web:\n image: nginx:alpine\n healthcheck: { test: ["CMD-SHELL", "wget -qO- http://127.0.0.1/ >/dev/null"], interval: 1s, retries: 30 }\n probe:\n image: alpine:3.20\n command: ["sh", "-c", "wget -qO- http://web/ | grep -q nginx && sleep 300"]\n depends_on: { web: { condition: service_healthy } }\n'},expect:/healthy/,teardown:[["compose","-f","{dir}/compose.yaml","-p","dockcompat","down","-v"]]},{id:"cli-cp-in",phase:"P7",argv:["exec","dock-cp","cat","/tmp/hello.txt"],files:{"hello.txt":"cp-in-ok\n"},expect:/cp-in-ok/,setup:[["rm","-f","dock-cp"],["run","-d","--name","dock-cp","alpine:3.20","sleep","300"],["cp","{dir}/hello.txt","dock-cp:/tmp/"]]},{id:"cli-cp-out",phase:"P7",argv:["cp","dock-cp:/tmp/hello.txt","-"],expect:/cp-in-ok/,teardown:[["rm","-f","dock-cp"]]},{id:"cli-stats",phase:"P7",argv:["stats","--no-stream","dock-stats"],expect:/dock-stats\s+\d+\.\d+%/,setup:[["rm","-f","dock-stats"],["run","-d","--name","dock-stats","alpine:3.20","sleep","300"]]},{id:"cli-top",phase:"P7",argv:["top","dock-stats"],expect:/sleep 300/,teardown:[["rm","-f","dock-stats"]]},{id:"cli-pause",phase:"P7",argv:["ps","-f","status=paused"],expect:/dock-pause/,setup:[["rm","-f","dock-pause"],["run","-d","--name","dock-pause","alpine:3.20","sleep","300"],["pause","dock-pause"]],teardown:[["rm","-f","dock-pause"]]},{id:"cli-rename",phase:"P7",argv:["ps","-a","-f","name=dock-ren2"],expect:/dock-ren2/,setup:[["rm","-f","dock-ren1","dock-ren2"],["create","--name","dock-ren1","alpine:3.20","true"],["rename","dock-ren1","dock-ren2"]],teardown:[["rm","-f","dock-ren2"]]},{id:"cli-update",phase:"P7",argv:["stats","--no-stream","dock-upd"],expect:/\/ 67\.1MB/,setup:[["rm","-f","dock-upd"],["run","-d","--name","dock-upd","alpine:3.20","sleep","300"],["update","--memory","64m","dock-upd"]],teardown:[["rm","-f","dock-upd"]]},{id:"cli-init",phase:"P7",argv:["run","--rm","--init","alpine:3.20","sh","-c",'tr "\\0" " " < /proc/1/cmdline'],expect:/tnc-init init/},{id:"cli-ulimit",phase:"P7",argv:["run","--rm","--ulimit","nofile=1024:2048","alpine:3.20","sh","-c","ulimit -n"],expect:/^1024$/m},{id:"cli-dns",phase:"P7",argv:["run","--rm","--dns","9.9.9.9","--dns-search","example.test","alpine:3.20","cat","/etc/resolv.conf"],expect:/nameserver 9\.9\.9\.9[\s\S]*search example\.test/},{id:"cli-mount",phase:"P7",argv:["run","--rm","--mount","type=tmpfs,target=/scratch","alpine:3.20","sh","-c",'grep " /scratch " /proc/mounts'],expect:/tmpfs/},{id:"cli-build-mounts",phase:"P7",argv:["build","--no-cache","-t","dock-compat-mounts:1","--secret","id=tok,src={dir}/tok.txt","{dir}"],files:{"tok.txt":"s3cret","ctx.txt":"bind-ok",[BUILD_FILE]:'FROM alpine:3.20\nRUN --mount=type=cache,target=/cache echo x >> /cache/f && test -s /cache/f\nRUN --mount=type=secret,id=tok test "$(cat /run/secrets/tok)" = s3cret && echo secret-ok\nRUN --mount=type=bind,source=ctx.txt,target=/mnt/ctx.txt cat /mnt/ctx.txt\nRUN <<EOF\necho heredoc-ok > /hd\nEOF\nCOPY <<EOF /greeting\nhello-heredoc\nEOF\nRUN test ! -e /run/secrets/tok && test ! -e /cache/f && cat /hd /greeting\n'},expect:/secret-ok[\s\S]*bind-ok[\s\S]*heredoc-ok\s+hello-heredoc/,teardown:[["rmi","-f","dock-compat-mounts:1"]]},{id:"cli-save-load",phase:"P7",argv:["load","-i","{dir}/a.tar"],files:{".keep":""},expect:/Loaded image: alpine:3\.20/,setup:[["save","-o","{dir}/a.tar","alpine:3.20"]]},{id:"cli-commit",phase:"P7",argv:["run","--rm","dock-compat-commit:1","cat","/c.txt"],expect:/committed/,setup:[["rm","-f","dock-cm"],["run","--name","dock-cm","alpine:3.20","sh","-c","echo committed > /c.txt"],["commit","-c","ENV X=1","dock-cm","dock-compat-commit:1"]],teardown:[["rm","-f","dock-cm"],["rmi","-f","dock-compat-commit:1"]]},{id:"cli-diff",phase:"P7",argv:["diff","dock-df"],expect:/^A \/newfile$/m,setup:[["rm","-f","dock-df"],["run","--name","dock-df","alpine:3.20","touch","/newfile"]],teardown:[["rm","-f","dock-df"]]},{id:"cli-history",phase:"P7",argv:["image","history","alpine:3.20"],expect:/CREATED BY/},{id:"cli-import",phase:"P7",argv:["run","--rm","dock-compat-import:1","cat","/etc/alpine-release"],files:{".keep":""},expect:/^3\.20/m,setup:[["rm","-f","dock-ex"],["create","--name","dock-ex","alpine:3.20","true"],["container","export","-o","{dir}/fs.tar","dock-ex"],["import","-c",'CMD ["sh"]',"{dir}/fs.tar","dock-compat-import:1"]],teardown:[["rm","-f","dock-ex"],["rmi","-f","dock-compat-import:1"]]},{id:"cli-compose-extends",phase:"P7",argv:["compose","-f","{dir}/compose.yaml","-p","dockcompat2","exec","-T","app","sh","-c","cat /run/secrets/tok /cfg; echo; echo $FROM_BASE"],files:{"compose.yaml":'include: [inc.yaml]\nservices:\n base:\n image: alpine:3.20\n command: ["sleep", "300"]\n environment: { FROM_BASE: "from-base" }\n app:\n extends: base\n secrets: [tok]\n configs: [{ source: cfg, target: /cfg }]\nsecrets:\n tok: { file: ./tok.txt }\nconfigs:\n cfg: { content: "config-ok" }\n',"inc.yaml":'services:\n extra:\n image: alpine:3.20\n command: ["sleep", "300"]\n',"tok.txt":"tok-ok\n"},expect:/tok-ok[\s\S]*config-ok[\s\S]*from-base/,setup:[["compose","-f","{dir}/compose.yaml","-p","dockcompat2","up","-d"]],teardown:[["compose","-f","{dir}/compose.yaml","-p","dockcompat2","down"]]},{id:"cli-host-internal",phase:"P7",argv:["run","--rm","alpine:3.20","getent","hosts",HOST_ALIAS],expect:new RegExp(HOST_ALIAS.replace(/\./g,"\\."))},{id:"cli-engine-socket",phase:"P7",argv:["run","--rm","-v",`${ENGINE_SOCKET}:${ENGINE_SOCKET}`,ENGINE_CLI_IMAGE,ENGINE_CLI,"version","--format","server={{.Server.APIVersion}}"],expect:/server=1\.\d+/},{id:"cli-bind-tmpdir",phase:"P7",argv:["run","--rm","-v","{dir}:/d","alpine:3.20","cat","/d/hello.txt"],files:{"hello.txt":"tmpdir-share-ok\n"},expect:/tmpdir-share-ok/},{id:"cli-platform",phase:"P8",argv:["run","--rm","--platform","linux/amd64","busybox:1.36","uname","-m"],expect:/x86_64/,teardown:[["rmi","-f","busybox:1.36"]]}],CLEANUP=[["DELETE","/containers/dock-compat?force=1"],["DELETE","/containers/dock-web?force=1"],["DELETE","/networks/dock-compat-net"],["DELETE","/containers/dock-compat-registry?force=1"],["DELETE","/images/dock-compat-commit:api?force=1"],...["dock-cp","dock-stats","dock-pause","dock-ren1","dock-ren2","dock-upd","dock-cm","dock-df","dock-ex"].map(e=>["DELETE",`/containers/${e}?force=1`])];module.exports={API_CASES,CLI_CASES,CLEANUP};
|
package/dist/src/dock/config.js
CHANGED
|
@@ -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"),os=require("os"),path=require("path");function configFile(){return
|
|
7
|
+
"use strict";const fs=require("fs"),os=require("os"),path=require("path"),LINUX_SYSTEM_CONFIG="/etc/thinknsh/config.json";function configFile(){return"linux"!==process.platform||process.env.TNC_DOCK_HOME||require("./paths").rootlessMode()?path.join(process.env.TNC_DOCK_HOME||path.join(os.homedir(),".tnc","dock"),"config.json"):LINUX_SYSTEM_CONFIG}function legacyConfigFile(){return"linux"!==process.platform||process.env.TNC_DOCK_HOME||require("./paths").rootlessMode()?null:path.join(os.homedir(),".tnc","dock","config.json")}let cache={file:null,mtimeMs:-1,value:{}};function readConfig(){let e=configFile();const t=legacyConfigFile();t&&!fs.existsSync(e)&&fs.existsSync(t)&&(e=t);try{const t=fs.statSync(e);if(cache.file===e&&cache.mtimeMs===t.mtimeMs)return structuredClone(cache.value);const s=JSON.parse(fs.readFileSync(e,"utf8"))||{};return cache={file:e,mtimeMs:t.mtimeMs,value:s},structuredClone(s)}catch(e){return{}}}function writeConfig(e){const t=configFile();try{fs.mkdirSync(path.dirname(t),{recursive:!0,mode:t===LINUX_SYSTEM_CONFIG?493:448});const s=`${t}.${process.pid}.tmp`;if(fs.writeFileSync(s,JSON.stringify(e,null,2)+"\n",{mode:384}),t===LINUX_SYSTEM_CONFIG){const e=fs.readFileSync("/etc/group","utf8").split("\n").find(e=>e.split(":")[0]===(process.env.TNC_DOCK_SOCKET_GROUP||"tnc-dock"));e&&(fs.chownSync(s,0,Number(e.split(":")[2])),fs.chmodSync(s,416))}fs.renameSync(s,t)}catch(e){if("EACCES"===e.code||"EPERM"===e.code){const e=new Error(`cannot write ${t}: dock settings on Linux are system-wide, change them with sudo (sudo thinknsh dock config …)`);throw e.statusCode=403,e}throw e}cache={file:null,mtimeMs:-1,value:{}}}function defaults(){const e=os.cpus().length||2;return{cpus:Math.max(2,Math.floor(e/2)),memoryMB:Math.min(4096,Math.floor(os.totalmem()/1024/1024/2)),diskGB:64,shares:[],publishAddress:"127.0.0.1",proxy:{},caCerts:[],registryMirrors:[],insecureRegistries:[],credentialStore:"auto",credentialHelpers:{}}}function effective(){const e={...defaults(),...readConfig()};Number(process.env.TNC_DOCK_VM_CPUS)&&(e.cpus=Number(process.env.TNC_DOCK_VM_CPUS)),Number(process.env.TNC_DOCK_VM_MEMORY_MB)&&(e.memoryMB=Number(process.env.TNC_DOCK_VM_MEMORY_MB)),e.shares=(e.shares||[]).map(e=>path.resolve(e)),process.env.TNC_DOCK_PUBLISH_ADDRESS&&(e.publishAddress=process.env.TNC_DOCK_PUBLISH_ADDRESS),e.proxy=e.proxy&&"object"==typeof e.proxy?e.proxy:{},e.caCerts=(e.caCerts||[]).map(e=>path.resolve(e));const t=(process.env.TNC_DOCK_INSECURE_REGISTRIES||"").split(",").map(e=>e.trim()).filter(Boolean);return e.insecureRegistries=[...new Set([...e.insecureRegistries||[],...t].map(e=>e.toLowerCase()))],e.registryMirrors=(e.registryMirrors||[]).filter(Boolean),e}function daemonEnv(e=effective()){const t={},s=e.proxy||{};s.http&&(t.HTTP_PROXY=t.http_proxy=s.http),s.https&&(t.HTTPS_PROXY=t.https_proxy=s.https),(s.http||s.https)&&(t.NODE_USE_ENV_PROXY="1",t.NO_PROXY=t.no_proxy=[s.noProxy,"localhost","127.0.0.1","::1"].filter(Boolean).join(","));const r=(e.caCerts||[]).filter(e=>fs.existsSync(e));if(1===r.length)t.NODE_EXTRA_CA_CERTS=r[0];else if(r.length>1){const e=path.join(path.dirname(configFile()),"ca-certs.pem");fs.writeFileSync(e,r.map(e=>fs.readFileSync(e,"utf8").trim()).join("\n")+"\n"),t.NODE_EXTRA_CA_CERTS=e}return t}function nodeHasEnvProxy(e=process.versions.node){const[t,s]=e.split(".").map(Number);return t>24||24===t&&s>=5||22===t&&s>=21}function containerProxyEnv(e=effective(),t){const s=e.proxy||{};if(!1===s.containers||!s.http&&!s.https)return{};const r=e=>e&&t?e.replace(/\/\/(localhost|127\.0\.0\.1|\[::1\])(?=[:/]|$)/,`//${t}`):e,o={};return s.http&&(o.HTTP_PROXY=o.http_proxy=r(s.http)),s.https&&(o.HTTPS_PROXY=o.https_proxy=r(s.https)),s.noProxy&&(o.NO_PROXY=o.no_proxy=s.noProxy),o}function applyPatch(e,t={}){const s=structuredClone(e||{}),r=(e,t)=>{const s=new Error(`${e}: ${t}`);throw s.statusCode=400,s},o=(e,t,s,o)=>{Array.isArray(t)||r(e,"must be a list");const n=[...new Set(t.map(e=>String(e).trim()).filter(Boolean))];for(const t of n)s(t)||r(e,`${t}: ${o}`);return n},n=(e,t,s,o)=>{const n=Number(t);return(!Number.isInteger(n)||n<s||n>o)&&r(e,`must be a whole number from ${s} to ${o}`),n};for(const[e,i]of Object.entries(t))if(null!==i)switch(e){case"cpus":s.cpus=n(e,i,1,Math.max(1,os.cpus().length));break;case"memoryMB":s.memoryMB=n(e,i,512,Math.floor(os.totalmem()/1024/1024));break;case"diskGB":s.diskGB=n(e,i,8,4096);break;case"shares":s.shares=o(e,i,e=>path.isAbsolute(e)&&fs.existsSync(e)&&fs.statSync(e).isDirectory(),"not an existing folder (absolute path)");break;case"proxy":{("object"!=typeof i||Array.isArray(i))&&r(e,"must be an object");const t={};for(const e of["http","https"])i[e]&&(/^https?:\/\/\S+$/.test(i[e])||r(`proxy.${e}`,"must be a URL like http://proxy.corp:3128"),t[e]=i[e]);i.noProxy&&(t.noProxy=String(i.noProxy).replace(/\s+/g,"")),!1===i.containers&&(t.containers=!1),s.proxy=t;break}case"caCerts":s.caCerts=o(e,i,e=>path.isAbsolute(e)&&fs.existsSync(e)&&/-----BEGIN CERTIFICATE-----/.test(fs.readFileSync(e,"utf8")),"not a PEM certificate file (absolute path)");break;case"registryMirrors":s.registryMirrors=o(e,i,e=>/^https?:\/\/[^\s/]+\/?$/.test(e),"must be a URL like https://mirror.gcr.io").map(e=>e.replace(/\/+$/,""));break;case"insecureRegistries":s.insecureRegistries=o(e,i,e=>/^[a-z0-9.-]+(:\d+)?$|^\[[0-9a-f:]+\](:\d+)?$/i.test(e),"must be host or host:port").map(e=>e.toLowerCase());break;case"credentialStore":["auto","file"].includes(i)||r(e,"must be auto or file"),s.credentialStore=i;break;case"usernsRemap":{const t=String(i).trim();t&&"default"!==t&&!/^[a-z_][a-z0-9_-]*(:[a-z_][a-z0-9_-]*)?$/i.test(t)&&r(e,"must be default, a user name, or user:group"),t?s.usernsRemap=t:delete s.usernsRemap;break}case"trustPolicy":Array.isArray(i)||r(e,"must be a list of { scope, key }"),s.trustPolicy=i.map(t=>(t&&/^[a-z0-9.*:/_-]+$/i.test(String(t.scope||""))||r(e,`${t?.scope}: scope must look like ghcr.io/me/* or *`),path.isAbsolute(String(t.key||""))&&fs.existsSync(t.key)&&/PUBLIC KEY/.test(fs.readFileSync(t.key,"utf8"))||r(e,`${t.key}: not a PEM public key file (absolute path)`),{scope:String(t.scope),key:String(t.key)}));break;case"publishAddress":require("net").isIP(String(i))||r(e,"must be an IP address (127.0.0.1, 0.0.0.0 …)"),s.publishAddress=String(i);break;case"credentialHelpers":{("object"!=typeof i||Array.isArray(i))&&r(e,"must be an object of registry host → helper name");const t={};for(const[s,o]of Object.entries(i))/^[a-z0-9.-]+(:\d+)?$/i.test(s)||r(e,`${s}: must be a registry host`),/^[A-Za-z0-9._-]+$/.test(String(o))||r(e,`${o}: must be a helper name like ecr-login`),t[s.toLowerCase()]=String(o);s.credentialHelpers=t;break}default:r(e,"unknown setting")}else delete s[e];return s}module.exports={readConfig,writeConfig,defaults,effective,configFile,LINUX_SYSTEM_CONFIG,daemonEnv,nodeHasEnvProxy,containerProxyEnv,applyPatch};
|
|
@@ -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{DockDaemon}=require("./daemon"),log=
|
|
7
|
+
"use strict";const fs=require("fs"),{spawn,spawnSync}=require("child_process"),{DockDaemon}=require("./daemon"),{daemonEnv}=require("./config"),log=e=>console.log(`${(new Date).toISOString()} ${e}`);function missingEnv(){if("1"===process.env.TNC_DOCK_ENV_APPLIED)return null;let e;try{e=daemonEnv()}catch(e){return log(`settings: ${e.message}`),null}return Object.entries(e).some(([e,s])=>process.env[e]!==s)?e:null}const SCOPE=["systemd-run","--user","--scope","--quiet","--collect","-p","Delegate=yes"];function needsScope(){if("1"===process.env.TNC_DOCK_SCOPED||!require("./paths").rootlessMode())return!1;try{const e=fs.readFileSync("/proc/self/cgroup","utf8").split("\n").find(e=>e.startsWith("0::")).slice(3).trim();return fs.accessSync(`/sys/fs/cgroup${e}/cgroup.procs`,fs.constants.W_OK),!1}catch(e){}return 0===spawnSync(SCOPE[0],[...SCOPE.slice(1),"true"],{stdio:"ignore",timeout:1e4}).status}const want=missingEnv(),scope=needsScope();if(want||scope){want&&log(`applying proxy/CA settings (${Object.keys(want).filter(e=>e===e.toUpperCase()).join(", ")})`),scope&&log("rootless: running in a systemd user scope with a delegated cgroup (resource limits)");const[e,...s]=scope?[...SCOPE,"--",process.execPath,__filename]:[process.execPath,__filename],n=spawn(e,s,{stdio:"inherit",env:{...process.env,...want||{},TNC_DOCK_ENV_APPLIED:"1",TNC_DOCK_SCOPED:"1"}});for(const e of["SIGINT","SIGTERM"])process.on(e,()=>n.kill(e));n.on("exit",(e,s)=>process.exit(e??(s?0:1)))}else{const e=new DockDaemon({log});let s=!1;const n=async n=>{s||(s=!0,log(`received ${n}, shutting down`),await e.stop(),process.exit(0))};process.on("SIGINT",()=>n("SIGINT")),process.on("SIGTERM",()=>n("SIGTERM")),e.start().catch(e=>{log(`failed to start: ${e.message}`),process.exit(e.alreadyRunning?0:1)})}
|
package/dist/src/dock/daemon.js
CHANGED
|
@@ -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"),Store=require("./state/store"),ApiServer=require("./api/server"),{Engine}=require("./engine"),{ping}=require("./client"),{dockPaths}=require("./paths");function pidAlive(e){try{return process.kill(e,0),!0}catch(e){return"EPERM"===e.code}}function readPid(e){try{const
|
|
7
|
+
"use strict";const fs=require("fs"),Store=require("./state/store"),ApiServer=require("./api/server"),{Engine}=require("./engine"),{ping}=require("./client"),{dockPaths}=require("./paths");function pidAlive(e){try{return process.kill(e,0),!0}catch(e){return"EPERM"===e.code}}function readPid(e){try{const t=parseInt(fs.readFileSync(e,"utf8").trim(),10);return Number.isInteger(t)&&t>0?t:null}catch(e){return null}}class DockDaemon{constructor({home:e,log:t}={}){this.paths=dockPaths(e),this.log=t||(()=>{}),this.store=null,this.engine=null,this.api=null}async start(){const{home:e,socket:t,db:s,pidFile:r}=this.paths;if(fs.mkdirSync(e,{recursive:!0,mode:448}),"win32"!==process.platform&&!t.startsWith(e+require("path").sep))try{fs.mkdirSync(require("path").dirname(t),{recursive:!0,mode:493})}catch(e){throw new Error(`cannot create ${require("path").dirname(t)} (${e.code}): dock runs as root on Linux, start it with: sudo thinknsh dock start`)}if(await ping(t)){const e=readPid(r),s=new Error(`dock daemon is already running${e?` (pid ${e})`:""} on ${t}`);throw s.alreadyRunning=!0,s}"win32"!==process.platform&&fs.existsSync(t)&&fs.rmSync(t,{force:!0});const i=readPid(r);i&&!pidAlive(i)&&fs.rmSync(r,{force:!0}),this.store=new Store(s),this.engine=new Engine({store:this.store,paths:this.paths,log:this.log}),this.api=new ApiServer({store:this.store,engine:this.engine,socketPath:t,home:e,log:this.log});try{await this.api.start()}catch(e){throw this.store.close(),e}"win32"!==process.platform&&fs.chmodSync(t,384),this._shareSocketWithGroup(t),fs.writeFileSync(r,String(process.pid)),this.store.recordEvent("daemon","start",String(process.pid)),this.log(`dock daemon listening on ${t} (pid ${process.pid}, schema v${this.store.schemaVersion()})`),this.engine.recover().catch(e=>this.log(`recover: ${e.message}`)),this.engine.startResourceManager()}_shareSocketWithGroup(e){if("linux"!==process.platform||0!==process.getuid?.())return;const t=process.env.TNC_DOCK_SOCKET_GROUP||"tnc-dock";try{const s=fs.readFileSync("/etc/group","utf8").split("\n").find(e=>e.split(":")[0]===t);if(!s)return;fs.chownSync(e,0,Number(s.split(":")[2])),fs.chmodSync(e,432),this.log(`socket shared with group ${t}`)}catch(e){this.log(`could not share socket with group ${t}: ${e.message}`)}}async stop(){const{socket:e,pidFile:t}=this.paths;this.engine&&this.engine.shutdown(),this.api&&await this.api.stop(),this.store&&(this.store.recordEvent("daemon","stop",String(process.pid)),this.store.close()),readPid(t)===process.pid&&fs.rmSync(t,{force:!0}),"win32"!==process.platform&&fs.rmSync(e,{force:!0}),this.api=this.engine=this.store=null}}module.exports={DockDaemon,readPid,pidAlive};
|
package/dist/src/dock/engine.js
CHANGED
|
@@ -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,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 i=new Error(e);return i.statusCode=t,i}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}class Engine extends EventEmitter{constructor({store:t,paths:e,log:i=()=>{},hostFactory:s=createLinuxHost}){super(),this.store=t,this.paths=e,this.log=i,this.hostFactory=s,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:i}),this.fsevents=new FsEventRelay({engine:this,log:i});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,i,s)=>this.emitEvent(t,e,i,s),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("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:Object.values(t).find(t=>t.gateway)?.gateway||null}emitEvent(t,e,i,s={}){const n=this.store.recordEvent(t,e,i,s);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){return e&&e.username&&"string"==typeof e.password?{username:e.username,password:e.password}:this.auth.get(t)}async pull(t,e=()=>{},{credentials:i,platform:s}={}){const n=parseReference(t),r="string"==typeof s?parsePlatform(s):s,{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,i)}),h=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}),h}async push(t,e,i=()=>{},{credentials:s}={}){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,s),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 s=(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:s,progress:i}),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 login({username:t,password:e,serveraddress:i}={}){const s=normalizeRegistry(i);if(!t||"string"!=typeof e||!e)throw httpError(400,"username and password are required");return await verifyLogin(s,{username:t,password:e}),this.auth.set(s,{username:t,password:e}),this.emitEvent("registry","login",s,{name:s}),s}logout(t){const e=normalizeRegistry(t),i=this.auth.remove(e);return i&&this.emitEvent("registry","logout",e,{name:e}),{host:e,removed:i}}tagImage(t,e,i="latest"){const s=this.requireImage(t),n=parseReference(i?`${e}:${i}`:e);this.store.saveImage({...s,repoTags:[n.familiar]}),this.emitEvent("image","tag",s.id,{name:n.familiar})}async removeImage(t,{force:e=!1}={}){const i=this.requireImage(t);let s=null;try{s=parseReference(t).familiar}catch(t){}const n=this.store.listContainers().filter(t=>t.image===i.id);if(s&&i.repoTags.includes(s)&&i.repoTags.length>1)return this.store.setImageTags(i.id,i.repoTags.filter(t=>t!==s)),this.emitEvent("image","untag",i.id,{name:s}),[{Untagged:s}];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(i.id)}`);if(n.length&&e)return this.store.setImageTags(i.id,[]),i.repoTags.map(t=>({Untagged:t}));this.store.deleteImage(i.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=[i.id,...(i.layers||[]).map(t=>t.digest)].filter(t=>!r.has(t));for(const t of o)fs.rmSync(path.join(this.paths.blobs,t.replace(/^sha256:/,"")),{force:!0});if(this._host&&await this.host.isUp().catch(()=>!1)){const t=(i.layers||[]).map(t=>t.digest).filter(t=>!r.has(t)).map(t=>t.replace(/^sha256:/,""));t.length&&await this.host.sh('cd "$1/layers" && shift && for h; do rm -rf "$h" "$h.done"; done',[this.rootfs.root,...t]).catch(()=>{})}return this.emitEvent("image","delete",i.id,{}),[...i.repoTags.map(t=>({Untagged:t})),...(i.repoDigests||[]).map(t=>({Untagged:t})),{Deleted:i.id}]}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,i){const{Builder:s,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,l=path.resolve(o,c);if(!l.startsWith(o+path.sep)||!fs.existsSync(l))throw httpError(400,`Cannot locate the build file: ${c}`);const u=(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 s(this,{...e,contextDir:o,buildDir:r,out:i,secretsDir:d,platform:parsePlatform(e.platform)}),g=await f.run(l),p=this.store.saveImage({...g,repoTags:u});if(i({aux:{ID:p.id}}),e.quiet)i({stream:`${p.id}\n`});else{i({stream:`Successfully built ${shortId(p.id)}\n`});for(const t of u)i({stream:`Successfully tagged ${t}\n`})}for(const t of u)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:i}={}){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 s=t?t.replace(/^\//,""):this._generateName(),n=this.store.findContainer(s);if(n&&n.name===s)throw httpError(409,`Conflict. The container name "/${s}" 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(i);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})`);for(const t of(e.HostConfig||{}).DeviceRequests||[])if("nvidia"===t.Driver||(t.Capabilities||[]).flat().includes("gpu"))throw httpError(400,"GPUs cannot be passed to containers by dock yet");const{HostConfig:a={},NetworkingConfig:h,...c}=e,l=joinedContainer(a.NetworkMode);if(l&&(this.findContainer(l),Object.keys(a.PortBindings||{}).length))throw httpError(400,"conflicting options: port publishing and the container network mode");const u=l?[]:this._wantedNetworks(a.NetworkMode,h?.EndpointsConfig);if(0===processArgs(c,r.config?.config||{}).length)throw httpError(400,"No command specified");const d=crypto.randomBytes(32).toString("hex"),m=this.volumes.resolve(a,r.config?.config||{});this._volumesFrom(a.VolumesFrom,m);const f=this.containerDir(d);fs.mkdirSync(f,{recursive:!0});const g=this.store.insertContainer({id:d,name:s,image:r.id,state:"created",config:{Config:c,HostConfig:a,imageRef:e.Image,networks:u},runtime:{restartCount:0,volumes:m.volumes,anonymousVolumes:m.anonymous,ociMounts:m.ociMounts,apiMounts:m.apiMounts}});return this.emitEvent("container","create",d,{...r.config?.config?.Labels||{},...c.Labels||{},image:e.Image,name:s}),{Id:g.id,Warnings:[]}}_volumesFrom(t=[],e){const i=new Set(e.ociMounts.map(t=>t.destination));for(const s of t||[]){const[t,n]=String(s).split(":"),r=this.findContainer(t),o="ro"===n;for(const t of r.runtime.ociMounts||[]){if("bind"!==t.type||i.has(t.destination))continue;const s=t.options.filter(t=>"ro"!==t&&"rw"!==t);e.ociMounts.push({...t,options:[...s,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}),i.add(t.destination)}for(const t of r.runtime.volumes||[])e.volumes.includes(t)||e.volumes.push(t)}}_wantedNetworks(t,e={}){this.host;const i=this.network.resolve(t),s=[{name:i.name,aliases:(n=i,e?.[n.name]?.Aliases||e?.[t]?.Aliases||[])}];var n;for(const t of Object.keys(e||{})){const n=this.network.resolve(t);if(!s.some(t=>t.name===n.name)){if(["host","null"].includes(i.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");s.push({name:n.name,aliases:e[t].Aliases||[]})}}return s}_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"],i=t.runtime.networks||{},s=Object.values(i).find(t=>t.ip);s&&e.push(`${s.ip}\t${this._hostname(t)}`);const n=new Set;for(const[s,r]of Object.entries(i)){let i;try{i=this.network.resolve(s)}catch(t){continue}if(i.dns&&r.ip)for(const i of this.store.listContainers()){if(i.id===t.id||"running"!==i.state)continue;const r=i.runtime.networks?.[s];if(!r?.ip||n.has(`${s}:${i.id}`))continue;n.add(`${s}:${i.id}`);const o=[...new Set([i.name,...r.aliases||[],this._hostname(i),i.id.slice(0,12)])];e.push(`${r.ip}\t${o.join(" ")}`)}}const r=this._hostGateway(i),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,i]of o){const s="host-gateway"===i?r:i;s&&e.push(`${s}\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){}}async _resolvConf(){if(this._resolv)return this._resolv;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){const i=e?(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 i of t||[]){const t=await this.host.run(["stat","-L","-c","%t %T %F %u %g %a",i.PathOnHost]);if(0!==t.code)throw httpError(400,`error gathering device information while adding custom device "${i.PathOnHost}": no such file or directory`);const[s,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,`${i.PathOnHost} is not a device`);e.push({path:i.PathInContainer||i.PathOnHost,type:/block/.test(o)?"b":"c",major:parseInt(s,16),minor:parseInt(n,16),fileMode:parseInt(c,8)|(/block/.test(o)?24576:8192),uid:Number(a),gid:Number(h),CgroupPermissions:i.CgroupPermissions||"rwm"})}return e}async start(t){const e=this.findContainer(t).id,i=this.starting.get(e);if(i)return await i.catch(()=>{}),"running"===this.store.getContainer(e)?.state?"notModified":this.start(e);const s=this._start(e);this.starting.set(e,s);try{return await s}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 i=this.store.getImage(e.image);if(!i)throw httpError(404,`No such image: ${e.config.imageRef}`);i.architecture&&i.architecture!==this.linuxArch()&&await this.emulation.ensure(i.architecture);const s=i.config?.config||{},{Config:n,HostConfig:r}=e.config,o=e.id,a=this.containerDir(o),h=t=>this.host.guestPath(t),c=n.WorkingDir||s.WorkingDir||"/",l=n.Hostname||o.slice(0,12);let u=null,d={};try{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)));const t=await this.rootfs.prepare(o,i,c),m=n.User||s.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=await this._devices(r.Devices),p=joinedContainer(r.NetworkMode);let w=null;if(p){if(w=this.findContainer(p),"running"!==w.state||!w.runtime.network)throw httpError(409,`cannot join the network of ${w.name}: it is not running`);u={nsPath:w.runtime.network.nsPath,endpoints:{}}}else u=await this.network.attachAll(o,e.config.networks||this._wantedNetworks(r.NetworkMode));const y=Object.values(u.endpoints)[0]||{};if(fs.writeFileSync(path.join(a,"hostname"),l+"\n"),w)for(const t of["hosts","resolv.conf"])fs.copyFileSync(path.join(this.containerDir(w.id),t),path.join(a,t));else fs.writeFileSync(path.join(a,"hosts"),this._hostsContent({...e,runtime:{...e.runtime,networks:u.endpoints}})),fs.writeFileSync(path.join(a,"resolv.conf"),await this._containerResolv(r,null===u.nsPath));const v=mergeEnv(s.Env,[`HOSTNAME=${l}`,...n.Env||[]]),{shim:C}=this.host.binaries(),S=processArgs(n,s),E=buildSpec({id:o,rootfs:t,cwd:c,user:f,hostname:l,nsPath:u.nsPath,hostConfig:r,terminal:!!n.Tty,devices:g,args:r.Init?["/sbin/tnc-init","init","--",...S]:S,env:v,mounts:[...r.Init?[{destination:"/sbin/tnc-init",type:"bind",source:C,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||[]]});fs.writeFileSync(path.join(a,"config.json"),JSON.stringify(E,null,2)),fs.rmSync(this.exitFile(o),{force:!0}),y.ip&&(d=await this.ports.start(o,r.PortBindings,y.ip));const{shim:b,crun:_}=this.host.binaries(),k=fs.existsSync(this.logFile(o))?fs.statSync(this.logFile(o)).size:0,$=["--log",h(this.logFile(o)),"--exit",h(this.exitFile(o)),"--ctl",this.rootfs.ctlFifo(o),...logRotationArgs(r.LogConfig)],I=[_,"--cgroup-manager=cgroupfs","run","--bundle",h(a)];n.OpenStdin&&$.push("--stdin",this.rootfs.stdinFifo(o)),n.Tty&&($.push("--console",this.rootfs.consoleSock(o)),I.push("--console-socket",this.rootfs.consoleSock(o))),await this.host.run([_,"delete","-f",o]),await this.host.check([b,...$,"--",...I,o]);const x=await this._waitForStart(o,k);return e=this.store.updateContainer(o,{state:"running",runtime:{...e.runtime,pid:x,health:null,networks:u.endpoints,network:{nsPath:u.nsPath},ip:y.ip||null,ports:d,startedAt:Date.now(),finishedAt:null,exitCode:0,error:"",manualStop:!1,paused:!1}}),this._refreshHosts(Object.keys(u.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),u&&await this.network.detachAll(o,u.endpoints),await this.rootfs.unmount(o).catch(()=>{});const i=t.exitCode??128;throw this.store.updateContainer(o,{state:"created"===e.state?"created":"exited",runtime:{...e.runtime,error:t.message,exitCode:i,finishedAt:Date.now()}}),t.statusCode?t:httpError(500,t.message)}}async _waitForStart(t,e){const{crun:i}=this.host.binaries();for(let s=0;s<40;s++){if(fs.existsSync(this.exitFile(t))){const i=parseInt(fs.readFileSync(this.exitFile(t),"utf8"),10),s=this._crunErrorSince(t,e);if(s){fs.rmSync(this.exitFile(t),{force:!0});const e=httpError(400,`failed to create task for container: OCI runtime create failed: ${s}`);throw e.exitCode=/not found|No such file/.test(s)?127:/ermission denied/.test(s)?126:i||127,e}return 0}const s=await this.host.run([i,"state",t]);if(0===s.code)try{const t=JSON.parse(s.stdout);if("running"===t.status)return t.pid||0}catch(t){}await sleep(50)}return 0}_crunErrorSince(t,e){try{const i=fs.openSync(this.logFile(t),"r"),s=fs.fstatSync(i).size,n=Buffer.alloc(Math.max(0,s-e));fs.readSync(i,n,0,n.length,e),fs.closeSync(i);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(i=>{this.waiters.has(t)||this.waiters.set(t,[]),this.waiters.get(t).push({condition:e,resolve:i})})}_resolveWaiters(t,e,i){const s=this.waiters.get(t)||[],n=[];for(const t of s)"removed"===e||"removed"!==t.condition?t.resolve({StatusCode:i,Error:null}):n.push(t);n.length?this.waiters.set(t,n):this.waiters.delete(t)}wait(t,e="not-running"){const i=this.findContainer(t);if(!["not-running","next-exit","removed"].includes(e))throw httpError(400,`invalid condition: "${e}"`);return"not-running"===e&&"running"!==i.state?Promise.resolve({StatusCode:i.runtime.exitCode||0,Error:null}):this._waitFor(i.id,e)}async _signal(t,e){const{crun:i}=this.host.binaries();return 0===(await this.host.run([i,"kill",t.id,String(e).replace(/^SIG/,"")])).code}async stop(t,e){const i=this.findContainer(t);if("running"!==i.state)return"notModified";await this.ready(),await this._thaw(i),this.store.updateContainer(i.id,{runtime:{...i.runtime,manualStop:!0}});const s=this.store.getImage(i.image)?.config?.config||{},n=i.config.Config.StopSignal||s.StopSignal||"SIGTERM",r=Number.isFinite(e)?e:i.config.Config.StopTimeout??10,o=this._waitFor(i.id,"next-exit");return await this._signal(i,n),await Promise.race([o,sleep(1e3*r).then(()=>null)])||(await this._signal(i,"SIGKILL"),await Promise.race([o,sleep(1e4)])),"stopped"}async kill(t,e="SIGKILL"){const i=this.findContainer(t);if("running"!==i.state)throw httpError(409,`Cannot kill container: ${t}: Container ${i.id} is not running`);if(await this.ready(),"KILL"!==String(e).replace(/^SIG/,"")&&9!==e&&"9"!==e||await this._thaw(i),this.store.updateContainer(i.id,{runtime:{...i.runtime,manualStop:!0}}),!await this._signal(i,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:i=!1}={}){let s=this.findContainer(t);if("running"===s.state&&!e)throw httpError(409,`You cannot remove a running container ${s.id}. Stop the container before attempting removal or force remove`);if("removing"===s.state)return;const n="running"===s.state;if(s=this.store.updateContainer(s.id,{state:"removing",runtime:{...s.runtime,manualStop:!0}}),n){await this.ready(),await this._thaw(s);const t=this._waitFor(s.id,"next-exit");await this._signal(s,"SIGKILL"),await Promise.race([t,sleep(1e4)]),s=this.store.getContainer(s.id)}await this._destroy(s,i)}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`);await this.ready();const{crun:i}=this.host.binaries(),s=await this.host.run([i,"pause",e.id]);if(0!==s.code)throw httpError(500,`Cannot pause container ${e.id}: ${(s.stderr||s.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 i=this.store.getContainer(t.id);if(!i?.runtime.paused)return;const{crun:s}=this.host.binaries(),n=await this.host.run([s,"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 i=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 s=e.replace(/^\//,"");if(s===i.name)throw httpError(409,"Renaming a container with the same name as its current name");const n=this.store.findContainer(s);if(n&&n.name===s)throw httpError(409,`Conflict. The container name "/${s}" 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(i.id,{name:s}),this._refreshHosts(Object.keys(i.runtime.networks||{})),this.emitEvent("container","rename",i.id,this._attrs({...i,name:s},{oldName:`/${i.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 i=this.findContainer(t),s=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={...i.config.HostConfig,...s};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"===i.state&&Object.keys(s).length){await this.ready();const{devices:t,...e}=buildSpec({id:i.id,rootfs:"/",args:["x"],env:[],cwd:"/",user:{uid:0,gid:0},hostname:"x",mounts:[],hostConfig:{...n,Ulimits:[],Sysctls:{}}}).linux.resources,s=path.join(this.containerDir(i.id),"update.json");fs.writeFileSync(s,JSON.stringify(e));const{crun:r}=this.host.binaries(),o=await this.host.run([r,"update","--resources",this.host.guestPath(s),i.id]);if(fs.rmSync(s,{force:!0}),0!==o.code)throw httpError(500,`Cannot update container ${i.id}: ${(o.stderr||o.stdout).trim()}`)}return this._saveConfig(i.id,{...i.config,HostConfig:n}),this.emitEvent("container","update",i.id,this._attrs(i)),{Warnings:[]}}async commit(t,{repo:e,tag:i,comment:s="",author:n="",pause:r=!0,changes:o=[],config:a={}}={}){const{applyChanges:h}=require("./build/changes"),{COMMIT:c}=require("./build/builder"),l=this.findContainer(t);await this.ready();const u=this.store.getImage(l.image);if(!u)throw httpError(404,`No such image: ${l.config.imageRef}`);const d="running"===l.state&&r&&!l.runtime.paused;let m;d&&await this.pause(l.id);try{m=await(async()=>{const t=this.rootfs.root,e=`${t}/containers/${l.id}/commit`,i=path.join(this.paths.blobs,`commit-${l.id.slice(0,12)}-${Date.now()}.tmp`),{shim:s}=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/${l.id}/upper`,e]);try{const t=(await this.host.sh(c,[e,this.host.guestPath(i),s,"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(i,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(l.id).catch(()=>{})}const f=u.config||{},g=h({...f.config||{},...stripEmpty(a)},o),p=(new Date).toISOString(),w={...f,created:p,...n?{author:n}:{},...s?{comment:s}:{},config:g,rootfs:{type:"layers",diff_ids:[...f.rootfs?.diff_ids||[],...m?[m.diffId]:[]]},history:[...f.history||[],{created:p,created_by:`thinknsh commit ${l.name}`,...s?{comment:s}:{},...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 C=[...u.layers||[],...m?[{digest:m.digest,size:m.size,mediaType:"application/vnd.oci.image.layer.v1.tar+gzip"}]:[]],S=e?[parseReference(`${e}:${i||"latest"}`).familiar]:[];this.store.saveImage({id:v,repoTags:S,repoDigests:[],digest:null,size:C.reduce((t,e)=>t+e.size,0),layers:C,config:w,architecture:w.architecture,os:"linux",createdAt:Date.now()}),this.emitEvent("container","commit",l.id,this._attrs(l,{comment:s}));for(const t of S)this.emitEvent("image","tag",v,{name:t});return{Id:v}}async importImage(t,{repo:e,tag:i,changes:s=[],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"]},s),comment:n}),c=e?[parseReference(i?`${e}:${i}`:e).familiar]:[],l=this.store.saveImage({...h,repoTags:c});return this.emitEvent("image","import",l.id,c.length?{name:c[0]}:{}),l}saveSelection(t){if(!t.length)throw httpError(400,"no images given to save");const e=new Map;for(const i of t){const t=this.requireImage(i);let s=[],n=null;try{n=parseReference(i).familiar}catch(t){}n&&t.repoTags.includes(n)?s=[n]:/^(sha256:)?[0-9a-f]{12,64}$/.test(i)||(s=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,...s])],e.set(t.id,r)}return[...e.values()]}async saveImages(t,e){const i=this.saveSelection(t);await archive.saveImages(i,{blobsDir:this.paths.blobs,sink:e});for(const{image:t}of i)this.emitEvent("image","save",t.id,{})}async loadImages(t,e){const i=path.join(this.paths.home,"build",`load-${crypto.randomBytes(6).toString("hex")}`);fs.mkdirSync(i,{recursive:!0});try{await archive.extractTo(t,i);const s=archive.readArchive(i,{blobsDir:this.paths.blobs,arch:this.linuxArch()});if(!s.length)throw httpError(400,"the archive holds no images");for(const t of s){const i=this.store.saveImage(t);if(this.emitEvent("image","load",i.id,{}),t.repoTags.length)for(const i of t.repoTags)e({stream:`Loaded image: ${i}\n`});else e({stream:`Loaded image ID: ${i.id}\n`})}}finally{fs.rmSync(i,{recursive:!0,force:!0})}}imageHistory(t){return archive.imageHistory(this.requireImage(t),t=>t.repoTags)}async _onExit(t,e,i=""){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 s="removing"===t.state;if(t=this.store.updateContainer(t.id,{state:s?"removing":"exited",runtime:{...t.runtime,exitCode:e,error:i,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),s)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 i=t.config.HostConfig.RestartPolicy||{},s=i.Name||"no";if("no"===s||t.runtime.manualStop)return;if("on-failure"===s){if(0===e)return;if(i.MaximumRetryCount>0&&t.runtime.restartCount>=i.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 i=null;if(e){const t=await this.host.run([this.host.binaries().crun,"list","-q"]);0===t.code&&(i=new Set(t.stdout.split("\n").map(t=>t.trim()).filter(Boolean)))}for(const s of t){const t=this.store.getContainer(s.id);t&&"running"===t.state&&!fs.existsSync(this.exitFile(s.id))&&(!e||i&&!i.has(s.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.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.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()}_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 i=(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:i(e.Interval,3e4),timeout:i(e.Timeout,3e4),startPeriod:i(e.StartPeriod,0),startInterval:i(e.StartInterval,5e3),retries:e.Retries>0?e.Retries:3}}_startHealth(t){const e=this._healthConfig(t);if(!e)return;const i=Date.now(),s={Status:"starting",FailingStreak:0,Log:[]};this._saveHealth(t.id,s);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 l=setTimeout(()=>h.kill("SIGKILL"),e.timeout);let u=await h.exited;clearTimeout(l),Date.now()-o>=e.timeout&&(u=-1,c=`Health check exceeded timeout (${e.timeout}ms)`);const d=this.store.getContainer(t.id)?.runtime.health||s,m=Date.now()-i<e.startPeriod,f=d.Status;if(0===u?(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:u,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()-i<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 i=this.store.getContainer(t);return!(!i||"running"!==i.state||(this.store.updateContainer(t,{runtime:{...i.runtime,health:e}}),0))}_stopHealth(t){clearTimeout(this.healthTimers.get(t)),this.healthTimers.delete(t)}createExec(t,e={}){const i=this.findContainer(t);if("running"!==i.state)throw httpError(409,`Container ${i.id} is not running`);if(i.runtime.paused)throw httpError(409,`Container ${i.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 s=crypto.randomBytes(32).toString("hex");return this.log(`exec create ${i.name}: ${JSON.stringify(redactExecBody(e))}`),this.execs.set(s,{ID:s,containerId:i.id,config:e,Running:!1,ExitCode:null,Pid:0}),this.emitEvent("container",`exec_create: ${e.Cmd.join(" ")}`,i.id,this._attrs(i,{execID:s})),{Id:s}}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 i=this.findContainer(e.containerId);if("running"!==i.state)throw httpError(409,`Container ${i.id} is not running`);await this.ready();const{crun:s,shim:n}=this.host.binaries();let r=[s,"exec"];if(e.config.Tty){const i=`${this.rootfs.root}/exec-${t.slice(0,16)}.sock`;e.ctl=`${this.rootfs.root}/exec-${t.slice(0,16)}.ctl`,r=[n,"relay","--console",i,"--ctl",e.ctl,"--",s,"exec","--tty","--console-socket",i]}e.config.WorkingDir&&r.push("--cwd",e.config.WorkingDir);const o=this.store.getImage(i.image)?.config?.config?.Env||[],a=this._hostname(i);for(const t of mergeEnv(o,[`HOSTNAME=${a}`,...i.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(i.id,"/etc/passwd"),await this.rootfs.readFile(i.id,"/etc/group")):resolveUser(e.config.User);r.push("--user",`${t.uid}:${t.gid}`)}r.push(i.id,...e.config.Cmd),e.Running=!0,this.emitEvent("container",`exec_start: ${e.config.Cmd.join(" ")}`,i.id,this._attrs(i,{execID:t}));const h=this.host.spawn(r);return h.exited.then(s=>{e.Running=!1,e.ExitCode=s,this.emitEvent("container","exec_die",i.id,this._attrs(i,{execID:t,exitCode:String(s)}))}),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 i=this.findContainer(t);if("running"!==i.state)return!1;const s=Object.values(i.runtime.networks||{}).find(t=>t.ip),n=s?.ip||(i.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 i=this.host.connect(n,e),s=e=>{clearTimeout(r),i.destroy(),t(e)},r=setTimeout(()=>s(!1),1500);i.once("ready",()=>s(!0)),i.once("connect",()=>s(!0)),i.once("error",()=>s(!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,i){const s=this.findContainer(t);if("running"!==s.state)throw httpError(409,`Container ${s.id} is not running`);s.config.Config.Tty&&e>0&&i>0&&await this._ctl(this.rootfs.ctlFifo(s.id),`resize ${e} ${i}`)}async resizeExec(t,e,i){const s=this.getExec(t);s.ctl&&s.Running&&e>0&&i>0&&await this._ctl(s.ctl,`resize ${e} ${i}`)}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=[],i=t.runtime.ports||{},s=this.store.getImage(t.image)?.config?.config?.ExposedPorts||{};for(const n of Object.keys({...s,...t.config.Config.ExposedPorts||{},...i})){const[t,s="tcp"]=n.split("/"),r=i[n];if(r?.length)for(const i of r)e.push({IP:i.HostIp,PrivatePort:Number(t),PublicPort:Number(i.HostPort),Type:s});else e.push({PrivatePort:Number(t),Type:s})}return e}_networks(t){const e={};for(const i of t.config.networks||[{name:"bridge",aliases:[]}]){const s=t.runtime.networks?.[i.name]||{};let n=null;try{n=this.network.resolve(i.name)}catch(t){}e[i.name]={IPAMConfig:null,Links:null,Aliases:i.aliases?.length?i.aliases:null,NetworkID:n?.id||i.name,EndpointID:s.ip?`${t.id.slice(0,32)}${(n?.id||"").slice(0,32)}`:"",Gateway:s.gateway||"",IPAddress:s.ip||"",IPPrefixLen:s.ip?s.prefixLen:0,IPv6Gateway:"",GlobalIPv6Address:"",GlobalIPv6PrefixLen:0,MacAddress:s.mac||"",DNSNames:s.ip&&n?.dns?[...new Set([t.name,...i.aliases||[],t.id.slice(0,12)])]:null}}return e}async connectNetwork(t,e,i={}){this.host;const s=this.network.resolve(t);let n=this.findContainer(e);const r=n.config.networks||[];if(r.some(t=>t.name===s.name))throw httpError(403,`endpoint with name ${n.name} already exists in network ${s.name}`);if(["host","null"].includes(s.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:s.name,aliases:i.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,i=await this.network.attachOne(n.id,s,e,o.aliases);n=this.store.updateContainer(n.id,{runtime:{...n.runtime,networks:{...n.runtime.networks,[s.name]:i}}})}this.store.db.prepare("UPDATE containers SET config = ? WHERE id = ?").run(JSON.stringify({...n.config,networks:a}),n.id),this.emitEvent("network","connect",s.id,{container:n.id,name:s.name,type:s.driver}),this._refreshHosts([s.name])}async disconnectNetwork(t,e){this.host;const i=this.network.resolve(t);let s=this.findContainer(e);const n=(s.config.networks||[]).filter(t=>t.name!==i.name);if(n.length===(s.config.networks||[]).length)throw httpError(404,`container ${s.id} is not connected to network ${i.name}`);const r=s.runtime.networks?.[i.name];if("running"===s.state&&r){await this.network.detachOne(s.id,r);const t={...s.runtime.networks};delete t[i.name],s=this.store.updateContainer(s.id,{runtime:{...s.runtime,networks:t}})}if(this.store.db.prepare("UPDATE containers SET config = ? WHERE id = ?").run(JSON.stringify({...s.config,networks:n}),s.id),this.emitEvent("network","disconnect",i.id,{container:s.id,name:i.name,type:i.driver}),this._refreshHosts([i.name]),"running"===s.state)try{fs.writeFileSync(path.join(this.containerDir(s.id),"hosts"),this._hostsContent(s))}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 i of this.store.listContainers()){const s=i.runtime.networks?.[t.name];"running"===i.state&&s&&(e[i.id]={Name:i.name,EndpointID:i.id,MacAddress:s.mac||"",IPv4Address:s.ip?`${s.ip}/${s.prefixLen}`:"",IPv6Address:""})}return{Name:t.name,Id:t.id,Created:t.createdAt?iso(t.createdAt):"0001-01-01T00:00:00Z",Scope:"local",Driver:t.driver,EnableIPv6:!1,IPAM:{Driver:"default",Options:null,Config:t.subnet?[{Subnet:t.subnet,Gateway:t.gateway}]:[]},Internal:!!t.internal,Attachable:!1,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 i=this.store.listContainers();t||e.status||(i=i.filter(t=>"running"===t.state));const s=(t,s)=>{e[t]?.length&&(i=i.filter(i=>e[t].some(t=>s(i,t))))};return s("status",(t,e)=>"paused"===e?"running"===t.state&&!!t.runtime.paused:"running"===e?"running"===t.state&&!t.runtime.paused:t.state===e),s("name",(t,e)=>t.name.includes(e.replace(/^\//,""))),s("id",(t,e)=>t.id.startsWith(e)),e.label?.length&&(i=i.filter(t=>e.label.every(e=>labelMatch(t.config.Config.Labels,e)))),s("ancestor",(t,e)=>{const i=this.findImage(e);return i&&i.id===t.image}),i.map(t=>this.containerSummary(t))}inspect(t){const e=this.findContainer(t),i=e.runtime,s=this.store.getImage(e.image)?.config?.config||{},n=processArgs(e.config.Config,s),r="running"===e.state,o=Object.values(i.networks||{})[0]||{};return{Id:e.id,Created:iso(e.createdAt),Path:n[0]||"",Args:n.slice(1),State:{Status:"removing"===e.state?"removing":r&&i.paused?"paused":e.state,Running:r,Paused:!(!r||!i.paused),Restarting:!1,OOMKilled:!1,Dead:!1,Pid:r&&i.pid||0,ExitCode:i.exitCode||0,Error:i.error||"",StartedAt:iso(i.startedAt),...r&&i.health?{Health:i.health}:{},FinishedAt:iso(i.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:i.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:i.apiMounts||[],Config:{Hostname:e.config.Config.Hostname||e.id.slice(0,12),Domainname:"",User:e.config.Config.User||s.User||"",AttachStdin:!!e.config.Config.AttachStdin,AttachStdout:!!e.config.Config.AttachStdout,AttachStderr:!!e.config.Config.AttachStderr,ExposedPorts:{...s.ExposedPorts||{},...e.config.Config.ExposedPorts||{}},Tty:!!e.config.Config.Tty,OpenStdin:!!e.config.Config.OpenStdin,StdinOnce:!!e.config.Config.StdinOnce,Env:mergeEnv(s.Env,e.config.Config.Env||[]),Cmd:e.config.Config.Cmd||s.Cmd||null,Image:e.config.imageRef,Volumes:s.Volumes||null,WorkingDir:e.config.Config.WorkingDir||s.WorkingDir||"",Entrypoint:e.config.Config.Entrypoint||s.Entrypoint||null,Labels:{...s.Labels||{},...e.config.Config.Labels||{}},StopSignal:e.config.Config.StopSignal||s.StopSignal,...s.Healthcheck||e.config.Config.Healthcheck?{Healthcheck:{...s.Healthcheck||{},...e.config.Config.Healthcheck||{}}}:{}},NetworkSettings:{Bridge:"",SandboxID:"",HairpinMode:!1,SandboxKey:i.network?.nsPath||"",Ports:i.ports||{},Gateway:o.gateway||"",IPAddress:o.ip||"",IPPrefixLen:o.ip?o.prefixLen: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,i)=>t[i]===e)))return;const i=fs.fstatSync(e).size;fs.writeSync(e,Buffer.alloc((512-i%512)%512+1024),0,void 0,i)}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 i=t?.Config||{},s=sizeBytes(i["max-size"]||process.env.TNC_DOCK_LOG_MAX_SIZE||"20m"),n=parseInt(i["max-file"]||process.env.TNC_DOCK_LOG_MAX_FILE||"5",10);return s>0?["--log-max-bytes",String(s),"--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 i=e.indexOf("=");return-1===i?Object.prototype.hasOwnProperty.call(t||{},e):(t||{})[e.slice(0,i)]===e.slice(i+1)}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 i=Math.round(e/60);if(1===i)return"About a minute";if(i<60)return`${i} minutes`;const s=Math.round(e/3600);if(1===s)return"About an hour";if(s<48)return`${s} hours`;const n=Math.round(s/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};
|
|
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};
|
|
@@ -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"),{DEFAULT_REGISTRY,DEFAULT_REGISTRY_HOST,DEFAULT_REGISTRY_API}=require("../api/wire"),DEFAULT_ALIASES=new Set([DEFAULT_REGISTRY_HOST,DEFAULT_REGISTRY_API,new URL(DEFAULT_REGISTRY).host]);function normalizeRegistry(e){let r=String(e||"").trim().toLowerCase();if(!r)return DEFAULT_REGISTRY_HOST;if(r=r.replace(/^[a-z][a-z0-9+.-]*:\/\//,"").split("/")[0],!r||/[\s@]/.test(r)){const r=new Error(`invalid registry address "${e}"`);throw r.statusCode=400,r}return DEFAULT_ALIASES.has(r)?DEFAULT_REGISTRY_HOST:r}class AuthStore{constructor(e){this.file=e}_read(){try{const e=JSON.parse(fs.readFileSync(this.file,"utf8"));return e&&"object"==typeof e?e:{}}catch(e){if("ENOENT"===e.code)return{};throw new Error(`cannot read ${this.file}: ${e.code||"invalid JSON"}`)}}_write(e){fs.mkdirSync(path.dirname(this.file),{recursive:!0,mode:448});const r=`${this.file}.${crypto.randomBytes(6).toString("hex")}.tmp`;fs.writeFileSync(r,JSON.stringify(e,null,2)+"\n",{mode:384}),fs.chmodSync(r,384),fs.renameSync(r,this.file)}get(e){const r=this._read()[
|
|
7
|
+
"use strict";const fs=require("fs"),path=require("path"),crypto=require("crypto"),{DEFAULT_REGISTRY,DEFAULT_REGISTRY_HOST,DEFAULT_REGISTRY_API}=require("../api/wire"),keychain=require("./keychain"),DEFAULT_ALIASES=new Set([DEFAULT_REGISTRY_HOST,DEFAULT_REGISTRY_API,new URL(DEFAULT_REGISTRY).host]);function normalizeRegistry(e){let r=String(e||"").trim().toLowerCase();if(!r)return DEFAULT_REGISTRY_HOST;if(r=r.replace(/^[a-z][a-z0-9+.-]*:\/\//,"").split("/")[0],!r||/[\s@]/.test(r)){const r=new Error(`invalid registry address "${e}"`);throw r.statusCode=400,r}return DEFAULT_ALIASES.has(r)?DEFAULT_REGISTRY_HOST:r}class AuthStore{constructor(e,{keychain:r}={}){this.file=e,this._kc=r}get keychain(){if(void 0!==this._kc)return this._kc;const e=path.join(require("os").homedir(),".tnc","dock","auth.json");return path.resolve(this.file)!==e&&"on"!==process.env.TNC_DOCK_KEYCHAIN||"file"===(require("../config").readConfig().credentialStore||"auto")?null:keychain.backend()}_read(){try{const e=JSON.parse(fs.readFileSync(this.file,"utf8"));return e&&"object"==typeof e?e:{}}catch(e){if("ENOENT"===e.code)return{};throw new Error(`cannot read ${this.file}: ${e.code||"invalid JSON"}`)}}_write(e){fs.mkdirSync(path.dirname(this.file),{recursive:!0,mode:448});const r=`${this.file}.${crypto.randomBytes(6).toString("hex")}.tmp`;fs.writeFileSync(r,JSON.stringify(e,null,2)+"\n",{mode:384}),fs.chmodSync(r,384),fs.renameSync(r,this.file)}get(e){const r=normalizeRegistry(e),t=this._read()[r];if(!t||!t.username)return null;if("keychain"===t.store){const e=this.keychain||keychain.backend(),i=e?e.get(r):null;return"string"==typeof i?{username:t.username,password:i}:null}if("string"!=typeof t.password)return null;if(this.keychain)try{this.set(r,{username:t.username,password:t.password})}catch(e){}return{username:t.username,password:t.password}}set(e,{username:r,password:t}){const i=normalizeRegistry(e),s=this._read(),n=this.keychain;if(n)try{return n.set(i,t,r),s[i]={username:r,store:"keychain"},void this._write(s)}catch(e){}s[i]={username:r,password:t},this._write(s)}remove(e){const r=this._read(),t=normalizeRegistry(e);if(!(t in r))return!1;if("keychain"===r[t].store)try{(this.keychain||keychain.backend())?.remove(t)}catch(e){}return delete r[t],this._write(r),!0}registries(){return Object.keys(this._read())}}module.exports={AuthStore,normalizeRegistry};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license ThinkNCollab Enterprise & Open Core Ecosystem
|
|
3
|
+
* Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
|
|
4
|
+
* PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
|
|
5
|
+
* UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
|
|
6
|
+
*/
|
|
7
|
+
"use strict";const fs=require("fs"),os=require("os"),path=require("path"),{spawnSync}=require("child_process"),{CREDENTIAL_HELPER_PREFIX,ENGINE_CLI_CONFIG_DIR,ENGINE_CLI_CONFIG_VAR,DEFAULT_REGISTRY,DEFAULT_REGISTRY_HOST}=require("../api/wire"),TTL_MS=6e4,cache=new Map;function cliConfig(e=process.env){const r=path.join(e[ENGINE_CLI_CONFIG_VAR]||path.join(os.homedir(),ENGINE_CLI_CONFIG_DIR),"config.json");try{return JSON.parse(fs.readFileSync(r,"utf8"))||{}}catch(e){return{}}}const bareHost=e=>String(e).replace(/^[a-z]+:\/\//i,"").split("/")[0].toLowerCase();function helperFor(e,{dockConfig:r=require("../config").readConfig(),engineConfig:n=cliConfig()}={}){const t=r=>{for(const[n,t]of Object.entries(r||{}))if(bareHost(n)===e&&t)return String(t);return null};return t(r.credentialHelpers)||t(n.credHelpers)||(n.credsStore?String(n.credsStore):null)}const serverUrl=e=>e===DEFAULT_REGISTRY_HOST?DEFAULT_REGISTRY:e;function run(e,r){if(!/^[A-Za-z0-9._-]+$/.test(e))return null;const n=`${CREDENTIAL_HELPER_PREFIX}${e}${"win32"===process.platform?".exe":""}`,t=spawnSync(n,["get"],{input:serverUrl(r),encoding:"utf8",timeout:1e4,windowsHide:!0});if(t.error||0!==t.status)return null;try{const r=JSON.parse(t.stdout);return r&&"string"==typeof r.Secret&&r.Secret?{username:r.Username||"",password:r.Secret,helper:e}:null}catch(e){return null}}function lookup(e,r={}){const n=cache.get(e);if(n&&Date.now()-n.at<6e4)return n.value;const t=(r.helperFor||helperFor)(e),o=t?(r.run||run)(t,e):null;return cache.set(e,{at:Date.now(),value:o}),o}module.exports={lookup,helperFor,cliConfig,bareHost,serverUrl,_clear:()=>cache.clear()};
|
|
@@ -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{spawnSync}=require("child_process"),SERVICE="tnc-dock";function sh(e,t,r){const n=spawnSync(e,t,{input:r,encoding:"utf8",timeout:15e3,windowsHide:!0});return{ok:0===n.status,stdout:n.stdout||"",stderr:n.stderr||"",missing:"ENOENT"===n.error?.code}}const macos={name:"macOS Keychain",available:()=>"darwin"===process.platform&&sh("/usr/bin/security",["list-keychains"]).ok,set(e,t){const r=Buffer.from(t,"utf8").toString("hex"),n=e=>`"${String(e).replace(/(["\\])/g,"\\$1")}"`,s=sh("/usr/bin/security",["-i"],`add-generic-password -U -s ${n(SERVICE)} -a ${n(e)} -l ${n(`${SERVICE} ${e}`)} -X ${r}\n`);if(!s.ok||/error/i.test(s.stderr))throw new Error(`keychain: ${s.stderr.trim()||"cannot store the password"}`)},get(e){const t=sh("/usr/bin/security",["find-generic-password","-s",SERVICE,"-a",e,"-w"]);return t.ok?t.stdout.replace(/\n$/,""):null},remove(e){sh("/usr/bin/security",["delete-generic-password","-s",SERVICE,"-a",e])}},WIN_SCRIPT="\n$ErrorActionPreference = 'Stop'\nAdd-Type -TypeDefinition @'\nusing System; using System.Runtime.InteropServices; using System.Text;\npublic static class TncCred {\n [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]\n struct CREDENTIAL { public int Flags; public int Type; public string TargetName; public string Comment; public long LastWritten;\n public int CredentialBlobSize; public IntPtr CredentialBlob; public int Persist; public int AttributeCount; public IntPtr Attributes;\n public string TargetAlias; public string UserName; }\n [DllImport(\"advapi32.dll\", CharSet = CharSet.Unicode, SetLastError = true)] static extern bool CredWrite(ref CREDENTIAL c, int flags);\n [DllImport(\"advapi32.dll\", CharSet = CharSet.Unicode, SetLastError = true)] static extern bool CredRead(string target, int type, int flags, out IntPtr c);\n [DllImport(\"advapi32.dll\", CharSet = CharSet.Unicode, SetLastError = true)] static extern bool CredDelete(string target, int type, int flags);\n [DllImport(\"advapi32.dll\")] static extern void CredFree(IntPtr c);\n public static void Set(string target, string user, string secret) {\n byte[] b = Encoding.UTF8.GetBytes(secret);\n var c = new CREDENTIAL { Type = 1, TargetName = target, UserName = user, Persist = 2, CredentialBlobSize = b.Length, CredentialBlob = Marshal.AllocHGlobal(b.Length) };\n Marshal.Copy(b, 0, c.CredentialBlob, b.Length);\n try { if (!CredWrite(ref c, 0)) throw new Exception(\"CredWrite failed: \" + Marshal.GetLastWin32Error()); } finally { Marshal.FreeHGlobal(c.CredentialBlob); }\n }\n public static string Get(string target) {\n IntPtr p; if (!CredRead(target, 1, 0, out p)) return null;\n try { var c = (CREDENTIAL)Marshal.PtrToStructure(p, typeof(CREDENTIAL)); byte[] b = new byte[c.CredentialBlobSize];\n Marshal.Copy(c.CredentialBlob, b, 0, b.Length); return Encoding.UTF8.GetString(b); } finally { CredFree(p); }\n }\n public static void Delete(string target) { CredDelete(target, 1, 0); }\n}\n'@\n$req = [Console]::In.ReadToEnd() | ConvertFrom-Json\nswitch ($req.op) {\n 'set' { [TncCred]::Set($req.target, $req.user, [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($req.secret))) }\n 'get' { $v = [TncCred]::Get($req.target); if ($null -ne $v) { [Console]::Out.Write([Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($v))) } else { exit 3 } }\n 'delete' { [TncCred]::Delete($req.target) }\n 'ping' { [Console]::Out.Write('ok') }\n}",WIN_ENCODED=Buffer.from(WIN_SCRIPT,"utf16le").toString("base64");function powershell(e){return sh("powershell.exe",["-NoProfile","-NonInteractive","-ExecutionPolicy","Bypass","-EncodedCommand",WIN_ENCODED],JSON.stringify(e))}const windows={name:"Windows Credential Manager",available:()=>"win32"===process.platform&&"ok"===powershell({op:"ping"}).stdout,set(e,t,r=""){const n=powershell({op:"set",target:`${SERVICE}:${e}`,user:r,secret:Buffer.from(t,"utf8").toString("base64")});if(!n.ok)throw new Error(`credential manager: ${n.stderr.trim()||"cannot store the password"}`)},get(e){const t=powershell({op:"get",target:`${SERVICE}:${e}`});return t.ok?Buffer.from(t.stdout.trim(),"base64").toString("utf8"):null},remove(e){powershell({op:"delete",target:`${SERVICE}:${e}`})}},linux={name:"Secret Service (secret-tool)",available(){if("linux"!==process.platform||!process.env.DBUS_SESSION_BUS_ADDRESS)return!1;const e=sh("secret-tool",["search","service",SERVICE]);return!e.missing&&!/error|cannot|failed/i.test(e.stderr)},set(e,t){const r=sh("secret-tool",["store","--label",`${SERVICE} ${e}`,"service",SERVICE,"registry",e],t);if(!r.ok)throw new Error(`secret-tool: ${r.stderr.trim()||"cannot store the password"}`)},get(e){const t=sh("secret-tool",["lookup","service",SERVICE,"registry",e]);return t.ok?t.stdout:null},remove(e){sh("secret-tool",["clear","service",SERVICE,"registry",e])}},BACKENDS={darwin:macos,win32:windows,linux};let cached;function backend(){const e=process.env.TNC_DOCK_KEYCHAIN;if("off"===e)return null;if("on"!==e&&process.env.TNC_DOCK_HOME&&require("path").resolve(process.env.TNC_DOCK_HOME)!==require("path").join(require("os").homedir(),".tnc","dock"))return null;if(void 0===cached){const e=BACKENDS[process.platform];try{cached=e&&e.available()?e:null}catch(e){cached=null}}return cached}function describe(){const e=backend();return e?e.name:"auth.json (no system keychain available)"}module.exports={backend,describe,SERVICE,_backends:{macos,windows,linux},_reset:()=>{cached=void 0}};
|