diff --git a/Dockerfile b/Dockerfile index 89ebba3..0332010 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,6 +16,7 @@ ARG LINUX_LOGLEVEL=7 ARG INIT_DEBUG=true ARG VM_MEMORY_SIZE_MB=128 ARG NO_VMTOUCH= +ARG EXTERNAL_BUNDLE= ARG OUTPUT_NAME=out.wasm # for wasi ARG JS_OUTPUT_NAME=out # for emscripten; must not include "." @@ -43,6 +44,7 @@ ARG TARGETPLATFORM ARG INIT_DEBUG ARG OPTIMIZATION_MODE ARG NO_VMTOUCH +ARG EXTERNAL_BUNDLE COPY --link --from=assets / /work WORKDIR /work RUN --mount=type=cache,target=/root/.cache/go-build \ @@ -60,12 +62,16 @@ RUN mkdir -p /out/oci/rootfs /out/oci/bundle && \ NO_VMTOUCH_F=false && \ if test "${OPTIMIZATION_MODE}" = "native" ; then NO_VMTOUCH_F=true ; fi && \ if test "${NO_VMTOUCH}" != "" ; then NO_VMTOUCH_F="${NO_VMTOUCH}" ; fi && \ - create-spec --debug=${INIT_DEBUG} --debug-init=${IS_WIZER} --no-vmtouch=${NO_VMTOUCH_F} \ + EXTERNAL_BUNDLE_F=false && \ + if test "${EXTERNAL_BUNDLE}" = "true" ; then EXTERNAL_BUNDLE_F=true ; fi && \ + create-spec --debug=${INIT_DEBUG} --debug-init=${IS_WIZER} --no-vmtouch=${NO_VMTOUCH_F} --external-bundle=${EXTERNAL_BUNDLE_F} \ --image-config-path=/oci/image.json \ --runtime-config-path=/oci/spec.json \ --rootfs-path=/oci/rootfs \ - /oci "${TARGETPLATFORM}" /out/oci/rootfs && \ - mv image.json spec.json /out/oci/ && mv initconfig.json /out/oci/ + /oci "${TARGETPLATFORM}" /out/oci/rootfs +RUN if test -f image.json; then mv image.json /out/oci/ ; fi && \ + if test -f spec.json; then mv spec.json /out/oci/ ; fi +RUN mv initconfig.json /out/oci/ FROM ubuntu:22.04 AS gcc-riscv64-linux-gnu-base RUN apt-get update && apt-get install -y gcc-riscv64-linux-gnu libc-dev-riscv64-cross git make diff --git a/Dockerfile.test b/Dockerfile.test index 4cc11e9..701b073 100644 --- a/Dockerfile.test +++ b/Dockerfile.test @@ -26,6 +26,11 @@ COPY ./tests/c2w-net-proxy-test /c2w-net-proxy-test WORKDIR /c2w-net-proxy-test RUN go build -o /out/c2w-net-proxy-test main.go +FROM golang:1.21 AS imagemounter-test-dev +COPY ./tests/imagemounter-test /imagemounter-test +WORKDIR /imagemounter-test +RUN go build -o /out/imagemounter-test main.go + FROM ubuntu:22.04 ARG BUILDX_VERSION ARG DOCKER_VERSION @@ -70,6 +75,7 @@ RUN wget https://github.com/WasmEdge/WasmEdge/releases/download/${WASMEDGE_VERSI COPY --from=wazero-test-dev /out/wazero-test /usr/local/bin/ COPY --from=httphello-dev /out/httphello /usr/local/bin/ COPY --from=c2w-net-proxy-test-dev /out/c2w-net-proxy-test /usr/local/bin/ +COPY --from=imagemounter-test-dev /out/imagemounter-test /usr/local/bin/ # install golang RUN wget https://go.dev/dl/go${GOLANG_VERSION}.linux-amd64.tar.gz @@ -83,6 +89,7 @@ WORKDIR /test/ RUN go build -o /usr/local/bin/ ./cmd/c2w RUN go build -o /usr/local/bin/ ./cmd/c2w-net RUN cd extras/c2w-net-proxy/ ; GOOS=wasip1 GOARCH=wasm go build -o /opt/c2w-net-proxy.wasm . +RUN cd extras/imagemounter/ ; GOOS=wasip1 GOARCH=wasm go build -o /opt/imagemounter.wasm . ENTRYPOINT ["dockerd-entrypoint.sh"] CMD [] diff --git a/Makefile b/Makefile index 1057804..d2b98a7 100644 --- a/Makefile +++ b/Makefile @@ -21,6 +21,9 @@ c2w-net: c2w-net-proxy.wasm: cd extras/c2w-net-proxy/ ; GOOS=wasip1 GOARCH=wasm go build -o $(PREFIX)/c2w-net-proxy.wasm . +imagemounter.wasm: + cd extras/imagemounter ; GOOS=wasip1 GOARCH=wasm go build -o $(PREFIX)/imagemounter.wasm . + install: install -D -m 755 $(PREFIX)/c2w $(CMD_DESTDIR)/bin install -D -m 755 $(PREFIX)/c2w-net $(CMD_DESTDIR)/bin diff --git a/README.md b/README.md index f895814..0be45a4 100644 --- a/README.md +++ b/README.md @@ -310,6 +310,7 @@ Re-compilation (and possibe re-implementation) of the application is needed. - [`./examples/`](./examples): Examples (python, php, on-browser, networking, etc.) - `vscode-container-wasm`: VSCode extension for running containers on VSCode on browser (e.g. `github.dev`), leveraging container2wasm: https://github.com/ktock/vscode-container-wasm +- [`./extras/imagemounter`]: A helper tool for enabling to distributing and running container images on browser without pre-conversion of the images. ## Acknowledgement diff --git a/cmd/c2w/main.go b/cmd/c2w/main.go index 4c283f2..fb8258b 100644 --- a/cmd/c2w/main.go +++ b/cmd/c2w/main.go @@ -66,6 +66,10 @@ func main() { Name: "legacy", Usage: "Use \"docker build\" instead of buildx (no support for assets flag)", }, + cli.BoolFlag{ + Name: "external-bundle", + Usage: "Do not embed container image to the Wasm image but mount it during runtime", + }, }, flags...) app.Action = rootAction if err := app.Run(os.Args); err != nil { @@ -80,9 +84,22 @@ func rootAction(clicontext *cli.Context) error { fmt.Printf("\n") return nil } - srcImgName := clicontext.Args().First() - if srcImgName == "" { - return fmt.Errorf("specify image name") + arg1 := clicontext.Args().First() + if arg1 == "" { + if clicontext.Bool("external-bundle") { + return fmt.Errorf("specify output image name") + } else { + return fmt.Errorf("specify image name") + } + } + var outputPath string + if clicontext.Bool("external-bundle") { + outputPath = arg1 + if clicontext.Args().Get(1) != "" { + return fmt.Errorf("command receives only 1 arg (output image path) with external-bundle") + } + } else { + outputPath = clicontext.Args().Get(1) } builderPath, err := exec.LookPath(clicontext.String("builder")) if err != nil { @@ -100,8 +117,8 @@ func rootAction(clicontext *cli.Context) error { if clicontext.Bool("to-js") { destFile = "" } - if o := clicontext.Args().Get(1); o != "" { - d, f := filepath.Split(o) + if outputPath != "" { + d, f := filepath.Split(outputPath) destDir, err = filepath.Abs(d) if err != nil { return err @@ -113,28 +130,34 @@ func rootAction(clicontext *cli.Context) error { if clicontext.Bool("to-js") && destFile != "" { return fmt.Errorf("output destination must be a slash-terminated directory path when using \"to-js\" option") } - if legacy { - return buildWithLegacyBuilder(builderPath, srcImgName, destDir, destFile, clicontext) + if a := clicontext.String("assets"); a != "" && legacy { + return fmt.Errorf("\"assets\" unsupported on docker build as of now; install docker buildx instead") } - return build(builderPath, srcImgName, destDir, destFile, clicontext) -} - -func build(builderPath string, srcImgName string, destDir, destFile string, clicontext *cli.Context) error { + srcImgName := arg1 tmpdir, err := os.MkdirTemp("", "container2wasm") if err != nil { return err } defer os.RemoveAll(tmpdir) - srcImgPath := filepath.Join(tmpdir, "img") if err := os.Mkdir(srcImgPath, 0755); err != nil { return err } - if err := prepareSourceImg(builderPath, srcImgName, srcImgPath, clicontext.String("target-arch")); err != nil { - return fmt.Errorf("failed to prepare image: %w", err) + if !clicontext.Bool("external-bundle") { + if err := prepareSourceImg(builderPath, srcImgName, srcImgPath, clicontext.String("target-arch")); err != nil { + return fmt.Errorf("failed to prepare image: %w", err) + } + } + + if legacy { + return buildWithLegacyBuilder(builderPath, srcImgPath, destDir, destFile, clicontext) } + return build(builderPath, srcImgPath, destDir, destFile, clicontext) +} + +func build(builderPath string, srcImgPath string, destDir, destFile string, clicontext *cli.Context) error { buildxArgs := []string{ "buildx", "build", "--progress=plain", "--build-arg", fmt.Sprintf("TARGETARCH=%s", clicontext.String("target-arch")), @@ -145,11 +168,15 @@ func build(builderPath string, srcImgName string, destDir, destFile string, clic if o := clicontext.String("dockerfile"); o != "" { dockerfilePath = o } else { - df := filepath.Join(tmpdir, "Dockerfile") - if err := os.WriteFile(df, vendor.Dockerfile, 0600); err != nil { + f, err := os.CreateTemp("", "container2wasm") + if err != nil { + return err + } + defer os.Remove(f.Name()) + if _, err := f.Write(vendor.Dockerfile); err != nil { return err } - dockerfilePath = df + dockerfilePath = f.Name() } buildxArgs = append(buildxArgs, "-f", dockerfilePath) if o := clicontext.String("assets"); o != "" { @@ -173,6 +200,9 @@ func build(builderPath string, srcImgName string, destDir, destFile string, clic "--build-arg", fmt.Sprintf("LINUX_LOGLEVEL=%d", linuxLogLevel), "--build-arg", fmt.Sprintf("INIT_DEBUG=%v", initDebug), ) + if clicontext.Bool("external-bundle") { + buildxArgs = append(buildxArgs, "--build-arg", "EXTERNAL_BUNDLE=true") + } for _, a := range clicontext.StringSlice("build-arg") { buildxArgs = append(buildxArgs, "--build-arg", a) } @@ -185,24 +215,7 @@ func build(builderPath string, srcImgName string, destDir, destFile string, clic return cmd.Run() } -func buildWithLegacyBuilder(builderPath string, srcImgName, destDir, destFile string, clicontext *cli.Context) error { - if o := clicontext.String("assets"); o != "" { - return fmt.Errorf("\"assets\" unsupported on docker build as of now; install docker buildx instead") - } - tmpdir, err := os.MkdirTemp("", "container2wasm") - if err != nil { - return err - } - defer os.RemoveAll(tmpdir) - - srcImgPath := filepath.Join(tmpdir, "img") - if err := os.Mkdir(srcImgPath, 0755); err != nil { - return err - } - if err := prepareSourceImg(builderPath, srcImgName, srcImgPath, clicontext.String("target-arch")); err != nil { - return fmt.Errorf("failed to prepare image: %w", err) - } - +func buildWithLegacyBuilder(builderPath string, srcImgPath, destDir, destFile string, clicontext *cli.Context) error { buildArgs := []string{ "build", "--progress=plain", "--platform=linux/amd64", @@ -212,11 +225,15 @@ func buildWithLegacyBuilder(builderPath string, srcImgName, destDir, destFile st if o := clicontext.String("dockerfile"); o != "" { dockerfilePath = o } else { - df := filepath.Join(tmpdir, "Dockerfile") - if err := os.WriteFile(df, vendor.Dockerfile, 0600); err != nil { + f, err := os.CreateTemp("", "container2wasm") + if err != nil { return err } - dockerfilePath = df + defer os.Remove(f.Name()) + if _, err := f.Write(vendor.Dockerfile); err != nil { + return err + } + dockerfilePath = f.Name() } buildArgs = append(buildArgs, "-f", dockerfilePath) if clicontext.Bool("to-js") { @@ -237,6 +254,9 @@ func buildWithLegacyBuilder(builderPath string, srcImgName, destDir, destFile st "--build-arg", fmt.Sprintf("LINUX_LOGLEVEL=%d", linuxLogLevel), "--build-arg", fmt.Sprintf("INIT_DEBUG=%v", initDebug), ) + if clicontext.Bool("external-bundle") { + buildArgs = append(buildArgs, "--build-arg", "EXTERNAL_BUNDLE=true") + } for _, a := range clicontext.StringSlice("build-arg") { buildArgs = append(buildArgs, "--build-arg", a) } diff --git a/cmd/create-spec/main.go b/cmd/create-spec/main.go index ce160b7..3c08d0d 100644 --- a/cmd/create-spec/main.go +++ b/cmd/create-spec/main.go @@ -39,6 +39,7 @@ func main() { runtimeConfigPath = flag.String("runtime-config-path", "/oci/spec.json", "path to runtime spec config used by init during runtime") imageRootfsPath = flag.String("rootfs-path", "/oci/rootfs", "path to rootfs used as overlayfs lowerdir of container rootfs") noVmtouch = flag.Bool("no-vmtouch", false, "do not perform vmtouch") + externalBundle = flag.Bool("external-bundle", false, "provide bundle externally during runtime") ) flag.Parse() args := flag.Args() @@ -46,23 +47,37 @@ func main() { platform := args[1] rootfs := args[2] - p, err := platforms.Parse(platform) - if err != nil { - panic(err) - } - cfg, err := unpack(context.TODO(), imgDir, &p, rootfs) - if err != nil { - panic(err) - } - cfgD, err := io.ReadAll(cfg) - if err != nil { - panic(err) - } - if err := os.WriteFile("image.json", cfgD, 0600); err != nil { - panic(err) - } - if err := createSpec(bytes.NewReader(cfgD), rootfs, *debug, *debugInit, *imageConfigPath, *runtimeConfigPath, *imageRootfsPath, *noVmtouch); err != nil { - panic(err) + if !*externalBundle { + p, err := platforms.Parse(platform) + if err != nil { + panic(err) + } + cfg, err := unpack(context.TODO(), imgDir, &p, rootfs) + if err != nil { + panic(err) + } + cfgD, err := io.ReadAll(cfg) + if err != nil { + panic(err) + } + if err := os.WriteFile("image.json", cfgD, 0600); err != nil { + panic(err) + } + if err := createSpec(bytes.NewReader(cfgD), rootfs, *debug, *debugInit, *imageConfigPath, *runtimeConfigPath, *imageRootfsPath, *noVmtouch); err != nil { + panic(err) + } + } else { + bootConfig, err := generateBootConfig(*debug, *debugInit, *imageConfigPath, *runtimeConfigPath, *imageRootfsPath, *noVmtouch, "", true) + if err != nil { + panic(err) + } + bd, err := json.Marshal(bootConfig) + if err != nil { + panic(err) + } + if err := os.WriteFile("initconfig.json", bd, 0600); err != nil { + panic(err) + } } } @@ -215,7 +230,11 @@ func createSpec(r io.Reader, rootfs string, debug bool, debugInit bool, imageCon if err != nil { return err } - bootConfig, err := generateBootConfig(config, debug, debugInit, imageConfigPath, runtimeConfigPath, imageRootfsPath, noVmtouch) + var binfmtArch string + if arch := config.Architecture; arch != "riscv64" && arch != "amd64" { + binfmtArch = arch + } + bootConfig, err := generateBootConfig(debug, debugInit, imageConfigPath, runtimeConfigPath, imageRootfsPath, noVmtouch, binfmtArch, false) if err != nil { return err } @@ -301,7 +320,7 @@ func generateSpec(config spec.Image, rootfs string) (_ *specs.Spec, err error) { return s, nil } -func generateBootConfig(config spec.Image, debug, debugInit bool, imageConfigPath, runtimeConfigPath, imageRootfsPath string, noVmtouch bool) (*inittype.BootConfig, error) { +func generateBootConfig(debug, debugInit bool, imageConfigPath, runtimeConfigPath, imageRootfsPath string, noVmtouch bool, binfmtArch string, externalBundle bool) (*inittype.BootConfig, error) { runcArgs := []string{"run", "-b", runtimeBundlePath, "foo"} if debug { runcArgs = append([]string{"--debug"}, runcArgs...) @@ -322,7 +341,9 @@ func generateBootConfig(config spec.Image, debug, debugInit bool, imageConfigPat Container: inittype.ContainerInfo{ BundlePath: runtimeBundlePath, ImageConfigPath: imageConfigPath, + ImageRootfsPath: imageRootfsPath, RuntimeConfigPath: runtimeConfigPath, + ExternalBundle: externalBundle, }, Mounts: []inittype.MountInfo{ { @@ -395,51 +416,56 @@ func generateBootConfig(config spec.Image, debug, debugInit bool, imageConfigPat }, }, }, + }, + } + rootfsMount := inittype.MountInfo{ + FSType: "overlay", + Src: "overlay", + Data: fmt.Sprintf("lowerdir=%s,upperdir=%s,workdir=%s", imageRootfsPath, "/run/rootfs-upper", "/run/rootfs-work"), + Dst: runtimeRootfsPath, + Dir: []inittype.DirInfo{ { - FSType: "overlay", - Src: "overlay", - Data: fmt.Sprintf("lowerdir=%s,upperdir=%s,workdir=%s", imageRootfsPath, "/run/rootfs-upper", "/run/rootfs-work"), - Dst: runtimeRootfsPath, - Dir: []inittype.DirInfo{ - { - Path: runtimeRootfsPath, - Mode: 0755, - }, - { - Path: "/run/rootfs-upper", - Mode: 0755, - }, - { - Path: "/run/rootfs-work", - Mode: 0755, - }, - }, - PostDir: []inittype.DirInfo{ - { - Path: "/run/rootfs/etc/", - Mode: 0644, - }, - { - Path: "/run/rootfs/etc/", - Mode: 0644, - }, - }, - PostFile: []inittype.FileInfo{ - { - Path: "/run/rootfs/etc/hosts", - Mode: 0644, - Contents: "127.0.0.1 localhost\n", - }, - { - Path: "/run/rootfs/etc/resolv.conf", - Mode: 0644, - Contents: "", - }, - }, + Path: runtimeRootfsPath, + Mode: 0755, + }, + { + Path: "/run/rootfs-upper", + Mode: 0755, + }, + { + Path: "/run/rootfs-work", + Mode: 0755, + }, + }, + PostDir: []inittype.DirInfo{ + { + Path: "/run/rootfs/etc/", + Mode: 0644, + }, + { + Path: "/run/rootfs/etc/", + Mode: 0644, + }, + }, + PostFile: []inittype.FileInfo{ + { + Path: "/run/rootfs/etc/hosts", + Mode: 0644, + Contents: "127.0.0.1 localhost\n", + }, + { + Path: "/run/rootfs/etc/resolv.conf", + Mode: 0644, + Contents: "", }, }, } - if arch := config.Architecture; arch != "riscv64" && arch != "amd64" { + if externalBundle { + bootConfig.PostMounts = append(bootConfig.PostMounts, rootfsMount) // mount rootfs after bundle is provided + } else { + bootConfig.Mounts = append(bootConfig.Mounts, rootfsMount) // mount embedded rootfs as soon as possible + } + if binfmtArch != "" { procfsPos, found := 0, false for i, m := range bootConfig.Mounts { if m.FSType == "proc" { @@ -456,7 +482,7 @@ func generateBootConfig(config spec.Image, debug, debugInit bool, imageConfigPat FSType: "binfmt_misc", Src: "binfmt_misc", Dst: "/proc/sys/fs/binfmt_misc", - Cmd: []string{"binfmt", "--install", arch}, + Cmd: []string{"binfmt", "--install", binfmtArch}, }) newMountInfo = append(newMountInfo, bootConfig.Mounts[procfsPos+1:]...) bootConfig.Mounts = newMountInfo diff --git a/cmd/init/main.go b/cmd/init/main.go index c2d5576..9deed3c 100644 --- a/cmd/init/main.go +++ b/cmd/init/main.go @@ -57,42 +57,23 @@ func doInit() error { } else { log.SetOutput(io.Discard) } - imageD, err := os.ReadFile(cfg.Container.ImageConfigPath) - if err != nil { - return err - } var imageConfig imagespec.Image - if err := json.Unmarshal(imageD, &imageConfig); err != nil { - return err - } - - var wg sync.WaitGroup - for _, m := range cfg.Mounts { - if m.Async { - m := m - wg.Add(1) - go func() { - defer wg.Done() - if err := mount(m); err != nil { - if m.Optional { - log.Printf("failed optional mount %+v: %v", m, err) - } else { - panic(err) - } - } - }() - } else { - if err := mount(m); err != nil { - if m.Optional { - log.Printf("failed optional mount %+v: %v", m, err) - } else { - return err - } - } + var externalBundle bool + if cfg.Container.ExternalBundle { + externalBundle = true + } else { + imageD, err := os.ReadFile(cfg.Container.ImageConfigPath) + if err != nil { + return err + } + if err := json.Unmarshal(imageD, &imageConfig); err != nil { + return err } } - wg.Wait() + if err := mountAll(cfg.Mounts); err != nil { + return err + } // WASI-related filesystems for _, tag := range []string{rootFSTag, packFSTag} { @@ -106,13 +87,15 @@ func doInit() error { break } } - specD, err := os.ReadFile(cfg.Container.RuntimeConfigPath) - if err != nil { - return err - } var s runtimespec.Spec - if err := json.Unmarshal(specD, &s); err != nil { - return err + if !externalBundle { + specD, err := os.ReadFile(cfg.Container.RuntimeConfigPath) + if err != nil { + return err + } + if err := json.Unmarshal(specD, &s); err != nil { + return err + } } for _, cmd := range cfg.CmdPreRun { log.Printf("executing(pre-run): %+v\n", cmd) @@ -157,24 +140,14 @@ func doInit() error { return err } log.Printf("INFO:\n%s\n", string(infoD)) - var withNet bool - var mac string - s, withNet, mac = patchSpec(s, infoD, imageConfig) - log.Printf("Running: %+v\n", s.Process.Args) - sd, err := json.Marshal(s) - if err != nil { - return err - } - if err := os.WriteFile(filepath.Join(cfg.Container.BundlePath, "config.json"), sd, 0600); err != nil { - return err - } + info := parseInfo(infoD) - if withNet { - if mac != "" { + if info.withNet { + if info.mac != "" { if o, err := exec.Command("ip", "link", "set", "dev", "eth0", "down").CombinedOutput(); err != nil { return fmt.Errorf("failed eth0 down: %v: %w", string(o), err) } - if o, err := exec.Command("ip", "link", "set", "dev", "eth0", "address", mac).CombinedOutput(); err != nil { + if o, err := exec.Command("ip", "link", "set", "dev", "eth0", "address", info.mac).CombinedOutput(); err != nil { return fmt.Errorf("failed change mac address of eth0: %v: %w", string(o), err) } } @@ -187,6 +160,78 @@ func doInit() error { o2, _ := exec.Command("ip", "a").CombinedOutput() log.Printf("finished udhcpc: %s\n %s\n", string(o), string(o2)) } + } + + if externalBundle { + if info.bundle == "" { + return fmt.Errorf("neither of embedded image nor external bundle is provided") + } + if !info.withNet { + return fmt.Errorf("networking must be enabled") + } + + // mount bundle + addr, ok := strings.CutPrefix(info.bundle, "9p=") + if !ok { + return fmt.Errorf("unsupported external bundle %q", info.bundle) + } + + bundle9pPath := "/run/9pbundle" + bundle9pSpecPath := filepath.Join(bundle9pPath, "config", "config.json") + bundle9pImageConfigPath := filepath.Join(bundle9pPath, "config", "imageconfig.json") + + if err := os.MkdirAll(bundle9pPath, os.FileMode(0755)); err != nil { + return fmt.Errorf("failed to create %q: %w", bundle9pPath, err) + } + if err := syscall.Mount(addr, bundle9pPath, "9p", 0, "trans=tcp,version=9p2000.L,msize=5000000,port=80,cache=loose,ro"); err != nil { + return fmt.Errorf("failed mounting oci %v", err) + } + + // make bundle usable + // TODO: ovrelay mount /run/bundle/rootfs directly to /run/rootfs + if cfg.Container.ImageRootfsPath == "" { + return fmt.Errorf("specify image rootfs path") + } + if err := os.MkdirAll(cfg.Container.ImageRootfsPath, os.FileMode(0755)); err != nil { + return fmt.Errorf("failed to create %q: %w", cfg.Container.ImageRootfsPath, err) + } + if err := syscall.Mount(filepath.Join(bundle9pPath, "rootfs"), cfg.Container.ImageRootfsPath, "", syscall.MS_BIND, ""); err != nil { + return fmt.Errorf("cannot bind mount 9p rootfs to %q: %w", cfg.Container.ImageRootfsPath, err) + } + + // parse config + f, err := os.Open(bundle9pSpecPath) + if err != nil { + return fmt.Errorf("failed to open spec file: %v", err) + } + if err := json.NewDecoder(f).Decode(&s); err != nil { + return err + } + f.Close() + f, err = os.Open(bundle9pImageConfigPath) + if err != nil { + return fmt.Errorf("failed to open image config file: %v", err) + } + if err := json.NewDecoder(f).Decode(&imageConfig); err != nil { + return err + } + f.Close() + } + + if err := mountAll(cfg.PostMounts); err != nil { + return err + } + + s = patchSpec(s, info, imageConfig) + log.Printf("Running: %+v\n", s.Process.Args) + sd, err := json.Marshal(s) + if err != nil { + return err + } + if err := os.WriteFile(filepath.Join(cfg.Container.BundlePath, "config.json"), sd, 0600); err != nil { + return err + } + if info.withNet { for _, f := range []string{"/etc/hosts", "/etc/resolv.conf"} { if err := syscall.Mount(f, filepath.Join("/run/rootfs", f), "", syscall.MS_BIND, ""); err != nil { return fmt.Errorf("cannot mount %q: %w", f, err) @@ -266,12 +311,58 @@ func mount(m inittype.MountInfo) error { return nil } +func mountAll(mounts []inittype.MountInfo) error { + if len(mounts) == 0 { + return nil + } + var wg sync.WaitGroup + for _, m := range mounts { + if m.Async { + m := m + wg.Add(1) + go func() { + defer wg.Done() + if err := mount(m); err != nil { + if m.Optional { + log.Printf("failed optional mount %+v: %v", m, err) + } else { + panic(err) + } + } + }() + } else { + if err := mount(m); err != nil { + if m.Optional { + log.Printf("failed optional mount %+v: %v", m, err) + } else { + return err + } + } + } + } + + wg.Wait() + + return nil +} + var ( delimLines = regexp.MustCompile(`[^\\]\n`) delimArgs = regexp.MustCompile(`[^\\] `) ) -func patchSpec(s runtimespec.Spec, infoD []byte, imageConfig imagespec.Image) (_ runtimespec.Spec, withNet bool, mac string) { +type runtimeFlags struct { + mounts []runtimespec.Mount + env []string + entrypoint []string + args []string + + withNet bool + mac string + bundle string +} + +func parseInfo(infoD []byte) (info runtimeFlags) { var options []string lmchs := delimLines.FindAllIndex(infoD, -1) prev := 0 @@ -282,8 +373,6 @@ func patchSpec(s runtimespec.Spec, infoD []byte, imageConfig imagespec.Image) (_ prev = m[1] } options = append(options, strings.ReplaceAll(string(infoD[prev:]), "\\\n", "\n")) - var entrypoint []string - var args []string for _, l := range options { elms := strings.SplitN(l, ":", 2) if len(elms) != 2 { @@ -297,7 +386,7 @@ func patchSpec(s runtimespec.Spec, infoD []byte, imageConfig imagespec.Image) (_ // no path is specified; nop continue } - s.Mounts = append(s.Mounts, runtimespec.Mount{ + info.mounts = append(info.mounts, runtimespec.Mount{ Type: "bind", Source: filepath.Join("/mnt/wasi0", o), Destination: filepath.Join("/", o), // TODO: ensure not outside of "/" @@ -305,39 +394,49 @@ func patchSpec(s runtimespec.Spec, infoD []byte, imageConfig imagespec.Image) (_ }) log.Printf("Prepared mount wasi0 => %q", o) case "c": - args = nil + info.args = nil mchs := delimArgs.FindAllIndex([]byte(o), -1) prev := 0 for _, m := range mchs { s := m[0] + 1 // spaces are quoted so we restore them here - args = append(args, strings.ReplaceAll(string(o[prev:s]), "\\ ", " ")) + info.args = append(info.args, strings.ReplaceAll(string(o[prev:s]), "\\ ", " ")) prev = m[1] } - args = append(args, strings.ReplaceAll(string(o[prev:]), "\\ ", " ")) + info.args = append(info.args, strings.ReplaceAll(string(o[prev:]), "\\ ", " ")) case "e": - entrypoint = []string{o} + info.entrypoint = []string{o} case "env": - s.Process.Env = append(s.Process.Env, o) + info.env = append(info.env, o) case "n": - withNet = true // TODO: check mode (e.g. dhcp, ...) - mac = o + info.withNet = true // TODO: check mode (e.g. dhcp, ...) + info.mac = o case "t": if o != "" { if err := exec.Command("date", "+%s", "-s", "@"+o).Run(); err != nil { log.Printf("failed setting date: %v", err) // TODO: return error } } + case "b": + info.bundle = o default: log.Printf("unsupported prefix: %q", inst) } } + return +} + +func patchSpec(s runtimespec.Spec, info runtimeFlags, imageConfig imagespec.Image) runtimespec.Spec { + s.Mounts = append(s.Mounts, info.mounts...) + s.Process.Env = append(s.Process.Env, info.env...) + entrypoint := info.entrypoint if len(entrypoint) == 0 { entrypoint = imageConfig.Config.Entrypoint } + args := info.args if len(args) == 0 { args = imageConfig.Config.Cmd } s.Process.Args = append(entrypoint, args...) - return s, withNet, mac + return s } diff --git a/cmd/init/types/types.go b/cmd/init/types/types.go index f675913..6b375f0 100644 --- a/cmd/init/types/types.go +++ b/cmd/init/types/types.go @@ -1,18 +1,21 @@ package init type BootConfig struct { - Mounts []MountInfo `json:"mounts"` - CmdPreRun [][]string `json:"cmd_pre_run,omitempty"` - Cmd [][]string `json:"cmd,omitempty"` - Debug bool `json:"debug,omitempty"` - DebugInit bool `json:"debug_init,omitempty"` - Container ContainerInfo `json:"container"` + Mounts []MountInfo `json:"mounts"` + CmdPreRun [][]string `json:"cmd_pre_run,omitempty"` + Cmd [][]string `json:"cmd,omitempty"` + Debug bool `json:"debug,omitempty"` + DebugInit bool `json:"debug_init,omitempty"` + Container ContainerInfo `json:"container"` + PostMounts []MountInfo `json:"post_mounts"` } type ContainerInfo struct { BundlePath string `json:"bundle_path"` ImageConfigPath string `json:"image_config_path"` + ImageRootfsPath string `json:"image_rootfs_path"` RuntimeConfigPath string `json:"runtime_config_path"` + ExternalBundle bool `json:"external_bundle"` } type MountInfo struct { diff --git a/examples/README.md b/examples/README.md index 1364792..fef5da9 100644 --- a/examples/README.md +++ b/examples/README.md @@ -12,3 +12,4 @@ Please refer to [README](./../README.md) for basic examples (e.g. Ubuntu on WASM - [`./php-riscv64`](./php-riscv64/): Running RISC-V PHP container on the wasm runtime and browser. - [`./target-aarch64`](./target-aarch64/): Running aarch64 container on the wasm runtime and browser. - [`./networking`](./networking/): Running container on WASM with networking support. +- [`./no-conversion`](./no-conversion/): Running container on browser without pre-conversion of container images. diff --git a/examples/no-conversion/README.md b/examples/no-conversion/README.md new file mode 100644 index 0000000..f50ed74 --- /dev/null +++ b/examples/no-conversion/README.md @@ -0,0 +1,44 @@ +# Running container on browser without pre-conversion of the container image + +This is an example to run a container on browser without pre-conversion of the container imgae. + +[`imagemounter`](./extras/imagemounter/) helper enables to directly mount a container image into the emulated Linux VM on Wasm, without container-to-wasm pre-conversion. + +## Example + +> Run this at the project repo root directory. + +The following outputs a Wasm image `out.wasm` that contains runc + Linux + CPU emulator, etc. but doesn't contain container image. + +```console +$ c2w --external-bundle out.wasm +``` + +Then, put a container image to the server in the standard [OCI Image Layout](https://github.com/opencontainers/image-spec/blob/v1.0.2/image-layout.md). +The following puts `ubuntu:22.04` container image to `/tmp/imageout/`. + +```console +$ IMAGE=ubuntu:22.04 +$ mkdir /tmp/imageout/ +$ docker buildx create --name container --driver=docker-container +$ echo "FROM $IMAGE" | docker buildx build --builder=container --output type=oci,dest=- - | tar -C /tmp/imageout/ -xf - +``` + +The following runs the image on browser. +The above OCI image is located at `http://localhost:8083/ubuntu-22.04/`. +That image can run on browser via `http://localhost:8083/?image=http://localhost:8083/ubuntu-22.04/` that fetches the container image into the browser and launches it. + +```console +$ mkdir -p /tmp/out-js3/ +$ cp -R ./examples/no-conversion/* /tmp/out-js3/ +$ cp -R /tmp/imageout /tmp/out-js3/htdocs/ubuntu-22.04 +$ make imagemounter.wasm && cat ./out/imagemounter.wasm | gzip > /tmp/out-js3/htdocs/imagemounter.wasm.gzip +$ cat out.wasm | gzip > /tmp/out-js3/htdocs/out.wasm.gzip +$ ( cd extras/runcontainerjs ; npx webpack && cp -R ./dist /tmp/out-js3/htdocs/ ) +$ docker run --rm -p 127.0.0.1:8083:80 \ + -v "/tmp/out-js3/htdocs:/usr/local/apache2/htdocs/:ro" \ + -v "/tmp/out-js3/xterm-pty.conf:/usr/local/apache2/conf/extra/xterm-pty.conf:ro" \ + --entrypoint=/bin/sh httpd -c 'echo "Include conf/extra/xterm-pty.conf" >> /usr/local/apache2/conf/httpd.conf && httpd-foreground' +``` + +For more details about imagemounter and other examples (e.g. using container registry for image distribution), please see also [`../../extras/imagemounter/README.md`](./../../extras/imagemounter/README.md). diff --git a/examples/no-conversion/htdocs/index.html b/examples/no-conversion/htdocs/index.html new file mode 100644 index 0000000..ee315e8 --- /dev/null +++ b/examples/no-conversion/htdocs/index.html @@ -0,0 +1,46 @@ + + + container + WASI on browser + + + +
+ + + + + + diff --git a/examples/no-conversion/htdocs/worker.js b/examples/no-conversion/htdocs/worker.js new file mode 100644 index 0000000..48cadb9 --- /dev/null +++ b/examples/no-conversion/htdocs/worker.js @@ -0,0 +1,15 @@ +importScripts("https://cdn.jsdelivr.net/npm/xterm-pty@0.9.4/workerTools.js"); +importScripts(location.origin + "/dist/worker-util.js"); + +var info; +var args; +onmessage = (msg) => { + const req_ = msg.data; + if ((typeof req_ == "object") && (req_.type == "init")) { + info = req_.info; + args = req_.args; + return; + } + var ttyClient = new TtyClient(msg.data); + RunContainer.startContainer(info, args, ttyClient); +}; diff --git a/examples/no-conversion/xterm-pty.conf b/examples/no-conversion/xterm-pty.conf new file mode 100644 index 0000000..90301bf --- /dev/null +++ b/examples/no-conversion/xterm-pty.conf @@ -0,0 +1,2 @@ +Header set Cross-Origin-Opener-Policy: same-origin +Header set Cross-Origin-Embedder-Policy: require-corp \ No newline at end of file diff --git a/extras/imagemounter/README.md b/extras/imagemounter/README.md new file mode 100644 index 0000000..129617e --- /dev/null +++ b/extras/imagemounter/README.md @@ -0,0 +1,160 @@ +# Distributing and running container images on browser without pre-conversion, using `imagemounter` (experimental) + +Not only converting container image to Wasm image, container2wasm also allow distributing unmodified container images and running it on browser, etc. (still with CPU emulation, though). + +`imagemounter` is a helper tool that enables to pull unmodified container images and mount it into the Linux VM running on Wasm. + +The benefits of this approach are that it eliminates the need for image conversion and allows the distributing unmodified container images directly from the container registry to the browser (but the registry needs to enable CORS). + +The following tools are used: + +- Wasm-formatted Linux VM with emulated CPU: Generated by `c2w --external-bundle out.wasm`. This contains runc but doesn't embed container image. External container image's rootfs needs to be mounted to the VM via 9p. +- `imagemounter.wasm`: This provides functionallity of pulling container images from remote location into the browser/node, and show the rootfs to the above Wasm image via 9p. + +## Supported image sources + +As of now, the following image server is supported: + +- HTTP server that serves OCI container imgaes in [OCI Image Layout](https://github.com/opencontainers/image-spec/blob/v1.0.2/image-layout.md): example is https://ktock.github.io/ubuntu-oci-images/ubuntu-22.04-org-amd64 + - Limitations + - To pull container images into the browser, the server needs to allow CORS access. +- Container registry configured with CORS support + - Limitations + - Authentication unsupported (expected to be fixed in the future) + - To pull container images into the browser, the headers need to be included in the response to support CORS access, for example: + - `Access-Control-Allow-Origin: *` + - `Access-Control-Allow-Headers: *` + - `Access-Control-Expose-Headers: Content-Range` + - Pulling gzip container image is very slow as decompression happens in the Wasm VM. Future version will fix it by performing decompression outside of the Wasm VM. + +## Examples + +First, build basic dependencies: + +```console +$ make imagemounter.wasm c2w +$ mkdir /tmp/outx/ +$ ./out/c2w --external-bundle /tmp/outx/out.wasm +``` + +### Example on browser + HTTP Server + +The following pulls `ubuntu:22.04` stored at https://ktock.github.io/ubuntu-oci-images/ubuntu-22.04-org-amd64 as [OCI Image Layout](https://github.com/opencontainers/image-spec/blob/v1.0.2/image-layout.md) and runs it on Wasm. + +```console +$ cd ./extras/imagemounter +$ mkdir -p /tmp/out-js3/ +$ cp -R ./../../examples/no-conversion/* /tmp/out-js3/ +$ ( cd ../runcontainerjs && npx webpack && cp -R ./dist /tmp/out-js3/htdocs/ ) +$ cat ../../out/imagemounter.wasm | gzip > /tmp/out-js3/htdocs/imagemounter.wasm.gzip +$ cat /tmp/outx/out.wasm | gzip > /tmp/out-js3/htdocs/out.wasm.gzip +$ docker run --rm -p 127.0.0.1:8083:80 \ + -v "/tmp/out-js3/htdocs:/usr/local/apache2/htdocs/:ro" \ + -v "/tmp/out-js3/xterm-pty.conf:/usr/local/apache2/conf/extra/xterm-pty.conf:ro" \ + --entrypoint=/bin/sh httpd -c 'echo "Include conf/extra/xterm-pty.conf" >> /usr/local/apache2/conf/httpd.conf && httpd-foreground' +``` + +Then access to: `localhost:8083?image=https://ktock.github.io/ubuntu-oci-images/ubuntu-22.04-org-amd64` + +### Example on browser + Registry + +> Note: As described in the above, registry authentication is unsupported as of now so you can't use public registry services. You can try this features using local registry like the following. This limitations is expected to be fixed in the future. + +First, launch a registry with enabling CORS. + +```console +$ mkdir /tmp/regconfig +$ cat < /tmp/regconfig/config.yml +version: 0.1 +http: + addr: :5000 + headers: + Access-Control-Allow-Origin: ["*"] + Access-Control-Allow-Headers: ["*"] + Access-Control-Expose-Headers: [Content-Range] + X-Content-Type-Options: [nosniff] +storage: + filesystem: + rootdirectory: /var/lib/registry +EOF +$ docker run --rm -d -p 127.0.0.1:5000:5000 -v /tmp/regconfig/config.yml:/etc/docker/registry/config.yml --name registry registry:2 +$ docker pull ubuntu:22.04 +$ docker tag ubuntu:22.04 localhost:5000/ubuntu:22.04 +$ docker push localhost:5000/ubuntu:22.04 +``` + +The above registry serves `localhost:5000/ubuntu:22.04` container image. + +The following serves a page to run that image. +When you access to `localhost:8083?image=localhost:5000/ubuntu:22.04`, that page fetches `localhost:5000/ubuntu:22.04` container image from the local registry and launches it on browser. + +```console +$ cd ./extras/imagemounter +$ mkdir -p /tmp/out-js3/ +$ cp -R ./../../examples/no-conversion/* /tmp/out-js3/ +$ ( cd ../runcontainerjs && npx webpack && cp -R ./dist /tmp/out-js3/htdocs/ ) +$ cat ../../out/imagemounter.wasm | gzip > /tmp/out-js3/htdocs/imagemounter.wasm.gzip +$ cat /tmp/outx/out.wasm | gzip > /tmp/out-js3/htdocs/out.wasm.gzip +$ docker run --rm -p 127.0.0.1:8083:80 \ + -v "/tmp/out-js3/htdocs:/usr/local/apache2/htdocs/:ro" \ + -v "/tmp/out-js3/xterm-pty.conf:/usr/local/apache2/conf/extra/xterm-pty.conf:ro" \ + --entrypoint=/bin/sh httpd -c 'echo "Include conf/extra/xterm-pty.conf" >> /usr/local/apache2/conf/httpd.conf && httpd-foreground' +``` + +### Example on CLI + HTTP Server + +Demo CLI is used for pulling and running the container image. + +```console +$ ( cd ./tests/imagemounter-test/ ; go build -o ../../out/imagemounter-test . ) +``` + +The following pulls ubuntu:22.04 stored at https://ktock.github.io/ubuntu-oci-images/ubuntu-22.04-org-amd64 as [OCI Image Layout](https://github.com/opencontainers/image-spec/blob/v1.0.2/image-layout.md) and runs it on Wasm. + +```console +$ ./out/imagemounter-test \ + --image https://ktock.github.io/ubuntu-oci-images/ubuntu-22.04-org-amd64 \ + --stack ./out/imagemounter.wasm \ + /tmp/outx/out.wasm --net=socket=listenfd=4 --external-bundle=9p=192.168.127.252 +``` + +### Example on CLI + Registry + +> Note: As described in the above, registry authentication is unsupported as of now so you can't use public registry services. You can try this features using local registry like the following. This limitations is expected to be fixed in the future. + +Demo CLI is used for pulling and running the container image. + +```console +$ ( cd ./tests/imagemounter-test/ ; go build -o ../../out/imagemounter-test . ) +``` + +First, launch a registry + +```console +$ docker run --rm -d -p 127.0.0.1:5000:5000 --name registry registry:2 +$ docker pull ubuntu:22.04 +$ docker tag ubuntu:22.04 localhost:5000/ubuntu:22.04 +$ docker push localhost:5000/ubuntu:22.04 +``` + +The following pulls `localhost:5000/ubuntu:22.04` container image and runs it on Wasm. + +```console +$ ./out/imagemounter-test \ + --image localhost:5000/ubuntu:22.04 \ + --stack ./out/imagemounter.wasm \ + /tmp/out.wasm --net=socket=listenfd=4 --external-bundle=9p=192.168.127.252 +``` + +## How to get container image formatted as OCI Image Layout + +Docker buildx suppors [exporting image in OCI Image Layout](https://docs.docker.com/engine/reference/commandline/buildx_build/#oci). + +The following puts `ubuntu:22.04` container image to `/tmp/imageout/` as OCI Image Layout. + +```console +$ IMAGE=ubuntu:22.04 +$ mkdir /tmp/imageout/ +$ docker buildx create --name container --driver=docker-container +$ echo "FROM $IMAGE" | docker buildx build --builder=container --output type=oci,dest=- - | tar -C /tmp/imageout/ -xf - +``` diff --git a/extras/imagemounter/go.mod b/extras/imagemounter/go.mod new file mode 100644 index 0000000..0e71a73 --- /dev/null +++ b/extras/imagemounter/go.mod @@ -0,0 +1,75 @@ +module stack + +go 1.21.0 + +require ( + github.com/containerd/containerd v1.7.8 + github.com/containerd/stargz-snapshotter v0.15.1 + github.com/containerd/stargz-snapshotter/estargz v0.15.1 + github.com/containers/gvisor-tap-vsock v0.7.0 + github.com/hugelgupf/p9 v0.0.0-00010101000000-000000000000 + github.com/opencontainers/go-digest v1.0.0 + github.com/opencontainers/image-spec v1.1.0-rc5 + github.com/opencontainers/runc v1.1.5 + github.com/opencontainers/runtime-spec v1.1.0 + github.com/sirupsen/logrus v1.9.3 + golang.org/x/sync v0.4.0 +) + +require ( + github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/Microsoft/hcsshim v0.11.1 // indirect + github.com/apparentlymart/go-cidr v1.1.0 // indirect + github.com/containerd/cgroups v1.1.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/ttrpc v1.2.2 // indirect + github.com/containerd/typeurl/v2 v2.1.1 // indirect + github.com/go-logr/logr v1.2.4 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/btree v1.0.1 // indirect + github.com/google/gopacket v1.1.19 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-retryablehttp v0.7.4 // indirect + github.com/insomniacslk/dhcp v0.0.0-20220504074936-1ca156eafb9f // indirect + github.com/klauspost/compress v1.17.2 // indirect + github.com/miekg/dns v1.1.55 // indirect + github.com/moby/locker v1.0.1 // indirect + github.com/moby/sys/mountinfo v0.6.2 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/u-root/uio v0.0.0-20210528114334-82958018845c // indirect + github.com/vbatts/tar-split v0.11.5 // indirect + go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/otel v1.14.0 // indirect + go.opentelemetry.io/otel/trace v1.14.0 // indirect + golang.org/x/crypto v0.14.0 // indirect + golang.org/x/mod v0.11.0 // indirect + golang.org/x/net v0.17.0 // indirect + golang.org/x/sys v0.13.0 // indirect + golang.org/x/time v0.3.0 // indirect + golang.org/x/tools v0.10.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect + google.golang.org/grpc v1.59.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect + gvisor.dev/gvisor v0.0.0-20230715022000-fd277b20b8db // indirect + inet.af/tcpproxy v0.0.0-20220326234310-be3ee21c9fa0 // indirect +) + +replace github.com/sirupsen/logrus => github.com/sirupsen/logrus v1.9.3-0.20230531171720-7165f5e779a5 + +// Patched for enabling to compile it to wasi +replace github.com/insomniacslk/dhcp => github.com/ktock/insomniacslk-dhcp v0.0.0-20230911142651-b86573a014b1 + +// Patched for enabling to compile it to wasi +replace github.com/u-root/uio => github.com/ktock/u-root-uio v0.0.0-20230911142931-5cf720bc8a29 + +replace github.com/hugelgupf/p9 => github.com/ktock/p9 v0.0.0-20240109162600-dac0f97fe886 + +replace github.com/containerd/stargz-snapshotter => github.com/ktock/stargz-snapshotter v0.0.1-0.20240109162903-e5206b85e4cc + +replace github.com/containerd/containerd => github.com/ktock/containerd v1.2.1-0.20240109162750-28ec64e033db diff --git a/extras/imagemounter/go.sum b/extras/imagemounter/go.sum new file mode 100644 index 0000000..8a53f15 --- /dev/null +++ b/extras/imagemounter/go.sum @@ -0,0 +1,453 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/Microsoft/hcsshim v0.11.1 h1:hJ3s7GbWlGK4YVV92sO88BQSyF4ZLVy7/awqOlPxFbA= +github.com/Microsoft/hcsshim v0.11.1/go.mod h1:nFJmaO4Zr5Y7eADdFOpYswDDlNVbvcIJJNJLECr5JQg= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU= +github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/armon/go-proxyproto v0.0.0-20210323213023-7e956b284f0a/go.mod h1:QmP9hvJ91BbJmGVGSbutW19IC0Q9phDCLGaomwTJbgU= +github.com/beevik/ntp v0.3.0/go.mod h1:hIHWr+l3+/clUnF44zdK+CWW7fO8dR5cIylAQ76NRpg= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/cenkalti/backoff/v4 v4.0.2/go.mod h1:eEew/i+1Q6OrCDZh3WiXYv3+nJwBASZ8Bog/87DQnVg= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E= +github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= +github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= +github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= +github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8= +github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/stargz-snapshotter/estargz v0.15.1 h1:eXJjw9RbkLFgioVaTG+G/ZW/0kEe2oEKCdS/ZxIyoCU= +github.com/containerd/stargz-snapshotter/estargz v0.15.1/go.mod h1:gr2RNwukQ/S9Nv33Lt6UC7xEx58C+LHRdoqbEKjz1Kk= +github.com/containerd/ttrpc v1.2.2 h1:9vqZr0pxwOF5koz6N0N3kJ0zDHokrcPxIR/ZR2YFtOs= +github.com/containerd/ttrpc v1.2.2/go.mod h1:sIT6l32Ph/H9cvnJsfXM5drIVzTr5A2flTf1G5tYZak= +github.com/containerd/typeurl/v2 v2.1.1 h1:3Q4Pt7i8nYwy2KmQWIw2+1hTvwTE/6w9FqcttATPO/4= +github.com/containerd/typeurl/v2 v2.1.1/go.mod h1:IDp2JFvbwZ31H8dQbEIY7sDl2L3o3HZj1hsSQlywkQ0= +github.com/containers/gvisor-tap-vsock v0.7.0 h1:lL5UpfXhl+tuTwK43CXhKWwfvAiZtde6G5alFeoO+Z8= +github.com/containers/gvisor-tap-vsock v0.7.0/go.mod h1:edQTwl8ar+ACuQOkazpQkgd/ZMF6TJ2Xr3fv+MKUaw8= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fanliao/go-promise v0.0.0-20141029170127-1890db352a72/go.mod h1:PjfxuH4FZdUyfMdtBio2lsRr1AKEaVPwelzuHuh8Lqc= +github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/gliderlabs/ssh v0.1.2-0.20181113160402-cbabf5414432/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/gojuno/minimock/v3 v3.0.4/go.mod h1:HqeqnwV8mAABn3pO5hqF+RE7gjA0jsN8cbbSogoGrzI= +github.com/gojuno/minimock/v3 v3.0.8/go.mod h1:TPKxc8tiB8O83YH2//pOzxvEjaI3TMhd6ev/GmlMiYA= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= +github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-tpm v0.1.2-0.20190725015402-ae6dd98980d4/go.mod h1:H9HbmUG2YgV/PHITkO7p6wxEEj/v5nlsVWIwumwH2NI= +github.com/google/go-tpm v0.2.1-0.20200615092505-5d8a91de9ae3/go.mod h1:iVLWvrPp/bHeEkxTFi9WG6K9w0iy2yIszHwZGHPbzAw= +github.com/google/go-tpm-tools v0.0.0-20190906225433-1614c142f845/go.mod h1:AVfHadzbdzHo54inR2x1v640jdi1YSi3NauM2DUsxk0= +github.com/google/goexpect v0.0.0-20191001010744-5b6988669ffa/go.mod h1:qtE5aAEkt0vOSA84DBh8aJsz6riL8ONfqfULY7lBjqc= +github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= +github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= +github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f/go.mod h1:nOFQdrUlIlx6M6ODdSpBj1NVA+VgLC6kmw60mkw34H4= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= +github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-retryablehttp v0.7.4 h1:ZQgVdpTdAL7WpMIwLzCfbalOcSUdkDZnpUv3/+BxzFA= +github.com/hashicorp/go-retryablehttp v0.7.4/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hexdigest/gowrap v1.1.7/go.mod h1:Z+nBFUDLa01iaNM+/jzoOA1JJ7sm51rnYFauKFUB5fs= +github.com/hexdigest/gowrap v1.1.8/go.mod h1:H/JiFmQMp//tedlV8qt2xBdGzmne6bpbaSuiHmygnMw= +github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714 h1:/jC7qQFrv8CrSJVmaolDVOxTfS9kc36uB6H40kdbQq8= +github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/intel-go/cpuid v0.0.0-20200819041909-2aa72927c3e2/go.mod h1:RmeVYf9XrPRbRc3XIx0gLYA8qOFvNoPOfaEZduRlEp4= +github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw= +github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ= +github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok= +github.com/jsimonetti/rtnetlink v0.0.0-20201110080708-d2c240429e6c/go.mod h1:huN4d1phzjhlOsNIjFsw2SVRbwIHj3fJDMEU2SDPTmg= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kaey/framebuffer v0.0.0-20140402104929-7b385489a1ff/go.mod h1:tS4qtlcKqtt3tCIHUflVSqeP3CLH5Qtv2szX9X2SyhU= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.10.6/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4= +github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/pgzip v1.2.4/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/ktock/containerd v1.2.1-0.20240109162750-28ec64e033db h1:so+Hwk3aTFB1Ee8iW6gUwzpbRALU5A4GaHDdgbv8MaU= +github.com/ktock/containerd v1.2.1-0.20240109162750-28ec64e033db/go.mod h1:L/Hn9qylJtUFT7cPeM0Sr3fATj+WjHwRQ0lyrYk3OPY= +github.com/ktock/insomniacslk-dhcp v0.0.0-20230911142651-b86573a014b1 h1:KD92SLhTiV7HRgw3BicNh7mPT1+OpRpaWvyIQLT9by8= +github.com/ktock/insomniacslk-dhcp v0.0.0-20230911142651-b86573a014b1/go.mod h1:h+MxyHxRg9NH3terB1nfRIUaQEcI0XOVkdR9LNBlp8E= +github.com/ktock/p9 v0.0.0-20240109162600-dac0f97fe886 h1:w26G030q4XUqgRPaGfQcyFi1a5R4Nt9VC+6P07c7CrU= +github.com/ktock/p9 v0.0.0-20240109162600-dac0f97fe886/go.mod h1:d5H7Bc5VS6NgKyPmAGLB6qMj5rvRpP/u0HaVI5pp7kY= +github.com/ktock/stargz-snapshotter v0.0.1-0.20240109162903-e5206b85e4cc h1:EXrPMV/2hLjwbJMTVSM1y6a/AnPxLLD5sXEY7R832zE= +github.com/ktock/stargz-snapshotter v0.0.1-0.20240109162903-e5206b85e4cc/go.mod h1:74D+J1m1RMXytLmWxegXWhtOSRHPWZKpKc2NdK3S+us= +github.com/ktock/u-root-uio v0.0.0-20230911142931-5cf720bc8a29 h1:BNd7VYl9yNjxsA4Bt9YKAyKJKIymo1v9f7M2cWhh9iU= +github.com/ktock/u-root-uio v0.0.0-20230911142931-5cf720bc8a29/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7 h1:lez6TS6aAau+8wXUP3G9I3TGlmPFEq2CTxBaRqY6AGE= +github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7/go.mod h1:U6ZQobyTjI/tJyq2HG+i/dfSoFUt8/aZCM+GKtmFk/Y= +github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA= +github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M= +github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY= +github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o= +github.com/mdlayher/raw v0.0.0-20190606142536-fef19f00fc18/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg= +github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065 h1:aFkJ6lx4FPip+S+Uw4aTegFMct9shDvP+79PsSxpm3w= +github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg= +github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo= +github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= +github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= +github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU= +github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= +github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= +github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0-rc5 h1:Ygwkfw9bpDvs+c9E34SdgGOj41dX/cbdlwvlWt0pnFI= +github.com/opencontainers/image-spec v1.1.0-rc5/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8= +github.com/opencontainers/runc v1.1.5 h1:L44KXEpKmfWDcS02aeGm8QNTFXTo2D+8MYGDIJ/GDEs= +github.com/opencontainers/runc v1.1.5/go.mod h1:1J5XiS+vdZ3wCyZybsuxXZWGrgSr8fFJHLXuG2PsnNg= +github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.1.0 h1:HHUyrt9mwHUjtasSbXSMvs4cyFxh+Bll4AjJ9odEGpg= +github.com/opencontainers/runtime-spec v1.1.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI= +github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/orangecms/go-framebuffer v0.0.0-20200613202404-a0700d90c330/go.mod h1:3Myb/UszJY32F2G7yGkUtcW/ejHpjlGfYLim7cv2uKA= +github.com/pborman/getopt/v2 v2.1.0/go.mod h1:4NtW75ny4eBw9fO1bhtNdYTlZKYX5/tBLtsOpwKIKd0= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pierrec/lz4/v4 v4.1.11/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pierrec/lz4/v4 v4.1.12/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI= +github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY= +github.com/rck/unit v0.0.3/go.mod h1:jTOnzP4s1OjIP1vdxb4n76b23QPKS4EurYg7sYMr2DM= +github.com/rekby/gpt v0.0.0-20200219180433-a930afbc6edc/go.mod h1:scrOqOnnHVKCHENvFw8k9ajCb88uqLQDA4BvuJNJ2ew= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/safchain/ethtool v0.0.0-20200218184317-f459e2d13664/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= +github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.9.3-0.20230531171720-7165f5e779a5 h1:4gcU4XfYM+65xu4TiRFTE0fVJ854zjKHq0tcMwszt2g= +github.com/sirupsen/logrus v1.9.3-0.20230531171720-7165f5e779a5/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/twitchtv/twirp v5.8.0+incompatible/go.mod h1:RRJoFSAmTEh2weEqWtpPE3vFK5YBhA6bqp2l1kfCC5A= +github.com/u-root/iscsinl v0.1.1-0.20210528121423-84c32645822a/go.mod h1:RWIgJWqm9/0gjBZ0Hl8iR6MVGzZ+yAda2uqqLmetE2I= +github.com/u-root/u-root v0.8.0/go.mod h1:But1FHzS4Ua4ywx6kZOaRzZTucUKIDKOPOLEKOckQ68= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/vbatts/tar-split v0.11.5 h1:3bHCTIheBm1qFTcgh9oPu+nNBtX+XJIupG/vacinCts= +github.com/vbatts/tar-split v0.11.5/go.mod h1:yZbwRsSeGjusneWgA781EKej9HF8vme8okylkAeNKLk= +github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= +github.com/vishvananda/netlink v1.1.1-0.20211118161826-650dca95af54/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= +github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= +github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= +github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= +github.com/vtolstov/go-ioctl v0.0.0-20151206205506-6be9cced4810/go.mod h1:dF0BBJ2YrV1+2eAIyEI+KeSidgA6HqoIP1u5XTlMq/o= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/otel v1.14.0 h1:/79Huy8wbf5DnIPhemGB+zEPVwnN6fuQybr/SRXa6hM= +go.opentelemetry.io/otel v1.14.0/go.mod h1:o4buv+dJzx8rohcUeRmWUZhqupFvzWis188WlggnNeU= +go.opentelemetry.io/otel/trace v1.14.0 h1:wp2Mmvj41tDsyAJXiWDWpfNsOiIyd38fy85pyKcFq/M= +go.opentelemetry.io/otel/trace v1.14.0/go.mod h1:8avnQLK+CG77yNLUae4ea2JDQ6iT+gozhnZjy/rw9G8= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU= +golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190419010253-1f3472d942ba/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= +golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190418153312-f0ce4c0180be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606122018-79a91cf218c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200121082415-34d275377bf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210820121016-41cdb8703e55/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210317153231-de623e64d2a6/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= +golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg= +golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= +google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gvisor.dev/gvisor v0.0.0-20230715022000-fd277b20b8db h1:WZSmkyu/hep9YhWIlBZefwGVBrnGE5yW8JPD56YRsXs= +gvisor.dev/gvisor v0.0.0-20230715022000-fd277b20b8db/go.mod h1:sQuqOkxbfJq/GS2uSnqHphtXclHyk/ZrAGhZBxxsq6g= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +inet.af/tcpproxy v0.0.0-20220326234310-be3ee21c9fa0 h1:PqdHrvQRVK1zapJkd0qf6+tevvSIcWdfenVqJd3PHWU= +inet.af/tcpproxy v0.0.0-20220326234310-be3ee21c9fa0/go.mod h1:Tojt5kmHpDIR2jMojxzZK2w2ZR7OILODmUo2gaSwjrk= +pack.ag/tftp v1.0.1-0.20181129014014-07909dfbde3c/go.mod h1:N1Pyo5YG+K90XHoR2vfLPhpRuE8ziqbgMn/r/SghZas= +src.elv.sh v0.16.3/go.mod h1:WxJAMoN8uQcg1ZwRvtjmbYAo6uKeJ8F7b25etVZ743w= diff --git a/extras/imagemounter/main.go b/extras/imagemounter/main.go new file mode 100644 index 0000000..f30d5f3 --- /dev/null +++ b/extras/imagemounter/main.go @@ -0,0 +1,2315 @@ +package main + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "context" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/json" + "encoding/pem" + "errors" + "flag" + "fmt" + "io" + "log" + "math/big" + "mime" + "net" + "net/http" + "os" + "path/filepath" + "sort" + "strconv" + "strings" + "sync" + "syscall" + "time" + "unsafe" + + ctdcontainers "github.com/containerd/containerd/containers" + ctdnamespaces "github.com/containerd/containerd/namespaces" + ctdoci "github.com/containerd/containerd/oci" + "github.com/containerd/containerd/platforms" + "github.com/containerd/containerd/reference" + "github.com/containerd/containerd/remotes" + "github.com/containerd/containerd/remotes/docker" + esgzcache "github.com/containerd/stargz-snapshotter/cache" + "github.com/containerd/stargz-snapshotter/estargz" + esgzconfig "github.com/containerd/stargz-snapshotter/fs/config" + esgzreader "github.com/containerd/stargz-snapshotter/fs/reader" + esgzremote "github.com/containerd/stargz-snapshotter/fs/remote" + esgzsource "github.com/containerd/stargz-snapshotter/fs/source" + esgzmetadata "github.com/containerd/stargz-snapshotter/metadata" + esgzmetadatamemory "github.com/containerd/stargz-snapshotter/metadata/memory" + esgztask "github.com/containerd/stargz-snapshotter/task" + esgzcontainerdutil "github.com/containerd/stargz-snapshotter/util/containerdutil" + gvntypes "github.com/containers/gvisor-tap-vsock/pkg/types" + gvnvirtualnetwork "github.com/containers/gvisor-tap-vsock/pkg/virtualnetwork" + p9staticfs "github.com/hugelgupf/p9/fsimpl/staticfs" + "github.com/hugelgupf/p9/fsimpl/templatefs" + "github.com/hugelgupf/p9/p9" + digest "github.com/opencontainers/go-digest" + imagespec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/opencontainers/runc/libcontainer/user" + runtimespec "github.com/opencontainers/runtime-spec/specs-go" + "github.com/sirupsen/logrus" + "golang.org/x/sync/errgroup" +) + +var proxyKey crypto.PrivateKey +var proxyCert *x509.Certificate + +const proxyIP = "192.168.127.253" + +const p9IP = "192.168.127.252" + +func handleTunneling(w http.ResponseWriter, r *http.Request) { + serverURL := r.URL + cert, err := generateCert(serverURL.Hostname()) + if err != nil { + log.Printf("failed to generate cert: %v\n", err) + http.Error(w, err.Error(), http.StatusServiceUnavailable) + return + } + w.WriteHeader(http.StatusOK) + hijacker, ok := w.(http.Hijacker) + if !ok { + http.Error(w, "Hijacking not supported", http.StatusInternalServerError) + return + } + client_conn, _, err := hijacker.Hijack() + if err != nil { + http.Error(w, err.Error(), http.StatusServiceUnavailable) + return + } + go func() { + defer client_conn.Close() + l := newListener(&stringAddr{"tcp", proxyIP + ":80"}, client_conn) + server := &http.Server{ + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Scheme == "" { + r.URL.Scheme = "https" + } + if r.URL.Host == "" { + r.URL.Host = serverURL.Host + } + handleHTTP(w, r) + }), + // Disable HTTP/2 (FIXME) + // https://pkg.go.dev/net/http#hdr-HTTP_2 + TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)), + } + server.TLSConfig = &tls.Config{ + Certificates: []tls.Certificate{*cert}, + } + log.Printf("serving server for %s...\n", serverURL.Host) + server.ServeTLS(l, "", "") + }() +} + +func generateCert(host string) (*tls.Certificate, error) { + ser, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 60)) + if err != nil { + return nil, err + } + cert := &x509.Certificate{ + SerialNumber: ser, + Subject: pkix.Name{ + CommonName: host, + }, + NotAfter: time.Now().Add(365 * 24 * time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + DNSNames: []string{host}, + } + if ip := net.ParseIP(host); ip != nil { + cert.IPAddresses = append(cert.IPAddresses, ip) + } + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, err + } + certD, err := x509.CreateCertificate(rand.Reader, cert, proxyCert, key.Public(), proxyKey) + if err != nil { + return nil, err + } + return &tls.Certificate{ + Certificate: [][]byte{certD}, + PrivateKey: key, + }, nil +} + +type listener struct { + ch net.Conn + addr net.Addr + closeCh chan struct{} +} + +func newListener(addr net.Addr, ch net.Conn) *listener { + return &listener{ + ch: ch, + addr: addr, + closeCh: make(chan struct{}), + } +} + +func (l *listener) Accept() (net.Conn, error) { + if l.ch == nil { + <-l.closeCh + return nil, fmt.Errorf("closed") + } + c := l.ch + l.ch = nil + return c, nil +} + +func (l *listener) Close() error { close(l.closeCh); return nil } + +func (l *listener) Addr() net.Addr { return l.addr } + +type stringAddr struct { + network string + address string +} + +func (a *stringAddr) Network() string { return a.network } +func (a *stringAddr) String() string { return a.address } + +type FetchParameters struct { + Method string `json:"method,omitempty"` + Headers map[string]string `json:"headers,omitempty"` +} + +type FetchResponse struct { + Headers map[string]string `json:"headers,omitempty"` + Status int `json:"status,omitempty"` + StatusText string `json:"statusText,omitempty"` +} + +func encodeHeader(h http.Header) map[string]string { + res := make(map[string]string) + for k, vs := range h { + res[k] = strings.Join(vs, ", ") + } + return res +} + +func decodeHeader(h map[string]string) http.Header { + res := make(http.Header) + for k, v := range h { + res.Add(k, v) + } + return res +} + +func httpRequestToFetchParameters(req *http.Request) *FetchParameters { + return &FetchParameters{ + Method: req.Method, + Headers: encodeHeader(req.Header), + } +} + +func contentLengthFromHeaders(h http.Header) int64 { + i, _ := strconv.Atoi(h.Get("Content-Length")) + return int64(i) +} + +func fetchResponseToHTTPResponse(resp *FetchResponse) *http.Response { + h := decodeHeader(resp.Headers) + return &http.Response{ + Status: resp.StatusText, + StatusCode: resp.Status, + Header: h, + ContentLength: contentLengthFromHeaders(h), // required by containerd resolver + } +} + +//go:wasmimport env http_send +func http_send(addressP uint32, addresslen uint32, reqP, reqlen uint32, idP uint32) uint32 + +//go:wasmimport env http_writebody +func http_writebody(id uint32, chunkP, len uint32, nwrittenP uint32, isEOF uint32) uint32 + +//go:wasmimport env http_isreadable +func http_isreadable(id uint32, isOKP uint32) uint32 + +//go:wasmimport env http_recv +func http_recv(id uint32, respP uint32, bufsize uint32, respsizeP uint32, isEOFP uint32) uint32 + +//go:wasmimport env http_readbody +func http_readbody(id uint32, bodyP uint32, bufsize uint32, bodysizeP uint32, isEOFP uint32) uint32 + +func doHttpRoundTrip(req *http.Request) (*http.Response, error) { + defer func() { + if req.Body != nil { + req.Body.Close() + } + }() + + address := req.URL.String() + if address == "" { + return nil, fmt.Errorf("specify destination address") + } + + fetchReqD, err := json.Marshal(httpRequestToFetchParameters(req)) + if err != nil { + return nil, err + } + if len(fetchReqD) == 0 { + return nil, fmt.Errorf("empty request") + } + + var id uint32 + res := http_send( + uint32(uintptr(unsafe.Pointer(&[]byte(address)[0]))), + uint32(len(address)), + uint32(uintptr(unsafe.Pointer(&[]byte(fetchReqD)[0]))), + uint32(len(fetchReqD)), + uint32(uintptr(unsafe.Pointer(&id))), + ) + if res != 0 { + return nil, fmt.Errorf("failed to send request") + } + + var isEOF uint32 + var reqBodyD []byte = make([]byte, 1048576) + var nwritten uint32 = 0 + idx := 0 + chunksize := 0 + bodyR := io.Reader(req.Body) + if bodyR == nil { + bodyR = bytes.NewReader(make([]byte, 0)) + } + for { + if idx >= chunksize { + // chunk is fully written. full another one. + chunksize, err = bodyR.Read(reqBodyD) + if err != nil && err != io.EOF { + return nil, err + } + if err == io.EOF { + isEOF = 1 + } + idx = 0 + } + res := http_writebody( + id, + uint32(uintptr(unsafe.Pointer(&[]byte(reqBodyD[idx:])[0]))), + uint32(chunksize), + uint32(uintptr(unsafe.Pointer(&nwritten))), + isEOF, + ) + if res != 0 { + return nil, fmt.Errorf("failed to write request body") + } + idx += int(nwritten) + if idx < chunksize { + // not fully written. retry for the remaining. + continue + } + if isEOF == 1 { + break + } + } + + var isOK uint32 = 0 + for { + res := http_isreadable(id, uint32(uintptr(unsafe.Pointer(&isOK)))) + if res != 0 { + return nil, fmt.Errorf("response body is not readable") + } + if isOK == 1 { + break + } + time.Sleep(1 * time.Millisecond) + } + + var respD []byte = make([]byte, 1048576) + var respsize uint32 + var respFull []byte + isEOF = 0 + for { + res := http_recv( + id, + uint32(uintptr(unsafe.Pointer(&[]byte(respD)[0]))), + 1048576, + uint32(uintptr(unsafe.Pointer(&respsize))), + uint32(uintptr(unsafe.Pointer(&isEOF))), + ) + if res != 0 { + return nil, fmt.Errorf("failed to receive response") + } + respFull = append(respFull, respD[:int(respsize)]...) + if isEOF == 1 { + break + } + } + var resp FetchResponse + if err := json.Unmarshal(respFull, &resp); err != nil { + return nil, err + } + + isEOF = 0 + pr, pw := io.Pipe() + go func() { + var body []byte = make([]byte, 1048576) + var bodysize uint32 + for { + res := http_readbody( + id, + uint32(uintptr(unsafe.Pointer(&[]byte(body)[0]))), + 1048576, + uint32(uintptr(unsafe.Pointer(&bodysize))), + uint32(uintptr(unsafe.Pointer(&isEOF))), + ) + if res != 0 { + pw.CloseWithError(fmt.Errorf("failed to read response body")) + return + } + if bodysize > 0 { + if _, err := pw.Write(body[:int(bodysize)]); err != nil { + pw.CloseWithError(err) + return + } + } + if isEOF == 1 { + break + } + } + pw.Close() + }() + r := fetchResponseToHTTPResponse(&resp) + r.Body = pr + return r, nil +} + +func handleHTTP(w http.ResponseWriter, req *http.Request) { + resp, err := doHttpRoundTrip(req) + if err != nil { + log.Printf("failed to proxy request: %v\n", err) + http.Error(w, err.Error(), http.StatusServiceUnavailable) + return + } + defer resp.Body.Close() + copyHeader(w.Header(), resp.Header) + w.WriteHeader(resp.StatusCode) + io.Copy(w, resp.Body) +} + +func copyHeader(dst, src http.Header) { + for k, vv := range src { + for _, v := range vv { + dst.Add(k, v) + } + } +} + +const ( + gatewayIP = "192.168.127.1" + vmIP = "192.168.127.3" +) + +func main() { + var listenFd int + flag.IntVar(&listenFd, "net-listenfd", 0, "fd to listen for the connection") + var certFd int + flag.IntVar(&certFd, "certfd", 0, "fd to output cert") + var certFile string + flag.StringVar(&certFile, "certfile", "", "file to output cert") + var debug bool + flag.BoolVar(&debug, "debug", false, "debug log") + var arch string + flag.StringVar(&arch, "arch", "amd64", "target image architecture") + var imageAddr string + flag.StringVar(&imageAddr, "image-addr", "", "base address of image structured as OCI Image Layout") + flag.Parse() + + if debug { + log.SetOutput(os.Stdout) + logrus.SetLevel(logrus.DebugLevel) + } else { + log.SetOutput(io.Discard) + logrus.SetLevel(logrus.FatalLevel) + } + + if imageAddr == "" { + panic("specify image to mount") + } + imageserver, err := NewImageServer(context.TODO(), imageAddr, imagespec.Platform{ + Architecture: arch, + OS: "linux", + }) + if err != nil { + panic(err) + } + + ser, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 60)) + if err != nil { + panic(err) + } + cert := &x509.Certificate{ + SerialNumber: ser, + Subject: pkix.Name{ + CommonName: proxyIP, + OrganizationalUnit: []string{"proxy"}, + Organization: []string{"proxy"}, + Country: []string{"US"}, + }, + NotAfter: time.Now().Add(365 * 24 * time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + DNSNames: []string{proxyIP}, + IPAddresses: []net.IP{net.ParseIP(proxyIP)}, + IsCA: true, + BasicConstraintsValid: true, + } + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + panic(err) + } + certD, err := x509.CreateCertificate(rand.Reader, cert, cert, key.Public(), key) + if err != nil { + panic(err) + } + var f io.WriteCloser + if certFd != 0 { + f = os.NewFile(uintptr(certFd), "") + } else if certFile != "" { + var err error + f, err = os.OpenFile(certFile, os.O_RDWR|os.O_CREATE, 0755) + if err != nil { + panic(err) + } + } else { + panic("specify cert destination") + } + if err := pem.Encode(f, &pem.Block{Type: "CERTIFICATE", Bytes: certD}); err != nil { + panic(err) + } + if err := f.Close(); err != nil { + panic(err) + } + tlsCert := &tls.Certificate{ + Certificate: [][]byte{certD}, + PrivateKey: key, + } + proxyCert, err = x509.ParseCertificate(tlsCert.Certificate[0]) + if err != nil { + panic(err) + } + proxyKey = key + + config := &gvntypes.Configuration{ + Debug: debug, + MTU: 1500, + Subnet: "192.168.127.0/24", + GatewayIP: gatewayIP, + GatewayMacAddress: "5a:94:ef:e4:0c:dd", + GatewayVirtualIPs: []string{proxyIP, p9IP}, + Protocol: gvntypes.QemuProtocol, + } + vn, err := gvnvirtualnetwork.New(config) + if err != nil { + panic(err) + } + + server := &http.Server{ + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodConnect { + handleTunneling(w, r) + } else { + handleHTTP(w, r) + } + }), + // Disable HTTP/2 (FIXME) + // https://pkg.go.dev/net/http#hdr-HTTP_2 + TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)), + } + + go func() { + s := server + s.Addr = "0.0.0.0:80" + l, err := vn.Listen("tcp", proxyIP+":80") + if err != nil { + panic(err) + } + log.Println("serving proxy with http") + log.Fatal(server.Serve(l)) + }() + go func() { + s := server + s.Addr = "0.0.0.0:443" + l, err := vn.Listen("tcp", proxyIP+":443") + if err != nil { + panic(err) + } + s.TLSConfig = &tls.Config{ + Certificates: make([]tls.Certificate, 1), + } + s.TLSConfig.Certificates[0] = *tlsCert + log.Println("serving proxy with https") + log.Fatal(server.ServeTLS(l, "", "")) + }() + go func() { + l, err := vn.Listen("tcp", p9IP+":80") + if err != nil { + panic(err) + } + if err != nil { + panic(err) + } + log.Fatal(imageserver.Serve(l)) + }() + ql, err := findListener(listenFd) + if err != nil { + panic(err) + } + if ql == nil { + panic("socket fd not found") + } + qconn, err := ql.Accept() + if err != nil { + panic(err) + } + if err := vn.AcceptQemu(context.TODO(), qconn); err != nil { + panic(err) + } +} + +func findListener(listenFd int) (net.Listener, error) { + if listenFd == 0 { + for preopenFd := 3; ; preopenFd++ { + var stat syscall.Stat_t + if err := syscall.Fstat(preopenFd, &stat); err != nil { + var se syscall.Errno + if errors.As(err, &se) && se == syscall.EBADF { + err = nil + } + log.Printf("findListner failed (fd=%d): %v\n", preopenFd, err) + return nil, err + } else if stat.Filetype == syscall.FILETYPE_SOCKET_STREAM { + listenFd = preopenFd + break + } + } + } + syscall.SetNonblock(listenFd, true) + f := os.NewFile(uintptr(listenFd), "") + defer f.Close() + log.Printf("using socket at fd=%d\n", listenFd) + return net.FileListener(f) +} + +func NewImageServer(ctx context.Context, imageAddr string, platform imagespec.Platform) (*p9.Server, error) { + config, rootNode, configD, err := fsFromImage(ctx, imageAddr, platform, true) + if err != nil { + return nil, err + } + s, err := generateSpec(*config, rootNode) + if err != nil { + return nil, err + } + specD, err := json.Marshal(s) + if err != nil { + return nil, err + } + initNodes := make(map[string]p9.File) + initNodes["rootfs"] = rootNode + for name, o := range map[string][]p9staticfs.Option{ + "config": { + p9staticfs.WithFile("config.json", string(specD)), + p9staticfs.WithFile("imageconfig.json", string(configD)), + }, + } { + a, err := p9staticfs.New(o...) + if err != nil { + return nil, err + } + n, err := a.Attach() + if err != nil { + return nil, err + } + initNodes[name] = n + } + a, err := NewRouteNode(initNodes) + if err != nil { + return nil, fmt.Errorf("failed to create route node: %w", err) + } + return p9.NewServer(a), nil +} + +const ( + unixPasswdPath = "/etc/passwd" + unixGroupPath = "/etc/group" +) + +func generateSpec(config imagespec.Image, rootNode p9.File) (_ *runtimespec.Spec, err error) { + ic := config.Config + ctdCtx := ctdnamespaces.WithNamespace(context.TODO(), "default") + p := "linux/riscv64" + if config.Architecture == "amd64" { + p = "linux/amd64" + } + s, err := ctdoci.GenerateSpecWithPlatform(ctdCtx, nil, p, &ctdcontainers.Container{}, + ctdoci.WithHostNamespace(runtimespec.NetworkNamespace), + ctdoci.WithoutRunMount, + ctdoci.WithEnv(ic.Env), + ctdoci.WithTTY, // TODO: make it configurable + ) + if err != nil { + return nil, fmt.Errorf("failed to generate spec: %w", err) + } + if username := ic.User; username != "" { + passwdR, passwdRClose, err := readFileFromNode(rootNode, unixPasswdPath) + if err != nil { + return nil, err + } + defer passwdRClose() + groupR, groupRClose, err := readFileFromNode(rootNode, unixGroupPath) + if err != nil { + return nil, err + } + defer groupRClose() + execUser, err := user.GetExecUser(username, nil, passwdR, groupR) + if err != nil { + return nil, fmt.Errorf("failed to resolve username %q: %w", username, err) + } + s.Process.User.UID = uint32(execUser.Uid) + s.Process.User.GID = uint32(execUser.Gid) + for _, g := range execUser.Sgids { + s.Process.User.AdditionalGids = append(s.Process.User.AdditionalGids, uint32(g)) + } + } + args := ic.Entrypoint + if len(ic.Cmd) > 0 { + args = append(args, ic.Cmd...) + } + if len(args) > 0 { + s.Process.Args = args + } + if ic.WorkingDir != "" { + s.Process.Cwd = ic.WorkingDir + } + + // TODO: support seccomp and apparmor + s.Linux.Seccomp = nil + s.Root = &runtimespec.Root{ + Path: "/run/rootfs", + } + // TODO: ports + return s, nil +} + +func fsFromImage(ctx context.Context, addr string, platform imagespec.Platform, insecure bool) (*imagespec.Image, *Node, []byte, error) { + var layers []NodeLayer + var config imagespec.Image + var configData []byte + if strings.HasPrefix(addr, "http://") || strings.HasPrefix(addr, "https://") { + log.Printf("Pulling from HTTP server %q\n", addr) + manifest, image, configD, err := fetchManifestAndConfigOCILayout(ctx, addr, platform) + if err != nil { + return nil, nil, nil, err + } + config = image + configData = configD + layers, err = fetchLayers(ctx, manifest, EStargzLayerConfig{ + NoBackgroundFetch: true, + // NoPrefetch: true, + }, nil, map[string]esgzremote.Handler{ + "url-reader": &layerOCILayoutURLHandler{addr}, + }, reference.Spec{}, newLayerOCILayoutExternalReaderAt(addr)) + // }, reference.Spec{}, newLayerOCILayoutURLReader(addr)) + if err != nil { + return nil, nil, nil, err + } + } else { + log.Printf("Pulling from registry %q\n", addr) + refspec, err := reference.Parse(addr) + if err != nil { + return nil, nil, nil, err + } + manifest, image, configD, fetcher, err := fetchManifestAndConfigRegistry(ctx, refspec, platform) + if err != nil { + return nil, nil, nil, err + } + config = image + configData = configD + layers, err = fetchLayers(ctx, manifest, EStargzLayerConfig{ + NoBackgroundFetch: true, + // NoPrefetch: true, + }, wasmRegistryHosts, nil, refspec, func(desc imagespec.Descriptor, withDecompression bool) (io.Reader, error) { + r, err := fetcher.Fetch(ctx, desc) + if err != nil { + return nil, err + } + defer r.Close() + if withDecompression { + raw := r + zr, err := gzip.NewReader(raw) + if err != nil { + return nil, err + } + defer zr.Close() + r = zr + } + data, err := io.ReadAll(r) + if err != nil { + return nil, err + } + return io.NewSectionReader(bytes.NewReader(data), 0, int64(len(data))), nil + }) + if err != nil { + return nil, nil, nil, err + } + } + imgFS := &applier{} + + for _, l := range layers { + if err := imgFS.ApplyNodes(l); err != nil { + return nil, nil, nil, err + } + } + return &config, imgFS.n, configData, nil +} + +var defaultClient = &http.Client{Transport: wasmRoundTripper{}} + +func fetchManifestAndConfigRegistry(ctx context.Context, refspec reference.Spec, platform platforms.Platform) (imagespec.Manifest, imagespec.Image, []byte, remotes.Fetcher, error) { + resolver := docker.NewResolver(docker.ResolverOptions{ + Hosts: func(host string) ([]docker.RegistryHost, error) { + if host != refspec.Hostname() { + return nil, fmt.Errorf("unexpected host %q for image ref %q", host, refspec.String()) + } + return wasmRegistryHosts(refspec) + }, + }) + _, img, err := resolver.Resolve(ctx, refspec.String()) + if err != nil { + return imagespec.Manifest{}, imagespec.Image{}, nil, nil, err + } + fetcher, err := resolver.Fetcher(ctx, refspec.String()) + if err != nil { + return imagespec.Manifest{}, imagespec.Image{}, nil, nil, err + } + manifest, err := esgzcontainerdutil.FetchManifestPlatform(ctx, fetcher, img, platform) + if err != nil { + return imagespec.Manifest{}, imagespec.Image{}, nil, nil, err + } + r, err := fetcher.Fetch(ctx, manifest.Config) + if err != nil { + return imagespec.Manifest{}, imagespec.Image{}, nil, nil, err + } + defer r.Close() + configD, err := io.ReadAll(r) + if err != nil { + return imagespec.Manifest{}, imagespec.Image{}, nil, nil, err + } + var config imagespec.Image + if err := json.NewDecoder(bytes.NewReader(configD)).Decode(&config); err != nil { + return imagespec.Manifest{}, imagespec.Image{}, nil, nil, err + } + return manifest, config, configD, fetcher, err +} + +func fetchManifestAndConfigOCILayout(ctx context.Context, addr string, platform platforms.Platform) (imagespec.Manifest, imagespec.Image, []byte, error) { + c := defaultClient + + // Fetch index + resp, err := c.Get(addr + "/index.json") + if err != nil { + return imagespec.Manifest{}, imagespec.Image{}, nil, err + } + var index imagespec.Index + if err := json.NewDecoder(resp.Body).Decode(&index); err != nil { + return imagespec.Manifest{}, imagespec.Image{}, nil, err + } + io.ReadAll(resp.Body) + resp.Body.Close() + + var target digest.Digest + found := false + for _, m := range index.Manifests { + p := platform + if m.Platform != nil { + p = *m.Platform + } + if platforms.NewMatcher(platform).Match(p) { + target, found = m.Digest, true + break + } + } + if !found { + return imagespec.Manifest{}, imagespec.Image{}, nil, fmt.Errorf("manifest not found for platform %v", platform) + } + + // Fetch manifest + resp, err = c.Get(addr + "/blobs/sha256/" + target.Encoded()) + if err != nil { + return imagespec.Manifest{}, imagespec.Image{}, nil, err + } + var manifest imagespec.Manifest + if err := json.NewDecoder(resp.Body).Decode(&manifest); err != nil { + return imagespec.Manifest{}, imagespec.Image{}, nil, err + } + io.ReadAll(resp.Body) + resp.Body.Close() + + // Fetch config + resp, err = c.Get(addr + "/blobs/sha256/" + manifest.Config.Digest.Encoded()) + if err != nil { + return imagespec.Manifest{}, imagespec.Image{}, nil, err + } + var config imagespec.Image + configD, err := io.ReadAll(resp.Body) + if err != nil { + return imagespec.Manifest{}, imagespec.Image{}, nil, err + } + if err := json.NewDecoder(bytes.NewReader(configD)).Decode(&config); err != nil { + return imagespec.Manifest{}, imagespec.Image{}, nil, err + } + resp.Body.Close() + + return manifest, config, configD, nil +} + +func fetchLayers(ctx context.Context, manifest imagespec.Manifest, config EStargzLayerConfig, hosts esgzsource.RegistryHosts, handlers map[string]esgzremote.Handler, refspec reference.Spec, unlazyReader func(imagespec.Descriptor, bool) (io.Reader, error)) ([]NodeLayer, error) { + layers, err := getEStargzLayers(ctx, manifest, EStargzLayerConfig{ + NoBackgroundFetch: true, + // NoPrefetch: true, + }, hosts, handlers, refspec) + if err == nil { + return layers, nil + } + + log.Printf("falling back to gzip mode (previous error: %v)\n", err) + layers, err = getTarLayers(ctx, manifest, true, unlazyReader) + if err == nil { + return layers, nil + } + + log.Printf("falling back to tar mode (previous error: %v)\n", err) + layers, err = getTarLayers(ctx, manifest, false, unlazyReader) + if err == nil { + return layers, nil + } + + return nil, fmt.Errorf("failed to fetch layers (last error: %v)", err) +} + +func getTarLayers(ctx context.Context, manifest imagespec.Manifest, withDecompression bool, getReader func(imagespec.Descriptor, bool) (io.Reader, error)) ([]NodeLayer, error) { + layers := make([]NodeLayer, len(manifest.Layers)) + q := new(qidSet) + eg, _ := errgroup.WithContext(ctx) + for i, l := range manifest.Layers { + i, l := i, l + eg.Go(func() error { + var r io.Reader + var err error + r, err = getReader(l, withDecompression) + if err != nil { + return err + } + data, err := io.ReadAll(r) + if err != nil { + return err + } + n, err := newTarNode(q, bytes.NewReader(data)) + if err != nil { + return err + } + layers[i] = *n + return nil + }) + } + if err := eg.Wait(); err != nil { + return nil, err + } + return layers, nil +} + +func newTarNode(q *qidSet, trRaw io.ReaderAt) (*NodeLayer, error) { + pw, err := NewPositionWatcher(trRaw) + if err != nil { + return nil, fmt.Errorf("Failed to make position watcher: %w", err) + } + tr := tar.NewReader(pw) + + // Walk functions for nodes + getormake := func(n *Node, base string) (c *Node, err error) { + var ok bool + if c, ok = n.children[base]; !ok { + // Make temporary dummy node (directory). + c = &Node{ + qid: p9.QID{ + Type: p9.TypeDir, + Path: q.newQID(), + }, + attr: p9.Attr{ + Mode: p9.ModeDirectory, + }, + } + if n.children == nil { + n.children = make(map[string]*Node) + } + n.children[base] = c + } + return c, nil + } + + var whiteouts []string + var opaqueWhiteouts []string + + n := &Node{ + qid: p9.QID{ + Type: p9.TypeDir, + Path: q.newQID(), + }, + attr: p9.Attr{ + Mode: p9.ModeDirectory, + }, + } // This layer's files. Will be merged with lower + + // Walk through all nodes. + for { + // Fetch and parse next header. + h, err := tr.Next() + if err != nil { + if err != io.EOF { + return nil, fmt.Errorf("failed to parse tar file: %w", err) + } + break + } + var ( + fullname = filepath.Clean(h.Name) + dir, base = filepath.Split(fullname) + parentDir *Node + existingNode *Node + // TODO :support xattr + ) + if parentDir, err = walkDown(n, dir, getormake); err != nil { + return nil, fmt.Errorf("failed to make dummy nodes: %w", err) + } + existingNode, _ = parentDir.children[base] + switch { + case existingNode != nil: + if !existingNode.attr.Mode.IsDir() { + return nil, fmt.Errorf("node %q is placeholder but not a directory", fullname) + } + // This is "placeholder node" which has been registered in previous loop as + // an intermediate directory. Now we update it with real one. + attr, qid := headerToAttr(h) + qid.Path = q.newQID() + existingNode.attr = attr + existingNode.qid = qid + case strings.HasPrefix(base, whiteoutPrefix): + if base == whiteoutOpaqueDir { + opaqueWhiteouts = append(opaqueWhiteouts, fullname) + } else { + whiteouts = append(whiteouts, fullname) + } + default: + // Normal node so simply create it. + if parentDir.children == nil { + parentDir.children = make(map[string]*Node) + } + attr, qid := headerToAttr(h) + qid.Path = q.newQID() + parentDir.children[base] = &Node{ + qid: qid, + attr: attr, + link: h.Linkname, + r: io.NewSectionReader(trRaw, pw.CurrentPos(), h.Size), + } + } + + continue + } + + return &NodeLayer{node: n, whiteouts: whiteouts, opaqueWhiteouts: opaqueWhiteouts}, nil +} + +type EStargzLayerConfig struct { + DisableChunkVerify bool + MaxConcurrency int64 + PrefetchTimeout time.Duration + NoPrefetch bool + NoBackgroundFetch bool +} + +func wasmRegistryHosts(ref reference.Spec) (hosts []docker.RegistryHost, _ error) { + host := ref.Hostname() + client := defaultClient + config := docker.RegistryHost{ + Client: client, + Host: host, + Scheme: "https", + Path: "/v2", + Capabilities: docker.HostCapabilityPull | docker.HostCapabilityResolve, + } + if localhost, _ := docker.MatchLocalhost(config.Host); localhost { + config.Scheme = "http" + } + if config.Host == "docker.io" { + config.Host = "registry-1.docker.io" + } + hosts = append(hosts, config) + return +} + +func getEStargzLayers(ctx context.Context, manifest imagespec.Manifest, config EStargzLayerConfig, hosts esgzsource.RegistryHosts, handlers map[string]esgzremote.Handler, refspec reference.Spec) ([]NodeLayer, error) { + layers := make([]NodeLayer, len(manifest.Layers)) + checkChunkDigests := config.DisableChunkVerify + maxConcurrency := config.MaxConcurrency + if maxConcurrency == 0 { + maxConcurrency = int64(2) + } + prefetchTimeout := config.PrefetchTimeout + if prefetchTimeout == 0 { + prefetchTimeout = time.Second * 10 + } + noPrefetch := config.NoPrefetch + noBackgroundFetch := config.NoBackgroundFetch + blobCache := esgzcache.NewMemoryCache() + readerCache := esgzcache.NewMemoryCache() + esgzresolver := esgzremote.NewResolver(esgzconfig.BlobConfig{}, handlers) + tm := esgztask.NewBackgroundTaskManager(maxConcurrency, 5*time.Second) + eg, _ := errgroup.WithContext(ctx) + for i, l := range manifest.Layers { + i, l := i, l + eg.Go(func() error { + b, err := esgzresolver.Resolve(ctx, hosts, refspec, l, blobCache) // TODO: use directory cache + if err != nil { + return err + } + mr, err := esgzmetadatamemory.NewReader( + io.NewSectionReader(readerAtFunc(func(p []byte, offset int64) (int, error) { + tm.DoPrioritizedTask() + defer tm.DonePrioritizedTask() + return b.ReadAt(p, offset) + }), 0, b.Size())) + if err != nil { + return err + } + vr, err := esgzreader.NewReader(mr, readerCache, l.Digest) // TODO: use directory cache + if err != nil { + return err + } + if !noPrefetch { + prefetchWaiter := newWaiter() + eg.Go(func() error { + return prefetchWaiter.wait(prefetchTimeout) + }) + go func() { + if err := esgzPrefetch(ctx, tm, prefetchWaiter, b, vr); err != nil { + log.Printf("failed to prefetch layer %v\n", l.Digest) + return + } + log.Printf("completed prefetch of layer %v\n", l.Digest) + }() + } + if !noBackgroundFetch { + go func() { + if err := esgzBackgroundFetch(ctx, tm, b, vr); err != nil { + log.Printf("failed background fetching of layer %v\n", l.Digest) + return + } + log.Printf("completed background fetch of layer %v\n", l.Digest) + }() + } + var rr esgzreader.Reader + if checkChunkDigests { + tocDgstStr, ok := l.Annotations[estargz.TOCJSONDigestAnnotation] + if !ok { + return fmt.Errorf("digest of TOC JSON must be passed") + } + tocDgst, err := digest.Parse(tocDgstStr) + if err != nil { + return fmt.Errorf("invalid TOC digest: %v: %w", tocDgst, err) + } + rr, err = vr.VerifyTOC(tocDgst) + if err != nil { + return fmt.Errorf("invalid estargz layer: %w", err) + } + } else { + rr = vr.SkipVerify() // TODO: verify TOC digest using layer annotation + } + n, err := newESGZNode(rr, rr.Metadata().RootID(), uint32(i), "") + if err != nil { + return err + } + layers[i] = *n + return nil + }) + } + if err := eg.Wait(); err != nil { + return nil, err + } + return layers, nil +} + +func esgzPrefetch(ctx context.Context, tm *esgztask.BackgroundTaskManager, prefetchWaiter *waiter, blob esgzremote.Blob, r *esgzreader.VerifiableReader) error { + tm.DoPrioritizedTask() + defer tm.DonePrioritizedTask() + defer prefetchWaiter.done() // Notify the completion + // Measuring the total time to complete prefetch (use defer func() because l.Info().PrefetchSize is set later) + rootID := r.Metadata().RootID() + var prefetchSize int64 + if _, _, err := r.Metadata().GetChild(rootID, estargz.NoPrefetchLandmark); err == nil { + // do not prefetch this layer + return nil + } else if id, _, err := r.Metadata().GetChild(rootID, estargz.PrefetchLandmark); err == nil { + offset, err := r.Metadata().GetOffset(id) + if err != nil { + return fmt.Errorf("failed to get offset of prefetch landmark: %w", err) + } + // override the prefetch size with optimized value + prefetchSize = offset + } + + // Fetch the target range + if err := blob.Cache(0, prefetchSize); err != nil { + return fmt.Errorf("failed to prefetch layer: %w", err) + } + + // Cache uncompressed contents of the prefetched range + if err := r.Cache(esgzreader.WithFilter(func(offset int64) bool { + return offset < prefetchSize // Cache only prefetch target + })); err != nil { + return fmt.Errorf("failed to cache prefetched layer: %w", err) + } + + return nil +} + +func esgzBackgroundFetch(ctx context.Context, tm *esgztask.BackgroundTaskManager, blob esgzremote.Blob, r *esgzreader.VerifiableReader) error { + br := io.NewSectionReader(readerAtFunc(func(p []byte, offset int64) (retN int, retErr error) { + tm.InvokeBackgroundTask(func(ctx context.Context) { + retN, retErr = blob.ReadAt( + p, + offset, + esgzremote.WithContext(ctx), // Make cancellable + esgzremote.WithCacheOpts(esgzcache.Direct()), // Do not pollute mem cache + ) + }, 120*time.Second) + return + }), 0, blob.Size()) + return r.Cache( + esgzreader.WithReader(br), // Read contents in background + esgzreader.WithCacheOpts(esgzcache.Direct()), // Do not pollute mem cache + ) +} + +type routeAttacher struct { + node *bindNode +} + +var _ p9.Attacher = &routeAttacher{} + +func NewRouteNode(children map[string]p9.File) (p9.Attacher, error) { + r := &RouteNode{ + qid: p9.QID{ + Type: p9.TypeDir, + Path: 0, + }, + attr: p9.Attr{ + Mode: p9.ModeDirectory, + }, + children: make(map[string]*bindNode), + } + fs := &bindFS{} + i := 0 + for name, f := range children { + r.children[name] = &bindNode{ + children: make(map[p9.QID]p9.File), // TODO: do it lazy + bind: make(map[p9.QID]p9.QID), // TODO: do it lazy + f: f, + fs: fs, + } + i++ + } + bindRoot := &bindNode{ + children: make(map[p9.QID]p9.File), // TODO: do it lazy + bind: make(map[p9.QID]p9.QID), // TODO: do it lazy + f: r, + fs: fs, + } + return &routeAttacher{node: bindRoot}, nil +} + +func (a *routeAttacher) Attach() (p9.File, error) { + return a.node, nil +} + +type bindFS struct { + curQID uint64 + mu sync.Mutex +} + +func (fs *bindFS) newQID() uint64 { + fs.mu.Lock() + defer fs.mu.Unlock() + fs.curQID++ + return fs.curQID +} + +type bindNode struct { + p9.DefaultWalkGetAttr + templatefs.NoopFile + templatefs.ReadOnlyFile + templatefs.ReadOnlyDir + + children map[p9.QID]p9.File // keyed by global qid + bind map[p9.QID]p9.QID // local to global (itself or chlidren) + // qid p9.QID + // attr p9.Attr + + f p9.File + + fs *bindFS +} + +// TODO: reuse Node.Walk +func (n *bindNode) Walk(names []string) ([]p9.QID, p9.File, error) { + log.Println("bindNode.Walk", names) + qids, f, err := n.f.Walk(names) + if err != nil { + return nil, nil, err + } + + // Translate to or register the global qid + var gqids []p9.QID + for _, q := range qids { + gq, ok := n.bind[q] + if !ok { + gq = p9.QID{ + Type: q.Type, + Version: q.Version, + Path: n.fs.newQID(), + } + n.bind[q] = gq + } + gqids = append(gqids, gq) + } + + lqid, _, _, err := f.GetAttr(p9.AttrMask{}) + if err != nil { + return nil, nil, err + } + gq, ok := n.bind[lqid] + if !ok { + gq = p9.QID{ + Type: lqid.Type, + Version: lqid.Version, + Path: n.fs.newQID(), + } + n.bind[lqid] = gq + } + gf, ok := n.children[gq] + if !ok { + gf = &bindNode{ + children: make(map[p9.QID]p9.File), // TODO: do it lazy + bind: make(map[p9.QID]p9.QID), // TODO: do it lazy + f: f, + fs: n.fs, + } + } + + return gqids, gf, nil +} + +func (n *bindNode) GetAttr(req p9.AttrMask) (p9.QID, p9.AttrMask, p9.Attr, error) { + log.Println("bindNode.GetAttr", req) + qid, mask, attr, err := n.f.GetAttr(req) + if err != nil { + return p9.QID{}, p9.AttrMask{}, p9.Attr{}, err + } + gq, ok := n.bind[qid] + if !ok { + gq = p9.QID{ + Type: qid.Type, + Version: qid.Version, + Path: n.fs.newQID(), + } + n.bind[qid] = gq + } + return gq, mask, attr, nil +} + +// TODO: reuse Node.Readdir +func (n *bindNode) Readdir(offset uint64, count uint32) (p9.Dirents, error) { + log.Println("bindNode.Readdir", offset, count) + ents, err := n.f.Readdir(offset, count) + if err != nil { + return nil, err + } + log.Println("bindNode.Readdir => len(ents) =", len(ents)) + + var gents p9.Dirents + for _, e := range ents { + log.Println("bindNode.Readdir => e =", e) + gq, ok := n.bind[e.QID] + if !ok { + gq = p9.QID{ + Type: e.QID.Type, + Version: e.QID.Version, + Path: n.fs.newQID(), + } + n.bind[e.QID] = gq + } + e2 := e // copy + e2.QID = gq + gents = append(gents, e2) + } + log.Println("bindNode.Readdir => len(gents) =", len(gents)) + return gents, nil +} + +func (n *bindNode) Close() error { + return nil +} + +func (n *bindNode) Open(mode p9.OpenFlags) (p9.QID, uint32, error) { + log.Println("bindNode.Open", mode) + // qid, s, err := n.f.Open(mode) + qid, s, err := n.f.Open(p9.ReadOnly) // TODO: pass the original mode; but it causes EROFS error when reading files + if err != nil { + return p9.QID{}, 0, err + } + gq, ok := n.bind[qid] + if !ok { + gq = p9.QID{ + Type: qid.Type, + Version: qid.Version, + Path: n.fs.newQID(), + } + n.bind[qid] = gq + } + return gq, s, nil +} + +func (n *bindNode) ReadAt(p []byte, offset int64) (int, error) { + return n.f.ReadAt(p, offset) +} + +func (n *bindNode) Readlink() (string, error) { + return n.f.Readlink() +} + +func (n *bindNode) FSync() error { + return syscall.EROFS +} + +func (n *bindNode) SetAttr(valid p9.SetAttrMask, attr p9.SetAttr) error { + return syscall.EROFS +} + +func (n *bindNode) Remove() error { + return syscall.EROFS +} + +func (n *bindNode) Rename(directory p9.File, name string) error { + return syscall.EROFS +} + +func (n *bindNode) WriteAt(p []byte, offset int64) (int, error) { + return 0, syscall.EROFS +} + +func (n *bindNode) Flush() error { + return nil +} + +func (n *bindNode) Create(name string, mode p9.OpenFlags, permissions p9.FileMode, _ p9.UID, _ p9.GID) (p9.File, p9.QID, uint32, error) { + return nil, p9.QID{}, 0, syscall.EROFS +} + +func (n *bindNode) Mkdir(name string, permissions p9.FileMode, _ p9.UID, _ p9.GID) (p9.QID, error) { + return p9.QID{}, syscall.EROFS +} + +func (n *bindNode) Symlink(oldname string, newname string, _ p9.UID, _ p9.GID) (p9.QID, error) { + return p9.QID{}, syscall.EROFS +} + +func (n *bindNode) Link(target p9.File, newname string) error { + return syscall.EROFS +} + +func (n *bindNode) Mknod(name string, mode p9.FileMode, major uint32, minor uint32, _ p9.UID, _ p9.GID) (p9.QID, error) { + return p9.QID{}, syscall.EROFS +} + +func (n *bindNode) RenameAt(oldname string, newdir p9.File, newname string) error { + return syscall.EROFS +} + +func (n *bindNode) UnlinkAt(name string, flags uint32) error { + return syscall.EROFS +} + +type RouteNode struct { + p9.DefaultWalkGetAttr + templatefs.NoopFile + templatefs.ReadOnlyFile + templatefs.ReadOnlyDir + + children map[string]*bindNode + qid p9.QID + attr p9.Attr +} + +func (n *RouteNode) Walk(names []string) ([]p9.QID, p9.File, error) { + log.Println("RouteNode.Walk", names) + if len(names) == 0 { + return []p9.QID{n.qid}, n, nil + } + var qids []p9.QID + last := p9.File(n) + for _, name := range names { + c, ok := n.children[name] + if !ok { + return nil, nil, os.ErrNotExist + } + cqid, _, _, err := c.GetAttr(p9.AttrMask{}) + if err != nil { + return nil, nil, err + } + qids = append(qids, cqid) + last = c + } + return qids, last, nil +} + +func (n *RouteNode) GetAttr(req p9.AttrMask) (p9.QID, p9.AttrMask, p9.Attr, error) { + log.Println("RouteNode.GetAttr", req) + return n.qid, req, n.attr, nil +} + +// TODO: reuse Node.Readdir +func (n *RouteNode) Readdir(offset uint64, count uint32) (p9.Dirents, error) { + log.Println("RouteNode.Readdir", offset, count) + if offset > uint64(len(n.children)) { + return nil, nil + } + end := int(offset) + int(count) + if end > int(len(n.children)) { + end = int(len(n.children)) + } + + var names []string + for k := range n.children { + names = append(names, k) + } + sort.Strings(names) + + var ents []p9.Dirent + for i, name := range names[offset:end] { + c, ok := n.children[name] + if !ok { + return nil, fmt.Errorf("child %q not found", name) + } + cqid, _, _, err := c.GetAttr(p9.AttrMask{}) + if err != nil { + return nil, err + } + ents = append(ents, p9.Dirent{ + QID: cqid, + // returned offset needs to start from 1 according to p9.localfs + Offset: offset + uint64(i) + 1, + Type: cqid.Type, + Name: name, + }) + } + + return ents, nil +} + +func (n *RouteNode) Close() error { + return nil +} + +const ( + // p9DefaultBlockSize = 4096 + p9DefaultBlockSize = 53248 +) + +func (n *RouteNode) Open(mode p9.OpenFlags) (p9.QID, uint32, error) { + return n.qid, p9DefaultBlockSize, nil +} + +func (n *RouteNode) ReadAt(p []byte, offset int64) (int, error) { + return 0, os.ErrInvalid +} + +func (n *RouteNode) Readlink() (string, error) { + return "", nil +} + +func (n *RouteNode) FSync() error { + return syscall.EROFS +} + +func (n *RouteNode) SetAttr(valid p9.SetAttrMask, attr p9.SetAttr) error { + return syscall.EROFS +} + +func (n *RouteNode) Remove() error { + return syscall.EROFS +} + +func (n *RouteNode) Rename(directory p9.File, name string) error { + return syscall.EROFS +} + +func (n *RouteNode) WriteAt(p []byte, offset int64) (int, error) { + return 0, syscall.EROFS +} + +func (n *RouteNode) Flush() error { + return nil +} + +func (n *RouteNode) Create(name string, mode p9.OpenFlags, permissions p9.FileMode, _ p9.UID, _ p9.GID) (p9.File, p9.QID, uint32, error) { + return nil, p9.QID{}, 0, syscall.EROFS +} + +func (n *RouteNode) Mkdir(name string, permissions p9.FileMode, _ p9.UID, _ p9.GID) (p9.QID, error) { + return p9.QID{}, syscall.EROFS +} + +func (n *RouteNode) Symlink(oldname string, newname string, _ p9.UID, _ p9.GID) (p9.QID, error) { + return p9.QID{}, syscall.EROFS +} + +func (n *RouteNode) Link(target p9.File, newname string) error { + return syscall.EROFS +} + +func (n *RouteNode) Mknod(name string, mode p9.FileMode, major uint32, minor uint32, _ p9.UID, _ p9.GID) (p9.QID, error) { + return p9.QID{}, syscall.EROFS +} + +func (n *RouteNode) RenameAt(oldname string, newdir p9.File, newname string) error { + return syscall.EROFS +} + +func (n *RouteNode) UnlinkAt(name string, flags uint32) error { + return syscall.EROFS +} + +type attacher struct { + node p9.File +} + +var _ p9.Attacher = &attacher{} + +func NodeAttacher(n p9.File) p9.Attacher { + return &attacher{node: n} +} + +func (a *attacher) Attach() (p9.File, error) { + return a.node, nil +} + +type Node struct { + p9.DefaultWalkGetAttr + templatefs.NoopFile + templatefs.ReadOnlyFile + templatefs.ReadOnlyDir + + qid p9.QID + attr p9.Attr + link string + + r io.ReaderAt + + children map[string]*Node +} + +func (n *Node) Walk(names []string) ([]p9.QID, p9.File, error) { + if len(names) == 0 { + return []p9.QID{n.qid}, n, nil + } + var qids []p9.QID + last := n + for _, name := range names { + c, ok := n.children[name] + if !ok { + return nil, nil, os.ErrNotExist + } + qids = append(qids, c.qid) + last = c + } + return qids, last, nil +} + +func (n *Node) GetAttr(req p9.AttrMask) (p9.QID, p9.AttrMask, p9.Attr, error) { + return n.qid, req, n.attr, nil +} + +func (n *Node) Readdir(offset uint64, count uint32) (p9.Dirents, error) { + if offset > uint64(len(n.children)) { + return nil, nil + } + end := int(offset) + int(count) + if end > int(len(n.children)) { + end = int(len(n.children)) + } + + var names []string + for k := range n.children { + names = append(names, k) + } + sort.Strings(names) + + var ents []p9.Dirent + for i, name := range names[offset:end] { + c, ok := n.children[name] + if !ok { + return nil, fmt.Errorf("child %q not found", name) + } + ents = append(ents, p9.Dirent{ + QID: c.qid, + // returned offset needs to start from 1 according to p9.localfs + Offset: offset + uint64(i) + 1, + Type: c.qid.Type, + Name: name, + }) + } + + return ents, nil +} + +func (n *Node) Close() error { + return nil +} + +func (n *Node) Open(mode p9.OpenFlags) (p9.QID, uint32, error) { + return n.qid, p9DefaultBlockSize, nil +} + +func (n *Node) ReadAt(p []byte, offset int64) (int, error) { + return n.r.ReadAt(p, offset) +} + +func (n *Node) Readlink() (string, error) { + return n.link, nil +} + +func (n *Node) FSync() error { + return syscall.EROFS +} + +func (n *Node) SetAttr(valid p9.SetAttrMask, attr p9.SetAttr) error { + return syscall.EROFS +} + +func (n *Node) Remove() error { + return syscall.EROFS +} + +func (n *Node) Rename(directory p9.File, name string) error { + return syscall.EROFS +} + +func (n *Node) WriteAt(p []byte, offset int64) (int, error) { + return 0, syscall.EROFS +} + +func (n *Node) Flush() error { + return nil +} + +func (n *Node) Create(name string, mode p9.OpenFlags, permissions p9.FileMode, _ p9.UID, _ p9.GID) (p9.File, p9.QID, uint32, error) { + return nil, p9.QID{}, 0, syscall.EROFS +} + +func (n *Node) Mkdir(name string, permissions p9.FileMode, _ p9.UID, _ p9.GID) (p9.QID, error) { + return p9.QID{}, syscall.EROFS +} + +func (n *Node) Symlink(oldname string, newname string, _ p9.UID, _ p9.GID) (p9.QID, error) { + return p9.QID{}, syscall.EROFS +} + +func (n *Node) Link(target p9.File, newname string) error { + return syscall.EROFS +} + +func (n *Node) Mknod(name string, mode p9.FileMode, major uint32, minor uint32, _ p9.UID, _ p9.GID) (p9.QID, error) { + return p9.QID{}, syscall.EROFS +} + +func (n *Node) RenameAt(oldname string, newdir p9.File, newname string) error { + return syscall.EROFS +} + +func (n *Node) UnlinkAt(name string, flags uint32) error { + return syscall.EROFS +} + +type applier struct { + n *Node //root +} + +func (a *applier) ApplyNodes(nodes NodeLayer) error { + getnode := func(n *Node, base string) (c *Node, err error) { + var ok bool + if c, ok = n.children[base]; !ok { + return nil, fmt.Errorf("Node %q not found", base) + } + return c, nil + } + for _, w := range nodes.whiteouts { + dir, base := filepath.Split(w) + if _, err := walkDown(a.n, filepath.Join(dir, base[len(whiteoutPrefix):]), getnode); err == nil { + p, err := walkDown(a.n, dir, getnode) + if err != nil { + return fmt.Errorf("parent node of whiteout %q is not found: %w", w, err) + } + delete(p.children, base) + } + } + for _, w := range nodes.opaqueWhiteouts { + dir, _ := filepath.Split(w) + p, err := walkDown(a.n, dir, getnode) + if err != nil { + return fmt.Errorf("parent node of whiteout %q is not found: %w", w, err) + } + p.children = nil + } + var err error + a.n, err = mergeNode(a.n, nodes.node) + if err != nil { + return err + } + a.n = addDots(a.n, nil) + return nil +} + +type qidSet struct { + curQID uint64 + mu sync.Mutex +} + +func (q *qidSet) newQID() uint64 { + q.mu.Lock() + defer q.mu.Unlock() + q.curQID++ + return q.curQID +} + +const ( + whiteoutPrefix = ".wh." + whiteoutOpaqueDir = whiteoutPrefix + whiteoutPrefix + ".opq" +) + +type NodeLayer struct { + node *Node + whiteouts []string + opaqueWhiteouts []string +} + +func mergeNode(a, b *Node) (*Node, error) { + if a == nil { + return b, nil + } + if b == nil { + return a, nil + } + ret := b // b is prioritized + for base, ac := range a.children { + bc, _ := b.children[base] + c2, err := mergeNode(ac, bc) // TODO: parallelize + if err != nil { + return nil, err + } + if c2 != nil { + if b.children == nil { + b.children = make(map[string]*Node) + } + b.children[base] = c2 + } + } + return ret, nil +} + +func addDots(a *Node, parent *Node) *Node { + if a == nil { + return nil + } + if a.qid.Type == p9.TypeDir { + if a.children == nil { + a.children = make(map[string]*Node) + } + a.children["."] = a + if parent != nil { + a.children[".."] = parent + } else { + a.children[".."] = a + } + } + for name, c := range a.children { + if name == "." || name == ".." { + continue + } + a.children[name] = addDots(c, a) + } + return a +} + +type walkFunc func(n *Node, base string) (*Node, error) + +func walkDown(n *Node, path string, walkFn walkFunc) (ino *Node, err error) { + ino = n + for _, comp := range strings.Split(path, "/") { + if len(comp) == 0 { + continue + } + if ino == nil { + return nil, fmt.Errorf("corresponding node of %q is not found", comp) + } + if ino, err = walkFn(ino, comp); err != nil { + return nil, err + } + } + return +} + +func newESGZNode(r esgzreader.Reader, id uint32, base uint32, curPath string) (nodes *NodeLayer, err error) { + mattr, err := r.Metadata().GetAttr(id) + if err != nil { + return nil, err + } + attr, qid := metadataToAttr(mattr) + qid.Path = (uint64(base) << 32) | uint64(id) + n := &Node{ + qid: qid, + attr: attr, + link: mattr.LinkName, + r: io.NewSectionReader(newESGZNodeReaderAt(r, id), 0, int64(attr.Size)), + } + var whiteouts []string + var opaqueWhiteouts []string + var childErr error + if err := r.Metadata().ForeachChild(id, func(name string, cid uint32, mode os.FileMode) bool { + fullname := filepath.Join(curPath, name) + if strings.HasPrefix(name, whiteoutPrefix) { + if name == whiteoutOpaqueDir { + opaqueWhiteouts = append(opaqueWhiteouts, fullname) + } else { + whiteouts = append(whiteouts, fullname) + } + // Do not add as a node but record as an whiteout + return true + } + cNodes, err := newESGZNode(r, cid, base, fullname) + whiteouts = append(whiteouts, cNodes.whiteouts...) + opaqueWhiteouts = append(opaqueWhiteouts, cNodes.opaqueWhiteouts...) + if err != nil { + childErr = err + return false + } + if n.children == nil { + n.children = make(map[string]*Node) + } + n.children[name] = cNodes.node + return true + }); err != nil { + return nil, err + } + if childErr != nil { + return nil, err + } + return &NodeLayer{node: n, whiteouts: whiteouts, opaqueWhiteouts: opaqueWhiteouts}, nil +} + +// mkdev calculates device number bits copmatible to Go +// https://cs.opensource.google/go/x/sys/+/refs/tags/v0.15.0:unix/dev_linux.go;l=36 +func mkdev(major, minor uint32) uint64 { + return ((uint64(major) & 0x00000fff) << 8) | + ((uint64(major) & 0xfffff000) << 32) | + ((uint64(minor) & 0x000000ff) << 0) | + ((uint64(minor) & 0xffffff00) << 12) +} + +func metadataToAttr(m esgzmetadata.Attr) (p9.Attr, p9.QID) { + out := p9.Attr{} + out.Mode = p9.ModeFromOS(m.Mode) + out.UID = p9.UID(m.UID) + out.GID = p9.GID(m.GID) + // out.NLink TOOD + out.RDev = p9.Dev(mkdev(uint32(m.DevMajor), uint32(m.DevMinor))) + out.Size = uint64(m.Size) + out.BlockSize = uint64(p9DefaultBlockSize) + out.Blocks = uint64(m.Size/p9DefaultBlockSize + 1) + out.ATimeSeconds = uint64(m.ModTime.Unix()) + out.ATimeNanoSeconds = uint64(m.ModTime.UnixNano()) + out.MTimeSeconds = uint64(m.ModTime.Unix()) + out.MTimeNanoSeconds = uint64(m.ModTime.UnixNano()) + out.CTimeSeconds = uint64(m.ModTime.Unix()) + out.CTimeNanoSeconds = uint64(m.ModTime.UnixNano()) + // out.BTimeSeconds TODO + // out.BTimeNanoSeconds TODO + out.Gen = 0 + out.DataVersion = 0 + + q := p9.QID{} + q.Type = p9.ModeFromOS(m.Mode).QIDType() + // if h.Typeflag == tar.TypeLink { + // q.Type = p9.TypeLink + // } else { + // q.Type = p9.ModeFromOS(m.Mode).QIDType() + // } + return out, q +} + +type esgzNodeReaderAt struct { + r esgzreader.Reader + id uint32 + ra io.ReaderAt + mu sync.Mutex +} + +func newESGZNodeReaderAt(r esgzreader.Reader, id uint32) *esgzNodeReaderAt { + return &esgzNodeReaderAt{r: r, id: id} +} + +func (r *esgzNodeReaderAt) ReadAt(p []byte, off int64) (int, error) { + r.mu.Lock() + if r.ra == nil { + ra, err := r.r.OpenFile(r.id) + if err != nil { + r.mu.Unlock() + return 0, err + } + r.ra = ra + } + r.mu.Unlock() + return r.ra.ReadAt(p, off) +} + +func readFileFromNode(n p9.File, p string) (*io.SectionReader, func() error, error) { + if p == "" { + _, _, attr, err := n.GetAttr(p9.AttrMask{}) + if err != nil { + return nil, nil, err + } + if _, _, err := n.Open(p9.ReadOnly); err != nil { + return nil, nil, err + } + return io.NewSectionReader(n, 0, int64(attr.Size)), n.Close, nil + } + rootdir, remain := rootdirPath(p) + _, f, err := n.Walk([]string{rootdir}) + if err != nil { + return nil, nil, err + } + return readFileFromNode(f, remain) +} + +func rootdirPath(p string) (rootdir string, remain string) { + d, f := filepath.Split(strings.TrimPrefix(filepath.Clean("/"+p), "/")) + if d == "" { + return f, "" + } + ro, re := rootdirPath(d) + return ro, filepath.Join(re, f) +} + +type readerAtFunc func([]byte, int64) (int, error) + +func (f readerAtFunc) ReadAt(p []byte, offset int64) (int, error) { return f(p, offset) } + +type wasmRoundTripper struct{} + +func (tr wasmRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + return doHttpRoundTrip(req) +} + +type layerOCILayoutURLHandler struct { + addr string +} + +func (h *layerOCILayoutURLHandler) Handle(ctx context.Context, desc imagespec.Descriptor) (fetcher esgzremote.Fetcher, size int64, err error) { + return &layerOCILayoutURLFetcher{newLayerOCILayoutURLReaderAt(h.addr, desc), h, desc}, desc.Size, nil +} + +type layerOCILayoutURLFetcher struct { + // refspec reference.Spec + r *io.SectionReader + h *layerOCILayoutURLHandler + desc imagespec.Descriptor +} + +func (f *layerOCILayoutURLFetcher) Fetch(ctx context.Context, off int64, size int64) (io.ReadCloser, error) { + return io.NopCloser(io.NewSectionReader(f.r, off, size)), nil +} + +func (f *layerOCILayoutURLFetcher) Check() error { + return nil +} + +func (f *layerOCILayoutURLFetcher) GenID(off int64, size int64) string { + sum := sha256.Sum256([]byte(fmt.Sprintf("%s-%d-%d", f.desc.Digest.String(), off, size))) + return fmt.Sprintf("%x", sum) +} + +//go:wasmimport env layer_request +func layer_request(addressP uint32, addresslen uint32, digestP uint32, digestlen uint32, withDecompression uint32, idP uint32) uint32 + +//go:wasmimport env layer_isreadable +func layer_isreadable(id uint32, isOKP uint32, sizeP uint32) uint32 + +//go:wasmimport env layer_readat +func layer_readat(id uint32, respP uint32, offset uint32, len uint32, respsizeP uint32) uint32 + +func newLayerOCILayoutExternalReaderAt(addr string) func(l imagespec.Descriptor, withDecompression bool) (io.Reader, error) { + return func(l imagespec.Descriptor, withDecompression bool) (io.Reader, error) { + dgst := l.Digest.Encoded() + layerAddr := addr + "/blobs/sha256/" + dgst + var id uint32 + var withDecompressionN uint32 = 0 + if withDecompression { + withDecompressionN = 1 + } + res := layer_request( + uint32(uintptr(unsafe.Pointer(&[]byte(layerAddr)[0]))), + uint32(len(layerAddr)), + uint32(uintptr(unsafe.Pointer(&[]byte(dgst)[0]))), + uint32(len(dgst)), + uint32(withDecompressionN), + uint32(uintptr(unsafe.Pointer(&id))), + ) + if res != 0 { + return nil, fmt.Errorf("failed to send layer request") + } + var isOK uint32 = 0 + var size uint32 + for { + res := layer_isreadable(id, + uint32(uintptr(unsafe.Pointer(&isOK))), + uint32(uintptr(unsafe.Pointer(&size)))) + if res != 0 { + return nil, fmt.Errorf("layer is not readable") + } + if isOK == 1 { + break + } + time.Sleep(1 * time.Millisecond) + } + return io.NewSectionReader(readerAtFunc(func(p []byte, offset int64) (retN int, retErr error) { + var respsize uint32 + res := layer_readat( + id, + uint32(uintptr(unsafe.Pointer(&[]byte(p)[0]))), + uint32(offset), // TODO: FIXME: 64bits + uint32(len(p)), + uint32(uintptr(unsafe.Pointer(&respsize))), + ) + if res != 0 { + return 0, fmt.Errorf("failed to receive layer response") + } + return int(respsize), nil + }), 0, int64(size)), nil + } +} + +func newLayerOCILayoutURLReader(addr string) func(l imagespec.Descriptor, withDecompression bool) (io.Reader, error) { + return func(l imagespec.Descriptor, withDecompression bool) (io.Reader, error) { + var r io.Reader + r = io.NewSectionReader(&urlReaderAt{url: addr + "/blobs/sha256/" + l.Digest.Encoded()}, 0, l.Size) + if withDecompression { + raw := r + zr, err := gzip.NewReader(raw) + if err != nil { + return nil, err + } + defer zr.Close() + r = zr + } + return r, nil + } +} + +func newLayerOCILayoutURLReaderAt(addr string, l imagespec.Descriptor) *io.SectionReader { + return io.NewSectionReader(&urlReaderAt{url: addr + "/blobs/sha256/" + l.Digest.Encoded()}, 0, l.Size) +} + +type urlReaderAt struct { + url string +} + +func (r *urlReaderAt) ReadAt(p []byte, off int64) (n int, err error) { + c := defaultClient + + req, err := http.NewRequest("GET", r.url, nil) + if err != nil { + return 0, err + } + req.Header.Add("Range", fmt.Sprintf("bytes=%d-%d", off, off+int64(len(p))-1)) + resp, err := c.Do(req) + if err != nil { + return 0, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusPartialContent { + return 0, fmt.Errorf("unexpected code: %d", resp.StatusCode) + } + mt := resp.Header.Get("Content-Type") + if mt != "" { + mediaType, _, err := mime.ParseMediaType(mt) + if err != nil { + return 0, fmt.Errorf("invalid media type %q: %w", mediaType, err) + } + if strings.HasPrefix(mediaType, "multipart/") { + return 0, fmt.Errorf("multipart unsupported") + } + } + bd, err := io.ReadAll(resp.Body) + if err != nil { + return 0, err + } + copy(p, bd) + return len(bd), nil +} + +func newWaiter() *waiter { + return &waiter{ + completionCond: sync.NewCond(&sync.Mutex{}), + } +} + +type waiter struct { + isDone bool + isDoneMu sync.Mutex + completionCond *sync.Cond +} + +func (w *waiter) done() { + w.isDoneMu.Lock() + w.isDone = true + w.isDoneMu.Unlock() + w.completionCond.Broadcast() +} + +func (w *waiter) wait(timeout time.Duration) error { + wait := func() <-chan struct{} { + ch := make(chan struct{}) + go func() { + w.isDoneMu.Lock() + isDone := w.isDone + w.isDoneMu.Unlock() + + w.completionCond.L.Lock() + if !isDone { + w.completionCond.Wait() + } + w.completionCond.L.Unlock() + ch <- struct{}{} + }() + return ch + } + select { + case <-time.After(timeout): + w.isDoneMu.Lock() + w.isDone = true + w.isDoneMu.Unlock() + w.completionCond.Broadcast() + return fmt.Errorf("timeout(%v)", timeout) + case <-wait(): + return nil + } +} + +func NewPositionWatcher(r io.ReaderAt) (*PositionWatcher, error) { + if r == nil { + return nil, fmt.Errorf("Target ReaderAt is empty") + } + pos := int64(0) + return &PositionWatcher{r: r, cPos: &pos}, nil +} + +type PositionWatcher struct { + r io.ReaderAt + cPos *int64 + + mu sync.Mutex +} + +func (pwr *PositionWatcher) Read(p []byte) (int, error) { + pwr.mu.Lock() + defer pwr.mu.Unlock() + + n, err := pwr.r.ReadAt(p, *pwr.cPos) + if err == nil { + *pwr.cPos += int64(n) + } + return n, err +} + +func (pwr *PositionWatcher) Seek(offset int64, whence int) (int64, error) { + pwr.mu.Lock() + defer pwr.mu.Unlock() + + switch whence { + default: + return 0, fmt.Errorf("Unknown whence: %v", whence) + case io.SeekStart: + case io.SeekCurrent: + offset += *pwr.cPos + case io.SeekEnd: + return 0, fmt.Errorf("Unsupported whence: %v", whence) + } + + if offset < 0 { + return 0, fmt.Errorf("invalid offset") + } + *pwr.cPos = offset + return offset, nil +} + +func (pwr *PositionWatcher) CurrentPos() int64 { + pwr.mu.Lock() + defer pwr.mu.Unlock() + + return *pwr.cPos +} + +func headerToAttr(h *tar.Header) (p9.Attr, p9.QID) { + out := p9.Attr{} + out.Mode = p9.ModeFromOS(h.FileInfo().Mode()) + out.UID = p9.UID(h.Uid) + out.GID = p9.GID(h.Gid) + // out.NLink TOOD + out.RDev = p9.Dev(mkdev(uint32(h.Devmajor), uint32(h.Devminor))) + out.Size = uint64(h.Size) + out.BlockSize = uint64(p9DefaultBlockSize) + out.Blocks = uint64(h.Size/p9DefaultBlockSize + 1) + out.ATimeSeconds = uint64(h.AccessTime.Unix()) + out.ATimeNanoSeconds = uint64(h.AccessTime.UnixNano()) + out.MTimeSeconds = uint64(h.ModTime.Unix()) + out.MTimeNanoSeconds = uint64(h.ModTime.UnixNano()) + out.CTimeSeconds = uint64(h.ChangeTime.Unix()) + out.CTimeNanoSeconds = uint64(h.ChangeTime.UnixNano()) + // out.BTimeSeconds TODO + // out.BTimeNanoSeconds TODO + out.Gen = 0 + out.DataVersion = 0 + + q := p9.QID{} + if h.Typeflag == tar.TypeLink { + q.Type = p9.TypeLink + } else { + q.Type = p9.ModeFromOS(h.FileInfo().Mode()).QIDType() + } + return out, q +} diff --git a/extras/runcontainerjs/.gitignore b/extras/runcontainerjs/.gitignore new file mode 100644 index 0000000..76add87 --- /dev/null +++ b/extras/runcontainerjs/.gitignore @@ -0,0 +1,2 @@ +node_modules +dist \ No newline at end of file diff --git a/extras/runcontainerjs/README.md b/extras/runcontainerjs/README.md new file mode 100644 index 0000000..6188eba --- /dev/null +++ b/extras/runcontainerjs/README.md @@ -0,0 +1,33 @@ +# runcontainer + +JS helper library for running containers on browser, used for some examples in this project. + +Container is expected to run in a web worker. + +## Example + +See [`./../../examples/no-conversion/`](./../../examples/no-conversion/) for running example. + +This example uses xterm-pty for the terminal of the container. + +Prepare container in the main worker: + +```js +const worker = new Worker("./worker.js"); +const vmImage = location.origin + "/out.wasm.gzip"; +const mounterImage = location.origin + "/imagemounter.wasm.gzip"; +const stackWorkerFile = location.origin + "/dist/stack-worker.js"; +const containerImageAddress = getImageParam(); +const infoP = RunContainer.createContainer(vmImage, containerImageAddress, stackWorkerFile, mounterImage); +infoP.then((info) => { + worker.postMessage({type: "init", info: info, args: ['/bin/sh']}); + new TtyServer(slave).start(worker); +}) +``` + +Start container in a worker: + +```js +var ttyClient = new TtyClient(msg.data); +RunContainer.startContainer(info, args, ttyClient); +``` diff --git a/extras/runcontainerjs/package-lock.json b/extras/runcontainerjs/package-lock.json new file mode 100644 index 0000000..a5f0cf2 --- /dev/null +++ b/extras/runcontainerjs/package-lock.json @@ -0,0 +1,1482 @@ +{ + "name": "runcontainer", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "runcontainer", + "version": "0.0.0", + "license": " Apache-2.0", + "dependencies": { + "@bjorn3/browser_wasi_shim": "^0.2.17", + "ts-loader": "^9.5.1", + "webpack": "^5.89.0" + }, + "devDependencies": { + "webpack-cli": "^5.1.4" + } + }, + "node_modules/@bjorn3/browser_wasi_shim": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/@bjorn3/browser_wasi_shim/-/browser_wasi_shim-0.2.17.tgz", + "integrity": "sha512-B2qcaGROo4e2s4nXb3VPATrczVrntM4BUXtAU1gEzUOfqKTcVuePq4NfhH5hmLBSvZ45YcT4gflDRUFYqLhkxA==" + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", + "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@types/eslint": { + "version": "8.44.7", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.7.tgz", + "integrity": "sha512-f5ORu2hcBbKei97U73mf+l9t4zTGl74IqZ0GQk4oVea/VS8tQZYkUveSYojk+frraAVYId0V2WC9O4PTNru2FQ==", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" + }, + "node_modules/@types/node": { + "version": "20.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.0.tgz", + "integrity": "sha512-D0WfRmU9TQ8I9PFx9Yc+EBHw+vSpIub4IDvQivcp26PtPrdMGAq5SDcpXEo/epqa/DXotVpekHiLNTg3iaKXBQ==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", + "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", + "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", + "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", + "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", + "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", + "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", + "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-opt": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6", + "@webassemblyjs/wast-printer": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", + "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", + "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", + "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", + "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/configtest": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", + "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", + "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", + "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" + }, + "node_modules/acorn": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", + "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-assertions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", + "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001541", + "electron-to-chromium": "^1.4.535", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001565", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001565.tgz", + "integrity": "sha512-xrE//a3O7TP0vaJ8ikzkD2c2NgcVUvsEe2IvFTntV4Yd1Z9FVzh+gW+enX96L0psrbaFMcVcH2l90xNuGDWc8w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.596", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.596.tgz", + "integrity": "sha512-zW3zbZ40Icb2BCWjm47nxwcFGYlIgdXkAx85XDO7cyky9J4QQfq8t0W19/TLZqq3JPQXtlv8BPIGmfa9Jb4scg==" + }, + "node_modules/enhanced-resolve": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", + "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/envinfo": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.11.0.tgz", + "integrity": "sha512-G9/6xF1FPbIw0TtalAMaVPpiq2aDEuKLXM314jPVAO9r2fo2a4BLqMNkmRS7O/xPPZ+COAhGIz3ETvHEV3eUcg==", + "dev": true, + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz", + "integrity": "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==" + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + }, + "node_modules/node-releases": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==" + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", + "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/terser": { + "version": "5.24.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.24.0.tgz", + "integrity": "sha512-ZpGR4Hy3+wBEzVEnHvstMvqpD/nABNelQn/z2r0fjVWGQsN3bpOLzQlqDxmb4CDZnXq5lpjnQ+mHQLAOpfM5iw==", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.9", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz", + "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.17", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.16.8" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-loader": { + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.1.tgz", + "integrity": "sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg==", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, + "node_modules/ts-loader/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/typescript": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz", + "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack": { + "version": "5.89.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.89.0.tgz", + "integrity": "sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw==", + "dependencies": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^1.0.0", + "@webassemblyjs/ast": "^1.11.5", + "@webassemblyjs/wasm-edit": "^1.11.5", + "@webassemblyjs/wasm-parser": "^1.11.5", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.9.0", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.15.0", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.7", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", + "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", + "dev": true, + "dependencies": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^2.1.1", + "@webpack-cli/info": "^2.0.2", + "@webpack-cli/serve": "^2.0.5", + "colorette": "^2.0.14", + "commander": "^10.0.1", + "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^5.7.3" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "5.x.x" + }, + "peerDependenciesMeta": { + "@webpack-cli/generators": { + "optional": true + }, + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "dev": true, + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } +} diff --git a/extras/runcontainerjs/package.json b/extras/runcontainerjs/package.json new file mode 100644 index 0000000..f10cec6 --- /dev/null +++ b/extras/runcontainerjs/package.json @@ -0,0 +1,19 @@ +{ + "name": "runcontainer", + "version": "0.0.0", + "description": "", + "main": "dist/runcontainer", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "ktock", + "license": " Apache-2.0", + "devDependencies": { + "webpack-cli": "^5.1.4" + }, + "dependencies": { + "@bjorn3/browser_wasi_shim": "^0.2.17", + "ts-loader": "^9.5.1", + "webpack": "^5.89.0" + } +} diff --git a/extras/runcontainerjs/src/web/runcontainer.js b/extras/runcontainerjs/src/web/runcontainer.js new file mode 100644 index 0000000..374148a --- /dev/null +++ b/extras/runcontainerjs/src/web/runcontainer.js @@ -0,0 +1,413 @@ +export async function createContainer(vmImage, imageAddr, stackWorkerPath, mounterWasmURL) { + let stackWorker = new Worker(stackWorkerPath); + return {vmImage: vmImage, net: createStack(stackWorker, imageAddr, mounterWasmURL)}; +} + +function createStack(stackWorker, imageAddr, mounterWasmURL) { + var proxyShared = new SharedArrayBuffer(12 + 1024 * 1024); + + var toShared = new SharedArrayBuffer(1024 * 1024); + var fromShared = new SharedArrayBuffer(1024 * 1024); + var toNetCtrl = new Int32Array(toShared, 0, 1); + var toNetBegin = new Int32Array(toShared, 4, 1); + var toNetEnd = new Int32Array(toShared, 8, 1); + var toNetNotify = new Int32Array(toShared, 12, 1); + var toNetData = new Uint8Array(toShared, 16); + var fromNetCtrl = new Int32Array(fromShared, 0, 1); + var fromNetBegin = new Int32Array(fromShared, 4, 1); + var fromNetEnd = new Int32Array(fromShared, 8, 1); + var fromNetData = new Uint8Array(fromShared, 12); + toNetCtrl[0] = 0; + toNetBegin[0] = 0; + toNetEnd[0] = 0; + toNetNotify[0] = 0; + fromNetCtrl[0] = 0; + fromNetBegin[0] = 0; + fromNetEnd[0] = 0; + + var metaFromShared = new SharedArrayBuffer(4096); + var metaFromNetCtrl = new Int32Array(metaFromShared, 0, 1); + var metaFromNetBegin = new Int32Array(metaFromShared, 4, 1); + var metaFromNetEnd = new Int32Array(metaFromShared, 8, 1); + var metaFromNetStatus = new Int32Array(metaFromShared, 12, 1); + var metaFromNetData = new Uint8Array(metaFromShared, 16); + metaFromNetCtrl[0] = 0; + metaFromNetBegin[0] = 0; + metaFromNetEnd[0] = 0; + metaFromNetStatus[0] = 0; + + stackWorker.onmessage = connect("proxy", proxyShared, toShared); + stackWorker.postMessage({type: "init", buf: proxyShared, toBuf: toShared, fromBuf: fromShared, imageAddr: imageAddr, mounterWasmURL: mounterWasmURL, metaFromBuf: metaFromShared}); + return { + toNet: toShared, + fromNet: fromShared, + metaFromNet: metaFromShared + }; +} + +function connect(name, shared, toNet) { + var streamCtrl = new Int32Array(shared, 0, 1); + var streamStatus = new Int32Array(shared, 4, 1); + var streamLen = new Int32Array(shared, 8, 1); + var streamData = new Uint8Array(shared, 12); + var httpConnections = []; + var curID = 0; + var maxID = 0x7FFFFFFF; // storable in streamStatus(signed 32bits) + function getID() { + var startID = curID; + while (true) { + if (httpConnections[curID] == undefined) { + return curID; + } + if (curID >= maxID) { + curID = 0; + } else { + curID++; + } + if (curID == startID) { + return -1; // exhausted + } + } + return curID; + } + function serveData(data, len) { + if (streamData.byteLength < data.byteLength) + console.log("small buffer(net)" + streamData.byteLength + " " + data.byteLength); + var length = len; + if (length > streamData.byteLength) + length = streamData.byteLength; + if (length > data.byteLength) + length = data.byteLength + var buf = data.subarray(0, length); + var remain = data.subarray(length, data.byteLength); + streamLen[0] = buf.byteLength; + streamData.set(buf, 0); + return remain; + } + function serveDataOffset(data, off, len) { + if (streamData.byteLength < data.byteLength) + console.log("small buffer(net)" + streamData.byteLength + " " + data.byteLength); + var length = len; + if (length > streamData.byteLength) + length = streamData.byteLength; + if (off + length > data.byteLength) + length = data.byteLength - off + var buf = data.subarray(off, off + length); + streamLen[0] = buf.byteLength; + streamData.set(buf, 0); + return; + } + var timeoutHandler = null; + var timeoutDeadlineMilli = null; + let toNetCtrl = new Int32Array(toNet, 0, 1); + let toNetBegin = new Int32Array(toNet, 4, 1); + let toNetEnd = new Int32Array(toNet, 8, 1); + let toNetNotify = new Int32Array(toNet, 12, 1); + let toNetData = new Uint8Array(toNet, 16); + function isBufReadable() { + let begin = toNetBegin[0]; //inclusive + let end = toNetEnd[0]; //exclusive + var len; + var round; + if (end >= begin) { + len = end - begin; + round = 0; + } else { + len = toNetData.byteLength - begin; + round = end; + } + if ((len + round) > 0) { + // ready + return true; + } else { + // not ready + return false; + } + } + return function(msg){ + const req_ = msg.data; + if (typeof req_ == "object" && req_.type) { + switch (req_.type) { + case "recv-is-readable-cancel": + if (timeoutHandler) { + clearTimeout(timeoutHandler); + timeoutHandler = null; + } else { + Atomics.store(streamCtrl, 0, 1); + Atomics.notify(streamCtrl, 0); + } + break; + case "recv-is-readable": + if (isBufReadable()) { + streamStatus[0] = 1; // ready for reading + Atomics.store(toNetNotify, 0, 1); + Atomics.notify(toNetNotify, 0); + } else { + if ((req_.timeout != undefined) && (req_.timeout > 0)) { + if (timeoutHandler) { + console.log("ONGOING POLL (stack)"); + clearTimeout(timeoutHandler); + timeoutHandler = null; + timeoutDeadlineMilli = null; + } + timeoutDeadlineMilli = Date.now() + req_.timeout * 1000; + function pollBuf() { + timeoutHandler = setInterval(() => { + if (isBufReadable()) { + streamStatus[0] = 1; // ready for reading + Atomics.store(toNetNotify, 0, 1); + } else { + if (Date.now() < timeoutDeadlineMilli) { + return; // try again + } + streamStatus[0] = 0; // timeout + Atomics.store(toNetNotify, 0, -1); + } + Atomics.notify(toNetNotify, 0); + clearTimeout(timeoutHandler); + timeoutHandler = null; + // streamStatus[0] = 0; + Atomics.store(streamCtrl, 0, 1); + Atomics.notify(streamCtrl, 0); + }, 0.01); + } + pollBuf(); + return; + } + streamStatus[0] = 0; // timeout + Atomics.store(toNetNotify, 0, -1); + Atomics.notify(toNetNotify, 0); + } + break; + case "http_send": + var reqObj = JSON.parse(new TextDecoder().decode(req_.req)); + reqObj.mode = "cors"; + reqObj.credentials = "omit"; + if (reqObj.headers && reqObj.headers["User-Agent"] != "") { + delete reqObj.headers["User-Agent"]; // Browser will add its own value. + } + var reqID = getID(); + if (reqID < 0) { + console.log(name + ":" + "failed to get id"); + streamStatus[0] = -1; + break; + } + var connObj = { + address: new TextDecoder().decode(req_.address), + request: reqObj, + requestSent: false, + reqBodybuf: new Uint8Array(0), + reqBodyEOF: false, + + response: null, + done: null, + respBodybuf: null, + }; + httpConnections[reqID] = connObj; + streamStatus[0] = reqID; + break; + case "http_writebody": + httpConnections[req_.id].reqBodybuf = appendData(httpConnections[req_.id].reqBodybuf, req_.body) + httpConnections[req_.id].reqBodyEOF = req_.isEOF; + streamStatus[0] = 0; + if (req_.isEOF && !httpConnections[req_.id].requestSent) { + httpConnections[req_.id].requestSent = true; + var connObj = httpConnections[req_.id]; + if ((connObj.request.method != "HEAD") && (connObj.request.method != "GET")) { + connObj.request.body = connObj.reqBodybuf; + } + fetch(connObj.address, connObj.request).then((resp) => { + var headers = {}; + for (const key of resp.headers.keys()) { + headers[key] = resp.headers.get(key); + } + connObj.response = new TextEncoder().encode(JSON.stringify({ + bodyUsed: resp.bodyUsed, + headers: headers, + redirected: resp.redirected, + status: resp.status, + statusText: resp.statusText, + type: resp.type, + url: resp.url + })), + connObj.done = false; + connObj.respBodybuf = new Uint8Array(0); + if (resp.ok) { + resp.arrayBuffer().then((data) => { + connObj.respBodybuf = new Uint8Array(data); + connObj.done = true; + }).catch((error) => { + connObj.respBodybuf = new Uint8Array(0); + connObj.done = true; + console.log("failed to fetch body: " + error); + }); + } else { + connObj.done = true; + } + }).catch((error) => { + connObj.response = new TextEncoder().encode(JSON.stringify({ + status: 503, + statusText: "Service Unavailable", + })) + connObj.respBodybuf = new Uint8Array(0); + connObj.done = true; + }); + } + break; + case "http_isreadable": + if ((httpConnections[req_.id] != undefined) && (httpConnections[req_.id].response != null)) { + streamData[0] = 1; // ready for reading + } else { + streamData[0] = 0; // nothing to read + } + streamStatus[0] = 0; + break; + case "http_recv": + if ((httpConnections[req_.id] == undefined) || (httpConnections[req_.id].response == null)) { + console.log(name + ":" + "response is not available"); + streamStatus[0] = -1; + break; + } + httpConnections[req_.id].response = serveData(httpConnections[req_.id].response, req_.len); + streamStatus[0] = 0; + if (httpConnections[req_.id].response.byteLength == 0) { + streamStatus[0] = 1; // isEOF + } + break; + case "http_readbody": + if ((httpConnections[req_.id] == undefined) || (httpConnections[req_.id].response == null)) { + console.log(name + ":" + "response body is not available"); + streamStatus[0] = -1; + break; + } + httpConnections[req_.id].respBodybuf = serveData(httpConnections[req_.id].respBodybuf, req_.len); + streamStatus[0] = 0; + if ((httpConnections[req_.id].done) && (httpConnections[req_.id].respBodybuf.byteLength == 0)) { + streamStatus[0] = 1; + delete httpConnections[req_.id]; // connection done + } + break; + case "layer_request": + var reqObj = { + method: "GET", + }; + reqObj.mode = "cors"; + reqObj.credentials = "omit"; + if (reqObj.headers && reqObj.headers["User-Agent"] != "") { + delete reqObj.headers["User-Agent"]; // Browser will add its own value. + } + var reqID = getID(); + if (reqID < 0) { + console.log(name + ":" + "failed to get id"); + streamStatus[0] = -1; + break; + } + var digest = new TextDecoder().decode(req_.digest); + var connObj = { + address: new TextDecoder().decode(req_.address), + request: reqObj, + requestSent: false, + reqBodybuf: new Uint8Array(0), + reqBodyEOF: false, + + response: null, + done: null, + respBodybuf: null, + }; + httpConnections[reqID] = connObj; + httpConnections[reqID].reqBodyEOF = 1; + streamStatus[0] = 0; + httpConnections[reqID].requestSent = true; + connObj = httpConnections[reqID]; + fetch(connObj.address, connObj.request).then((resp) => { + var headers = {}; + for (const key of resp.headers.keys()) { + headers[key] = resp.headers.get(key); + } + connObj.response = new TextEncoder().encode(JSON.stringify({ + bodyUsed: resp.bodyUsed, + headers: headers, + redirected: resp.redirected, + status: resp.status, + statusText: resp.statusText, + type: resp.type, + url: resp.url + })), + connObj.done = false; + connObj.respBodybuf = new Uint8Array(0); + if (resp.ok) { + resp.arrayBuffer().then((data) => { + crypto.subtle.digest("SHA-256", data).then((hash) => { + const hashArray = Array.from(new Uint8Array(hash)); + const hashHex = hashArray + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); // convert bytes to hex string + if (hashHex != digest) { + // TODO: return error + connObj.respBodybuf = new Uint8Array(0); + connObj.done = true; + console.log("failed to fetch layer: " + error); + return; + } + if (req_.isGzipN == 0) { + connObj.respBodybuf = new Uint8Array(data); + connObj.done = true; + } else { + const ds = new DecompressionStream("gzip") + new Response(new Blob([data]).stream().pipeThrough(ds))['arrayBuffer']().then((data) => { + connObj.respBodybuf = new Uint8Array(data); + connObj.done = true; + }) + } + }); + }) + } else { + connObj.done = true; + } + }).catch((error) => { + connObj.response = new TextEncoder().encode(JSON.stringify({ + status: 503, + statusText: "Service Unavailable", + })) + connObj.respBodybuf = new Uint8Array(0); + connObj.done = true; + }); + streamStatus[0] = reqID; + break; + case "layer_isreadable": + if ((httpConnections[req_.id] != undefined) && (httpConnections[req_.id].response != null) && (httpConnections[req_.id].done)) { + streamData[0] = 1; // ready for reading + streamStatus[0] = httpConnections[req_.id].respBodybuf.byteLength; + + } else { + streamData[0] = 0; // nothing to read + streamStatus[0] = 0; + } + break; + case "layer_readat": + if ((httpConnections[req_.id] == undefined) || (httpConnections[req_.id].response == null) && (httpConnections[req_.id].done)) { + console.log(name + ":" + "response body is not available"); + streamStatus[0] = -1; + break; + } + serveDataOffset(httpConnections[req_.id].respBodybuf, req_.offset, req_.len); + streamStatus[0] = 0; + break; + default: + console.log(name + ":" + "unknown request: " + req_.type) + return; + } + Atomics.store(streamCtrl, 0, 1); + Atomics.notify(streamCtrl, 0); + } else { + console.log("UNKNOWN MSG " + msg); + } + } +} + +function appendData(data1, data2) { + let buf2 = new Uint8Array(data1.byteLength + data2.byteLength); + buf2.set(new Uint8Array(data1), 0); + buf2.set(new Uint8Array(data2), data1.byteLength); + return buf2; +} diff --git a/extras/runcontainerjs/src/web/stack-worker.js b/extras/runcontainerjs/src/web/stack-worker.js new file mode 100644 index 0000000..7eb91c8 --- /dev/null +++ b/extras/runcontainerjs/src/web/stack-worker.js @@ -0,0 +1,579 @@ +import { WASI } from "@bjorn3/browser_wasi_shim"; +import * as wasitype from "@bjorn3/browser_wasi_shim"; +import { Event, EventType, Subscription, SubscriptionClock, SubscriptionFdReadWrite, SubscriptionU, wasiHackSocket } from './wasi-util'; + +onmessage = (msg) => { + var info = serveIfInitMsg(msg); + if (info == null) { + return; + } + var fds = [ + undefined, // 0: stdin + undefined, // 1: stdout + undefined, // 2: stderr + undefined, // 3: receive certificates + undefined, // 4: socket listenfd + undefined, // 5: accepted socket fd (multi-connection is unsupported) + // 6...: used by wasi shim + ]; + var certfd = 3; + var listenfd = 4; + var args = ['arg0', '--certfd='+certfd, '--net-listenfd='+listenfd, '--image-addr='+info.imageAddr]; + var env = []; + var wasi = new WASI(args, env, fds); + wasiHack(wasi, certfd, 5); + wasiHackSocket(wasi, listenfd, 5, sockAccept, sockSend, sockRecv); + fetch(info.mounterWasmURL).then((resp) => { + resp['blob']().then((blob) => { + const ds = new DecompressionStream("gzip") + new Response(blob.stream().pipeThrough(ds))['arrayBuffer']().then((wasm) => { + WebAssembly.instantiate(wasm, { + "wasi_snapshot_preview1": wasi.wasiImport, + "env": envHack(wasi), + }).then((inst) => { + wasi.start(inst.instance); + }); + + }); + }); + }); +}; + +// definition from wasi-libc https://github.com/WebAssembly/wasi-libc/blob/wasi-sdk-19/expected/wasm32-wasi/predefined-macros.txt +const ERRNO_INVAL = 28; +const ERRNO_AGAIN= 6; + +function wasiHack(wasi, certfd, connfd) { + var certbuf = new Uint8Array(0); + var _fd_close = wasi.wasiImport.fd_close; + wasi.wasiImport.fd_close = (fd) => { + if (fd == certfd) { + sendCert(certbuf); + return 0; + } + return _fd_close.apply(wasi.wasiImport, [fd]); + } + var _fd_fdstat_get = wasi.wasiImport.fd_fdstat_get; + wasi.wasiImport.fd_fdstat_get = (fd, fdstat_ptr) => { + if (fd == certfd) { + return 0; + } + return _fd_fdstat_get.apply(wasi.wasiImport, [fd, fdstat_ptr]); + } + wasi.wasiImport.fd_fdstat_set_flags = (fd, fdflags) => { + // TODO + return 0; + } + var _fd_write = wasi.wasiImport.fd_write; + wasi.wasiImport.fd_write = (fd, iovs_ptr, iovs_len, nwritten_ptr) => { + if ((fd == 1) || (fd == 2) || (fd == certfd)) { + var buffer = new DataView(wasi.inst.exports.memory.buffer); + var buffer8 = new Uint8Array(wasi.inst.exports.memory.buffer); + var iovecs = wasitype.wasi.Ciovec.read_bytes_array(buffer, iovs_ptr, iovs_len); + var wtotal = 0 + for (var i = 0; i < iovecs.length; i++) { + var iovec = iovecs[i]; + var buf = buffer8.slice(iovec.buf, iovec.buf + iovec.buf_len); + if (buf.length == 0) { + continue; + } + console.log(new TextDecoder().decode(buf)); + if (fd == certfd) { + certbuf = appendData(certbuf, buf); + } + wtotal += buf.length; + } + buffer.setUint32(nwritten_ptr, wtotal, true); + return 0; + } + console.log("fd_write: unknown fd " + fd); + return _fd_write.apply(wasi.wasiImport, [fd, iovs_ptr, iovs_len, nwritten_ptr]); + } + wasi.wasiImport.poll_oneoff = (in_ptr, out_ptr, nsubscriptions, nevents_ptr) => { + if (nsubscriptions == 0) { + return ERRNO_INVAL; + } + let buffer = new DataView(wasi.inst.exports.memory.buffer); + let in_ = Subscription.read_bytes_array(buffer, in_ptr, nsubscriptions); + let isReadPollStdin = false; + let isReadPollConn = false; + let isClockPoll = false; + let pollSubStdin; + let pollSubConn; + let clockSub; + let timeout = Number.MAX_VALUE; + for (let sub of in_) { + if (sub.u.tag.variant == "fd_read") { + if ((sub.u.data.fd != 0) && (sub.u.data.fd != connfd)) { + return ERRNO_INVAL; // only fd=0 and connfd is supported as of now (FIXME) + } + if (sub.u.data.fd == 0) { + isReadPollStdin = true; + pollSubStdin = sub; + } else { + isReadPollConn = true; + pollSubConn = sub; + } + } else if (sub.u.tag.variant == "clock") { + if (sub.u.data.timeout < timeout) { + timeout = sub.u.data.timeout + isClockPoll = true; + clockSub = sub; + } + } else { + return ERRNO_INVAL; // FIXME + } + } + if (!isClockPoll) { + timeout = 0; + } + let events = []; + if (isReadPollStdin || isReadPollConn || isClockPoll) { + var sockreadable = sockWaitForReadable(timeout / 1000000000); + if (isReadPollConn) { + if (sockreadable == errStatus) { + return ERRNO_INVAL; + } else if (sockreadable == true) { + let event = new Event(); + event.userdata = pollSubConn.userdata; + event.error = 0; + event.type = new EventType("fd_read"); + events.push(event); + } + } + if (isClockPoll) { + let event = new Event(); + event.userdata = clockSub.userdata; + event.error = 0; + event.type = new EventType("clock"); + events.push(event); + } + } + var len = events.length; + Event.write_bytes_array(buffer, out_ptr, events); + buffer.setUint32(nevents_ptr, len, true); + return 0; + } +} + +function envHack(wasi){ + return { + http_send: function(addressP, addresslen, reqP, reqlen, idP){ + var buffer = new DataView(wasi.inst.exports.memory.buffer); + var address = new Uint8Array(wasi.inst.exports.memory.buffer, addressP, addresslen); + var req = new Uint8Array(wasi.inst.exports.memory.buffer, reqP, reqlen); + streamCtrl[0] = 0; + postMessage({ + type: "http_send", + address: address.slice(0, address.length), + req: req.slice(0, req.length), + }); + Atomics.wait(streamCtrl, 0, 0); + if (streamStatus[0] < 0) { + return ERRNO_INVAL; + } + var id = streamStatus[0]; + buffer.setUint32(idP, id, true); + return 0; + }, + http_writebody: function(id, bodyP, bodylen, nwrittenP, isEOF){ + var buffer = new DataView(wasi.inst.exports.memory.buffer); + var body = new Uint8Array(wasi.inst.exports.memory.buffer, bodyP, bodylen); + streamCtrl[0] = 0; + postMessage({ + type: "http_writebody", + id: id, + body: body.slice(0, body.length), + isEOF: isEOF, + }); + Atomics.wait(streamCtrl, 0, 0); + if (streamStatus[0] < 0) { + return ERRNO_INVAL; + } + buffer.setUint32(nwrittenP, bodylen, true); + return 0; + }, + http_isreadable: function(id, isOKP){ + var buffer = new DataView(wasi.inst.exports.memory.buffer); + streamCtrl[0] = 0; + postMessage({type: "http_isreadable", id: id}); + Atomics.wait(streamCtrl, 0, 0); + if (streamStatus[0] < 0) { + return ERRNO_INVAL; + } + var readable = 0; + if (streamData[0] == 1) { + readable = 1; + } + buffer.setUint32(isOKP, readable, true); + return 0; + }, + http_recv: function(id, respP, bufsize, respsizeP, isEOFP){ + var buffer = new DataView(wasi.inst.exports.memory.buffer); + var buffer8 = new Uint8Array(wasi.inst.exports.memory.buffer); + + streamCtrl[0] = 0; + postMessage({type: "http_recv", id: id, len: bufsize}); + Atomics.wait(streamCtrl, 0, 0); + if (streamStatus[0] < 0) { + return ERRNO_INVAL; + } + var ddlen = streamLen[0]; + var resp = streamData.subarray(0, ddlen); + buffer8.set(resp, respP); + buffer.setUint32(respsizeP, ddlen, true); + if (streamStatus[0] == 1) { + buffer.setUint32(isEOFP, 1, true); + } else { + buffer.setUint32(isEOFP, 0, true); + } + return 0; + }, + http_readbody: function(id, bodyP, bufsize, bodysizeP, isEOFP){ + var buffer = new DataView(wasi.inst.exports.memory.buffer); + var buffer8 = new Uint8Array(wasi.inst.exports.memory.buffer); + + streamCtrl[0] = 0; + postMessage({type: "http_readbody", id: id, len: bufsize}); + Atomics.wait(streamCtrl, 0, 0); + if (streamStatus[0] < 0) { + return ERRNO_INVAL; + } + var ddlen = streamLen[0]; + var body = streamData.subarray(0, ddlen); + buffer8.set(body, bodyP); + buffer.setUint32(bodysizeP, ddlen, true); + if (streamStatus[0] == 1) { + buffer.setUint32(isEOFP, 1, true); + } else { + buffer.setUint32(isEOFP, 0, true); + } + return 0; + }, + layer_request: function (addressP, addresslen, digestP, digestlen, isGzipN, idP) { + var buffer = new DataView(wasi.inst.exports.memory.buffer); + var address = new Uint8Array(wasi.inst.exports.memory.buffer, addressP, addresslen); + var digest = new Uint8Array(wasi.inst.exports.memory.buffer, digestP, digestlen); + streamCtrl[0] = 0; + postMessage({ + type: "layer_request", + address: address.slice(0, address.length), + digest: digest, + isGzipN: isGzipN, + }); + Atomics.wait(streamCtrl, 0, 0); + if (streamStatus[0] < 0) { + return ERRNO_INVAL; + } + var id = streamStatus[0]; + buffer.setUint32(idP, id, true); + return 0; + }, + layer_isreadable: function(id, isOKP, sizeP) { + var buffer = new DataView(wasi.inst.exports.memory.buffer); + streamCtrl[0] = 0; + postMessage({type: "layer_isreadable", id: id}); + Atomics.wait(streamCtrl, 0, 0); + if (streamStatus[0] < 0) { + return ERRNO_INVAL; + } + var readable = 0; + if (streamData[0] == 1) { + readable = 1; + } + buffer.setUint32(isOKP, readable, true); + var size = streamStatus[0]; + buffer.setUint32(sizeP, size, true); + return 0; + }, + layer_readat: function(id, respP, offset, len, respsizeP) { + var buffer = new DataView(wasi.inst.exports.memory.buffer); + var buffer8 = new Uint8Array(wasi.inst.exports.memory.buffer); + + streamCtrl[0] = 0; + postMessage({ + type: "layer_readat", + id: id, + offset: offset, + len: len, + }); + Atomics.wait(streamCtrl, 0, 0); + if (streamStatus[0] < 0) { + return ERRNO_INVAL; + } + var ddlen = streamLen[0]; + if (ddlen > len) { + ddlen = len + } + var body = streamData.subarray(0, ddlen); + buffer8.set(body, respP); + buffer.setUint32(respsizeP, ddlen, true); + return 0; + }, + }; +} + +var streamCtrl; +var streamStatus; +var streamLen; +var streamData; +function registerSocketBuffer(shared){ + streamCtrl = new Int32Array(shared, 0, 1); + streamStatus = new Int32Array(shared, 4, 1); + streamLen = new Int32Array(shared, 8, 1); + streamData = new Uint8Array(shared, 12); +} + +var toNetCtrl; +var toNetBegin; +var toNetEnd; +var toNetNotify; +var toNetData; +var fromNetCtrl; +var fromNetBegin; +var fromNetEnd; +var fromNetData; +function registerConnBuffer(to, from) { + toNetCtrl = new Int32Array(to, 0, 1); + toNetBegin = new Int32Array(to, 4, 1); + toNetEnd = new Int32Array(to, 8, 1); + toNetNotify = new Int32Array(to, 12, 1); + toNetData = new Uint8Array(to, 16); + fromNetCtrl = new Int32Array(from, 0, 1); + fromNetBegin = new Int32Array(from, 4, 1); + fromNetEnd = new Int32Array(from, 8, 1); + fromNetData = new Uint8Array(from, 12); +} + +var metaFromNetCtrl; +var metaFromNetBegin; +var metaFromNetEnd; +var metaFromNetStatus; +var metaFromNetData; +function registerMetaBuffer(meta) { + metaFromNetCtrl = new Int32Array(meta, 0, 1); + metaFromNetBegin = new Int32Array(meta, 4, 1); + metaFromNetEnd = new Int32Array(meta, 8, 1); + metaFromNetStatus = new Int32Array(meta, 12, 1); + metaFromNetData = new Uint8Array(meta, 16); +} + +function serveIfInitMsg(msg) { + const req_ = msg.data; + if (typeof req_ == "object"){ + if (req_.type == "init") { + if (req_.buf) { + var shared = req_.buf; + registerSocketBuffer(shared); + registerConnBuffer(req_.toBuf, req_.fromBuf); + registerMetaBuffer(req_.metaFromBuf); + } + return { + imageAddr: req_.imageAddr, + mounterWasmURL: req_.mounterWasmURL + }; + } + } + + return null; +} + +const errStatus = { + val: 0, +}; + +var accepted = false; +function sockAccept(){ + accepted = true; + return true; +} + +function sockSend(data){ + if (!accepted) { + return -1; + } + + for(;;) { + if (Atomics.compareExchange(fromNetCtrl, 0, 0, 1) == 0) { + break; + } + Atomics.wait(fromNetCtrl, 0, 1); + } + let begin = fromNetBegin[0]; //inclusive + let end = fromNetEnd[0]; //exclusive + var len; + var round; + if (end >= begin) { + len = fromNetData.byteLength - end; + round = begin; + } else { + len = begin - end; + round = 0; + } + if ((len + round) < data.length) { + // buffer is full; drop packets + // TODO: preserve this + console.log("FIXME: buffer full; dropping packets"); + } else { + if (len > 0) { + if (len > data.length) { + len = data.length + } + fromNetData.set(data.subarray(0, len), end); + fromNetEnd[0] = end + len; + } + if ((round > 0) && (data.length > len)) { + if (round > data.length - len) { + round = data.length - len + } + fromNetData.set(data.subarray(len, len + round), 0); + fromNetEnd[0] = round; + } + } + if (Atomics.compareExchange(fromNetCtrl, 0, 1, 0) != 1) { + console.log("UNEXPECTED STATUS"); + } + Atomics.notify(fromNetCtrl, 0, 1); + return 0; +} + +function sockRecv(targetBuf, targetOffset, targetLen){ + if (!accepted) { + return -1; + } + + for(;;) { + if (Atomics.compareExchange(toNetCtrl, 0, 0, 1) == 0) { + break; + } + Atomics.wait(toNetCtrl, 0, 1); + } + let begin = toNetBegin[0]; //inclusive + let end = toNetEnd[0]; //exclusive + var len; + var round; + if (end >= begin) { + len = end - begin; + round = 0; + } else { + len = toNetData.byteLength - begin; + round = end; + } + if (targetLen < len) { + len = targetLen; + round = 0; + } else if (targetLen < len + round) { + round = targetLen - len; + } + if (len > 0) { + targetBuf.set(toNetData.subarray(begin, begin + len), targetOffset); + toNetBegin[0] = begin + len; + } + if (round > 0) { + targetBuf.set(toNetData.subarray(0, round), targetOffset + len); + toNetBegin[0] = round; + } + if (Atomics.compareExchange(toNetCtrl, 0, 1, 0) != 1) { + console.log("UNEXPECTED STATUS"); + } + Atomics.notify(toNetCtrl, 0, 1); + + return (len + round); +} + +function sockWaitForReadable(timeout){ + if (!accepted) { + errStatus.val = -1; + return errStatus; + } + + for(;;) { + if (Atomics.compareExchange(toNetCtrl, 0, 0, 1) == 0) { + break; + } + Atomics.wait(toNetCtrl, 0, 1); + } + let begin = toNetBegin[0]; //inclusive + let end = toNetEnd[0]; //exclusive + var len; + var round; + if (end >= begin) { + len = end - begin; + round = 0; + } else { + len = toNetData.byteLength - begin; + round = end; + } + var ready; + if ((len + round) > 0) { + ready = true; + } else { + ready = false; + } + if (Atomics.compareExchange(toNetCtrl, 0, 1, 0) != 1) { + console.log("UNEXPECTED STATUS"); + } + Atomics.notify(toNetCtrl, 0, 1); + + if (ready) { + return true; + } else if (timeout == 0) { + return false; + } + + // buffer not ready; wait for readable. + streamCtrl[0] = 0; + Atomics.store(toNetNotify, 0, 0); + postMessage({type: "recv-is-readable", timeout: timeout}); + Atomics.wait(toNetNotify, 0, 0); + var res = Atomics.load(toNetNotify, 0); + + postMessage({type: "recv-is-readable-cancel"}); + Atomics.wait(streamCtrl, 0, 0); + + Atomics.store(toNetNotify, 0, 0); + return res == 1; +} + +function sendCert(data){ + var curOff = 0; + var done = false; + for(;;) { + for(;;) { + if (Atomics.compareExchange(metaFromNetCtrl, 0, 0, 1) == 0) { + break; + } + Atomics.wait(metaFromNetCtrl, 0, 1); + } + let end = metaFromNetEnd[0]; //exclusive + if (end == 0) { + var len = metaFromNetData.length; + var remain = data.byteLength - curOff; + if (remain == 0) { + metaFromNetStatus[0] = 1; // done + done = true; + } else { + if (remain < len) { + len = remain; + } + metaFromNetData.set(data.subarray(curOff, len), 0); + metaFromNetEnd[0] = len; + curOff += len; + } + } + if (Atomics.compareExchange(metaFromNetCtrl, 0, 1, 0) != 1) { + console.log("UNEXPECTED STATUS"); + } + Atomics.notify(metaFromNetCtrl, 0, 1); + if (done) { + break; + } + } +} + +function appendData(data1, data2) { + let buf2 = new Uint8Array(data1.byteLength + data2.byteLength); + buf2.set(new Uint8Array(data1), 0); + buf2.set(new Uint8Array(data2), data1.byteLength); + return buf2; +} diff --git a/extras/runcontainerjs/src/web/wasi-util.js b/extras/runcontainerjs/src/web/wasi-util.js new file mode 100644 index 0000000..00f675e --- /dev/null +++ b/extras/runcontainerjs/src/web/wasi-util.js @@ -0,0 +1,242 @@ +import * as wasitype from "@bjorn3/browser_wasi_shim"; + +//////////////////////////////////////////////////////////// +// +// event-related classes adopted from the on-going discussion +// towards poll_oneoff support in browser_wasi_sim project. +// Ref: https://github.com/bjorn3/browser_wasi_shim/issues/14#issuecomment-1450351935 +// +//////////////////////////////////////////////////////////// + +export class EventType { + /*:: variant: "clock" | "fd_read" | "fd_write"*/ + + constructor(variant/*: "clock" | "fd_read" | "fd_write"*/) { + this.variant = variant; + } + + static from_u8(data/*: number*/)/*: EventType*/ { + switch (data) { + case wasitype.wasi.EVENTTYPE_CLOCK: + return new EventType("clock"); + case wasitype.wasi.EVENTTYPE_FD_READ: + return new EventType("fd_read"); + case wasitype.wasi.EVENTTYPE_FD_WRITE: + return new EventType("fd_write"); + default: + throw "Invalid event type " + String(data); + } + } + + to_u8()/*: number*/ { + switch (this.variant) { + case "clock": + return wasitype.wasi.EVENTTYPE_CLOCK; + case "fd_read": + return wasitype.wasi.EVENTTYPE_FD_READ; + case "fd_write": + return wasitype.wasi.EVENTTYPE_FD_WRITE; + default: + throw "unreachable"; + } + } +} + +export class Event { + /*:: userdata: UserData*/ + /*:: error: number*/ + /*:: type: EventType*/ + /*:: fd_readwrite: EventFdReadWrite | null*/ + + write_bytes(view/*: DataView*/, ptr/*: number*/) { + view.setBigUint64(ptr, this.userdata, true); + view.setUint8(ptr + 8, this.error); + view.setUint8(ptr + 9, 0); + view.setUint8(ptr + 10, this.type.to_u8()); + // if (this.fd_readwrite) { + // this.fd_readwrite.write_bytes(view, ptr + 16); + // } + } + + static write_bytes_array(view/*: DataView*/, ptr/*: number*/, events/*: Array*/) { + for (let i = 0; i < events.length; i++) { + events[i].write_bytes(view, ptr + 32 * i); + } + } +} + +export class SubscriptionClock { + /*:: timeout: number*/ + + static read_bytes(view/*: DataView*/, ptr/*: number*/)/*: SubscriptionFdReadWrite*/ { + let self = new SubscriptionClock(); + self.timeout = Number(view.getBigUint64(ptr + 8, true)); + return self; + } +} + +export class SubscriptionFdReadWrite { + /*:: fd: number*/ + + static read_bytes(view/*: DataView*/, ptr/*: number*/)/*: SubscriptionFdReadWrite*/ { + let self = new SubscriptionFdReadWrite(); + self.fd = view.getUint32(ptr, true); + return self; + } +} + +export class SubscriptionU { + /*:: tag: EventType */ + /*:: data: SubscriptionClock | SubscriptionFdReadWrite */ + + static read_bytes(view/*: DataView*/, ptr/*: number*/)/*: SubscriptionU*/ { + let self = new SubscriptionU(); + self.tag = EventType.from_u8(view.getUint8(ptr)); + switch (self.tag.variant) { + case "clock": + self.data = SubscriptionClock.read_bytes(view, ptr + 8); + break; + case "fd_read": + case "fd_write": + self.data = SubscriptionFdReadWrite.read_bytes(view, ptr + 8); + break; + default: + throw "unreachable"; + } + return self; + } +} + +export class Subscription { + /*:: userdata: UserData */ + /*:: u: SubscriptionU */ + + static read_bytes(view/*: DataView*/, ptr/*: number*/)/*: Subscription*/ { + let subscription = new Subscription(); + subscription.userdata = view.getBigUint64(ptr, true); + subscription.u = SubscriptionU.read_bytes(view, ptr + 8); + return subscription; + } + + static read_bytes_array(view/*: DataView*/, ptr/*: number*/, len/*: number*/)/*: Array*/ { + let subscriptions = []; + for (let i = 0; i < len; i++) { + subscriptions.push(Subscription.read_bytes(view, ptr + 48 * i)); + } + return subscriptions; + } +} + +export function wasiHackSocket(wasi, listenfd, connfd, sockAccept, sockSend, sockRecv) { + // definition from wasi-libc https://github.com/WebAssembly/wasi-libc/blob/wasi-sdk-19/expected/wasm32-wasi/predefined-macros.txt + const ERRNO_INVAL = 28; + const ERRNO_AGAIN= 6; + var connfdUsed = false; + var connbuf = new Uint8Array(0); + var _fd_close = wasi.wasiImport.fd_close; + wasi.wasiImport.fd_close = (fd) => { + if (fd == connfd) { + connfdUsed = false; + return 0; + } + return _fd_close.apply(wasi.wasiImport, [fd]); + } + var _fd_read = wasi.wasiImport.fd_read; + wasi.wasiImport.fd_read = (fd, iovs_ptr, iovs_len, nread_ptr) => { + if (fd == connfd) { + return wasi.wasiImport.sock_recv(fd, iovs_ptr, iovs_len, 0, nread_ptr, 0); + } + return _fd_read.apply(wasi.wasiImport, [fd, iovs_ptr, iovs_len, nread_ptr]); + } + var _fd_write = wasi.wasiImport.fd_write; + wasi.wasiImport.fd_write = (fd, iovs_ptr, iovs_len, nwritten_ptr) => { + if (fd == connfd) { + return wasi.wasiImport.sock_send(fd, iovs_ptr, iovs_len, 0, nwritten_ptr); + } + return _fd_write.apply(wasi.wasiImport, [fd, iovs_ptr, iovs_len, nwritten_ptr]); + } + var _fd_fdstat_get = wasi.wasiImport.fd_fdstat_get; + wasi.wasiImport.fd_fdstat_get = (fd, fdstat_ptr) => { + if ((fd == listenfd) || (fd == connfd) && connfdUsed){ + let buffer = new DataView(wasi.inst.exports.memory.buffer); + // https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fdstat-struct + buffer.setUint8(fdstat_ptr, 6); // filetype = 6 (socket_stream) + buffer.setUint8(fdstat_ptr + 1, 2); // fdflags = 2 (nonblock) + return 0; + } + return _fd_fdstat_get.apply(wasi.wasiImport, [fd, fdstat_ptr]); + } + wasi.wasiImport.sock_accept = (fd, flags, fd_ptr) => { + if (fd != listenfd) { + console.log("sock_accept: unknown fd " + fd); + return ERRNO_INVAL; + } + if (connfdUsed) { + console.log("sock_accept: multi-connection is unsupported"); + return ERRNO_INVAL; + } + if (!sockAccept()) { + return ERRNO_AGAIN; + } + connfdUsed = true; + var buffer = new DataView(wasi.inst.exports.memory.buffer); + buffer.setUint32(fd_ptr, connfd, true); + return 0; + } + wasi.wasiImport.sock_send = (fd, iovs_ptr, iovs_len, si_flags/*not defined*/, nwritten_ptr) => { + if (fd != connfd) { + console.log("sock_send: unknown fd " + fd); + return ERRNO_INVAL; + } + var buffer = new DataView(wasi.inst.exports.memory.buffer); + var buffer8 = new Uint8Array(wasi.inst.exports.memory.buffer); + var iovecs = wasitype.wasi.Ciovec.read_bytes_array(buffer, iovs_ptr, iovs_len); + var wtotal = 0 + for (var i = 0; i < iovecs.length; i++) { + var iovec = iovecs[i]; + if (iovec.buf_len == 0) { + continue; + } + var ret = sockSend(buffer8.subarray(iovec.buf, iovec.buf + iovec.buf_len)); + if (ret < 0) { + return ERRNO_INVAL; + } + wtotal += iovec.buf_len; + } + buffer.setUint32(nwritten_ptr, wtotal, true); + return 0; + } + wasi.wasiImport.sock_recv = (fd, iovs_ptr, iovs_len, ri_flags, nread_ptr, ro_flags_ptr) => { + if (ri_flags != 0) { + console.log("ri_flags are unsupported"); // TODO + } + if (fd != connfd) { + console.log("sock_recv: unknown fd " + fd); + return ERRNO_INVAL; + } + var buffer = new DataView(wasi.inst.exports.memory.buffer); + var buffer8 = new Uint8Array(wasi.inst.exports.memory.buffer); + var iovecs = wasitype.wasi.Iovec.read_bytes_array(buffer, iovs_ptr, iovs_len); + var nread = 0; + for (var i = 0; i < iovecs.length; i++) { + var iovec = iovecs[i]; + if (iovec.buf_len == 0) { + continue; + } + var retlen = sockRecv(buffer8, iovec.buf, iovec.buf_len); + if ((retlen <= 0) && (i == 0)) { + return ERRNO_AGAIN; + } + nread += retlen; + } + buffer.setUint32(nread_ptr, nread, true); + // TODO: support ro_flags_ptr + return 0; + } + wasi.wasiImport.sock_shutdown = (fd, sdflags) => { + if (fd == connfd) { + connfdUsed = false; + } + return 0; + } +} diff --git a/extras/runcontainerjs/src/web/worker-util.js b/extras/runcontainerjs/src/web/worker-util.js new file mode 100644 index 0000000..6dbad8f --- /dev/null +++ b/extras/runcontainerjs/src/web/worker-util.js @@ -0,0 +1,442 @@ +import { Event, EventType, Subscription, SubscriptionClock, SubscriptionFdReadWrite, SubscriptionU, wasiHackSocket } from './wasi-util'; +import { WASI, PreopenDirectory } from "@bjorn3/browser_wasi_shim"; +import * as wasitype from "@bjorn3/browser_wasi_shim"; + +export function startContainer(info, cargs, ttyClient) { + var connBuf = info.net; + registerConnBuffer(connBuf.toNet, connBuf.fromNet); + registerMetaBuffer(connBuf.metaFromNet); + var args = []; + var env = []; + var fds = []; + var listenfd = 3; + fetch(info.vmImage, { credentials: 'same-origin' }).then((resp) => { + resp['blob']().then((blob) => { + const ds = new DecompressionStream("gzip") + new Response(blob.stream().pipeThrough(ds))['arrayBuffer']().then((wasm) => { + recvCert().then((cert) => { + var certDir = getCertDir(cert); + fds = [ + undefined, // 0: stdin + undefined, // 1: stdout + undefined, // 2: stderr + certDir, // 3: certificates dir + undefined, // 4: socket listenfd + undefined, // 5: accepted socket fd (multi-connection is unsupported) + // 6...: used by wasi shim + ]; + args = ['arg0', '--net=socket=listenfd=4', '--mac', genmac(), '--external-bundle=9p=192.168.127.252']; + args = args.concat(cargs); + env = [ + "SSL_CERT_FILE=/.wasmenv/proxy.crt", + "https_proxy=http://192.168.127.253:80", + "http_proxy=http://192.168.127.253:80", + "HTTPS_PROXY=http://192.168.127.253:80", + "HTTP_PROXY=http://192.168.127.253:80" + ]; + listenfd = 4; + startWasi(wasm, ttyClient, args, env, fds, listenfd, 5); + }); + return; + }) + }) + }); +} + +function startWasi(wasm, ttyClient, args, env, fds, listenfd, connfd) { + var wasi = new WASI(args, env, fds); + wasiHack(wasi, ttyClient, connfd); + wasiHackSocket(wasi, listenfd, connfd, sockAccept, sockSend, sockRecv); + WebAssembly.instantiate(wasm, { + "wasi_snapshot_preview1": wasi.wasiImport, + }).then((inst) => { + wasi.start(inst.instance); + }); +} + +// wasiHack patches wasi object for integrating it to xterm-pty. +function wasiHack(wasi, ttyClient, connfd) { + // definition from wasi-libc https://github.com/WebAssembly/wasi-libc/blob/wasi-sdk-19/expected/wasm32-wasi/predefined-macros.txt + const ERRNO_INVAL = 28; + const ERRNO_AGAIN= 6; + var _fd_read = wasi.wasiImport.fd_read; + wasi.wasiImport.fd_read = (fd, iovs_ptr, iovs_len, nread_ptr) => { + if (fd == 0) { + var buffer = new DataView(wasi.inst.exports.memory.buffer); + var buffer8 = new Uint8Array(wasi.inst.exports.memory.buffer); + var iovecs = wasitype.wasi.Iovec.read_bytes_array(buffer, iovs_ptr, iovs_len); + var nread = 0; + for (var i = 0; i < iovecs.length; i++) { + var iovec = iovecs[i]; + if (iovec.buf_len == 0) { + continue; + } + var data = ttyClient.onRead(iovec.buf_len); + buffer8.set(data, iovec.buf); + nread += data.length; + } + buffer.setUint32(nread_ptr, nread, true); + return 0; + } else { + console.log("fd_read: unknown fd " + fd); + return _fd_read.apply(wasi.wasiImport, [fd, iovs_ptr, iovs_len, nread_ptr]); + } + return ERRNO_INVAL; + } + var _fd_write = wasi.wasiImport.fd_write; + wasi.wasiImport.fd_write = (fd, iovs_ptr, iovs_len, nwritten_ptr) => { + if ((fd == 1) || (fd == 2)) { + var buffer = new DataView(wasi.inst.exports.memory.buffer); + var buffer8 = new Uint8Array(wasi.inst.exports.memory.buffer); + var iovecs = wasitype.wasi.Ciovec.read_bytes_array(buffer, iovs_ptr, iovs_len); + var wtotal = 0 + for (var i = 0; i < iovecs.length; i++) { + var iovec = iovecs[i]; + var buf = buffer8.slice(iovec.buf, iovec.buf + iovec.buf_len); + if (buf.length == 0) { + continue; + } + ttyClient.onWrite(Array.from(buf)); + wtotal += buf.length; + } + buffer.setUint32(nwritten_ptr, wtotal, true); + return 0; + } else { + console.log("fd_write: unknown fd " + fd); + return _fd_write.apply(wasi.wasiImport, [fd, iovs_ptr, iovs_len, nwritten_ptr]); + } + return ERRNO_INVAL; + } + wasi.wasiImport.poll_oneoff = (in_ptr, out_ptr, nsubscriptions, nevents_ptr) => { + if (nsubscriptions == 0) { + return ERRNO_INVAL; + } + let buffer = new DataView(wasi.inst.exports.memory.buffer); + let in_ = Subscription.read_bytes_array(buffer, in_ptr, nsubscriptions); + let isReadPollStdin = false; + let isReadPollConn = false; + let isClockPoll = false; + let pollSubStdin; + let pollSubConn; + let clockSub; + let timeout = Number.MAX_VALUE; + for (let sub of in_) { + if (sub.u.tag.variant == "fd_read") { + if ((sub.u.data.fd != 0) && (sub.u.data.fd != connfd)) { + console.log("poll_oneoff: unknown fd " + sub.u.data.fd); + return ERRNO_INVAL; // only fd=0 and connfd is supported as of now (FIXME) + } + if (sub.u.data.fd == 0) { + isReadPollStdin = true; + pollSubStdin = sub; + } else { + isReadPollConn = true; + pollSubConn = sub; + } + } else if (sub.u.tag.variant == "clock") { + if (sub.u.data.timeout < timeout) { + timeout = sub.u.data.timeout + isClockPoll = true; + clockSub = sub; + } + } else { + console.log("poll_oneoff: unknown variant " + sub.u.tag.variant); + return ERRNO_INVAL; // FIXME + } + } + if (!isClockPoll) { + timeout = 0; + } + let events = []; + if (isReadPollStdin || isReadPollConn || isClockPoll) { + var readable = false; + if (isReadPollStdin || (isClockPoll && timeout > 0)) { + readable = ttyClient.onWaitForReadable(timeout / 1000000000); + } + if (readable && isReadPollStdin) { + let event = new Event(); + event.userdata = pollSubStdin.userdata; + event.error = 0; + event.type = new EventType("fd_read"); + events.push(event); + } + if (isReadPollConn) { + var sockreadable = sockWaitForReadable(); + if (sockreadable == errStatus) { + return ERRNO_INVAL; + } else if (sockreadable == true) { + let event = new Event(); + event.userdata = pollSubConn.userdata; + event.error = 0; + event.type = new EventType("fd_read"); + events.push(event); + } + } + if (isClockPoll) { + let event = new Event(); + event.userdata = clockSub.userdata; + event.error = 0; + event.type = new EventType("clock"); + events.push(event); + } + } + var len = events.length; + Event.write_bytes_array(buffer, out_ptr, events); + buffer.setUint32(nevents_ptr, len, true); + return 0; + } +} + +function genmac(){ + return "02:XX:XX:XX:XX:XX".replace(/X/g, function() { + return "0123456789ABCDEF".charAt(Math.floor(Math.random() * 16)) + }); +} + +var toNetCtrl; +var toNetBegin; +var toNetEnd; +var toNetNotify; +var toNetData; +var fromNetCtrl; +var fromNetBegin; +var fromNetEnd; +var fromNetData; +function registerConnBuffer(to, from) { + toNetCtrl = new Int32Array(to, 0, 1); + toNetBegin = new Int32Array(to, 4, 1); + toNetEnd = new Int32Array(to, 8, 1); + toNetNotify = new Int32Array(to, 12, 1); + toNetData = new Uint8Array(to, 16); + fromNetCtrl = new Int32Array(from, 0, 1); + fromNetBegin = new Int32Array(from, 4, 1); + fromNetEnd = new Int32Array(from, 8, 1); + fromNetData = new Uint8Array(from, 12); +} + +var metaFromNetCtrl; +var metaFromNetBegin; +var metaFromNetEnd; +var metaFromNetStatus; +var metaFromNetData; +function registerMetaBuffer(meta) { + metaFromNetCtrl = new Int32Array(meta, 0, 1); + metaFromNetBegin = new Int32Array(meta, 4, 1); + metaFromNetEnd = new Int32Array(meta, 8, 1); + metaFromNetStatus = new Int32Array(meta, 12, 1); + metaFromNetData = new Uint8Array(meta, 16); +} + +const errStatus = { + val: 0, +}; + +var accepted = false; +function sockAccept(){ + accepted = true; + return true; +} + +function sockSend(data){ + if (!accepted) { + return -1; + } + + for(;;) { + if (Atomics.compareExchange(toNetCtrl, 0, 0, 1) == 0) { + break; + } + Atomics.wait(toNetCtrl, 0, 1); + } + let begin = toNetBegin[0]; //inclusive + let end = toNetEnd[0]; //exclusive + var len; + var round; + if (end >= begin) { + len = toNetData.byteLength - end; + round = begin; + } else { + len = begin - end; + round = 0; + } + if ((len + round) < data.length) { + // buffer is full; drop packets + // TODO: preserve this + console.log("FIXME: buffer full; dropping packets"); + } else { + if (len > 0) { + if (len > data.length) { + len = data.length + } + toNetData.set(data.subarray(0, len), end); + toNetEnd[0] = end + len; + } + if ((round > 0) && (data.length > len)) { + if (round > data.length - len) { + round = data.length - len + } + toNetData.set(data.subarray(len, len + round), 0); + toNetEnd[0] = round; + } + } + if (Atomics.compareExchange(toNetCtrl, 0, 1, 0) != 1) { + console.log("UNEXPECTED STATUS"); + } + Atomics.notify(toNetCtrl, 0, 1); + + Atomics.store(toNetNotify, 0, 1); + Atomics.notify(toNetNotify, 0); + return 0; +} + +function sockRecv(targetBuf, targetOffset, targetLen){ + if (!accepted) { + return -1; + } + + for(;;) { + if (Atomics.compareExchange(fromNetCtrl, 0, 0, 1) == 0) { + break; + } + Atomics.wait(fromNetCtrl, 0, 1); + } + let begin = fromNetBegin[0]; //inclusive + let end = fromNetEnd[0]; //exclusive + var len; + var round; + if (end >= begin) { + len = end - begin; + round = 0; + } else { + len = fromNetData.byteLength - begin; + round = end; + } + if (targetLen < len) { + len = targetLen; + round = 0; + } else if (targetLen < len + round) { + round = targetLen - len; + } + if (len > 0) { + targetBuf.set(fromNetData.subarray(begin, begin + len), targetOffset); + fromNetBegin[0] = begin + len; + } + if (round > 0) { + targetBuf.set(fromNetData.subarray(0, round), targetOffset + len); + fromNetBegin[0] = round; + } + if (Atomics.compareExchange(fromNetCtrl, 0, 1, 0) != 1) { + console.log("UNEXPECTED STATUS"); + } + Atomics.notify(fromNetCtrl, 0, 1); + + return (len + round); +} + +function sockWaitForReadable(){ + if (!accepted) { + errStatus.val = -1; + return errStatus; + } + + for(;;) { + if (Atomics.compareExchange(fromNetCtrl, 0, 0, 1) == 0) { + break; + } + Atomics.wait(fromNetCtrl, 0, 1); + } + let begin = fromNetBegin[0]; //inclusive + let end = fromNetEnd[0]; //exclusive + var len; + var round; + if (end >= begin) { + len = end - begin; + round = 0; + } else { + len = fromNetData.byteLength - begin; + round = end; + } + var ready; + if ((len + round) > 0) { + ready = true; + } else { + ready = false; + } + if (Atomics.compareExchange(fromNetCtrl, 0, 1, 0) != 1) { + console.log("UNEXPECTED STATUS"); + } + Atomics.notify(fromNetCtrl, 0, 1); + + return ready; +} + +function recvCert(){ + var buf = new Uint8Array(0); + return new Promise((resolve, reject) => { + function getCert(){ + var done = false; + for(;;) { + if (Atomics.compareExchange(metaFromNetCtrl, 0, 0, 1) == 0) { + break; + } + Atomics.wait(metaFromNetCtrl, 0, 1); + } + let end = metaFromNetEnd[0]; //exclusive + if (end > 0) { + buf = appendData(buf, metaFromNetData.slice(0, end)); + metaFromNetEnd[0] = 0; + } + if (metaFromNetStatus[0] == 1) { + done = true; + } + if (Atomics.compareExchange(metaFromNetCtrl, 0, 1, 0) != 1) { + console.log("UNEXPECTED STATUS"); + } + Atomics.notify(metaFromNetCtrl, 0, 1); + + if (done) { + resolve(buf); // EOF + } else { + setTimeout(getCert, 0); + return; + } + } + getCert(); + }); +} + +function appendData(data1, data2) { + var buf2 = new Uint8Array(data1.byteLength + data2.byteLength); + buf2.set(new Uint8Array(data1), 0); + buf2.set(new Uint8Array(data2), data1.byteLength); + return buf2; +} + +function getCertDir(cert) { + var certDir = new PreopenDirectory("/.wasmenv", { + "proxy.crt": new File(cert, {}) + }); + var _path_open = certDir.path_open; + certDir.path_open = (e, r, s, n, a, d) => { + var ret = _path_open.apply(certDir, [e, r, s, n, a, d]); + if (ret.fd_obj != null) { + var o = ret.fd_obj; + ret.fd_obj.fd_pread = (view8, iovs, offset) => { + var old_offset = o.file_pos; + var r = o.fd_seek(offset, WHENCE_SET); + if (r.ret != 0) { + return { ret: -1, nread: 0 }; + } + var read_ret = o.fd_read(view8, iovs); + r = o.fd_seek(old_offset, WHENCE_SET); + if (r.ret != 0) { + return { ret: -1, nread: 0 }; + } + return read_ret; + } + } + return ret; + } + certDir.dir.contents["."] = certDir.dir; + return certDir; +} diff --git a/extras/runcontainerjs/tsconfig.json b/extras/runcontainerjs/tsconfig.json new file mode 100644 index 0000000..309d924 --- /dev/null +++ b/extras/runcontainerjs/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "ES2020", + "sourceMap": true, + "outDir": "dist", + "lib": [ + "ES2020", "WebWorker", + "dom.iterable" + ], + "sourceMap": true, + "rootDir": "src", + "strict": false + } +} diff --git a/extras/runcontainerjs/webpack.config.js b/extras/runcontainerjs/webpack.config.js new file mode 100644 index 0000000..9e8ed80 --- /dev/null +++ b/extras/runcontainerjs/webpack.config.js @@ -0,0 +1,36 @@ +const path = require('path'); + +module.exports = { + mode: 'development', + entry: { + 'runcontainer': './src/web/runcontainer.js', + 'stack-worker': './src/web/stack-worker.js', + 'worker-util': './src/web/worker-util.js', + }, + output: { + path: path.resolve(__dirname, 'dist'), + filename: '[name].js', + library: { + name: 'RunContainer', + type: 'var', + }, + }, + module: { + rules: [ + { + test: /\.ts$/, + exclude: /node_modules/, + use: [{ + loader: 'ts-loader' + }] + }, + ] + }, + resolve: { + mainFields: ['browser', 'module', 'main'], // look for `browser` entry point in imported node modules + extensions: ['.ts', '.js'], // support ts-files and js-files + alias: { + // provides alternate implementation for node module and source files + } + } +} diff --git a/patches/bochs/Bochs/bochs/main.cc b/patches/bochs/Bochs/bochs/main.cc index 01c526c..1b269fa 100644 --- a/patches/bochs/Bochs/bochs/main.cc +++ b/patches/bochs/Bochs/bochs/main.cc @@ -431,6 +431,26 @@ int write_net(FSVirtFile *f, int pos1, const char *mac) return pos - pos1; } +int write_bundle(FSVirtFile *f, int pos1, const char *bundle) +{ + int p, pos = pos1; + + p = write_info(f, pos, 3, "b: "); + if (p < 0) { + return -1; + } + pos += p; + for (int j = 0; j < strlen(bundle); j++) { + if (putchar_info(f, pos++, bundle[j]) != 1) { + return -1; + } + } + if (putchar_info(f, pos++, '\n') != 1) { + return -1; + } + return pos - pos1; +} + int write_time(FSVirtFile *f, int pos1, const char *timestr) { int p, pos = pos1; @@ -457,6 +477,7 @@ static struct option options[] = { { "entrypoint", required_argument }, { "net", required_argument }, { "mac", required_argument }, + { "external-bundle", required_argument }, { NULL }, }; @@ -466,10 +487,11 @@ void print_usage(void) " [COMMAND] [ARG...]: command to run in the container. (default: commands specified in the image config)\n" "\n" "OPTIONS:\n" - " -entrypoint : entrypoint command. (default: entrypoint specified in the image config)\n" - " -no-stdin : disable stdin. (default: false)\n" - " -net : enable networking with the specified mode (default: disabled. supported mode: \"socket\")\n" - " -mac : use a custom mac address for the VM\n" + " -entrypoint : entrypoint command. (default: entrypoint specified in the image config)\n" + " -no-stdin : disable stdin. (default: false)\n" + " -net : enable networking with the specified mode (default: disabled. supported mode: \"socket\")\n" + " -mac : use a custom mac address for the VM\n" + " -external-bundle
: externally mount container bundle\n" "\n" "This tool is based on Bochs emulator.\n" ); @@ -485,7 +507,7 @@ int init_vm(int argc, char **argv, FSVirtFile *info) info->lim = 1024; /* const char *cmdline, *build_preload_file; */ - char *entrypoint = NULL, *net = NULL, *mac = NULL; + char *entrypoint = NULL, *net = NULL, *mac = NULL, *bundle = NULL; bool enable_stdin = true; int pos, c, option_index, i; for(;;) { @@ -510,6 +532,9 @@ int init_vm(int argc, char **argv, FSVirtFile *info) case 4: /* mac */ mac = optarg; break; + case 5: /* external-bundle */ + bundle = optarg; + break; default: fprintf(stderr, "unknown option index: %d\n", option_index); exit(1); @@ -582,6 +607,15 @@ int init_vm(int argc, char **argv, FSVirtFile *info) pos += p; } + if (bundle != NULL) { + int p = write_bundle(info, pos, bundle); + if (p < 0) { + printf("failed to prepare net info\n"); + exit(1); + } + pos += p; + } + char buf[32]; snprintf(buf, sizeof(buf), "%d", (unsigned)time(NULL)); int ps = write_time(info, pos, buf); diff --git a/patches/bochs/Bochs/bochs/wasm.cc b/patches/bochs/Bochs/bochs/wasm.cc index ac37fe8..1f82bba 100644 --- a/patches/bochs/Bochs/bochs/wasm.cc +++ b/patches/bochs/Bochs/bochs/wasm.cc @@ -3028,7 +3028,7 @@ void bx_virtio_net_ctrl_c::rx_timer_handler(void *this_ptr) if (watch_res < 0) { duration = 1000000; } else if (n_ret <= 0) { - duration = 100000; + duration = 5000; } bx_pc_system.activate_timer(class_ptr->timer_id, duration, false); /* not continuous */ } @@ -3046,7 +3046,7 @@ int bx_virtio_net_ctrl_c::device_recv(int queue_idx, int desc_idx, int read_size if (memcpy_from_queue(&h, queue_idx, desc_idx, 0, s1->header_size) < 0) return 0; len = read_size - s1->header_size; - buf = (uint8_t *)malloc(len); + buf = es->alloc_packet(es, len); memcpy_from_queue(buf, queue_idx, desc_idx, s1->header_size, len); es->write_packet(es, buf, len); free(buf); @@ -3075,20 +3075,14 @@ static void socket_write_packet(EthernetDevice *net, const uint8_t *buf, int len) { SocketState *s = (SocketState *)net->opaque; - uint32_t size = htonl(len); + // uint32_t size = htonl(len); int ret; if (s->fd < 0) { return; } - ret = send(s->fd, &size, 4, 0); // TODO: check error - if (ret < 0) { - close(s->fd); - s->fd = -1; // invalid fd. hopefully the watch loop will recover this. - return; - } - ret = send(s->fd, buf, len, 0); // TODO: check error + ret = send(s->fd, buf - 4, len + 4, 0); // TODO: check error if (ret < 0) { close(s->fd); s->fd = -1; // invalid fd. hopefully the watch loop will recover this. @@ -3096,6 +3090,14 @@ static void socket_write_packet(EthernetDevice *net, } } +static uint8_t * socket_alloc_packet(EthernetDevice *net, int len) +{ + uint8_t * buf = (uint8_t *)malloc(len + 4); + uint32_t size = htonl(len); + put_le32(buf, size); + return &buf[4]; +} + static int try_get_fd(SocketState *s); static int socket_watch1(EthernetDevice *net) @@ -3213,6 +3215,7 @@ static EthernetDevice *socket_net_init() s->tmpfd = -1; s->enabled = false; net->opaque = s; + net->alloc_packet = socket_alloc_packet; net->write_packet = socket_write_packet; net->select_fill = socket_select_fill1; net->select_poll = socket_select_poll1; diff --git a/patches/bochs/Bochs/bochs/wasm.h b/patches/bochs/Bochs/bochs/wasm.h index a99423d..35a572f 100644 --- a/patches/bochs/Bochs/bochs/wasm.h +++ b/patches/bochs/Bochs/bochs/wasm.h @@ -551,6 +551,7 @@ struct EthernetDevice { uint8_t mac_addr[6]; /* mac address of the interface */ void (*write_packet)(EthernetDevice *net, const uint8_t *buf, int len); + uint8_t * (*alloc_packet)(EthernetDevice *net, int len); void *opaque; bx_virtio_net_ctrl_c *virtio_device; void (*select_fill)(EthernetDevice *net, int *pfd_max, diff --git a/patches/tinyemu/tinyemu/temu.c b/patches/tinyemu/tinyemu/temu.c index 40bfdae..92cb196 100644 --- a/patches/tinyemu/tinyemu/temu.c +++ b/patches/tinyemu/tinyemu/temu.c @@ -956,6 +956,7 @@ static struct option options[] = { { "entrypoint", required_argument }, { "net", required_argument }, { "mac", required_argument }, + { "external-bundle", required_argument }, // The following flags are unsupported as of now: // { "ctrlc", no_argument }, // { "rw", no_argument }, @@ -972,10 +973,11 @@ void help(void) " [COMMAND] [ARG...]: command to run in the container. (default: commands specified in the image config)\n" "\n" "OPTIONS:\n" - " -entrypoint : entrypoint command. (default: entrypoint specified in the image config)\n" - " -no-stdin : disable stdin. (default: false)\n" - " -net : enable networking with the specified mode (default: disabled. supported mode: \"socket\")\n" - " -mac : use a custom mac address for the VM\n" + " -entrypoint : entrypoint command. (default: entrypoint specified in the image config)\n" + " -no-stdin : disable stdin. (default: false)\n" + " -net : enable networking with the specified mode (default: disabled. supported mode: \"socket\")\n" + " -mac : use a custom mac address for the VM\n" + " -external-bundle
: externally mount container bundle\n" "\n" "This tool is based on:\n" "temu version 2019-12-21, Copyright (c) 2016-2018 Fabrice Bellard\n" @@ -1273,6 +1275,26 @@ int write_net(FSVirtFile *f, int pos1, const char *mac) return pos - pos1; } +int write_bundle(FSVirtFile *f, int pos1, const char *bundle) +{ + int p, pos = pos1; + + p = write_info(f, pos, 3, "b: "); + if (p < 0) { + return -1; + } + pos += p; + for (int j = 0; j < strlen(bundle); j++) { + if (putchar_info(f, pos++, bundle[j]) != 1) { + return -1; + } + } + if (putchar_info(f, pos++, '\n') != 1) { + return -1; + } + return pos - pos1; +} + int write_time(FSVirtFile *f, int pos1, const char *timestr) { int p, pos = pos1; @@ -1303,7 +1325,7 @@ int main(int argc, char **argv) #endif /* const char *cmdline, *build_preload_file; */ - char *entrypoint = NULL, *net = NULL, *mac = NULL; + char *entrypoint = NULL, *net = NULL, *mac = NULL, *bundle = NULL; int pos, c, option_index, i, enable_stdin = TRUE; for(;;) { c = getopt_long_only(argc, argv, "+h", options, &option_index); @@ -1327,6 +1349,9 @@ int main(int argc, char **argv) case 4: /* mac */ mac = optarg; break; + case 5: /* external-bundle */ + bundle = optarg; + break; default: fprintf(stderr, "unknown option index: %d\n", option_index); exit(1); @@ -1397,6 +1422,15 @@ int main(int argc, char **argv) pos += p; } + if (bundle != NULL) { + int p = write_bundle(info, pos, bundle); + if (p < 0) { + printf("failed to prepare net info\n"); + exit(1); + } + pos += p; + } + char buf[32]; snprintf(buf, sizeof(buf), "%d", (unsigned)time(NULL)); int ps = write_time(info, pos, buf); diff --git a/tests/httphello/main.go b/tests/httphello/main.go index b641c5d..3979596 100644 --- a/tests/httphello/main.go +++ b/tests/httphello/main.go @@ -7,9 +7,15 @@ import ( ) func main() { - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintf(w, "hello") - }) + if len(os.Args) == 2 { + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "hello") + }) + } else if len(os.Args) > 2 { + http.Handle("/", http.FileServer(http.Dir(os.Args[2]))) + } else { + panic("specify args") + } if err := http.ListenAndServe(os.Args[1], nil); err != nil { fmt.Println(err) } diff --git a/tests/imagemounter-test/go.mod b/tests/imagemounter-test/go.mod new file mode 100644 index 0000000..6385eee --- /dev/null +++ b/tests/imagemounter-test/go.mod @@ -0,0 +1,8 @@ +module c2w-net-proxy-test + +go 1.19 + +require ( + github.com/opencontainers/go-digest v1.0.0 + github.com/tetratelabs/wazero v1.5.0 +) diff --git a/tests/imagemounter-test/go.sum b/tests/imagemounter-test/go.sum new file mode 100644 index 0000000..fadf778 --- /dev/null +++ b/tests/imagemounter-test/go.sum @@ -0,0 +1,4 @@ +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/tetratelabs/wazero v1.5.0 h1:Yz3fZHivfDiZFUXnWMPUoiW7s8tC1sjdBtlJn08qYa0= +github.com/tetratelabs/wazero v1.5.0/go.mod h1:0U0G41+ochRKoPKCJlh0jMg1CHkyfK8kDqiirMmKY8A= diff --git a/tests/imagemounter-test/main.go b/tests/imagemounter-test/main.go new file mode 100644 index 0000000..960cfce --- /dev/null +++ b/tests/imagemounter-test/main.go @@ -0,0 +1,525 @@ +package main + +import ( + "bytes" + "context" + crand "crypto/rand" + "encoding/json" + "flag" + "fmt" + "io" + "log" + "net" + "net/http" + "os" + "path/filepath" + "strings" + "sync" + "time" + "compress/gzip" + + "github.com/tetratelabs/wazero" + "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/experimental/sock" + "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1" + digest "github.com/opencontainers/go-digest" +) + +func main() { + var ( + stack = flag.String("stack", "", "path to the imagemoutner wasm path") + stackPort = flag.Int("stack-port", 8999, "listen port of network stack") + vmPort = flag.Int("vm-port", 1234, "listen port of vm") + debug = flag.Bool("debug", false, "enable debug log") + imageAddr = flag.String("image", "", "address of image to run") + ) + var envs envFlags + flag.Var(&envs, "env", "environment variables") + flag.Parse() + if *debug { + log.SetOutput(os.Stdout) + } else { + log.SetOutput(io.Discard) + } + if *imageAddr == "" { + panic("specify container image") + } + args := flag.Args() + + if stack == nil || *stack == "" { + panic("specify network stack wasm") + } + + crtDir, err := os.MkdirTemp("", "test") + if err != nil { + panic(err) + } + defer func() { + os.Remove(filepath.Join(crtDir, "ca.crt")) + os.Remove(crtDir) + }() + + go func() { + ctx := context.Background() + sockCfg := sock.NewConfig().WithTCPListener("127.0.0.1", *stackPort) + ctx = sock.WithConfig(ctx, sockCfg) + c, err := os.ReadFile(*stack) + if err != nil { + panic(err) + } + r := wazero.NewRuntime(ctx) + defer func() { + r.Close(ctx) + }() + wasi_snapshot_preview1.MustInstantiate(ctx, r) + b := r.NewHostModuleBuilder("env"). + NewFunctionBuilder().WithFunc(http_send).Export("http_send"). + NewFunctionBuilder().WithFunc(http_writebody).Export("http_writebody"). + NewFunctionBuilder().WithFunc(http_isreadable).Export("http_isreadable"). + NewFunctionBuilder().WithFunc(http_recv).Export("http_recv"). + NewFunctionBuilder().WithFunc(http_readbody).Export("http_readbody"). + NewFunctionBuilder().WithFunc(layer_request).Export("layer_request"). + NewFunctionBuilder().WithFunc(layer_isreadable).Export("layer_isreadable"). + NewFunctionBuilder().WithFunc(layer_readat).Export("layer_readat") + if _, err := b.Instantiate(ctx); err != nil { + panic(err) + } + compiled, err := r.CompileModule(ctx, c) + if err != nil { + panic(err) + } + stackFSConfig := wazero.NewFSConfig() + stackFSConfig = stackFSConfig.WithDirMount(crtDir, "/test") + flagargs := []string{"--certfile=/test/proxy.crt", "--image-addr="+*imageAddr} + if *debug { + flagargs = append(flagargs, "--debug") + } + conf := wazero.NewModuleConfig().WithSysWalltime().WithSysNanotime().WithSysNanosleep().WithRandSource(crand.Reader).WithStdout(os.Stdout).WithStderr(os.Stderr).WithFSConfig(stackFSConfig).WithArgs(append([]string{"arg0"}, flagargs...)...) + _, err = r.InstantiateModule(ctx, compiled, conf) + if err != nil { + panic(err) + } + }() + go func() { + var conn1 net.Conn + var conn2 net.Conn + var err error + for { + conn1, err = net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", *stackPort)) + if err == nil { + break + } + log.Println("conn1 retry...") + time.Sleep(10 * time.Millisecond) + } + for { + conn2, err = net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", *vmPort)) + if err == nil { + break + } + log.Println("conn2 retry...") + time.Sleep(10 * time.Millisecond) + } + done1 := make(chan struct{}) + done2 := make(chan struct{}) + go func() { + if _, err := io.Copy(conn1, conn2); err != nil { + panic(err) + } + close(done1) + }() + go func() { + if _, err := io.Copy(conn2, conn1); err != nil { + panic(err) + } + close(done2) + }() + <-done1 + <-done2 + }() + + fsConfig := wazero.NewFSConfig() + crtFile := filepath.Join(crtDir, "proxy.crt") + for { + if _, err := os.Stat(crtFile); err == nil { + break + } + log.Println("waiting for cert", crtFile) + time.Sleep(time.Second) + } + + fsConfig = fsConfig.WithDirMount(crtDir, "/.wasmenv") + ctx := context.Background() + sockCfg := sock.NewConfig().WithTCPListener("127.0.0.1", *vmPort) + ctx = sock.WithConfig(ctx, sockCfg) + c, err := os.ReadFile(args[0]) + if err != nil { + panic(err) + } + r := wazero.NewRuntime(ctx) + defer func() { + r.Close(ctx) + }() + wasi_snapshot_preview1.MustInstantiate(ctx, r) + compiled, err := r.CompileModule(ctx, c) + if err != nil { + panic(err) + } + conf := wazero.NewModuleConfig().WithSysWalltime().WithSysNanotime().WithSysNanosleep().WithRandSource(crand.Reader).WithStdout(os.Stdout).WithStderr(os.Stderr).WithStdin(os.Stdin).WithFSConfig(fsConfig).WithArgs(append([]string{"arg0"}, args[1:]...)...) + envs = append(envs, []string{ + "SSL_CERT_FILE=/.wasmenv/proxy.crt", + "https_proxy=http://192.168.127.253:80", + "http_proxy=http://192.168.127.253:80", + "HTTPS_PROXY=http://192.168.127.253:80", + "HTTP_PROXY=http://192.168.127.253:80", + }...) + for _, v := range envs { + es := strings.SplitN(v, "=", 2) + if len(es) == 2 { + conf = conf.WithEnv(es[0], es[1]) + } else { + panic("env must be a key value pair") + } + } + _, err = r.InstantiateModule(ctx, compiled, conf) + if err != nil { + panic(err) + } +} + +var respMap = make(map[uint32]*http.Response) +var respMapMu sync.Mutex + +var respRMap = make(map[uint32]io.Reader) +var respRMapMu sync.Mutex + +var layerMap = make(map[uint32][]byte) +var layerMapMu sync.Mutex + +type fetchParameters struct { + Method string `json:"method,omitempty"` + Headers map[string]string `json:"headers,omitempty"` +} + +var reqMap = make(map[uint32]*io.PipeWriter) +var reqMapMu sync.Mutex +var reqMapId uint32 + +const ERRNO_INVAL = 28 + +func layer_request(ctx context.Context, m api.Module, addressP uint32, addresslen uint32, digestP uint32, digestlen uint32, withDecompression uint32, idP uint32) uint32 { + mem := m.Memory() + + addressB, ok := mem.Read(addressP, uint32(addresslen)) + if !ok { + log.Println("failed to get layer address") + return ERRNO_INVAL + } + address := string(addressB) + + digestB, ok := mem.Read(digestP, uint32(digestlen)) + if !ok { + log.Println("failed to get layer address") + return ERRNO_INVAL + } + + req, err := http.NewRequest("GET", address, nil) + if err != nil { + log.Println("failed to create layer req:", err) + return ERRNO_INVAL + } + + reqMapMu.Lock() + id := reqMapId + reqMapId++ + reqMapMu.Unlock() + go func() { + c := &http.Client{} + resp, err := c.Do(req) + if err != nil { + log.Println("failed to do layer request:", err) + return + } + defer resp.Body.Close() + respMapMu.Lock() + respMap[id] = resp + respMapMu.Unlock() + + v := digest.Digest("sha256:"+string(digestB)).Verifier() + r := io.TeeReader(resp.Body, v) + if withDecompression == 1 { + raw := r + zr, err := gzip.NewReader(raw) + if err != nil { + log.Println("failed to prepare layer decompressor:", err) + return + } + defer zr.Close() + r = zr + } + data, err := io.ReadAll(r) + if err != nil { + log.Println("failed to read layer:", err) + return + } + if !v.Verified() { + log.Println("failed to veriy layer (%q)", string(digestB)) + return + } + layerMapMu.Lock() + layerMap[id] = data + layerMapMu.Unlock() + }() + if !mem.WriteUint32Le(idP, id) { + log.Println("failed to pass id") + return ERRNO_INVAL + } + return 0 +} + +func layer_isreadable(ctx context.Context, m api.Module, id uint32, isOKP uint32, sizeP uint32) uint32 { + mem := m.Memory() + layerMapMu.Lock() + buf, ok := layerMap[id] + layerMapMu.Unlock() + v := uint32(0) + if ok { + v = 1 + if !mem.WriteUint32Le(sizeP, uint32(len(buf))) { + log.Println("failed to pass status") + return ERRNO_INVAL + } + } + if !mem.WriteUint32Le(isOKP, v) { + log.Println("failed to pass status") + return ERRNO_INVAL + } + return 0 +} + +func layer_readat(ctx context.Context, m api.Module, id uint32, respP uint32, offset uint32, wantlen uint32, respsizeP uint32) uint32 { + mem := m.Memory() + layerMapMu.Lock() + buf, ok := layerMap[id] + layerMapMu.Unlock() + if !ok { + log.Printf("no layer found for id %d\n", id) + return ERRNO_INVAL + } + + if uint32(len(buf)) < offset + wantlen { + wantlen = uint32(len(buf)) - offset + } + if !mem.Write(respP, buf[offset:offset+wantlen]) { + log.Println("failed to write layer") + return ERRNO_INVAL + } + if !mem.WriteUint32Le(respsizeP, uint32(wantlen)) { + log.Println("failed to write resp body size") + return ERRNO_INVAL + } + return 0 +} + + +func http_send(ctx context.Context, m api.Module, addressP uint32, addresslen uint32, reqP, reqlen uint32, idP uint32) uint32 { + mem := m.Memory() + + addressB, ok := mem.Read(addressP, uint32(addresslen)) + if !ok { + log.Println("failed to get address") + return ERRNO_INVAL + } + address := string(addressB) + + reqB, ok := mem.Read(reqP, uint32(reqlen)) + if !ok { + log.Println("failed to get req") + return ERRNO_INVAL + } + var fetchReq fetchParameters + if err := json.Unmarshal(reqB, &fetchReq); err != nil { + log.Println("failed to marshal req:", err) + return ERRNO_INVAL + } + + pr, pw := io.Pipe() + req, err := http.NewRequest(fetchReq.Method, address, pr) + if err != nil { + log.Println("failed to create req:", err) + return ERRNO_INVAL + } + if req.Header == nil { + req.Header = make(map[string][]string) + } + for k, v := range fetchReq.Headers { + req.Header.Add(k, v) + } + + reqMapMu.Lock() + id := reqMapId + reqMapId++ + reqMapMu.Unlock() + reqMap[id] = pw + go func() { + c := &http.Client{} + resp, err := c.Do(req) + if err != nil { + log.Println("failed to do request:", err) + return + } + respMapMu.Lock() + respMap[id] = resp + respMapMu.Unlock() + }() + if !mem.WriteUint32Le(idP, id) { + log.Println("failed to pass id") + return ERRNO_INVAL + } + return 0 +} + +func http_writebody(ctx context.Context, m api.Module, id uint32, chunkP, len uint32, nwrittenP uint32, isEOF uint32) uint32 { + mem := m.Memory() + + chunkB, ok := mem.Read(chunkP, uint32(len)) + if !ok { + log.Println("failed to get chunk") + return ERRNO_INVAL + } + reqMapMu.Lock() + w := reqMap[id] + reqMapMu.Unlock() + if _, err := w.Write(chunkB); err != nil { + w.CloseWithError(err) + log.Println("failed to write req:", err) + return ERRNO_INVAL + } + if isEOF == 1 { + w.Close() + } + if !mem.WriteUint32Le(nwrittenP, len) { + log.Println("failed to pass written number") + return ERRNO_INVAL + } + return 0 +} + +func http_isreadable(ctx context.Context, m api.Module, id uint32, isOKP uint32) uint32 { + mem := m.Memory() + respMapMu.Lock() + _, ok := respMap[id] + respMapMu.Unlock() + v := uint32(0) + if ok { + v = 1 + } + if !mem.WriteUint32Le(isOKP, v) { + log.Println("failed to pass status") + return ERRNO_INVAL + } + return 0 +} + +func http_recv(ctx context.Context, m api.Module, id uint32, respP uint32, bufsize uint32, respsizeP uint32, isEOFP uint32) uint32 { + mem := m.Memory() + respRMapMu.Lock() + respR, ok := respRMap[id] + respRMapMu.Unlock() + if !ok { + respMapMu.Lock() + resp, ok := respMap[id] + respMapMu.Unlock() + if !ok { + log.Println("failed to get resp") + return ERRNO_INVAL + } + var fetchResp struct { + Headers map[string]string `json:"headers,omitempty"` + Status int `json:"status,omitempty"` + StatusText string `json:"statusText,omitempty"` + } + fetchResp.Headers = make(map[string]string) + for k, v := range resp.Header { + fetchResp.Headers[k] = strings.Join(v, ", ") + } + fetchResp.Status = resp.StatusCode + fetchResp.StatusText = resp.Status + respD, err := json.Marshal(fetchResp) + if err != nil { + log.Println("failed to marshal resp:", err) + return ERRNO_INVAL + } + respR = bytes.NewReader(respD) + respRMapMu.Lock() + respRMap[id] = respR + respRMapMu.Unlock() + } + + isEOF := uint32(0) + buf := make([]byte, bufsize) + n, err := respR.Read(buf) + if err != nil && err != io.EOF { + log.Println("failed to read resp:", err) + return ERRNO_INVAL + } else if err == io.EOF { + isEOF = 1 + } + if !mem.Write(respP, buf[:n]) { + log.Println("failed to write resp") + return ERRNO_INVAL + } + if !mem.WriteUint32Le(respsizeP, uint32(n)) { + log.Println("failed to write resp size") + return ERRNO_INVAL + } + if !mem.WriteUint32Le(isEOFP, isEOF) { + log.Println("failed to write EOF status") + return ERRNO_INVAL + } + return 0 +} + +func http_readbody(ctx context.Context, m api.Module, id uint32, bodyP uint32, bufsize uint32, bodysizeP uint32, isEOFP uint32) uint32 { + mem := m.Memory() + respMapMu.Lock() + resp, ok := respMap[id] + respMapMu.Unlock() + if !ok { + log.Println("failed to get resp") + return ERRNO_INVAL + } + + isEOF := uint32(0) + buf := make([]byte, bufsize) + n, err := resp.Body.Read(buf) + if err != nil && err != io.EOF { + log.Println("failed to read resp body:", err) + return ERRNO_INVAL + } else if err == io.EOF { + isEOF = 1 + } + if !mem.Write(bodyP, buf[:n]) { + log.Println("failed to write resp body") + return ERRNO_INVAL + } + if !mem.WriteUint32Le(bodysizeP, uint32(n)) { + log.Println("failed to write resp body size") + return ERRNO_INVAL + } + if !mem.WriteUint32Le(isEOFP, isEOF) { + log.Println("failed to write resp body EOF status") + return ERRNO_INVAL + } + return 0 +} + +type envFlags []string + +func (i *envFlags) String() string { + return fmt.Sprintf("%v", []string(*i)) +} +func (i *envFlags) Set(value string) error { + *i = append(*i, value) + return nil +} diff --git a/tests/integration/utils/utils.go b/tests/integration/utils/utils.go index 28446e8..1208ede 100644 --- a/tests/integration/utils/utils.go +++ b/tests/integration/utils/utils.go @@ -22,6 +22,7 @@ import ( const AssetPath = "/test/" const C2wBin = "c2w" const C2wNetProxyBin = "/opt/c2w-net-proxy.wasm" +const ImageMounterBin = "/opt/imagemounter.wasm" type Architecture int @@ -50,18 +51,26 @@ type Input struct { ConvertOpts []string Architecture Architecture Dockerfile string + Mirror bool + Store string + External bool +} + +type Env struct { + Input Input + Workdir string } type TestSpec struct { Name string Inputs []Input - Prepare func(t *testing.T, workdir string) - Finalize func(t *testing.T, workdir string) + Prepare func(t *testing.T, env Env) + Finalize func(t *testing.T, env Env) ImageName string // default: test.wasm Runtime string - RuntimeOpts func(t *testing.T, workdir string) []string - Args func(t *testing.T, workdir string) []string - Want func(t *testing.T, workdir string, in io.Writer, out io.Reader) + RuntimeOpts func(t *testing.T, env Env) []string + Args func(t *testing.T, env Env) []string + Want func(t *testing.T, env Env, in io.Writer, out io.Reader) NoParallel bool IgnoreExitCode bool } @@ -91,18 +100,52 @@ func RunTestRuntimes(t *testing.T, tests ...TestSpec) { dcmd.Stderr = os.Stderr assert.NilError(t, dcmd.Run()) } + if in.Mirror { + if err := exec.Command("docker", "image", "inspect", in.Image).Run(); err != nil { + assert.NilError(t, exec.Command("docker", "pull", in.Image).Run()) + } + assert.NilError(t, exec.Command("docker", "tag", in.Image, "localhost:5000/"+in.Image).Run()) + dcmd := exec.Command("docker", "push", "localhost:5000/"+in.Image) + dcmd.Stdout = os.Stdout + dcmd.Stderr = os.Stderr + assert.NilError(t, dcmd.Run()) + } + if in.Store != "" { + waitForBuildxBuilder(t, "container") + if err := exec.Command("docker", "image", "inspect", in.Image).Run(); err != nil { + assert.NilError(t, exec.Command("docker", "pull", in.Image).Run()) + } + df := filepath.Join(tmpdir, "Dockerfile-integrationtest-store") + tmpdest := filepath.Join(tmpdir, "Dockerfile-integrationtest-store-out.tar") + assert.NilError(t, os.WriteFile(df, []byte("FROM "+in.Image), 0755)) + dcmd := exec.Command("docker", "buildx", "build", "--builder=container", "--output", "type=oci,dest="+tmpdest, "--progress=plain", "-f", df, AssetPath) + dcmd.Stdout = os.Stdout + dcmd.Stderr = os.Stderr + assert.NilError(t, dcmd.Run()) + + storeout := filepath.Join(tmpdir, in.Store) + assert.NilError(t, os.Mkdir(storeout, 0755)) + assert.NilError(t, exec.Command("tar", "-C", storeout, "-xf", tmpdest).Run()) + } testWasm := filepath.Join(tmpdir, "test.wasm") - c2wCmd := exec.Command(C2wBin, append(in.ConvertOpts, "--assets="+AssetPath, in.Image, testWasm)...) + var convertargs []string + if in.Image != "" && !in.External { + convertargs = []string{in.Image, testWasm} + } else { + convertargs = []string{testWasm} + } + c2wCmd := exec.Command(C2wBin, append(append(in.ConvertOpts, "--assets="+AssetPath), convertargs...)...) c2wCmd.Stdout = os.Stdout c2wCmd.Stderr = os.Stderr assert.NilError(t, c2wCmd.Run()) + envInfo := Env{Input: in, Workdir: tmpdir} if tt.Prepare != nil { - tt.Prepare(t, tmpdir) + tt.Prepare(t, envInfo) } if tt.Finalize != nil { - defer tt.Finalize(t, tmpdir) + defer tt.Finalize(t, envInfo) } targetWasm := testWasm @@ -111,11 +154,11 @@ func RunTestRuntimes(t *testing.T, tests ...TestSpec) { } var runtimeOpts []string if tt.RuntimeOpts != nil { - runtimeOpts = tt.RuntimeOpts(t, tmpdir) + runtimeOpts = tt.RuntimeOpts(t, envInfo) } var args []string if tt.Args != nil { - args = tt.Args(t, tmpdir) + args = tt.Args(t, envInfo) } testCmd := exec.Command(tt.Runtime, append(append(runtimeOpts, targetWasm), args...)...) outR, err := testCmd.StdoutPipe() @@ -128,7 +171,7 @@ func RunTestRuntimes(t *testing.T, tests ...TestSpec) { assert.NilError(t, testCmd.Start()) - tt.Want(t, tmpdir, inW, io.TeeReader(outR, os.Stdout)) + tt.Want(t, envInfo, inW, io.TeeReader(outR, os.Stdout)) inW.Close() if !tt.IgnoreExitCode { @@ -147,16 +190,39 @@ func RunTestRuntimes(t *testing.T, tests ...TestSpec) { } } -func WantString(wantstr string) func(t *testing.T, workdir string, in io.Writer, out io.Reader) { - return func(t *testing.T, workdir string, in io.Writer, out io.Reader) { +func waitForBuildxBuilder(t *testing.T, builder string) { + tmpdir, err := os.MkdirTemp("", "testc2w") + assert.NilError(t, err) + defer func() { + assert.NilError(t, os.RemoveAll(tmpdir)) + }() + df := filepath.Join(tmpdir, "buildxwaiter") + assert.NilError(t, os.WriteFile(df, []byte("FROM ubuntu:22.04"), 0755)) + for i := 0; i < 10; i++ { + dcmd := exec.Command("docker", "buildx", "build", "--builder="+builder, "--progress=plain", "-f", df, tmpdir) + dcmd.Stdout = os.Stdout + dcmd.Stderr = os.Stderr + if err := dcmd.Run(); err != nil { + t.Logf("failed to access to buildkit: %v", err) + time.Sleep(time.Second) + continue + } + t.Logf("builder working") + return + } + t.Fatalf("failed to launch builder (timeout)") +} + +func WantString(wantstr string) func(t *testing.T, env Env, in io.Writer, out io.Reader) { + return func(t *testing.T, env Env, in io.Writer, out io.Reader) { outstr, err := io.ReadAll(out) assert.NilError(t, err) assert.Equal(t, string(outstr), wantstr) } } -func WantPrompt(prompt string, inputoutput ...[2]string) func(t *testing.T, workdir string, in io.Writer, out io.Reader) { - return func(t *testing.T, workdir string, in io.Writer, out io.Reader) { +func WantPrompt(prompt string, inputoutput ...[2]string) func(t *testing.T, env Env, in io.Writer, out io.Reader) { + return func(t *testing.T, env Env, in io.Writer, out io.Reader) { ctx := context.TODO() // Wait for prompt @@ -187,9 +253,9 @@ func WantPrompt(prompt string, inputoutput ...[2]string) func(t *testing.T, work } } -func WantPromptWithWorkdir(prompt string, inputoutputFunc func(workdir string) [][2]string) func(t *testing.T, workdir string, in io.Writer, out io.Reader) { - return func(t *testing.T, workdir string, in io.Writer, out io.Reader) { - WantPrompt(prompt, inputoutputFunc(workdir)...)(t, workdir, in, out) +func WantPromptWithWorkdir(prompt string, inputoutputFunc func(workdir string) [][2]string) func(t *testing.T, env Env, in io.Writer, out io.Reader) { + return func(t *testing.T, env Env, in io.Writer, out io.Reader) { + WantPrompt(prompt, inputoutputFunc(env.Workdir)...)(t, env, in, out) } } @@ -208,8 +274,8 @@ func readUntilPrompt(ctx context.Context, prompt string, outR io.Reader) (out [] } } -func StringFlags(opts ...string) func(t *testing.T, workdir string) []string { - return func(t *testing.T, workdir string) []string { return opts } +func StringFlags(opts ...string) func(t *testing.T, env Env) []string { + return func(t *testing.T, env Env) []string { return opts } } var usedPorts = make(map[int]struct{}) @@ -269,3 +335,27 @@ func StartHelloServer(t *testing.T) (pid int, port int) { } return cmd.Process.Pid, port } + +func StartDirServer(t *testing.T, dir string) (pid int, port int) { + port = GetPort(t) + t.Logf("launching server on %d", port) + cmd := exec.Command("httphello", fmt.Sprintf("localhost:%d", port), dir) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + assert.NilError(t, cmd.Start()) + go func() { + if err := cmd.Wait(); err != nil { + t.Logf("dir server error: %v\n", err) + } + DonePort(port) + }() + for { + if cmd.Process != nil { + if _, err := http.Get(fmt.Sprintf("http://localhost:%d/", port)); err == nil { + break + } + } + time.Sleep(1 * time.Millisecond) + } + return cmd.Process.Pid, port +} diff --git a/tests/integration/wamr_test.go b/tests/integration/wamr_test.go index 3426921..a8023a9 100644 --- a/tests/integration/wamr_test.go +++ b/tests/integration/wamr_test.go @@ -21,8 +21,8 @@ func TestWamr(t *testing.T) { {Image: "riscv64/alpine:20221110", ConvertOpts: []string{"--target-arch=riscv64"}, Architecture: utils.RISCV64}, }, ImageName: "test2.wasm", - Prepare: func(t *testing.T, workdir string) { - assert.NilError(t, exec.Command("wamrc", "-o", filepath.Join(workdir, "test2.wasm"), filepath.Join(workdir, "test.wasm")).Run()) + Prepare: func(t *testing.T, env utils.Env) { + assert.NilError(t, exec.Command("wamrc", "-o", filepath.Join(env.Workdir, "test2.wasm"), filepath.Join(env.Workdir, "test.wasm")).Run()) }, Args: utils.StringFlags("echo", "-n", "hello"), Want: utils.WantString("hello"), @@ -35,8 +35,8 @@ func TestWamr(t *testing.T) { {Image: "riscv64/alpine:20221110", ConvertOpts: []string{"--target-arch=riscv64"}, Architecture: utils.RISCV64}, }, ImageName: "test2.wasm", - Prepare: func(t *testing.T, workdir string) { - assert.NilError(t, exec.Command("wamrc", "-o", filepath.Join(workdir, "test2.wasm"), filepath.Join(workdir, "test.wasm")).Run()) + Prepare: func(t *testing.T, env utils.Env) { + assert.NilError(t, exec.Command("wamrc", "-o", filepath.Join(env.Workdir, "test2.wasm"), filepath.Join(env.Workdir, "test.wasm")).Run()) }, Args: utils.StringFlags("sh"), Want: utils.WantPrompt("/ # ", [2]string{"echo -n hello\n", "hello"}), @@ -49,22 +49,22 @@ func TestWamr(t *testing.T) { {Image: "riscv64/alpine:20221110", ConvertOpts: []string{"--target-arch=riscv64"}, Architecture: utils.RISCV64}, }, ImageName: "test2.wasm", - Prepare: func(t *testing.T, workdir string) { - assert.NilError(t, exec.Command("wamrc", "-o", filepath.Join(workdir, "test2.wasm"), filepath.Join(workdir, "test.wasm")).Run()) - mapdirTestDir := filepath.Join(workdir, "wamr-mapdirtest/testdir") + Prepare: func(t *testing.T, env utils.Env) { + assert.NilError(t, exec.Command("wamrc", "-o", filepath.Join(env.Workdir, "test2.wasm"), filepath.Join(env.Workdir, "test.wasm")).Run()) + mapdirTestDir := filepath.Join(env.Workdir, "wamr-mapdirtest/testdir") assert.NilError(t, os.MkdirAll(mapdirTestDir, 0755)) assert.NilError(t, os.WriteFile(filepath.Join(mapdirTestDir, "hi"), []byte("teststring"), 0755)) }, - Finalize: func(t *testing.T, workdir string) { - mapdirTestDir := filepath.Join(workdir, "wamr-mapdirtest/testdir") + Finalize: func(t *testing.T, env utils.Env) { + mapdirTestDir := filepath.Join(env.Workdir, "wamr-mapdirtest/testdir") assert.NilError(t, os.Remove(filepath.Join(mapdirTestDir, "hi"))) assert.NilError(t, os.Remove(mapdirTestDir)) }, - RuntimeOpts: func(t *testing.T, workdir string) []string { - return []string{"--dir=" + filepath.Join(workdir, "wamr-mapdirtest/testdir")} + RuntimeOpts: func(t *testing.T, env utils.Env) []string { + return []string{"--dir=" + filepath.Join(env.Workdir, "wamr-mapdirtest/testdir")} }, - Args: func(t *testing.T, workdir string) []string { - return []string{"cat", filepath.Join(workdir, "wamr-mapdirtest/testdir/hi")} + Args: func(t *testing.T, env utils.Env) []string { + return []string{"cat", filepath.Join(env.Workdir, "wamr-mapdirtest/testdir/hi")} }, Want: utils.WantString("teststring"), }, @@ -76,8 +76,8 @@ func TestWamr(t *testing.T) { {Image: "riscv64/alpine:20221110", ConvertOpts: []string{"--target-arch=riscv64"}, Architecture: utils.RISCV64}, }, ImageName: "test2.wasm", - Prepare: func(t *testing.T, workdir string) { - assert.NilError(t, exec.Command("wamrc", "-o", filepath.Join(workdir, "test2.wasm"), filepath.Join(workdir, "test.wasm")).Run()) + Prepare: func(t *testing.T, env utils.Env) { + assert.NilError(t, exec.Command("wamrc", "-o", filepath.Join(env.Workdir, "test2.wasm"), filepath.Join(env.Workdir, "test.wasm")).Run()) }, Args: utils.StringFlags("sh"), Want: utils.WantPrompt("/ # ", @@ -93,14 +93,14 @@ func TestWamr(t *testing.T) { {Image: "riscv64/alpine:20221110", ConvertOpts: []string{"--target-arch=riscv64"}, Architecture: utils.RISCV64}, }, ImageName: "test2.wasm", - Prepare: func(t *testing.T, workdir string) { - assert.NilError(t, exec.Command("wamrc", "-o", filepath.Join(workdir, "test2.wasm"), filepath.Join(workdir, "test.wasm")).Run()) - mapdirTestDir := filepath.Join(workdir, "wamr-mapdirtest-io") + Prepare: func(t *testing.T, env utils.Env) { + assert.NilError(t, exec.Command("wamrc", "-o", filepath.Join(env.Workdir, "test2.wasm"), filepath.Join(env.Workdir, "test.wasm")).Run()) + mapdirTestDir := filepath.Join(env.Workdir, "wamr-mapdirtest-io") assert.NilError(t, os.MkdirAll(mapdirTestDir, 0755)) assert.NilError(t, os.WriteFile(filepath.Join(mapdirTestDir, "hi"), []byte("teststring"), 0755)) }, - Finalize: func(t *testing.T, workdir string) { - mapdirTestDir := filepath.Join(workdir, "wamr-mapdirtest-io") + Finalize: func(t *testing.T, env utils.Env) { + mapdirTestDir := filepath.Join(env.Workdir, "wamr-mapdirtest-io") // check data from guest data, err := os.ReadFile(filepath.Join(mapdirTestDir, "from-guest", "testhello")) @@ -113,8 +113,8 @@ func TestWamr(t *testing.T) { assert.NilError(t, os.Remove(filepath.Join(mapdirTestDir, "hi"))) assert.NilError(t, os.Remove(mapdirTestDir)) }, - RuntimeOpts: func(t *testing.T, workdir string) []string { - return []string{"--dir=" + filepath.Join(workdir, "wamr-mapdirtest-io")} + RuntimeOpts: func(t *testing.T, env utils.Env) []string { + return []string{"--dir=" + filepath.Join(env.Workdir, "wamr-mapdirtest-io")} }, Args: utils.StringFlags("sh"), Want: utils.WantPromptWithWorkdir("/ # ", @@ -135,8 +135,8 @@ func TestWamr(t *testing.T) { {Image: "riscv64/alpine:20221110", ConvertOpts: []string{"--target-arch=riscv64"}, Architecture: utils.RISCV64}, }, ImageName: "test2.wasm", - Prepare: func(t *testing.T, workdir string) { - assert.NilError(t, exec.Command("wamrc", "-o", filepath.Join(workdir, "test2.wasm"), filepath.Join(workdir, "test.wasm")).Run()) + Prepare: func(t *testing.T, env utils.Env) { + assert.NilError(t, exec.Command("wamrc", "-o", filepath.Join(env.Workdir, "test2.wasm"), filepath.Join(env.Workdir, "test.wasm")).Run()) }, RuntimeOpts: utils.StringFlags("--env=AAA=hello", "--env=BBB=world"), Args: utils.StringFlags("/bin/sh", "-c", "echo -n $AAA $BBB"), diff --git a/tests/integration/wasmedge_test.go b/tests/integration/wasmedge_test.go index 5a3a23a..f9a6a55 100644 --- a/tests/integration/wasmedge_test.go +++ b/tests/integration/wasmedge_test.go @@ -21,8 +21,8 @@ func TestWasmedge(t *testing.T) { {Image: "riscv64/alpine:20221110", ConvertOpts: []string{"--target-arch=riscv64"}, Architecture: utils.RISCV64}, }, ImageName: "test2.wasm", - Prepare: func(t *testing.T, workdir string) { - assert.NilError(t, exec.Command("wasmedgec", filepath.Join(workdir, "test.wasm"), filepath.Join(workdir, "test2.wasm")).Run()) + Prepare: func(t *testing.T, env utils.Env) { + assert.NilError(t, exec.Command("wasmedgec", filepath.Join(env.Workdir, "test.wasm"), filepath.Join(env.Workdir, "test2.wasm")).Run()) }, Args: utils.StringFlags("--no-stdin", "echo", "-n", "hello"), // NOTE: stdin unsupported on wasmedge as of now Want: utils.WantString("hello"), @@ -37,20 +37,20 @@ func TestWasmedge(t *testing.T) { {Image: "riscv64/alpine:20221110", ConvertOpts: []string{"--target-arch=riscv64"}, Architecture: utils.RISCV64}, }, ImageName: "test2.wasm", - Prepare: func(t *testing.T, workdir string) { - assert.NilError(t, exec.Command("wasmedgec", filepath.Join(workdir, "test.wasm"), filepath.Join(workdir, "test2.wasm")).Run()) + Prepare: func(t *testing.T, env utils.Env) { + assert.NilError(t, exec.Command("wasmedgec", filepath.Join(env.Workdir, "test.wasm"), filepath.Join(env.Workdir, "test2.wasm")).Run()) - mapdirTestDir := filepath.Join(workdir, "wasmedge-mapdirtest/testdir") + mapdirTestDir := filepath.Join(env.Workdir, "wasmedge-mapdirtest/testdir") assert.NilError(t, os.MkdirAll(mapdirTestDir, 0755)) assert.NilError(t, os.WriteFile(filepath.Join(mapdirTestDir, "hi"), []byte("teststring"), 0755)) }, - Finalize: func(t *testing.T, workdir string) { - mapdirTestDir := filepath.Join(workdir, "wasmedge-mapdirtest/testdir") + Finalize: func(t *testing.T, env utils.Env) { + mapdirTestDir := filepath.Join(env.Workdir, "wasmedge-mapdirtest/testdir") assert.NilError(t, os.Remove(filepath.Join(mapdirTestDir, "hi"))) assert.NilError(t, os.Remove(mapdirTestDir)) }, - RuntimeOpts: func(t *testing.T, workdir string) []string { - return []string{"--dir=/map/dir:" + filepath.Join(workdir, "wasmedge-mapdirtest/testdir")} + RuntimeOpts: func(t *testing.T, env utils.Env) []string { + return []string{"--dir=/map/dir:" + filepath.Join(env.Workdir, "wasmedge-mapdirtest/testdir")} }, Args: utils.StringFlags("--no-stdin", "cat", "/map/dir/hi"), Want: utils.WantString("teststring"), @@ -63,8 +63,8 @@ func TestWasmedge(t *testing.T) { {Image: "riscv64/alpine:20221110", ConvertOpts: []string{"--target-arch=riscv64"}, Architecture: utils.RISCV64}, }, ImageName: "test2.wasm", - Prepare: func(t *testing.T, workdir string) { - assert.NilError(t, exec.Command("wasmedgec", filepath.Join(workdir, "test.wasm"), filepath.Join(workdir, "test2.wasm")).Run()) + Prepare: func(t *testing.T, env utils.Env) { + assert.NilError(t, exec.Command("wasmedgec", filepath.Join(env.Workdir, "test.wasm"), filepath.Join(env.Workdir, "test2.wasm")).Run()) }, RuntimeOpts: utils.StringFlags("--env=AAA=hello", "--env=BBB=world"), Args: utils.StringFlags("--no-stdin", "/bin/sh", "-c", "echo -n $AAA $BBB"), // NOTE: stdin unsupported on wasmedge as of now diff --git a/tests/integration/wasmer_test.go b/tests/integration/wasmer_test.go index 2a2a4d3..3152919 100644 --- a/tests/integration/wasmer_test.go +++ b/tests/integration/wasmer_test.go @@ -31,18 +31,18 @@ func TestWasmer(t *testing.T) { {Image: "alpine:3.17", Architecture: utils.X86_64}, {Image: "riscv64/alpine:20221110", ConvertOpts: []string{"--target-arch=riscv64"}, Architecture: utils.RISCV64}, }, - Prepare: func(t *testing.T, workdir string) { - mapdirTestDir := filepath.Join(workdir, "wasmer-mapdirtest/testdir") + Prepare: func(t *testing.T, env utils.Env) { + mapdirTestDir := filepath.Join(env.Workdir, "wasmer-mapdirtest/testdir") assert.NilError(t, os.MkdirAll(mapdirTestDir, 0755)) assert.NilError(t, os.WriteFile(filepath.Join(mapdirTestDir, "hi"), []byte("teststring"), 0755)) }, - Finalize: func(t *testing.T, workdir string) { - mapdirTestDir := filepath.Join(workdir, "wasmer-mapdirtest/testdir") + Finalize: func(t *testing.T, env utils.Env) { + mapdirTestDir := filepath.Join(env.Workdir, "wasmer-mapdirtest/testdir") assert.NilError(t, os.Remove(filepath.Join(mapdirTestDir, "hi"))) assert.NilError(t, os.Remove(mapdirTestDir)) }, - RuntimeOpts: func(t *testing.T, workdir string) []string { - return []string{"--mapdir=/mapped/dir/test::" + filepath.Join(workdir, "wasmer-mapdirtest/testdir")} + RuntimeOpts: func(t *testing.T, env utils.Env) []string { + return []string{"--mapdir=/mapped/dir/test::" + filepath.Join(env.Workdir, "wasmer-mapdirtest/testdir")} }, Args: utils.StringFlags("--", "--no-stdin", "cat", "/mapped/dir/test/hi"), Want: utils.WantString("teststring"), diff --git a/tests/integration/wasmtime_test.go b/tests/integration/wasmtime_test.go index e3f69d3..b76337c 100644 --- a/tests/integration/wasmtime_test.go +++ b/tests/integration/wasmtime_test.go @@ -46,18 +46,18 @@ func TestWasmtime(t *testing.T) { {Image: "alpine:3.17", Architecture: utils.X86_64}, {Image: "riscv64/alpine:20221110", ConvertOpts: []string{"--target-arch=riscv64"}, Architecture: utils.RISCV64}, }, - Prepare: func(t *testing.T, workdir string) { - mapdirTestDir := filepath.Join(workdir, "wasmtime-mapdirtest") + Prepare: func(t *testing.T, env utils.Env) { + mapdirTestDir := filepath.Join(env.Workdir, "wasmtime-mapdirtest") assert.NilError(t, os.MkdirAll(mapdirTestDir, 0755)) assert.NilError(t, os.WriteFile(filepath.Join(mapdirTestDir, "hi"), []byte("teststring"), 0755)) }, - Finalize: func(t *testing.T, workdir string) { - mapdirTestDir := filepath.Join(workdir, "wasmtime-mapdirtest") + Finalize: func(t *testing.T, env utils.Env) { + mapdirTestDir := filepath.Join(env.Workdir, "wasmtime-mapdirtest") assert.NilError(t, os.Remove(filepath.Join(mapdirTestDir, "hi"))) assert.NilError(t, os.Remove(mapdirTestDir)) }, - RuntimeOpts: func(t *testing.T, workdir string) []string { - return []string{"--dir=" + filepath.Join(workdir, "wasmtime-mapdirtest") + "::/mapped/dir/test"} + RuntimeOpts: func(t *testing.T, env utils.Env) []string { + return []string{"--dir=" + filepath.Join(env.Workdir, "wasmtime-mapdirtest") + "::/mapped/dir/test"} }, Args: utils.StringFlags("cat", "/mapped/dir/test/hi"), Want: utils.WantString("teststring"), @@ -81,13 +81,13 @@ func TestWasmtime(t *testing.T) { {Image: "alpine:3.17", Architecture: utils.X86_64}, {Image: "riscv64/alpine:20221110", ConvertOpts: []string{"--target-arch=riscv64"}, Architecture: utils.RISCV64}, }, - Prepare: func(t *testing.T, workdir string) { - mapdirTestDir := filepath.Join(workdir, "wasmtime-mapdirtest-io") + Prepare: func(t *testing.T, env utils.Env) { + mapdirTestDir := filepath.Join(env.Workdir, "wasmtime-mapdirtest-io") assert.NilError(t, os.MkdirAll(mapdirTestDir, 0755)) assert.NilError(t, os.WriteFile(filepath.Join(mapdirTestDir, "hi"), []byte("teststring"), 0755)) }, - Finalize: func(t *testing.T, workdir string) { - mapdirTestDir := filepath.Join(workdir, "wasmtime-mapdirtest-io") + Finalize: func(t *testing.T, env utils.Env) { + mapdirTestDir := filepath.Join(env.Workdir, "wasmtime-mapdirtest-io") // check data from guest data, err := os.ReadFile(filepath.Join(mapdirTestDir, "from-guest", "testhello")) @@ -101,8 +101,8 @@ func TestWasmtime(t *testing.T) { assert.NilError(t, os.Remove(mapdirTestDir)) }, Runtime: "wasmtime", - RuntimeOpts: func(t *testing.T, workdir string) []string { - return []string{"--dir=" + filepath.Join(workdir, "wasmtime-mapdirtest-io") + "::/mapped/dir/test"} + RuntimeOpts: func(t *testing.T, env utils.Env) []string { + return []string{"--dir=" + filepath.Join(env.Workdir, "wasmtime-mapdirtest-io") + "::/mapped/dir/test"} }, Args: utils.StringFlags("sh"), Want: utils.WantPrompt("/ # ", @@ -128,29 +128,29 @@ func TestWasmtime(t *testing.T) { {Image: "alpine:3.17", Architecture: utils.X86_64}, {Image: "riscv64/alpine:20221110", ConvertOpts: []string{"--target-arch=riscv64"}, Architecture: utils.RISCV64}, }, - Prepare: func(t *testing.T, workdir string) { - assert.NilError(t, os.WriteFile(filepath.Join(workdir, "httphello-wasi-port"), []byte(fmt.Sprintf("%d", utils.GetPort(t))), 0755)) + Prepare: func(t *testing.T, env utils.Env) { + assert.NilError(t, os.WriteFile(filepath.Join(env.Workdir, "httphello-wasi-port"), []byte(fmt.Sprintf("%d", utils.GetPort(t))), 0755)) pid, port := utils.StartHelloServer(t) - assert.NilError(t, os.WriteFile(filepath.Join(workdir, "httphello-pid"), []byte(fmt.Sprintf("%d", pid)), 0755)) - assert.NilError(t, os.WriteFile(filepath.Join(workdir, "httphello-port"), []byte(fmt.Sprintf("%d", port)), 0755)) + assert.NilError(t, os.WriteFile(filepath.Join(env.Workdir, "httphello-pid"), []byte(fmt.Sprintf("%d", pid)), 0755)) + assert.NilError(t, os.WriteFile(filepath.Join(env.Workdir, "httphello-port"), []byte(fmt.Sprintf("%d", port)), 0755)) }, - Finalize: func(t *testing.T, workdir string) { - p, err := os.FindProcess(utils.ReadInt(t, filepath.Join(workdir, "httphello-pid"))) + Finalize: func(t *testing.T, env utils.Env) { + p, err := os.FindProcess(utils.ReadInt(t, filepath.Join(env.Workdir, "httphello-pid"))) assert.NilError(t, err) assert.NilError(t, p.Kill()) if _, err := p.Wait(); err != nil { t.Logf("hello server error: %v\n", err) } - utils.DonePort(utils.ReadInt(t, filepath.Join(workdir, "httphello-wasi-port"))) + utils.DonePort(utils.ReadInt(t, filepath.Join(env.Workdir, "httphello-wasi-port"))) }, Runtime: "c2w-net", - RuntimeOpts: func(t *testing.T, workdir string) []string { - t.Logf("wasi-addr is %s", fmt.Sprintf("localhost:%d", utils.ReadInt(t, filepath.Join(workdir, "httphello-wasi-port")))) - return []string{"--invoke", "--wasi-addr", fmt.Sprintf("localhost:%d", utils.ReadInt(t, filepath.Join(workdir, "httphello-wasi-port")))} + RuntimeOpts: func(t *testing.T, env utils.Env) []string { + t.Logf("wasi-addr is %s", fmt.Sprintf("localhost:%d", utils.ReadInt(t, filepath.Join(env.Workdir, "httphello-wasi-port")))) + return []string{"--invoke", "--wasi-addr", fmt.Sprintf("localhost:%d", utils.ReadInt(t, filepath.Join(env.Workdir, "httphello-wasi-port")))} }, - Args: func(t *testing.T, workdir string) []string { - t.Logf("RUNNING: %s", fmt.Sprintf("wget -q -O - http://%s:%d/", hostVirtIP, utils.ReadInt(t, filepath.Join(workdir, "httphello-port")))) - return []string{"--net=socket", "sh", "-c", fmt.Sprintf("wget -q -O - http://%s:%d/", hostVirtIP, utils.ReadInt(t, filepath.Join(workdir, "httphello-port")))} + Args: func(t *testing.T, env utils.Env) []string { + t.Logf("RUNNING: %s", fmt.Sprintf("wget -q -O - http://%s:%d/", hostVirtIP, utils.ReadInt(t, filepath.Join(env.Workdir, "httphello-port")))) + return []string{"--net=socket", "sh", "-c", fmt.Sprintf("wget -q -O - http://%s:%d/", hostVirtIP, utils.ReadInt(t, filepath.Join(env.Workdir, "httphello-port")))} }, Want: utils.WantString("hello"), }, @@ -178,27 +178,27 @@ COPY --from=dev /out/httphello / ENTRYPOINT ["/httphello", "0.0.0.0:80"] `}, }, - Prepare: func(t *testing.T, workdir string) { - assert.NilError(t, os.WriteFile(filepath.Join(workdir, "httphello-port"), []byte(fmt.Sprintf("%d", utils.GetPort(t))), 0755)) - assert.NilError(t, os.WriteFile(filepath.Join(workdir, "httphello-wasi-port"), []byte(fmt.Sprintf("%d", utils.GetPort(t))), 0755)) + Prepare: func(t *testing.T, env utils.Env) { + assert.NilError(t, os.WriteFile(filepath.Join(env.Workdir, "httphello-port"), []byte(fmt.Sprintf("%d", utils.GetPort(t))), 0755)) + assert.NilError(t, os.WriteFile(filepath.Join(env.Workdir, "httphello-wasi-port"), []byte(fmt.Sprintf("%d", utils.GetPort(t))), 0755)) }, - Finalize: func(t *testing.T, workdir string) { - utils.DonePort(utils.ReadInt(t, filepath.Join(workdir, "httphello-port"))) - utils.DonePort(utils.ReadInt(t, filepath.Join(workdir, "httphello-wasi-port"))) + Finalize: func(t *testing.T, env utils.Env) { + utils.DonePort(utils.ReadInt(t, filepath.Join(env.Workdir, "httphello-port"))) + utils.DonePort(utils.ReadInt(t, filepath.Join(env.Workdir, "httphello-wasi-port"))) }, Runtime: "c2w-net", - RuntimeOpts: func(t *testing.T, workdir string) []string { - t.Logf("wasi-addr is %s", fmt.Sprintf("localhost:%d", utils.ReadInt(t, filepath.Join(workdir, "httphello-wasi-port")))) - wasiPort := utils.ReadInt(t, filepath.Join(workdir, "httphello-wasi-port")) - port := utils.ReadInt(t, filepath.Join(workdir, "httphello-port")) + RuntimeOpts: func(t *testing.T, env utils.Env) []string { + t.Logf("wasi-addr is %s", fmt.Sprintf("localhost:%d", utils.ReadInt(t, filepath.Join(env.Workdir, "httphello-wasi-port")))) + wasiPort := utils.ReadInt(t, filepath.Join(env.Workdir, "httphello-wasi-port")) + port := utils.ReadInt(t, filepath.Join(env.Workdir, "httphello-port")) t.Logf("port is %s", fmt.Sprintf("localhost:%d:80", port)) return []string{"--invoke", "--wasi-addr", fmt.Sprintf("localhost:%d", wasiPort), "-p", fmt.Sprintf("localhost:%d:80", port)} }, - Args: func(t *testing.T, workdir string) []string { + Args: func(t *testing.T, env utils.Env) []string { return []string{"--net=socket"} }, - Want: func(t *testing.T, workdir string, in io.Writer, out io.Reader) { - port := utils.ReadInt(t, filepath.Join(workdir, "httphello-port")) + Want: func(t *testing.T, env utils.Env, in io.Writer, out io.Reader) { + port := utils.ReadInt(t, filepath.Join(env.Workdir, "httphello-port")) cmd := exec.Command("wget", "-q", "-O", "-", fmt.Sprintf("localhost:%d", port)) cmd.Stderr = os.Stderr d, err := cmd.Output() @@ -214,8 +214,8 @@ ENTRYPOINT ["/httphello", "0.0.0.0:80"] {Image: "alpine:3.17", Architecture: utils.X86_64}, {Image: "riscv64/alpine:20221110", ConvertOpts: []string{"--target-arch=riscv64"}, Architecture: utils.RISCV64}, }, - Prepare: func(t *testing.T, workdir string) { - assert.NilError(t, os.WriteFile(filepath.Join(workdir, "httphello-wasi-port"), []byte(fmt.Sprintf("%d", utils.GetPort(t))), 0755)) + Prepare: func(t *testing.T, env utils.Env) { + assert.NilError(t, os.WriteFile(filepath.Join(env.Workdir, "httphello-wasi-port"), []byte(fmt.Sprintf("%d", utils.GetPort(t))), 0755)) pid, port := utils.StartHelloServer(t) var v [5]int64 for i := 0; i < 5; i++ { @@ -223,28 +223,28 @@ ENTRYPOINT ["/httphello", "0.0.0.0:80"] assert.NilError(t, err) v[i] = n.Int64() } - assert.NilError(t, os.WriteFile(filepath.Join(workdir, "mac"), []byte(fmt.Sprintf("02:%02x:%02x:%02x:%02x:%02x", v[0], v[1], v[2], v[3], v[4])), 0755)) - assert.NilError(t, os.WriteFile(filepath.Join(workdir, "httphello-pid"), []byte(fmt.Sprintf("%d", pid)), 0755)) - assert.NilError(t, os.WriteFile(filepath.Join(workdir, "httphello-port"), []byte(fmt.Sprintf("%d", port)), 0755)) + assert.NilError(t, os.WriteFile(filepath.Join(env.Workdir, "mac"), []byte(fmt.Sprintf("02:%02x:%02x:%02x:%02x:%02x", v[0], v[1], v[2], v[3], v[4])), 0755)) + assert.NilError(t, os.WriteFile(filepath.Join(env.Workdir, "httphello-pid"), []byte(fmt.Sprintf("%d", pid)), 0755)) + assert.NilError(t, os.WriteFile(filepath.Join(env.Workdir, "httphello-port"), []byte(fmt.Sprintf("%d", port)), 0755)) }, - Finalize: func(t *testing.T, workdir string) { - p, err := os.FindProcess(utils.ReadInt(t, filepath.Join(workdir, "httphello-pid"))) + Finalize: func(t *testing.T, env utils.Env) { + p, err := os.FindProcess(utils.ReadInt(t, filepath.Join(env.Workdir, "httphello-pid"))) assert.NilError(t, err) assert.NilError(t, p.Kill()) if _, err := p.Wait(); err != nil { t.Logf("hello server error: %v\n", err) } - utils.DonePort(utils.ReadInt(t, filepath.Join(workdir, "httphello-wasi-port"))) + utils.DonePort(utils.ReadInt(t, filepath.Join(env.Workdir, "httphello-wasi-port"))) }, Runtime: "c2w-net", - RuntimeOpts: func(t *testing.T, workdir string) []string { - t.Logf("wasi-addr is %s", fmt.Sprintf("localhost:%d", utils.ReadInt(t, filepath.Join(workdir, "httphello-wasi-port")))) - return []string{"--invoke", "--wasi-addr", fmt.Sprintf("localhost:%d", utils.ReadInt(t, filepath.Join(workdir, "httphello-wasi-port")))} + RuntimeOpts: func(t *testing.T, env utils.Env) []string { + t.Logf("wasi-addr is %s", fmt.Sprintf("localhost:%d", utils.ReadInt(t, filepath.Join(env.Workdir, "httphello-wasi-port")))) + return []string{"--invoke", "--wasi-addr", fmt.Sprintf("localhost:%d", utils.ReadInt(t, filepath.Join(env.Workdir, "httphello-wasi-port")))} }, - Args: func(t *testing.T, workdir string) []string { - t.Logf("RUNNING: %s", fmt.Sprintf("wget -q -O - http://%s:%d/", hostVirtIP, utils.ReadInt(t, filepath.Join(workdir, "httphello-port")))) - t.Logf("MAC: %s", utils.ReadString(t, filepath.Join(workdir, "mac"))) - return []string{"--net=socket", fmt.Sprintf("--mac=%s", utils.ReadString(t, filepath.Join(workdir, "mac"))), "sh"} + Args: func(t *testing.T, env utils.Env) []string { + t.Logf("RUNNING: %s", fmt.Sprintf("wget -q -O - http://%s:%d/", hostVirtIP, utils.ReadInt(t, filepath.Join(env.Workdir, "httphello-port")))) + t.Logf("MAC: %s", utils.ReadString(t, filepath.Join(env.Workdir, "mac"))) + return []string{"--net=socket", fmt.Sprintf("--mac=%s", utils.ReadString(t, filepath.Join(env.Workdir, "mac"))), "sh"} }, Want: utils.WantPromptWithWorkdir("/ # ", func(workdir string) [][2]string { diff --git a/tests/integration/wazero_test.go b/tests/integration/wazero_test.go index 3866713..3b49ece 100644 --- a/tests/integration/wazero_test.go +++ b/tests/integration/wazero_test.go @@ -44,19 +44,19 @@ func TestWazero(t *testing.T) { {Image: "alpine:3.17", Architecture: utils.X86_64}, {Image: "riscv64/alpine:20221110", ConvertOpts: []string{"--target-arch=riscv64"}, Architecture: utils.RISCV64}, }, - Prepare: func(t *testing.T, workdir string) { - mapdirTestDir := filepath.Join(workdir, "wazero-mapdirtest/testdir") + Prepare: func(t *testing.T, env utils.Env) { + mapdirTestDir := filepath.Join(env.Workdir, "wazero-mapdirtest/testdir") assert.NilError(t, os.MkdirAll(mapdirTestDir, 0755)) assert.NilError(t, os.WriteFile(filepath.Join(mapdirTestDir, "hi"), []byte("teststring"), 0755)) }, - Finalize: func(t *testing.T, workdir string) { - mapdirTestDir := filepath.Join(workdir, "wazero-mapdirtest/testdir") + Finalize: func(t *testing.T, env utils.Env) { + mapdirTestDir := filepath.Join(env.Workdir, "wazero-mapdirtest/testdir") assert.NilError(t, os.Remove(filepath.Join(mapdirTestDir, "hi"))) assert.NilError(t, os.Remove(mapdirTestDir)) }, - RuntimeOpts: func(t *testing.T, workdir string) []string { + RuntimeOpts: func(t *testing.T, env utils.Env) []string { // NOTE: wazero supports single-level mapped directory - return []string{"--mapdir=/mapdir::" + filepath.Join(workdir, "wazero-mapdirtest/testdir")} + return []string{"--mapdir=/mapdir::" + filepath.Join(env.Workdir, "wazero-mapdirtest/testdir")} }, Args: utils.StringFlags("cat", "/mapdir/hi"), Want: utils.WantString("teststring"), @@ -81,13 +81,13 @@ func TestWazero(t *testing.T) { {Image: "alpine:3.17", Architecture: utils.X86_64}, {Image: "riscv64/alpine:20221110", ConvertOpts: []string{"--target-arch=riscv64"}, Architecture: utils.RISCV64}, }, - Prepare: func(t *testing.T, workdir string) { - mapdirTestDir := filepath.Join(workdir, "wazero-mapdirtest-io") + Prepare: func(t *testing.T, env utils.Env) { + mapdirTestDir := filepath.Join(env.Workdir, "wazero-mapdirtest-io") assert.NilError(t, os.MkdirAll(mapdirTestDir, 0755)) assert.NilError(t, os.WriteFile(filepath.Join(mapdirTestDir, "hi"), []byte("teststring"), 0755)) }, - Finalize: func(t *testing.T, workdir string) { - mapdirTestDir := filepath.Join(workdir, "wazero-mapdirtest-io") + Finalize: func(t *testing.T, env utils.Env) { + mapdirTestDir := filepath.Join(env.Workdir, "wazero-mapdirtest-io") // check data from guest data, err := os.ReadFile(filepath.Join(mapdirTestDir, "from-guest", "testhello")) @@ -100,9 +100,9 @@ func TestWazero(t *testing.T) { assert.NilError(t, os.Remove(filepath.Join(mapdirTestDir, "hi"))) assert.NilError(t, os.Remove(mapdirTestDir)) }, - RuntimeOpts: func(t *testing.T, workdir string) []string { + RuntimeOpts: func(t *testing.T, env utils.Env) []string { // NOTE: wazero supports single-level mapped directory - return []string{"--mapdir=/mapdir::" + filepath.Join(workdir, "wazero-mapdirtest-io")} + return []string{"--mapdir=/mapdir::" + filepath.Join(env.Workdir, "wazero-mapdirtest-io")} }, Args: utils.StringFlags("sh"), Want: utils.WantPrompt("/ # ", @@ -135,32 +135,32 @@ FROM riscv64/debian:sid-slim RUN apt-get update && apt-get install -y wget `}, }, - Prepare: func(t *testing.T, workdir string) { - assert.NilError(t, os.WriteFile(filepath.Join(workdir, "httphello-vm-port"), []byte(fmt.Sprintf("%d", utils.GetPort(t))), 0755)) - assert.NilError(t, os.WriteFile(filepath.Join(workdir, "httphello-stack-port"), []byte(fmt.Sprintf("%d", utils.GetPort(t))), 0755)) + Prepare: func(t *testing.T, env utils.Env) { + assert.NilError(t, os.WriteFile(filepath.Join(env.Workdir, "httphello-vm-port"), []byte(fmt.Sprintf("%d", utils.GetPort(t))), 0755)) + assert.NilError(t, os.WriteFile(filepath.Join(env.Workdir, "httphello-stack-port"), []byte(fmt.Sprintf("%d", utils.GetPort(t))), 0755)) pid, port := utils.StartHelloServer(t) - assert.NilError(t, os.WriteFile(filepath.Join(workdir, "httphello-pid"), []byte(fmt.Sprintf("%d", pid)), 0755)) - assert.NilError(t, os.WriteFile(filepath.Join(workdir, "httphello-port"), []byte(fmt.Sprintf("%d", port)), 0755)) + assert.NilError(t, os.WriteFile(filepath.Join(env.Workdir, "httphello-pid"), []byte(fmt.Sprintf("%d", pid)), 0755)) + assert.NilError(t, os.WriteFile(filepath.Join(env.Workdir, "httphello-port"), []byte(fmt.Sprintf("%d", port)), 0755)) }, - Finalize: func(t *testing.T, workdir string) { - p, err := os.FindProcess(utils.ReadInt(t, filepath.Join(workdir, "httphello-pid"))) + Finalize: func(t *testing.T, env utils.Env) { + p, err := os.FindProcess(utils.ReadInt(t, filepath.Join(env.Workdir, "httphello-pid"))) assert.NilError(t, err) assert.NilError(t, p.Kill()) if _, err := p.Wait(); err != nil { t.Logf("hello server error: %v\n", err) } - utils.DonePort(utils.ReadInt(t, filepath.Join(workdir, "httphello-vm-port"))) - utils.DonePort(utils.ReadInt(t, filepath.Join(workdir, "httphello-stack-port"))) + utils.DonePort(utils.ReadInt(t, filepath.Join(env.Workdir, "httphello-vm-port"))) + utils.DonePort(utils.ReadInt(t, filepath.Join(env.Workdir, "httphello-stack-port"))) }, - RuntimeOpts: func(t *testing.T, workdir string) []string { + RuntimeOpts: func(t *testing.T, env utils.Env) []string { return []string{ "--stack=" + utils.C2wNetProxyBin, - fmt.Sprintf("--stack-port=%d", utils.ReadInt(t, filepath.Join(workdir, "httphello-stack-port"))), - fmt.Sprintf("--vm-port=%d", utils.ReadInt(t, filepath.Join(workdir, "httphello-vm-port"))), + fmt.Sprintf("--stack-port=%d", utils.ReadInt(t, filepath.Join(env.Workdir, "httphello-stack-port"))), + fmt.Sprintf("--vm-port=%d", utils.ReadInt(t, filepath.Join(env.Workdir, "httphello-vm-port"))), } }, - Args: func(t *testing.T, workdir string) []string { - return []string{"--net=socket=listenfd=4", "sh", "-c", fmt.Sprintf("for I in $(seq 1 50) ; do if wget -O - http://127.0.0.1:%d/ 2>/dev/null ; then break ; fi ; sleep 1 ; done", utils.ReadInt(t, filepath.Join(workdir, "httphello-port")))} + Args: func(t *testing.T, env utils.Env) []string { + return []string{"--net=socket=listenfd=4", "sh", "-c", fmt.Sprintf("for I in $(seq 1 50) ; do if wget -O - http://127.0.0.1:%d/ 2>/dev/null ; then break ; fi ; sleep 1 ; done", utils.ReadInt(t, filepath.Join(env.Workdir, "httphello-port")))} }, Want: utils.WantString("hello"), }, @@ -170,29 +170,29 @@ RUN apt-get update && apt-get install -y wget {Image: "alpine:3.17", Architecture: utils.X86_64}, {Image: "riscv64/alpine:20221110", ConvertOpts: []string{"--target-arch=riscv64"}, Architecture: utils.RISCV64}, }, - Prepare: func(t *testing.T, workdir string) { - assert.NilError(t, os.WriteFile(filepath.Join(workdir, "httphello-wasi-port"), []byte(fmt.Sprintf("%d", utils.GetPort(t))), 0755)) + Prepare: func(t *testing.T, env utils.Env) { + assert.NilError(t, os.WriteFile(filepath.Join(env.Workdir, "httphello-wasi-port"), []byte(fmt.Sprintf("%d", utils.GetPort(t))), 0755)) pid, port := utils.StartHelloServer(t) - assert.NilError(t, os.WriteFile(filepath.Join(workdir, "httphello-pid"), []byte(fmt.Sprintf("%d", pid)), 0755)) - assert.NilError(t, os.WriteFile(filepath.Join(workdir, "httphello-port"), []byte(fmt.Sprintf("%d", port)), 0755)) + assert.NilError(t, os.WriteFile(filepath.Join(env.Workdir, "httphello-pid"), []byte(fmt.Sprintf("%d", pid)), 0755)) + assert.NilError(t, os.WriteFile(filepath.Join(env.Workdir, "httphello-port"), []byte(fmt.Sprintf("%d", port)), 0755)) }, - Finalize: func(t *testing.T, workdir string) { - p, err := os.FindProcess(utils.ReadInt(t, filepath.Join(workdir, "httphello-pid"))) + Finalize: func(t *testing.T, env utils.Env) { + p, err := os.FindProcess(utils.ReadInt(t, filepath.Join(env.Workdir, "httphello-pid"))) assert.NilError(t, err) assert.NilError(t, p.Kill()) if _, err := p.Wait(); err != nil { t.Logf("hello server error: %v\n", err) } - utils.DonePort(utils.ReadInt(t, filepath.Join(workdir, "httphello-wasi-port"))) + utils.DonePort(utils.ReadInt(t, filepath.Join(env.Workdir, "httphello-wasi-port"))) }, Runtime: "wazero-test", - RuntimeOpts: func(t *testing.T, workdir string) []string { - t.Logf("wasi-addr is %s", fmt.Sprintf("localhost:%d", utils.ReadInt(t, filepath.Join(workdir, "httphello-wasi-port")))) - return []string{"-net", "--wasi-addr", fmt.Sprintf("localhost:%d", utils.ReadInt(t, filepath.Join(workdir, "httphello-wasi-port")))} + RuntimeOpts: func(t *testing.T, env utils.Env) []string { + t.Logf("wasi-addr is %s", fmt.Sprintf("localhost:%d", utils.ReadInt(t, filepath.Join(env.Workdir, "httphello-wasi-port")))) + return []string{"-net", "--wasi-addr", fmt.Sprintf("localhost:%d", utils.ReadInt(t, filepath.Join(env.Workdir, "httphello-wasi-port")))} }, - Args: func(t *testing.T, workdir string) []string { - t.Logf("RUNNING: %s", fmt.Sprintf("wget -q -O - http://%s:%d/", hostVirtIP, utils.ReadInt(t, filepath.Join(workdir, "httphello-port")))) - return []string{"--net=socket", "sh", "-c", fmt.Sprintf("wget -q -O - http://%s:%d/", hostVirtIP, utils.ReadInt(t, filepath.Join(workdir, "httphello-port")))} + Args: func(t *testing.T, env utils.Env) []string { + t.Logf("RUNNING: %s", fmt.Sprintf("wget -q -O - http://%s:%d/", hostVirtIP, utils.ReadInt(t, filepath.Join(env.Workdir, "httphello-port")))) + return []string{"--net=socket", "sh", "-c", fmt.Sprintf("wget -q -O - http://%s:%d/", hostVirtIP, utils.ReadInt(t, filepath.Join(env.Workdir, "httphello-port")))} }, Want: utils.WantString("hello"), }, @@ -220,27 +220,27 @@ COPY --from=dev /out/httphello / ENTRYPOINT ["/httphello", "0.0.0.0:80"] `}, }, - Prepare: func(t *testing.T, workdir string) { - assert.NilError(t, os.WriteFile(filepath.Join(workdir, "httphello-port"), []byte(fmt.Sprintf("%d", utils.GetPort(t))), 0755)) - assert.NilError(t, os.WriteFile(filepath.Join(workdir, "httphello-wasi-port"), []byte(fmt.Sprintf("%d", utils.GetPort(t))), 0755)) + Prepare: func(t *testing.T, env utils.Env) { + assert.NilError(t, os.WriteFile(filepath.Join(env.Workdir, "httphello-port"), []byte(fmt.Sprintf("%d", utils.GetPort(t))), 0755)) + assert.NilError(t, os.WriteFile(filepath.Join(env.Workdir, "httphello-wasi-port"), []byte(fmt.Sprintf("%d", utils.GetPort(t))), 0755)) }, - Finalize: func(t *testing.T, workdir string) { - utils.DonePort(utils.ReadInt(t, filepath.Join(workdir, "httphello-port"))) - utils.DonePort(utils.ReadInt(t, filepath.Join(workdir, "httphello-wasi-port"))) + Finalize: func(t *testing.T, env utils.Env) { + utils.DonePort(utils.ReadInt(t, filepath.Join(env.Workdir, "httphello-port"))) + utils.DonePort(utils.ReadInt(t, filepath.Join(env.Workdir, "httphello-wasi-port"))) }, Runtime: "wazero-test", - RuntimeOpts: func(t *testing.T, workdir string) []string { - t.Logf("wasi-addr is %s", fmt.Sprintf("localhost:%d", utils.ReadInt(t, filepath.Join(workdir, "httphello-wasi-port")))) - wasiPort := utils.ReadInt(t, filepath.Join(workdir, "httphello-wasi-port")) - port := utils.ReadInt(t, filepath.Join(workdir, "httphello-port")) + RuntimeOpts: func(t *testing.T, env utils.Env) []string { + t.Logf("wasi-addr is %s", fmt.Sprintf("localhost:%d", utils.ReadInt(t, filepath.Join(env.Workdir, "httphello-wasi-port")))) + wasiPort := utils.ReadInt(t, filepath.Join(env.Workdir, "httphello-wasi-port")) + port := utils.ReadInt(t, filepath.Join(env.Workdir, "httphello-port")) t.Logf("port is %s", fmt.Sprintf("localhost:%d:80", port)) return []string{"-net", "--wasi-addr", fmt.Sprintf("localhost:%d", wasiPort), "-p", fmt.Sprintf("localhost:%d:80", port)} }, - Args: func(t *testing.T, workdir string) []string { + Args: func(t *testing.T, env utils.Env) []string { return []string{"--net=socket"} }, - Want: func(t *testing.T, workdir string, in io.Writer, out io.Reader) { - port := utils.ReadInt(t, filepath.Join(workdir, "httphello-port")) + Want: func(t *testing.T, env utils.Env, in io.Writer, out io.Reader) { + port := utils.ReadInt(t, filepath.Join(env.Workdir, "httphello-port")) cmd := exec.Command("wget", "-q", "-O", "-", fmt.Sprintf("localhost:%d", port)) cmd.Stderr = os.Stderr d, err := cmd.Output() @@ -256,8 +256,8 @@ ENTRYPOINT ["/httphello", "0.0.0.0:80"] {Image: "alpine:3.17", Architecture: utils.X86_64}, {Image: "riscv64/alpine:20221110", ConvertOpts: []string{"--target-arch=riscv64"}, Architecture: utils.RISCV64}, }, - Prepare: func(t *testing.T, workdir string) { - assert.NilError(t, os.WriteFile(filepath.Join(workdir, "httphello-wasi-port"), []byte(fmt.Sprintf("%d", utils.GetPort(t))), 0755)) + Prepare: func(t *testing.T, env utils.Env) { + assert.NilError(t, os.WriteFile(filepath.Join(env.Workdir, "httphello-wasi-port"), []byte(fmt.Sprintf("%d", utils.GetPort(t))), 0755)) pid, port := utils.StartHelloServer(t) var v [5]int64 for i := 0; i < 5; i++ { @@ -265,28 +265,28 @@ ENTRYPOINT ["/httphello", "0.0.0.0:80"] assert.NilError(t, err) v[i] = n.Int64() } - assert.NilError(t, os.WriteFile(filepath.Join(workdir, "mac"), []byte(fmt.Sprintf("02:%02x:%02x:%02x:%02x:%02x", v[0], v[1], v[2], v[3], v[4])), 0755)) - assert.NilError(t, os.WriteFile(filepath.Join(workdir, "httphello-pid"), []byte(fmt.Sprintf("%d", pid)), 0755)) - assert.NilError(t, os.WriteFile(filepath.Join(workdir, "httphello-port"), []byte(fmt.Sprintf("%d", port)), 0755)) + assert.NilError(t, os.WriteFile(filepath.Join(env.Workdir, "mac"), []byte(fmt.Sprintf("02:%02x:%02x:%02x:%02x:%02x", v[0], v[1], v[2], v[3], v[4])), 0755)) + assert.NilError(t, os.WriteFile(filepath.Join(env.Workdir, "httphello-pid"), []byte(fmt.Sprintf("%d", pid)), 0755)) + assert.NilError(t, os.WriteFile(filepath.Join(env.Workdir, "httphello-port"), []byte(fmt.Sprintf("%d", port)), 0755)) }, - Finalize: func(t *testing.T, workdir string) { - p, err := os.FindProcess(utils.ReadInt(t, filepath.Join(workdir, "httphello-pid"))) + Finalize: func(t *testing.T, env utils.Env) { + p, err := os.FindProcess(utils.ReadInt(t, filepath.Join(env.Workdir, "httphello-pid"))) assert.NilError(t, err) assert.NilError(t, p.Kill()) if _, err := p.Wait(); err != nil { t.Logf("hello server error: %v\n", err) } - utils.DonePort(utils.ReadInt(t, filepath.Join(workdir, "httphello-wasi-port"))) + utils.DonePort(utils.ReadInt(t, filepath.Join(env.Workdir, "httphello-wasi-port"))) }, Runtime: "wazero-test", - RuntimeOpts: func(t *testing.T, workdir string) []string { - t.Logf("wasi-addr is %s", fmt.Sprintf("localhost:%d", utils.ReadInt(t, filepath.Join(workdir, "httphello-wasi-port")))) - return []string{"-net", "--wasi-addr", fmt.Sprintf("localhost:%d", utils.ReadInt(t, filepath.Join(workdir, "httphello-wasi-port")))} + RuntimeOpts: func(t *testing.T, env utils.Env) []string { + t.Logf("wasi-addr is %s", fmt.Sprintf("localhost:%d", utils.ReadInt(t, filepath.Join(env.Workdir, "httphello-wasi-port")))) + return []string{"-net", "--wasi-addr", fmt.Sprintf("localhost:%d", utils.ReadInt(t, filepath.Join(env.Workdir, "httphello-wasi-port")))} }, - Args: func(t *testing.T, workdir string) []string { - t.Logf("RUNNING: %s", fmt.Sprintf("wget -q -O - http://%s:%d/", hostVirtIP, utils.ReadInt(t, filepath.Join(workdir, "httphello-port")))) - t.Logf("MAC: %s", utils.ReadString(t, filepath.Join(workdir, "mac"))) - return []string{"--net=socket", fmt.Sprintf("--mac=%s", utils.ReadString(t, filepath.Join(workdir, "mac"))), "sh"} + Args: func(t *testing.T, env utils.Env) []string { + t.Logf("RUNNING: %s", fmt.Sprintf("wget -q -O - http://%s:%d/", hostVirtIP, utils.ReadInt(t, filepath.Join(env.Workdir, "httphello-port")))) + t.Logf("MAC: %s", utils.ReadString(t, filepath.Join(env.Workdir, "mac"))) + return []string{"--net=socket", fmt.Sprintf("--mac=%s", utils.ReadString(t, filepath.Join(env.Workdir, "mac"))), "sh"} }, Want: utils.WantPromptWithWorkdir("/ # ", func(workdir string) [][2]string { @@ -297,5 +297,72 @@ ENTRYPOINT ["/httphello", "0.0.0.0:80"] }, ), }, + { + Name: "wazero-imagemounter-registry", + Runtime: "imagemounter-test", + Inputs: []utils.Input{ + {Image: "alpine:3.17", Mirror: true, Architecture: utils.X86_64, ConvertOpts: []string{"--external-bundle"}, External: true}, + {Image: "ghcr.io/stargz-containers/ubuntu:22.04-esgz", Mirror: true, Architecture: utils.X86_64, ConvertOpts: []string{"--external-bundle"}, External: true}, + {Image: "riscv64/alpine:20221110", Mirror: true, Architecture: utils.RISCV64, ConvertOpts: []string{"--target-arch=riscv64", "--external-bundle"}, External: true}, + }, + Prepare: func(t *testing.T, env utils.Env) { + assert.NilError(t, os.WriteFile(filepath.Join(env.Workdir, "imagemountertest-vm-port"), []byte(fmt.Sprintf("%d", utils.GetPort(t))), 0755)) + assert.NilError(t, os.WriteFile(filepath.Join(env.Workdir, "imagemountertest-stack-port"), []byte(fmt.Sprintf("%d", utils.GetPort(t))), 0755)) + }, + Finalize: func(t *testing.T, env utils.Env) { + utils.DonePort(utils.ReadInt(t, filepath.Join(env.Workdir, "imagemountertest-vm-port"))) + utils.DonePort(utils.ReadInt(t, filepath.Join(env.Workdir, "imagemountertest-stack-port"))) + }, + RuntimeOpts: func(t *testing.T, env utils.Env) []string { + return []string{ + "--image", "localhost:5000/" + env.Input.Image, + "--stack", utils.ImageMounterBin, + fmt.Sprintf("--stack-port=%d", utils.ReadInt(t, filepath.Join(env.Workdir, "imagemountertest-stack-port"))), + fmt.Sprintf("--vm-port=%d", utils.ReadInt(t, filepath.Join(env.Workdir, "imagemountertest-vm-port"))), + } + }, + Args: func(t *testing.T, env utils.Env) []string { + return []string{"--net=socket=listenfd=4", "--external-bundle=9p=192.168.127.252", "echo", "-n", "hello"} + }, + Want: utils.WantString("hello"), + }, + { + Name: "wazero-imagemounter-store", + Runtime: "imagemounter-test", + Inputs: []utils.Input{ + {Image: "alpine:3.17", Store: "testimage", Architecture: utils.X86_64, ConvertOpts: []string{"--external-bundle"}, External: true}, + {Image: "ghcr.io/stargz-containers/ubuntu:22.04-esgz", Store: "testimage", Architecture: utils.X86_64, ConvertOpts: []string{"--external-bundle"}, External: true}, + {Image: "riscv64/alpine:20221110", Store: "testimage", Architecture: utils.RISCV64, ConvertOpts: []string{"--target-arch=riscv64", "--external-bundle"}, External: true}, + }, + Prepare: func(t *testing.T, env utils.Env) { + assert.NilError(t, os.WriteFile(filepath.Join(env.Workdir, "imagemountertest-vm-port"), []byte(fmt.Sprintf("%d", utils.GetPort(t))), 0755)) + assert.NilError(t, os.WriteFile(filepath.Join(env.Workdir, "imagemountertest-stack-port"), []byte(fmt.Sprintf("%d", utils.GetPort(t))), 0755)) + pid, port := utils.StartDirServer(t, filepath.Join(env.Workdir, env.Input.Store)) + assert.NilError(t, os.WriteFile(filepath.Join(env.Workdir, "httpdir-pid"), []byte(fmt.Sprintf("%d", pid)), 0755)) + assert.NilError(t, os.WriteFile(filepath.Join(env.Workdir, "httpdir-port"), []byte(fmt.Sprintf("%d", port)), 0755)) + }, + Finalize: func(t *testing.T, env utils.Env) { + p, err := os.FindProcess(utils.ReadInt(t, filepath.Join(env.Workdir, "httpdir-pid"))) + assert.NilError(t, err) + assert.NilError(t, p.Kill()) + if _, err := p.Wait(); err != nil { + t.Logf("dir server error: %v\n", err) + } + utils.DonePort(utils.ReadInt(t, filepath.Join(env.Workdir, "imagemountertest-vm-port"))) + utils.DonePort(utils.ReadInt(t, filepath.Join(env.Workdir, "imagemountertest-stack-port"))) + }, + RuntimeOpts: func(t *testing.T, env utils.Env) []string { + return []string{ + "--image", fmt.Sprintf("http://localhost:%d/", utils.ReadInt(t, filepath.Join(env.Workdir, "httpdir-port"))), + "--stack", utils.ImageMounterBin, + fmt.Sprintf("--stack-port=%d", utils.ReadInt(t, filepath.Join(env.Workdir, "imagemountertest-stack-port"))), + fmt.Sprintf("--vm-port=%d", utils.ReadInt(t, filepath.Join(env.Workdir, "imagemountertest-vm-port"))), + } + }, + Args: func(t *testing.T, env utils.Env) []string { + return []string{"--net=socket=listenfd=4", "--external-bundle=9p=192.168.127.252", "echo", "-n", "hello"} + }, + Want: utils.WantString("hello"), + }, }...) } diff --git a/tests/test.sh b/tests/test.sh index a5b9872..5582661 100755 --- a/tests/test.sh +++ b/tests/test.sh @@ -12,5 +12,7 @@ do echo "retrying..." sleep 3 done +docker exec -w /test $CONTAINER docker run --rm -d --name testregistry -p 127.0.0.1:5000:5000 registry:2 +docker exec -w /test $CONTAINER docker buildx create --name container --driver=docker-container docker exec -w /test $CONTAINER go test ${GO_TEST_FLAGS:-} -timeout $TIMEOUT -v ./tests/integration docker kill $CONTAINER