diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e0155de --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +/.vscode +/snapshotter +/ctr-cli +/experiments +/test.dimg +/*.cdimg +/httpd-* +/mysql-* +/postgres-* +/redis-* +*.log +/images +/diff +/pack +/server diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4709147 --- /dev/null +++ b/Makefile @@ -0,0 +1,24 @@ +.PHONY: snapshotter ctr-cli diff-tools server + +all: snapshotter ctr-cli diff-tools server + +run: + make clean + make snapshotter + sudo ./snapshotter + +snapshotter: + go build -o snapshotter ./cmd/snapshotter + +ctr-cli: + go build -o ctr-cli ./cmd/ctr-cli + +diff-tools: + go build -o diff ./cmd/diff + go build -o pack ./cmd/pack + +server: + go build -o server ./cmd/server + +clean: + rm -f snapshotter ctr-cli diff pack server diff --git a/README.md b/README.md new file mode 100644 index 0000000..5cb90c0 --- /dev/null +++ b/README.md @@ -0,0 +1,110 @@ +# D4C: Delta updating for Container images +This repository is PoC implementation of delta updating for containers. + +**D4C is in early development stage and more works are left.** + +# How to use +## Dependency +D4C depends on the below software. + +- go +- docker +- docker-squash +- containerd +- fuse3 + +For Ubuntu 22.04, install these packages. +```sh +sudo apt install -y docker-ce containerd.io golang-go fuse3 +pip install docker-squash +``` + +## Configure containerd +D4C provides snapshotter plugin for containerd. +Configure containerd with `install_snapshotter.sh` +```sh +./install_snapshotter.sh +``` + +## Build binaries +```sh +make all +``` + +## Squash docker image layers +```sh +mkdir images +./ctr-cli convert --image nginx:1.23.1 --output ./images/nginx-1.23.1 +./ctr-cli convert --image nginx:1.23.2 --output ./images/nginx-1.23.2 +./ctr-cli convert --image nginx:1.23.3 --output ./images/nginx-1.23.3 +./ctr-cli convert --image nginx:1.23.4 --output ./images/nginx-1.23.4 +``` + +## Extract files +```sh +mkdir ./images/nginx-1.23.1/root +sudo tar -xf ./images/nginx-1.23.1/layer.tar -C ./images/nginx-1.23.1/root +mkdir ./images/nginx-1.23.2/root +sudo tar -xf ./images/nginx-1.23.2/layer.tar -C ./images/nginx-1.23.2/root +mkdir ./images/nginx-1.23.3/root +sudo tar -xf ./images/nginx-1.23.3/layer.tar -C ./images/nginx-1.23.3/root +mkdir ./images/nginx-1.23.4/root +sudo tar -xf ./images/nginx-1.23.4/layer.tar -C ./images/nginx-1.23.4/root +``` + +## Generate deltas +```sh +sudo ./diff "" images/nginx-1.23.1/root images/base_nginx-1.23.1 images/base_nginx-1.23.1.json binary-diff +sudo ./diff images/nginx-1.23.1/root images/nginx-1.23.2/root images/diff_nginx-1.23.1-2 images/diff_nginx-1.23.1-2.json binary-diff +sudo ./diff images/nginx-1.23.2/root images/nginx-1.23.3/root images/diff_nginx-1.23.2-3 images/diff_nginx-1.23.2-3.json binary-diff +sudo ./diff images/nginx-1.23.3/root images/nginx-1.23.4/root images/diff_nginx-1.23.3-4 images/diff_nginx-1.23.3-4.json binary-diff +``` + +## Pack deltas +`.dimg` files are the body of delta files +```sh +sudo ./pack images/base_nginx-1.23.1 images/base_nginx-1.23.1.json "" images/base_nginx-1.23.1.dimg +sudo ./pack images/diff_nginx-1.23.1-2 images/diff_nginx-1.23.1-2.json images/base_nginx-1.23.1.dimg images/diff_nginx-1.23.1-2.dimg +sudo ./pack images/diff_nginx-1.23.2-3 images/diff_nginx-1.23.2-3.json images/diff_nginx-1.23.1-2.dimg images/diff_nginx-1.23.2-3.dimg +sudo ./pack images/diff_nginx-1.23.3-4 images/diff_nginx-1.23.3-4.json images/diff_nginx-1.23.2-3.dimg images/diff_nginx-1.23.3-4.dimg +``` + +## Pack deltas as portable format +`.cdimg` files are portable delta format. +Users can simply load container images with them. +```sh +./ctr-cli pack --manifest=./images/nginx-1.23.1/manifset.json --config=./images/nginx-1.23.1/config.json --dimg=./images/base_nginx-1.23.1.dimg --out=./images/base_nginx-1.23.1.cdimg +./ctr-cli pack --manifest=./images/nginx-1.23.2/manifset.json --config=./images/nginx-1.23.2/config.json --dimg=./images/diff_nginx-1.23.1-2.dimg --out=./images/diff_nginx-1.23.1-2.cdimg +./ctr-cli pack --manifest=./images/nginx-1.23.3/manifset.json --config=./images/nginx-1.23.3/config.json --dimg=./images/diff_nginx-1.23.2-3.dimg --out=./images/diff_nginx-1.23.2-3.cdimg +./ctr-cli pack --manifest=./images/nginx-1.23.4/manifset.json --config=./images/nginx-1.23.4/config.json --dimg=./images/diff_nginx-1.23.3-4.dimg --out=./images/diff_nginx-1.23.3-4.cdimg +``` + +## Run snapshotter plugin +```sh +sudo ./snapshotter +``` + +## Load images +```sh +sudo ./ctr-cli load --image=d4c-nginx:1.23.1 --dimg=./images/base_nginx-1.23.1.cdimg +sudo ./ctr-cli load --image=d4c-nginx:1.23.2 --dimg=./images/diff_nginx-1.23.1-2.cdimg +sudo ./ctr-cli load --image=d4c-nginx:1.23.3 --dimg=./images/diff_nginx-1.23.2-3.cdimg +sudo ./ctr-cli load --image=d4c-nginx:1.23.4 --dimg=./images/diff_nginx-1.23.3-4.cdimg +``` + +## Run container +```sh +sudo ctr run --rm --snapshotter=di3fs --net-host d4c-nginx:1.23.4 test-nginx-1.23.4 +``` + + +## Push container image deltas +```sh +./push_nginx.sh +``` + +## Pull container images +```sh +sudo ./ctr-cli pull --image d4c-nginx:1.23.1 --host localhost:8081 +sudo ./ctr-cli pull --image d4c-nginx:1.23.3 --host localhost:8081 +``` diff --git a/clean_fuse.sh b/clean_fuse.sh new file mode 100755 index 0000000..1c21125 --- /dev/null +++ b/clean_fuse.sh @@ -0,0 +1 @@ +sudo fusermount3 -u /tmp/di3fs/sn/snapshots/1/fs diff --git a/cmd/ctr-cli/convert/convert.go b/cmd/ctr-cli/convert/convert.go new file mode 100644 index 0000000..70031ee --- /dev/null +++ b/cmd/ctr-cli/convert/convert.go @@ -0,0 +1,263 @@ +package convert + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + + "github.com/containerd/containerd/log" + di3fsImage "github.com/naoki9911/fuse-diff-containerd/pkg/image" + "github.com/naoki9911/fuse-diff-containerd/pkg/utils" + v1 "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" +) + +var logger = log.G(context.TODO()) + +var Flags = []cli.Flag{ + &cli.StringFlag{ + Name: "image", + Usage: "image name to convert", + Required: true, + }, + &cli.StringFlag{ + Name: "output", + Usage: "output path", + Required: true, + }, + &cli.BoolFlag{ + Name: "dimg", + Usage: "output dimg image (Root required)", + Required: false, + }, + &cli.BoolFlag{ + Name: "cdimg", + Usage: "output cdimg image (Root required)", + Required: false, + }, +} + +var workDir = filepath.Join(os.TempDir(), "ctr-cli") + +type dockerImageManifest struct { + Config string `json:"Config"` + RepoTags []string `json:"RepoTags"` + Layers []string `json:"Layers"` +} + +func decodeDockerImageManifest(manifsetPath string) (*dockerImageManifest, error) { + f, err := os.Open(manifsetPath) + if err != nil { + return nil, fmt.Errorf("failed to open %q : %v", manifsetPath, err) + } + c, err := io.ReadAll(f) + if err != nil { + return nil, fmt.Errorf("failed to read : %v", err) + } + var man []dockerImageManifest + err = json.Unmarshal(c, &man) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal : %v", err) + } + + if len(man) != 1 { + logger.Errorf("invalid manifest: %v", man) + return nil, fmt.Errorf("invalid manifest") + } + + return &man[0], err +} + +func execCmd(name string, args ...string) { + cmd := exec.Command(name, args...) + var stderr bytes.Buffer + var stdout bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err := cmd.Run() + if err != nil { + logger.WithFields(logrus.Fields{ + "name": name, + "args": args, + }).Fatalf("failed to exec command : %v\n%v", err, stderr.String()) + } + logger.Debugf("exec done\n%v", stdout.String()) +} + +func Action(c *cli.Context) error { + image := c.String("image") + outputPath := c.String("output") + outputDimg := c.Bool("dimg") + outputCdimg := c.Bool("cdimg") + + if outputDimg && os.Geteuid() != 0 { + return fmt.Errorf("root required") + } + + imageSquashed := image + "-squashed" + imageSquashedPath := filepath.Join(workDir, imageSquashed+".tar") + imageDir := filepath.Join(workDir, image) + + os.RemoveAll(workDir) + err := os.MkdirAll(workDir, 0755) + if err != nil { + return err + } + err = os.MkdirAll(outputPath, 0755) + if err != nil { + return err + } + + logger.Infof("pulling image %q", image) + execCmd("docker", "pull", image) + logger.Info("pull done") + + logger.Infof("squashing image %q to %q", image, imageSquashed) + execCmd("docker-squash", "-t", imageSquashed, image) + logger.Info("squash done") + + logger.Infof("saving image %q to %q", imageSquashed, imageSquashedPath) + execCmd("docker", "save", imageSquashed, "-o", imageSquashedPath) + logger.Info("save done") + + logger.Infof("extracting %q to %q", imageSquashedPath, imageDir) + err = os.MkdirAll(imageDir, 0755) + if err != nil { + return err + } + execCmd("tar", "-C", imageDir, "-xvf", imageSquashedPath) + logger.Info("extract done") + + man, err := decodeDockerImageManifest(filepath.Join(imageDir, "manifest.json")) + if err != nil { + return err + } + + if len(man.Layers) != 1 { + logger.Errorf("unexpected manifest format \n%v", man) + return fmt.Errorf("unexpected manifset format") + } + + layerPath := filepath.Join(imageDir, man.Layers[0]) + layerNewPath := filepath.Join(outputPath, "layer.tar") + logger.Infof("moving %q to %q as base layer", layerPath, layerNewPath) + execCmd("mv", layerPath, layerNewPath) + logger.Info("move done") + + configPath := filepath.Join(imageDir, man.Config) + configNewPath := filepath.Join(outputPath, "config.json") + logger.Infof("moving %q to %q as config", configPath, configNewPath) + execCmd("mv", configPath, configNewPath) + logger.Info("move done") + + configSize, configDigest, err := utils.GetFileSizeAndDigest(configNewPath) + if err != nil { + return fmt.Errorf("failed to get size and digest for %q : %v", configNewPath, err) + } + + layerSize, layerDigest, err := utils.GetFileSizeAndDigest(layerNewPath) + if err != nil { + return fmt.Errorf("failed to get size and digest for %q : %v", configNewPath, err) + } + + manifest := v1.Manifest{ + MediaType: v1.MediaTypeImageManifest, + Config: v1.Descriptor{ + MediaType: v1.MediaTypeImageConfig, + Size: configSize, + Digest: *configDigest, + }, + Layers: []v1.Descriptor{ + { + MediaType: v1.MediaTypeImageLayer, + Size: layerSize, + Digest: *layerDigest, + }, + }, + } + manifest.SchemaVersion = 2 + manifestBytes, err := json.Marshal(manifest) + if err != nil { + return err + } + manifestPath := filepath.Join(outputPath, "manifest.json") + manifestFile, err := os.Create(manifestPath) + if err != nil { + return err + } + defer manifestFile.Close() + manifestFile.Write(manifestBytes) + + logger.Infof("manifest is written to %q", manifestPath) + + if !(outputDimg || outputCdimg) { + return nil + } + + // convert layer.tar to dimg + tempDir, err := ioutil.TempDir("/tmp/ctr-cli", "*") + if err != nil { + return err + } + defer os.RemoveAll(tempDir) + + logger.Infof("extracting layer(%s) to %s", layerNewPath, tempDir) + err = exec.Command("tar", "-xf", layerNewPath, "-C", tempDir).Run() + if err != nil { + return fmt.Errorf("failed to extract layer(%s) to %s: %v", layerNewPath, tempDir, err) + } + + tempDiffDir, err := ioutil.TempDir("/tmp/ctr-cli", "*") + if err != nil { + return err + } + defer os.RemoveAll(tempDiffDir) + + logger.Info("generating Di3FS file entries") + entry, err := di3fsImage.GenerateDiffFromDir("", tempDir, tempDiffDir, true, false) + if err != nil { + return fmt.Errorf("failed to generate diffs: %v", err) + } + + logger.Info("packing dimg") + dimgPath := filepath.Join(outputPath, "image.dimg") + err = di3fsImage.PackDimg(logger, tempDiffDir, entry, "", dimgPath) + if err != nil { + return fmt.Errorf("failed to pack dimg: %v", err) + } + + logger.Infof("successfully packed dimg image %s to %s", image, dimgPath) + if outputDimg && !outputCdimg { + return nil + } + + logger.Info("packing cdimg") + cdimgPath := filepath.Join(outputPath, "image.cdimg") + err = di3fsImage.PackCdimg(configNewPath, dimgPath, cdimgPath) + if err != nil { + return fmt.Errorf("failed to pack cdimg: %v", err) + } + + logger.Infof("successfully packed cdimg image %s to %s", image, cdimgPath) + return nil +} + +func Command() *cli.Command { + cmd := cli.Command{ + Name: "convert", + Usage: "Convert contaier image into squashed image", + Action: func(context *cli.Context) error { + return Action(context) + }, + Flags: Flags, + } + + return &cmd +} diff --git a/cmd/ctr-cli/load/load.go b/cmd/ctr-cli/load/load.go new file mode 100644 index 0000000..41416c5 --- /dev/null +++ b/cmd/ctr-cli/load/load.go @@ -0,0 +1,174 @@ +package load + +import ( + "bytes" + "context" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "time" + + "github.com/containerd/containerd/content" + "github.com/containerd/containerd/images" + "github.com/containerd/containerd/log" + "github.com/naoki9911/fuse-diff-containerd/pkg/image" + sns "github.com/naoki9911/fuse-diff-containerd/pkg/snapshotter" + v1 "github.com/opencontainers/image-spec/specs-go/v1" + + "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" +) + +var Flags = []cli.Flag{ + &cli.StringFlag{ + Name: "image", + Usage: "image name to be loaded", + Required: true, + }, + &cli.StringFlag{ + Name: "dimg", + Usage: "path to dimg to be loaded", + Required: true, + }, +} + +func LoadImage(snClient *sns.Client, ctx context.Context, imageName, imageVersion string, image *image.Di3FSImage) error { + cs := snClient.CtrClient.ContentStore() + cs.Delete(ctx, image.Header.ManifestDigest) + err := content.WriteBlob(ctx, cs, image.Header.ManifestDigest.Hex(), bytes.NewReader(image.ManifestBytes), + v1.Descriptor{ + Size: int64(len(image.ManifestBytes)), + Digest: image.Header.ManifestDigest, + }, + content.WithLabels(map[string]string{ + sns.NerverGC: "hoghoge", + sns.ImageLabelPuller: "di3fs", + fmt.Sprintf("%s.config", sns.ContentLabelContainerdGC): image.Manifest.Config.Digest.String(), + //fmt.Sprintf("%s.di3fs", ContentLabelContainerdGC): dId.String(), + }), + ) + if err != nil { + return err + } + log.G(ctx).Debug("load manifest done") + + err = content.WriteBlob( + ctx, cs, image.Manifest.Config.Digest.Hex(), bytes.NewReader(image.ConfigBytes), + v1.Descriptor{ + Size: int64(len(image.ConfigBytes)), + Digest: image.Manifest.Config.Digest, + }, + content.WithLabels(map[string]string{ + sns.NerverGC: "hoghoge", + }), + ) + if err != nil { + return err + } + log.G(ctx).Debug("load config done") + + // register image + is := snClient.CtrClient.ImageService() + is.Delete(ctx, "test-image") + _, err = is.Create(ctx, images.Image{ + Name: imageName + ":" + imageVersion, + Target: v1.Descriptor{ + MediaType: sns.ImageMediaTypeManifestV2, + Digest: image.Header.ManifestDigest, + Size: int64(len(image.ManifestBytes)), + }, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Labels: map[string]string{ + sns.TargetSnapshotLabel: "di3fs", + sns.SnapshotLabelImageName: imageName, + sns.SnapshotLabelImageVersion: imageVersion, + }, + }) + if err != nil { + return err + } + + // now ready to create snapshot + err = sns.CreateSnapshot(ctx, snClient.SnClient, &image.Header.ManifestDigest, &image.DImgDigest) + if err != nil { + return err + } + + log.G(ctx).WithFields(logrus.Fields{ + "header": image.Header, + "manifest": image.Manifest, + "config": image.Config, + }).Debugf("image loaded") + + return nil +} + +func Load(ctx context.Context, imgNameWithVersion, imgPath string) error { + snClient, err := sns.NewClient() + if err != nil { + return err + } + + imgNames := strings.Split(imgNameWithVersion, ":") + if len(imgNames) != 2 { + return fmt.Errorf("invalid image name %s", imgNameWithVersion) + } + imgName := imgNames[0] + imgVersion := imgNames[1] + log.G(ctx).WithFields(logrus.Fields{"imageName": imgName, "imageVersion": imgVersion}).Infof("loading image from %q", imgPath) + // load image + image, err := image.Load(imgPath) + if err != nil { + return err + } + log.G(ctx).Info("loaded image") + + // extract dimg + imagePath := filepath.Join(snClient.SnImageStorePath, image.DImgDigest.String()+".dimg") + _, err = image.Image.Seek(image.DImgOffset, 0) + if err != nil { + return err + } + dimgFile, err := os.Create(imagePath) + if err != nil { + return err + } + defer dimgFile.Close() + _, err = io.Copy(dimgFile, image.Image) + if err != nil { + return err + } + + err = LoadImage(snClient, ctx, imgName, imgVersion, image) + if err != nil { + return err + } + + return nil +} + +func Action(c *cli.Context) error { + imgName := c.String("image") + imgPath := c.String("dimg") + err := Load(context.TODO(), imgName, imgPath) + if err != nil { + return err + } + return nil +} + +func Command() *cli.Command { + cmd := cli.Command{ + Name: "load", + Usage: "Load dimg", + Action: func(context *cli.Context) error { + return Action(context) + }, + Flags: Flags, + } + + return &cmd +} diff --git a/cmd/ctr-cli/main.go b/cmd/ctr-cli/main.go new file mode 100644 index 0000000..2986907 --- /dev/null +++ b/cmd/ctr-cli/main.go @@ -0,0 +1,39 @@ +package main + +import ( + "fmt" + "os" + + "github.com/naoki9911/fuse-diff-containerd/cmd/ctr-cli/convert" + "github.com/naoki9911/fuse-diff-containerd/cmd/ctr-cli/load" + "github.com/naoki9911/fuse-diff-containerd/cmd/ctr-cli/pack" + "github.com/naoki9911/fuse-diff-containerd/cmd/ctr-cli/pull" + "github.com/urfave/cli/v2" +) + +func main() { + app := NewApp() + err := app.Run(os.Args) + if err != nil { + fmt.Fprintf(os.Stderr, "ctr-cli: %v\n", err) + os.Exit(1) + } +} + +func NewApp() *cli.App { + app := cli.NewApp() + + app.Name = "ctr-cli" + app.Version = "0.0.0" + app.Usage = "CLI tool for di3fs-containerd" + app.EnableBashCompletion = true + app.Flags = []cli.Flag{} + app.Commands = []*cli.Command{ + convert.Command(), + pack.Command(), + load.Command(), + pull.Command(), + } + + return app +} diff --git a/cmd/ctr-cli/pack/pack.go b/cmd/ctr-cli/pack/pack.go new file mode 100644 index 0000000..f96748b --- /dev/null +++ b/cmd/ctr-cli/pack/pack.go @@ -0,0 +1,71 @@ +package pack + +import ( + "context" + + "github.com/containerd/containerd/log" + "github.com/naoki9911/fuse-diff-containerd/pkg/image" + "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" +) + +var logger = log.G(context.TODO()) + +var ( + Flags = []cli.Flag{ + &cli.StringFlag{ + Name: "manifest", + Usage: "path to manifest file", + Required: true, + }, + &cli.StringFlag{ + Name: "config", + Usage: "path to config file", + Required: true, + }, + &cli.StringFlag{ + Name: "dimg", + Usage: "path to dimg", + Required: true, + }, + &cli.StringFlag{ + Name: "out", + Usage: "output file name", + Required: true, + }, + } +) + +func Action(c *cli.Context) error { + logger.Logger.SetLevel(logrus.DebugLevel) + manifestPath := c.String("manifest") + configPath := c.String("config") + dimgPath := c.String("dimg") + outPath := c.String("out") + logger.WithFields(logrus.Fields{ + "manifestPath": manifestPath, + "configPath": configPath, + "dimg": dimgPath, + "outPath": outPath, + }).Info("starting to pack") + + err := image.PackCdimg(configPath, dimgPath, outPath) + if err != nil { + return err + } + logger.Info("pack done") + return nil +} + +func Command() *cli.Command { + cmd := cli.Command{ + Name: "pack", + Usage: "Pack diffs into distributable form", + Action: func(context *cli.Context) error { + return Action(context) + }, + Flags: Flags, + } + + return &cmd +} diff --git a/cmd/ctr-cli/pull/pull.go b/cmd/ctr-cli/pull/pull.go new file mode 100644 index 0000000..a542824 --- /dev/null +++ b/cmd/ctr-cli/pull/pull.go @@ -0,0 +1,239 @@ +package pull + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/containerd/containerd/log" + "github.com/naoki9911/fuse-diff-containerd/cmd/ctr-cli/load" + "github.com/naoki9911/fuse-diff-containerd/pkg/benchmark" + "github.com/naoki9911/fuse-diff-containerd/pkg/image" + sns "github.com/naoki9911/fuse-diff-containerd/pkg/snapshotter" + "github.com/naoki9911/fuse-diff-containerd/pkg/update" + "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" +) + +var logger = log.G(context.TODO()) + +var ( + Flags = []cli.Flag{ + &cli.StringFlag{ + Name: "image", + Usage: "image to be pulled", + Required: true, + }, + &cli.BoolFlag{ + Name: "benchmark", + Usage: "enable benchmark", + Required: false, + }, + &cli.StringFlag{ + Name: "host", + Usage: "server host", + Required: true, + }, + } +) + +func Action(c *cli.Context) error { + logger.Logger.SetLevel(logrus.InfoLevel) + imageName := c.String("image") + host := c.String("host") + benchmark := c.Bool("benchmark") + logger.WithFields(logrus.Fields{ + "imageName": imageName, + "host": host, + }).Info("starting to pull") + + err := pullImage(host, imageName, benchmark) + if err != nil { + return err + } + + logger.Info("pull done") + return nil +} + +func Command() *cli.Command { + cmd := cli.Command{ + Name: "pull", + Usage: "Pull image", + Action: func(context *cli.Context) error { + return Action(context) + }, + Flags: Flags, + } + + return &cmd +} + +func pullImage(host string, imageNameWithVersion string, bench bool) error { + var b *benchmark.Benchmark = nil + var err error + if bench { + b, err = benchmark.NewBenchmark("./benchmark.log") + if err != nil { + return err + } + defer b.Close() + } + start := time.Now() + snClient, err := sns.NewClient() + if err != nil { + return err + } + + reqImgNames := strings.Split(imageNameWithVersion, ":") + if len(reqImgNames) != 2 { + return fmt.Errorf("invalid image name %s", imageNameWithVersion) + } + reqImgName := reqImgNames[0] + reqImgVersion := reqImgNames[1] + + imgStore := snClient.CtrClient.ImageService() + images, err := imgStore.List(context.TODO()) + if err != nil { + return err + } + + localImages := make([]update.Image, 0) + for _, img := range images { + targetSns, ok := img.Labels[sns.TargetSnapshotLabel] + if !ok { + continue + } + if targetSns != "di3fs" { + continue + } + localImgName := img.Labels[sns.SnapshotLabelImageName] + localImgVersion := img.Labels[sns.SnapshotLabelImageVersion] + + // if the requested image exists local, nothing to do. + if localImgName == reqImgName && localImgVersion == reqImgVersion { + logger.Infof("%s is already pulled", imageNameWithVersion) + return nil + } + + localImg := update.Image{ + Name: localImgName, + Version: localImgVersion, + } + localImages = append(localImages, localImg) + } + logger.WithField("localImages", localImages).Debug("local images collected") + + reqBody := update.UpdateDataRequest{ + RequestImage: update.Image{ + Name: reqImgName, + Version: reqImgVersion, + }, + LocalImages: localImages, + } + + reqBodyBytes, err := json.Marshal(reqBody) + if err != nil { + return err + } + client := &http.Client{} + logger.WithField("reqBody", string(reqBodyBytes)).Debug("request update") + req, err := http.NewRequest("GET", "http://"+host+"/update", bytes.NewBuffer(reqBodyBytes)) + if err != nil { + return err + } + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + logger.Debug("received response") + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("failed to pull: server status=%d", resp.StatusCode) + } + resJsonLength, err := strconv.Atoi(resp.Header.Get("Update-Response-Length")) + if err != nil { + return err + } + resJsonBytes := make([]byte, resJsonLength) + readSize, err := resp.Body.Read(resJsonBytes) + if err != nil { + return err + } + if resJsonLength != int(readSize) { + return fmt.Errorf("invalid length response expected=%d actual=%d", resJsonLength, readSize) + } + var resJson update.UpdateDataResponse + err = json.Unmarshal(resJsonBytes, &resJson) + if err != nil { + return err + } + logger.Infof("recieved response imageName=%s Version=%s baseVersion=%s", resJson.Name, resJson.Version, resJson.BaseVersion) + + image, err := image.LoadHeader(resp.Body) + if err != nil { + return err + } + logger.WithField("dimgSize", image.Header.DimgSize).Debug("got image header") + + dimgPath := filepath.Join(snClient.SnImageStorePath, image.DImgDigest.String()+".dimg") + dimgFile, err := os.Create(dimgPath) + if err != nil { + return err + } + dimgSize, err := io.Copy(dimgFile, resp.Body) + if err != nil { + return err + } + if dimgSize != image.Header.DimgSize { + return fmt.Errorf("invalid dimg (expected=%d actual=%d)", image.Header.DimgSize, dimgSize) + } + logger.WithField("dimgPath", dimgPath).Info("dimg saved") + + if b != nil { + metricDownload := benchmark.Metric{ + TaskName: "pull-download", + ElapsedMilli: int(time.Since(start).Milliseconds()), + Labels: []string{ + "imageName:" + resJson.Name, + "version:" + resJson.Version, + "baseVersion:" + resJson.BaseVersion, + }, + } + err = b.AppendResult(metricDownload) + if err != nil { + return err + } + } + + err = load.LoadImage(snClient, context.TODO(), reqImgName, reqImgVersion, image) + if err != nil { + return err + } + if b != nil { + metricDownload := benchmark.Metric{ + TaskName: "pull", + ElapsedMilli: int(time.Since(start).Milliseconds()), + Labels: []string{ + "imageName:" + resJson.Name, + "version:" + resJson.Version, + "baseVersion:" + resJson.BaseVersion, + }, + } + err = b.AppendResult(metricDownload) + if err != nil { + return err + } + } + + return nil +} diff --git a/cmd/diff/main.go b/cmd/diff/main.go new file mode 100644 index 0000000..cffb7fd --- /dev/null +++ b/cmd/diff/main.go @@ -0,0 +1,104 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "os" + "time" + + "github.com/containerd/containerd/log" + "github.com/naoki9911/fuse-diff-containerd/pkg/benchmark" + "github.com/naoki9911/fuse-diff-containerd/pkg/image" + "github.com/sirupsen/logrus" +) + +var logger = log.G(context.TODO()) + +func main() { + logger.Logger.SetLevel(logrus.WarnLevel) + if len(os.Args) < 6 { + fmt.Println("diff base-dir new-dir output-dir json-file mode [benchmark]") + fmt.Println("diff dimg base-dimg new-dimg parent-dimg output-dimg mode") + os.Exit(1) + } + + if os.Args[1] == "dimg" { + baseDimgPath := os.Args[2] + newDimgPath := os.Args[3] + parentDimgPath := os.Args[4] + outputDimgPath := os.Args[5] + mode := os.Args[6] + if mode != "binary-diff" && mode != "file-diff" { + fmt.Println("mode is \"binary-diff\" or \"file-diff\"") + os.Exit(1) + } + + err := image.GenerateDiffFromDimg(baseDimgPath, newDimgPath, parentDimgPath, outputDimgPath, mode == "binary-diff") + if err != nil { + panic(err) + } + } else { + baseDir := os.Args[1] + newDir := os.Args[2] + outputDir := os.Args[3] + jsonPath := os.Args[4] + mode := os.Args[5] + benchmarkEnabled := false + if mode != "binary-diff" && mode != "file-diff" { + fmt.Println("mode is \"binary-diff\" or \"file-diff\"") + os.Exit(1) + } + + os.RemoveAll(outputDir) + os.RemoveAll(jsonPath) + + if len(os.Args) == 7 { + benchmarkEnabled = os.Args[6] == "benchmark" + } + var b *benchmark.Benchmark = nil + var err error + if benchmarkEnabled { + b, err = benchmark.NewBenchmark("./benchmark.log") + if err != nil { + panic(err) + } + defer b.Close() + } + start := time.Now() + + entry, err := image.GenerateDiffFromDir(baseDir, newDir, outputDir, mode == "binary-diff", baseDir != "") + if err != nil { + panic(err) + } + + //entry.print("", true) + entryJson, err := json.MarshalIndent(entry, "", " ") + if err != nil { + panic(err) + } + jsonFile, err := os.Create(jsonPath) + if err != nil { + panic(err) + } + defer jsonFile.Close() + jsonFile.Write(entryJson) + + if benchmarkEnabled { + elapsedMilli := time.Since(start).Milliseconds() + metric := benchmark.Metric{ + TaskName: "diff", + ElapsedMilli: int(elapsedMilli), + Labels: []string{ + "base:" + baseDir, + "new:" + newDir, + mode, + }, + } + err = b.AppendResult(metric) + if err != nil { + panic(err) + } + } + } +} diff --git a/cmd/fuse-diff/main.go b/cmd/fuse-diff/main.go new file mode 100644 index 0000000..2ad10d5 --- /dev/null +++ b/cmd/fuse-diff/main.go @@ -0,0 +1,227 @@ +// Copyright 2016 the Go-FUSE Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This is main program driver for the loopback filesystem from +// github.com/hanwen/go-fuse/fs/, a filesystem that shunts operations +// to an underlying file system. +package main + +import ( + "encoding/json" + "flag" + "fmt" + "os" + "os/signal" + "path" + "path/filepath" + "runtime/pprof" + "syscall" + "time" + + "github.com/hanwen/go-fuse/v2/fs" + "github.com/naoki9911/fuse-diff-containerd/pkg/benchmark" + "github.com/naoki9911/fuse-diff-containerd/pkg/di3fs" + log "github.com/sirupsen/logrus" +) + +func writeMemProfile(fn string, sigs <-chan os.Signal) { + i := 0 + for range sigs { + fn := fmt.Sprintf("%s-%d.memprof", fn, i) + i++ + + log.Printf("Writing mem profile to %s\n", fn) + f, err := os.Create(fn) + if err != nil { + log.Printf("Create: %v", err) + continue + } + pprof.WriteHeapProfile(f) + if err := f.Close(); err != nil { + log.Printf("close %v", err) + } + } +} + +func main() { + start := time.Now() + customFormatter := new(log.TextFormatter) + customFormatter.TimestampFormat = "2006-01-02 15:04:05" + customFormatter.FullTimestamp = true + log.SetFormatter(customFormatter) + log.SetLevel(log.InfoLevel) + + // Scans the arg list and sets up flags + debug := flag.Bool("debug", false, "print debugging messages.") + other := flag.Bool("allow-other", false, "mount with -o allowother.") + bench := flag.Bool("benchmark", false, "measure benchmark") + cpuprofile := flag.String("cpuprofile", "", "write cpu profile to this file") + memprofile := flag.String("memprofile", "", "write memory profile to this file") + metafile := flag.String("metafile", "", "metadata to be read") + baseDir := flag.String("basedir", "", "base directory to be patched") + patchDir := flag.String("patchdir", "", "patch directory") + mode := flag.String("mode", "dir", "diff image type [dimg|dir]") + flag.Parse() + if flag.NArg() < 1 { + fmt.Printf("usage: %s MOUNTPOINT\n", path.Base(os.Args[0])) + fmt.Printf("\noptions:\n") + flag.PrintDefaults() + os.Exit(2) + } + + var b *benchmark.Benchmark = nil + var err error + if *bench { + b, err = benchmark.NewBenchmark("./benchmark.log") + if err != nil { + panic(err) + } + } + if *cpuprofile != "" { + fmt.Printf("Writing cpu profile to %s\n", *cpuprofile) + f, err := os.Create(*cpuprofile) + if err != nil { + fmt.Println(err) + os.Exit(3) + } + pprof.StartCPUProfile(f) + defer pprof.StopCPUProfile() + } + if *memprofile != "" { + log.Printf("send SIGUSR1 to %d to dump memory profile", os.Getpid()) + profSig := make(chan os.Signal, 1) + signal.Notify(profSig, syscall.SIGUSR1) + go writeMemProfile(*memprofile, profSig) + } + if *cpuprofile != "" || *memprofile != "" { + fmt.Printf("Note: You must unmount gracefully, otherwise the profile file(s) will stay empty!\n") + } + + if *patchDir == "" { + fmt.Println("please specify patchdir") + os.Exit(1) + } + patchDirAbs, err := filepath.Abs(*patchDir) + if err != nil { + panic(err) + } + if *mode == "" { + fmt.Println("please specify mode") + os.Exit(1) + } + + var metaJson = di3fs.FileEntry{} + var imageFile *os.File = nil + imageBodyOffset := int64(0) + var baseMetaJson *di3fs.FileEntry = nil + var baseImageFile *os.File = nil + var baseImageBodyOffset = int64(0) + baseNeeded := true + + if *mode == "dimg" { + var imageHeader *di3fs.ImageHeader + imageHeader, imageFile, imageBodyOffset, err = di3fs.LoadImage(patchDirAbs) + if err != nil { + panic(err) + } + metaJson = imageHeader.FileEntry + baseNeeded = imageHeader.BaseId != "" + + if imageHeader.BaseId != "" { + var baseImageHeader *di3fs.ImageHeader + var baseDirAbs string + if *baseDir != "" { + baseDirAbs, err = filepath.Abs(*baseDir) + if err != nil { + panic(err) + } + } else { + imageStore, _ := filepath.Split(patchDirAbs) + baseDirAbs = filepath.Join(imageStore, imageHeader.BaseId+".dimg") + } + baseImageHeader, baseImageFile, baseImageBodyOffset, err = di3fs.LoadImage(baseDirAbs) + if err != nil { + panic(err) + } + baseMetaJson = &baseImageHeader.FileEntry + baseNeeded = false + } + } else { + if *metafile == "" { + fmt.Println("please specify metafile") + os.Exit(1) + } + metaJsonRaw, err := os.ReadFile(*metafile) + if err != nil { + panic(err) + } + err = json.Unmarshal(metaJsonRaw, &metaJson) + if err != nil { + panic(err) + } + } + + if baseNeeded && *baseDir == "" { + fmt.Println("please specify basedir") + os.Exit(1) + } + baseDirAbs, err := filepath.Abs(*baseDir) + if err != nil { + panic(err) + } + + sec := time.Second + opts := &fs.Options{ + // These options are to be compatible with libfuse defaults, + // making benchmarking easier. + AttrTimeout: &sec, + EntryTimeout: &sec, + } + if *debug { + log.SetLevel(log.TraceLevel) + } + opts.Debug = *debug + opts.AllowOther = *other + if opts.AllowOther { + // Make the kernel check file permissions for us + opts.MountOptions.Options = append(opts.MountOptions.Options, "default_permissions") + } + // mount only with read-only + opts.MountOptions.Options = append(opts.MountOptions.Options, "ro") + // First column in "df -T": original dir + opts.MountOptions.Options = append(opts.MountOptions.Options, "fsname=fuse-diff") + // Second column in "df -T" will be shown as "fuse." + Name + opts.MountOptions.Name = "fuse-diff" + // Leave file permissions on "000" files as-is + opts.NullPermissions = true + + di3fsRoot, err := di3fs.NewDi3fsRoot(opts, []string{baseDirAbs}, patchDirAbs, &metaJson, []*di3fs.FileEntry{baseMetaJson}, []*os.File{baseImageFile}, []int64{baseImageBodyOffset}, imageFile, imageBodyOffset) + if err != nil { + log.Fatalf("creating Di3fsRoot failed: %v\n", err) + } + + server, err := fs.Mount(flag.Arg(0), di3fsRoot.RootNode, opts) + if err != nil { + log.Fatalf("Mount fail: %v\n", err) + } + log.Infof("Mounted!") + fmt.Printf("elapsed = %v\n", (time.Since(start).Milliseconds())) + if *bench { + elapsedMilli := time.Since(start).Milliseconds() + metric := benchmark.Metric{ + TaskName: "di3fs", + ElapsedMilli: int(elapsedMilli), + Labels: []string{ + "base:" + *baseDir, + "patch:" + *patchDir, + *mode, + }, + } + err = b.AppendResult(metric) + if err != nil { + panic(err) + } + } + server.Wait() +} diff --git a/cmd/merge/main.go b/cmd/merge/main.go new file mode 100644 index 0000000..6853a44 --- /dev/null +++ b/cmd/merge/main.go @@ -0,0 +1,114 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + "time" + + "github.com/jinzhu/copier" + "github.com/naoki9911/fuse-diff-containerd/pkg/benchmark" + "github.com/naoki9911/fuse-diff-containerd/pkg/di3fs" + log "github.com/sirupsen/logrus" +) + +func main() { + log.SetLevel(log.WarnLevel) + if len(os.Args) < 5 { + fmt.Println("merge dir lower-diff lower-json upper-diff upper-json merged-diff merged-json") + fmt.Println("merge dimg lower-dimg upper-dimg merged-dimg benchmark") + os.Exit(1) + } + isImage := os.Args[1] == "dimg" + if isImage { + lowerDimg := os.Args[2] + upperDimg := os.Args[3] + mergedDimg := os.Args[4] + var b *benchmark.Benchmark = nil + var err error + if len(os.Args) > 5 && os.Args[5] == "benchmark" { + b, err = benchmark.NewBenchmark("./benchmark.log") + if err != nil { + panic(err) + } + defer b.Close() + } + + start := time.Now() + + mergeFile, err := os.Create(mergedDimg) + if err != nil { + panic(err) + } + defer mergeFile.Close() + err = di3fs.MergeDimg(lowerDimg, upperDimg, mergeFile) + if err != nil { + panic(err) + } + + if b != nil { + metric := benchmark.Metric{ + TaskName: "merge", + ElapsedMilli: int(time.Since(start).Milliseconds()), + Labels: []string{ + "lower:" + lowerDimg, + "upper:" + upperDimg, + "dimg", + }, + } + err = b.AppendResult(metric) + if err != nil { + panic(err) + } + } + } else { + lowerDiff := os.Args[2] + lowerJson := os.Args[3] + upperDiff := os.Args[4] + upperJson := os.Args[5] + mergedDiff := os.Args[6] + mergedJson := os.Args[7] + + lowerJsonRaw, err := os.ReadFile(lowerJson) + if err != nil { + panic(err) + } + lowerEntry := di3fs.FileEntry{} + err = json.Unmarshal(lowerJsonRaw, &lowerEntry) + if err != nil { + panic(err) + } + + upperJsonRaw, err := os.ReadFile(upperJson) + if err != nil { + panic(err) + } + upperEntry := di3fs.FileEntry{} + err = json.Unmarshal(upperJsonRaw, &upperEntry) + if err != nil { + panic(err) + } + mergedEntry := di3fs.NewFileEntry() + err = copier.Copy(mergedEntry, upperEntry) + if err != nil { + panic(err) + } + + err = di3fs.MergeDiff(lowerDiff, upperDiff, mergedDiff, &lowerEntry, &upperEntry, mergedEntry) + if err != nil { + panic(err) + } + + //entry.print("", true) + entryJson, err := json.MarshalIndent(mergedEntry, "", " ") + if err != nil { + panic(err) + } + jsonFile, err := os.Create(mergedJson) + if err != nil { + panic(err) + } + defer jsonFile.Close() + jsonFile.Write(entryJson) + } +} diff --git a/cmd/pack/pack.go b/cmd/pack/pack.go new file mode 100644 index 0000000..5727170 --- /dev/null +++ b/cmd/pack/pack.go @@ -0,0 +1,40 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "os" + + "github.com/containerd/containerd/log" + "github.com/naoki9911/fuse-diff-containerd/pkg/di3fs" + "github.com/naoki9911/fuse-diff-containerd/pkg/image" + "github.com/sirupsen/logrus" +) + +var logger = log.G(context.TODO()) + +func main() { + logger.Logger.SetLevel(logrus.WarnLevel) + if len(os.Args) < 5 { + fmt.Println("diff diff-dir json-file baseDImg ouput") + os.Exit(1) + } + diffDir := os.Args[1] + jsonPath := os.Args[2] + baseDImg := os.Args[3] + outputPath := os.Args[4] + + jsonRaw, err := os.ReadFile(jsonPath) + if err != nil { + panic(err) + } + logger.WithFields(logrus.Fields{"diffDir": diffDir, "diffJsonPath": jsonPath, "baseDImg": baseDImg, "outputPath": outputPath}).Info("start packing") + entry := &di3fs.FileEntry{} + json.Unmarshal(jsonRaw, entry) + err = image.PackDimg(logger, diffDir, entry, baseDImg, outputPath) + if err != nil { + logger.Fatal("failed to pack dimg") + } + logger.Info("pack done") +} diff --git a/cmd/patch/main.go b/cmd/patch/main.go new file mode 100644 index 0000000..40a83ae --- /dev/null +++ b/cmd/patch/main.go @@ -0,0 +1,259 @@ +package main + +import ( + "bytes" + "compress/gzip" + "context" + "encoding/json" + "fmt" + "io" + "os" + "path" + "path/filepath" + "time" + + "github.com/containerd/containerd/log" + "github.com/icedream/go-bsdiff" + "github.com/klauspost/compress/zstd" + "github.com/naoki9911/fuse-diff-containerd/pkg/benchmark" + "github.com/naoki9911/fuse-diff-containerd/pkg/di3fs" + cp "github.com/otiai10/copy" + "github.com/sirupsen/logrus" +) + +var logger = log.G(context.TODO()) + +func applyFilePatch(baseFilePath, newFilePath string, patch io.Reader) error { + //fmt.Println(newFilePath) + baseFile, err := os.Open(baseFilePath) + if err != nil { + return err + } + defer baseFile.Close() + newFile, err := os.Create(newFilePath) + if err != nil { + return err + } + defer newFile.Close() + err = bsdiff.Patch(baseFile, newFile, patch) + if err != nil { + return err + } + + return nil +} + +func applyFilePatchForGz(baseFilePath, newFilePath string, patch io.Reader) error { + baseFile, err := os.Open(baseFilePath) + if err != nil { + return err + } + defer baseFile.Close() + newFile, err := os.Create(newFilePath) + if err != nil { + return err + } + defer newFile.Close() + + gzipNewWriter := gzip.NewWriter(newFile) + defer gzipNewWriter.Close() + err = bsdiff.Patch(baseFile, gzipNewWriter, patch) + if err != nil { + return err + } + + return nil +} + +func applyPatch(basePath, newPath, patchPath string, dirEntry di3fs.FileEntry, image *os.File, imageStartOffset int64, isBase bool) error { + fName := dirEntry.Name + isImage := image != nil + baseFilePath := path.Join(basePath, fName) + newFilePath := path.Join(newPath, fName) + patchFilePath := path.Join(patchPath, dirEntry.DiffName) + + if isBase && !dirEntry.IsDir() && !dirEntry.IsSymlink() && !dirEntry.IsNew() { + return fmt.Errorf("invalid base image %q", newFilePath) + } + + if dirEntry.Type == di3fs.FILE_ENTRY_SYMLINK { + prevDir, err := filepath.Abs(".") + if err != nil { + return err + } + + err = os.Chdir(newPath) + if err != nil { + return err + } + + err = os.Symlink(dirEntry.RealPath, fName) + if err != nil { + return err + } + + err = os.Chdir(prevDir) + if err != nil { + return err + } + } else if dirEntry.IsDir() { + patchFilePath = path.Join(patchPath, dirEntry.Name) + err := os.Mkdir(newFilePath, os.ModePerm) + if err != nil { + return err + } + for _, c := range dirEntry.Childs { + err = applyPatch(baseFilePath, newFilePath, patchFilePath, c, image, imageStartOffset, isBase) + if err != nil { + return err + } + } + } else if dirEntry.Type == di3fs.FILE_ENTRY_FILE_SAME { + err := cp.Copy(baseFilePath, newFilePath) + if err != nil { + return err + } + } else if dirEntry.Type == di3fs.FILE_ENTRY_FILE_NEW { + //if strings.Contains(dirEntry.Name, ".wh") { + // fmt.Println(newFilePath) + //} + if isImage { + logger.Debugf("copy %q from image(offset=%d size=%d)", newFilePath, dirEntry.Offset, dirEntry.CompressedSize) + patchBytes := make([]byte, dirEntry.CompressedSize) + _, err := image.ReadAt(patchBytes, dirEntry.Offset+imageStartOffset) + if err != nil { + return err + } + patchBuf := bytes.NewBuffer(patchBytes) + patchReader, err := zstd.NewReader(patchBuf) + if err != nil { + return err + } + defer patchReader.Close() + + newFile, err := os.Create(newFilePath) + if err != nil { + return err + } + defer newFile.Close() + + io.Copy(newFile, patchReader) + } else { + err := cp.Copy(patchFilePath, newFilePath) + if err != nil { + return err + } + } + } else if dirEntry.Type == di3fs.FILE_ENTRY_FILE_DIFF { + var patchReader io.Reader + if isImage { + logger.Debugf("applying diff to %q from image(offset=%d size=%d)", newFilePath, dirEntry.Offset, dirEntry.CompressedSize) + patchBytes := make([]byte, dirEntry.CompressedSize) + _, err := image.ReadAt(patchBytes, dirEntry.Offset+imageStartOffset) + if err != nil { + return err + } + patchReader = bytes.NewBuffer(patchBytes) + } else { + patchFile, err := os.Open(patchFilePath) + if err != nil { + return err + } + defer patchFile.Close() + patchReader = patchFile + } + if dirEntry.UncompressedGz { + err := applyFilePatchForGz(baseFilePath, newFilePath, patchReader) + if err != nil { + return err + } + } else { + err := applyFilePatch(baseFilePath, newFilePath, patchReader) + if err != nil { + return err + } + } + } else { + return fmt.Errorf("unexpected error type=%v", dirEntry.Type) + } + + for _, o := range dirEntry.OaqueFiles { + f, err := os.Create(path.Join(newFilePath, o)) + if err != nil { + return err + } + err = f.Chmod(os.FileMode(0755)) + if err != nil { + return err + } + f.Close() + } + return nil +} + +func main() { + logger.Logger.SetLevel(logrus.WarnLevel) + if len(os.Args) < 5 { + fmt.Println("diff dir base-dir new-dir [patch-dir|patch-img] json-file") + fmt.Println("diff dimg base-dir new-dir patch-img [benchmark]") + os.Exit(1) + } + mode := os.Args[1] + baseDir := os.Args[2] + newDir := os.Args[3] + patchDir := os.Args[4] + + os.RemoveAll(newDir) + + //entry.print("", false) + if mode == "dimg" { + var b *benchmark.Benchmark = nil + var err error + if len(os.Args) > 5 && os.Args[5] == "benchmark" { + b, err = benchmark.NewBenchmark("./benchmark.log") + if err != nil { + panic(err) + } + defer b.Close() + } + start := time.Now() + imageHeader, imageFile, curOffset, err := di3fs.LoadImage(patchDir) + if err != nil { + panic(err) + } + err = applyPatch(baseDir, newDir, patchDir, imageHeader.FileEntry, imageFile, curOffset, imageHeader.BaseId == "") + if err != nil { + panic(err) + } + if b != nil { + metric := benchmark.Metric{ + TaskName: "patch", + ElapsedMilli: int(time.Since(start).Milliseconds()), + Labels: []string{ + "base:" + baseDir, + "new:" + newDir, + mode, + }, + } + err = b.AppendResult(metric) + if err != nil { + panic(err) + } + } + } else { + entry := di3fs.FileEntry{} + jsonPath := os.Args[5] + jsonRaw, err := os.ReadFile(jsonPath) + if err != nil { + panic(err) + } + err = json.Unmarshal(jsonRaw, &entry) + if err != nil { + panic(err) + } + err = applyPatch(baseDir, newDir, patchDir, entry, nil, 0, false) + if err != nil { + panic(err) + } + } +} diff --git a/cmd/server/main.go b/cmd/server/main.go new file mode 100644 index 0000000..33d9f66 --- /dev/null +++ b/cmd/server/main.go @@ -0,0 +1,294 @@ +package main + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "strconv" + "sync" + + "github.com/containerd/containerd/log" + "github.com/hashicorp/go-version" + "github.com/naoki9911/fuse-diff-containerd/pkg/algorithm" + "github.com/naoki9911/fuse-diff-containerd/pkg/di3fs" + "github.com/naoki9911/fuse-diff-containerd/pkg/image" + "github.com/naoki9911/fuse-diff-containerd/pkg/update" + "github.com/naoki9911/fuse-diff-containerd/pkg/utils" + "github.com/sirupsen/logrus" +) + +var logger = log.G(context.TODO()) + +var DiffDataGraphs map[string]*algorithm.DirectedGraph = map[string]*algorithm.DirectedGraph{} +var DiffDatas map[string]*update.DiffData = map[string]*update.DiffData{} +var DiffDatasLock = sync.Mutex{} +var tempDiffDir = "/tmp/d4c-server" + +func getDiffTag(imageName, baseVersion, v string) string { + return fmt.Sprintf("%s_%s-%s", imageName, baseVersion, v) +} + +func handleDeleteDiffData(w http.ResponseWriter, r *http.Request) { + if r.Method != "DELETE" { + logger.Errorf("invalid method %s", r.Method) + w.WriteHeader(http.StatusBadRequest) + return + } + + DiffDatasLock.Lock() + defer DiffDatasLock.Unlock() + + DiffDataGraphs = map[string]*algorithm.DirectedGraph{} + DiffDatas = map[string]*update.DiffData{} + logger.Info("cleaned DiffDatas") +} + +func handlePostDiffData(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + logger.Errorf("invalid method %s", r.Method) + w.WriteHeader(http.StatusBadRequest) + return + } + + diffData, err := utils.UnmarshalJsonFromReader[update.DiffData](r.Body) + if err != nil { + logger.Errorf("invalid request err=%v", err) + w.WriteHeader(http.StatusBadRequest) + return + } + + _, err = os.Stat(diffData.FileName) + if err != nil { + logger.Errorf("failed to stat dimg %s err=%v", diffData.FileName, err) + w.WriteHeader(http.StatusBadRequest) + return + } + + _, err = os.Stat(diffData.ConfigPath) + if err != nil { + logger.Errorf("failed to stat config %s err=%v", diffData.ConfigPath, err) + w.WriteHeader(http.StatusBadRequest) + return + } + + DiffDatasLock.Lock() + defer DiffDatasLock.Unlock() + + if _, ok := DiffDataGraphs[diffData.ImageName]; !ok { + DiffDataGraphs[diffData.ImageName] = algorithm.NewDirectedGraph() + } + v, err := version.NewVersion(diffData.Version) + if err != nil { + logger.Errorf("invalid request err=%v", err) + w.WriteHeader(http.StatusBadRequest) + return + } + + baseVersion := "base" + if diffData.BaseVersion != "" { + baseVer, err := version.NewVersion(diffData.BaseVersion) + if err != nil { + logger.Errorf("invalid request err=%v", err) + w.WriteHeader(http.StatusBadRequest) + return + } + baseVersion = baseVer.String() + } + DiffDataGraphs[diffData.ImageName].Add(baseVersion, v.String(), 1) + DiffDatas[getDiffTag(diffData.ImageName, baseVersion, v.String())] = diffData + logger.WithFields(logrus.Fields{"baseVersion": baseVersion, "version": v.String(), "Name": diffData.ImageName}).Infof("added diffData to dependency graph") + + logger.WithField("diffData", diffData).Info("added DiffDatas") +} + +func handleGetUpdateData(w http.ResponseWriter, r *http.Request) { + req, err := utils.UnmarshalJsonFromReader[update.UpdateDataRequest](r.Body) + if err != nil { + logger.Errorf("invalid request err=%v", err) + w.WriteHeader(http.StatusBadRequest) + return + } + + // select base image + logger.WithFields(logrus.Fields{"requsetImage": req.RequestImage, "localImages": req.LocalImages}).Info("GetUpdateData") + targetVersion, err := version.NewVersion(req.RequestImage.Version) + if err != nil { + logger.Errorf("invalid request err=%v", err) + w.WriteHeader(http.StatusBadRequest) + return + } + + var selectedVersion *version.Version = nil + localImages := make([]update.Image, 0) + for i, img := range req.LocalImages { + if img.Name != req.RequestImage.Name { + continue + } + localImages = append(localImages, req.LocalImages[i]) + v, err := version.NewVersion(img.Version) + if err != nil { + logger.Errorf("invalid request err=%v", err) + w.WriteHeader(http.StatusBadRequest) + return + } + if targetVersion.LessThan(v) { + continue + } + if selectedVersion == nil { + selectedVersion = v + continue + } + if v.LessThan(selectedVersion) { + continue + } + selectedVersion = v + } + + logger.WithFields(logrus.Fields{"targetVersion": targetVersion, "selectedVersion": selectedVersion, "localImages": localImages}).Info("base version is selected") + + DiffDatasLock.Lock() + defer DiffDatasLock.Unlock() + + graph, ok := DiffDataGraphs[req.RequestImage.Name] + if !ok { + logger.Errorf("DiffDatas for image %s not found", req.RequestImage.Name) + w.WriteHeader(http.StatusNotFound) + return + } + + // Find diff data(selectedVersion -> targetVersion) + baseVersion := "base" + if selectedVersion != nil { + baseVersion = selectedVersion.String() + } + + logger.WithFields(logrus.Fields{"baseVersion": baseVersion, "version": targetVersion.String(), "Name": req.RequestImage.Name}).Infof("start to find best diffs") + path, err := graph.ShortestPath(baseVersion, targetVersion.String()) + if err != nil { + logger.Errorf("DiffDatas commbination for %s not found (base=%s target=%s)", req.RequestImage.Name, baseVersion, targetVersion.String()) + w.WriteHeader(http.StatusNotFound) + return + } + + pathStr := "" + for i, p := range path { + if i == 0 { + pathStr = p.GetName() + } else { + pathStr += fmt.Sprintf(" -> %s", p.GetName()) + } + } + logger.WithField("Diffs", pathStr).Info("Diff Data to be transfered found") + + diffDataBytes := bytes.Buffer{} + configPath := "" + + if len(path) == 2 { + tag := getDiffTag(req.RequestImage.Name, path[0].GetName(), path[1].GetName()) + diffData := DiffDatas[tag] + diffF, err := os.Open(diffData.FileName) + if err != nil { + logger.Errorf("failed to load=%v", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + _, err = io.Copy(&diffDataBytes, diffF) + if err != nil { + logger.Errorf("failed to load=%v", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + logger.Info("load done") + configPath = diffData.ConfigPath + } else { + upperIdx := 2 + lowerTag := getDiffTag(req.RequestImage.Name, path[0].GetName(), path[1].GetName()) + lowerDiff := DiffDatas[lowerTag] + lowerFileName := lowerDiff.FileName + upperTag := getDiffTag(req.RequestImage.Name, path[1].GetName(), path[2].GetName()) + for upperIdx < len(path) { + upperDiff := DiffDatas[upperTag] + logger.WithFields(logrus.Fields{"lower": lowerFileName, "upper": upperDiff.FileName}).Info("merge") + diffDataBytes = bytes.Buffer{} + err = di3fs.MergeDimg(lowerFileName, upperDiff.FileName, &diffDataBytes) + if err != nil { + logger.Errorf("failed to merge=%v", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + logger.Info("merge done") + if lowerFileName != lowerDiff.FileName { + os.Remove(lowerFileName) + } + configPath = upperDiff.ConfigPath + + upperIdx += 1 + if upperIdx != len(path) { + tempFile, err := os.CreateTemp(tempDiffDir, "diff-") + if err != nil { + logger.Error("failed to create temp file: %w", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + tempFile.Write(diffDataBytes.Bytes()) + lowerFileName = tempFile.Name() + logger.Infof("temp file saved at %s", lowerFileName) + tempFile.Close() + upperTag = getDiffTag(req.RequestImage.Name, path[upperIdx-1].GetName(), path[upperIdx].GetName()) + } + } + } + + res := update.UpdateDataResponse{ + Name: req.RequestImage.Name, + Version: req.RequestImage.Version, + BaseVersion: path[0].GetName(), + } + if res.BaseVersion == "base" { + res.BaseVersion = "" + } + resBytes, err := json.Marshal(res) + if err != nil { + logger.Errorf("failed to marshal json err=%v", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + w.Header().Add("Update-Response-Length", strconv.Itoa(len(resBytes))) + _, err = w.Write(resBytes) + if err != nil { + logger.Errorf("failed to send response json err=%v", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + configFile, err := os.Open(configPath) + if err != nil { + logger.Errorf("failed to open config file err=%v", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + defer configFile.Close() + err = image.PackIo(configFile, diffDataBytes.Bytes(), w) + if err != nil { + logger.Errorf("failed to pack config file err=%v", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + logger.Infof("update sent") +} + +func main() { + os.RemoveAll(tempDiffDir) + err := os.Mkdir(tempDiffDir, os.ModePerm) + if err != nil { + logger.Fatalf("failed to create tempDiffDir %s", tempDiffDir) + } + http.HandleFunc("/update", handleGetUpdateData) + http.HandleFunc("/diffData/add", handlePostDiffData) + http.HandleFunc("/diffData/cleanup", handleDeleteDiffData) + logger.Info("started") + http.ListenAndServe(":8081", nil) +} diff --git a/cmd/show/main.go b/cmd/show/main.go new file mode 100644 index 0000000..2bcfec1 --- /dev/null +++ b/cmd/show/main.go @@ -0,0 +1,165 @@ +package main + +import ( + "bytes" + "compress/bzip2" + "encoding/binary" + "fmt" + "io" + "log" + "os" + + "github.com/pkg/errors" +) + +var ErrInvalidMagic = errors.New("Invalid magic") +var sizeEncoding = binary.BigEndian +var magicText = []byte("ENDSLEY/BSDIFF43") + +func ReadHeader(r io.Reader) (size uint64, err error) { + magicBuf := make([]byte, len(magicText)) + n, err := r.Read(magicBuf) + if err != nil { + return + } + if n < len(magicText) { + err = ErrInvalidMagic + return + } + + err = binary.Read(r, sizeEncoding, &size) + + return +} + +func readPatch(reader io.Reader) (io.Reader, uint64, error) { + newLen, err := ReadHeader(reader) + if err != nil { + return nil, 0, err + } + fmt.Printf("newBytes: %d\n", newLen) + + // Decompression + bz2Reader := bzip2.NewReader(reader) + content, err := io.ReadAll(bz2Reader) + if err != nil { + return nil, 0, err + } + + return bytes.NewReader(content), newLen, nil +} + +func readInt64(reader io.Reader) (int64, error) { + buf := make([]byte, 8) + readSize, err := reader.Read(buf) + if err != nil { + return 0, err + } + if readSize != 8 { + return 0, fmt.Errorf("invalid size") + } + + isNegative := (buf[7]&0x80 > 0) + buf[7] = buf[7] & 0x7F + res := binary.LittleEndian.Uint64(buf) + if isNegative { + return -int64(res), nil + } else { + return int64(res), nil + } +} + +func readContent(newSize uint64, reader io.Reader, oldFile *os.File) error { + newPos := int64(0) + oldPos := int64(0) + + //fmt.Printf("OP,OldOffset,NewOffset,NewValue,OldValue\n") + fmt.Printf("Offset,NOUPDATE,ADD,INTERT,MOVE\n") + for newPos < int64(newSize) { + ctrl0, err := readInt64(reader) + if err != nil { + return err + } + ctrl1, err := readInt64(reader) + if err != nil { + return err + } + ctrl2, err := readInt64(reader) + if err != nil { + return err + } + + if uint64(newPos+ctrl0) > newSize { + return fmt.Errorf("newPos + ctrl0 exceeds newSize") + } + //fmt.Printf("ctrl0=%d\n", ctrl0) + //fmt.Printf("ctrl1=%d\n", ctrl1) + //fmt.Printf("ctrl2=%d\n", ctrl2) + + diff := make([]byte, ctrl0) + diffSize, err := reader.Read(diff) + if err != nil { + return err + } + if int(ctrl0) != diffSize { + return fmt.Errorf("invalid size expected=%d actual=%d", ctrl0, diffSize) + } + for i := int64(0); i < int64(diffSize); i++ { + if diff[i] == 0 { + fmt.Printf("%d,1,,,\n", newPos+i) + continue + } + oldC := make([]byte, 1) + _, err = oldFile.ReadAt(oldC, oldPos+i) + if err != nil { + return err + } + //fmt.Printf("ADD,%d,%d,%d,%d\n", oldPos+i, newPos+i, oldC[0]+diff[i], oldC[0]) + fmt.Printf("%d,,1,,\n", newPos+i) + } + + newPos += ctrl0 + oldPos += ctrl0 + + insert := make([]byte, ctrl1) + insertSize, err := reader.Read(insert) + if err != nil { + return err + } + if int(ctrl1) != insertSize { + return fmt.Errorf("invalid size expected=%d actual=%d", ctrl1, insertSize) + } + for i := int64(0); i < int64(insertSize); i++ { + fmt.Printf("%d,,,1,\n", newPos+i) + //fmt.Printf("INSERT,%d,%d,%d\n", oldPos+i, newPos+i, insert[i]) + } + //if ctrl1 != 0 { + // for i := int64(0); i < ctrl1; i++ { + // fmt.Printf("%d,,,,1\n", newPos+i) + // } + //} + newPos += ctrl1 + oldPos += ctrl2 + } + + return nil +} + +func main() { + oldFile, err := os.Open(os.Args[1]) + if err != nil { + log.Fatal(err) + } + file, err := os.Open(os.Args[2]) + if err != nil { + log.Fatal(err) + } + reader, newLen, err := readPatch(file) + if err != nil { + log.Fatal(err) + } + err = readContent(newLen, reader, oldFile) + if err != nil { + fmt.Printf("%+v\n", err) + } +} diff --git a/cmd/snapshotter/fs.go b/cmd/snapshotter/fs.go new file mode 100644 index 0000000..fe43bb4 --- /dev/null +++ b/cmd/snapshotter/fs.go @@ -0,0 +1,77 @@ +package main + +import ( + "context" + "os/exec" + "path" + + "github.com/containerd/containerd/errdefs" + "github.com/containerd/containerd/log" + "github.com/naoki9911/fuse-diff-containerd/pkg/di3fs" + sns "github.com/naoki9911/fuse-diff-containerd/pkg/snapshotter" + "github.com/sirupsen/logrus" +) + +var imageStore = "/tmp/di3fs/sn/images" + +type DummyFS struct { +} + +func (f *DummyFS) Mount(ctx context.Context, mountpoint string, labels map[string]string) error { + log.G(ctx).WithFields(logrus.Fields{ + "mountpoint": mountpoint, + "labels": labels, + }).Info("DummyFS Mount called") + d, ok := labels[sns.SnapshotLabelRefUncompressed] + if !ok { + return errdefs.ErrNotFound + } + + err := f.mountDImg(ctx, mountpoint, d) + if err != nil { + return err + } + log.G(ctx).Infof("success to mount %q", d) + return nil +} + +func (f *DummyFS) mountDImg(ctx context.Context, mountpoint, dimgDigest string) error { + log.G(ctx).WithFields(logrus.Fields{ + "mountpoint": mountpoint, + "dimgDigest": dimgDigest, + }).Info("start to mount DImg") + patchDirPath := path.Join(imageStore, dimgDigest+".dimg") + log.G(ctx).WithFields(logrus.Fields{ + "patchDirPath": patchDirPath, + }).Info("mounting di3fs") + go func() { + err := di3fs.Do(patchDirPath, mountpoint) + if err != nil { + log.G(ctx).WithFields(logrus.Fields{ + "patchDirPath": patchDirPath, + }).Errorf("failed to mount di3fs : %v", err) + } + }() + return nil +} + +func (f *DummyFS) Check(ctx context.Context, mountpoint string, labels map[string]string) error { + log.G(ctx).WithFields(logrus.Fields{ + "mountpoint": mountpoint, + "labels": labels, + }).Info("DummyFS Check called") + return nil +} + +func (f *DummyFS) Unmount(ctx context.Context, mountpoint string) error { + log.G(ctx).WithFields(logrus.Fields{ + "mountpoint": mountpoint, + }).Info("DummyFS Unmount called") + + err := exec.Command("fusermount3", "-u", mountpoint).Run() + if err != nil { + log.G(ctx).Errorf("failed to unmount %s", mountpoint) + return err + } + return nil +} diff --git a/cmd/snapshotter/main.go b/cmd/snapshotter/main.go new file mode 100644 index 0000000..b28e4ad --- /dev/null +++ b/cmd/snapshotter/main.go @@ -0,0 +1,192 @@ +// partially copied from https://github.com/mc256/starlight/blob/25af23b9655e133ab0e5f31a7dde5aaec2241bfa/client/client.go + +package main + +import ( + "context" + "net" + "os" + "os/exec" + "path/filepath" + "strings" + "sync" + + "github.com/containerd/containerd" + snapshotsapi "github.com/containerd/containerd/api/services/snapshots/v1" + "github.com/containerd/containerd/contrib/snapshotservice" + "github.com/containerd/containerd/log" + "github.com/containerd/containerd/snapshots" + sns "github.com/naoki9911/fuse-diff-containerd/pkg/snapshotter" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "google.golang.org/grpc" +) + +type Client struct { + ctx context.Context + + ctr *containerd.Client + + snRootPath string + snImageStorePath string + snSocketPath string + snGrpcServer *grpc.Server + snSnapshotter *snapshotter + snListener net.Listener +} + +func unmountDi3FS() error { + cmd := exec.Command("mount", "-l") + mountList, err := cmd.Output() + if err != nil { + return err + } + mounts := strings.Split(string(mountList), "\n") + for _, m := range mounts { + if !strings.HasPrefix(m, "fuse-diff") { + continue + } + cols := strings.Split(m, " ") + di3fsMountPath := cols[2] + err = exec.Command("fusermount3", "-u", di3fsMountPath).Run() + if err != nil { + log.G(context.TODO()).Errorf("failed to unmount %s", di3fsMountPath) + return err + } + log.G(context.TODO()).Infof("unmounted %s", di3fsMountPath) + } + + return nil +} + +func removeImages(ctx context.Context, ctrClient *containerd.Client) error { + imgStore := ctrClient.ImageService() + imgs, err := imgStore.List(ctx) + if err != nil { + return err + } + for _, img := range imgs { + targetSns, ok := img.Labels[sns.TargetSnapshotLabel] + if !ok { + continue + } + if targetSns != "di3fs" { + continue + } + err = imgStore.Delete(ctx, img.Name) + if err != nil { + log.G(ctx).Errorf("failed to remove image %s", img.Name) + } + log.G(ctx).Infof("removed image %s", img.Name) + } + + return nil +} + +func NewClient() (*Client, error) { + ctr, err := containerd.New("/run/containerd/containerd.sock", containerd.WithDefaultNamespace("default")) + if err != nil { + return nil, err + } + + c := &Client{ + ctx: context.TODO(), + ctr: ctr, + snRootPath: "/tmp/di3fs/sn", + snSocketPath: "/run/di3fs/snapshotter.sock", + } + + c.snImageStorePath = filepath.Join(c.snRootPath, "images") + + return c, nil +} + +func main() { + log.GetLogger(context.TODO()).Logger.SetLevel(logrus.DebugLevel) + err := unmountDi3FS() + if err != nil { + log.G(context.TODO()).WithError(err).Fatal("failed to unmount di3fs") + } + client, err := NewClient() + if err != nil { + log.G(context.TODO()).WithError(err).Error("failed to create client") + os.Exit(1) + } + + err = client.initSnapshotter() + if err != nil { + log.G(client.ctx).WithError(err).Error("failed to init snapsohtter") + os.Exit(1) + } + + client.startSnapshotter() +} + +func (c *Client) initSnapshotter() error { + log.G(c.ctx).Debug("initializing snapshotter") + c.snGrpcServer = grpc.NewServer() + var err error + fs := &DummyFS{} + err = removeImages(context.TODO(), c.ctr) + if err != nil { + log.G(context.TODO()).WithError(err).Error("failed to remove images") + os.Exit(1) + } + if err = os.RemoveAll(c.snRootPath); err != nil { + return errors.Wrapf(err, "failed to remove %q", c.snRootPath) + } + if err = os.MkdirAll(c.snRootPath, 0755); err != nil { + return errors.Wrapf(err, "failed to create directory %q", c.snRootPath) + } + if err = os.MkdirAll(c.snImageStorePath, 0755); err != nil { + return errors.Wrapf(err, "failed to create directory %q", c.snRootPath) + } + c.snSnapshotter, err = NewSnapshotter(c.ctx, c.snRootPath, fs) + if err != nil { + return err + } + + svc := snapshotservice.FromSnapshotter(c.snSnapshotter) + socketDir := filepath.Dir(c.snSocketPath) + if err = os.MkdirAll(socketDir, 0700); err != nil { + return errors.Wrapf(err, "failed to create directory %q", socketDir) + } + + if err = os.RemoveAll(c.snSocketPath); err != nil { + return errors.Wrapf(err, "failed to remove %q", c.snSocketPath) + } + + snapshotsapi.RegisterSnapshotsServer(c.snGrpcServer, svc) + + return nil +} + +func (c *Client) startSnapshotter() { + log.G(c.ctx).Debug("snapshotter service starting") + // Listen and serve + var err error + c.snListener, err = net.Listen("unix", c.snSocketPath) + if err != nil { + log.G(c.ctx).WithError(err).Errorf("failed to listen on %q", c.snSocketPath) + return + } + + log.G(c.ctx). + WithField("socket", c.snSocketPath). + Info("di3fs snapshotter service started") + + c.snSnapshotter.Walk(c.ctx, func(ctx context.Context, i snapshots.Info) error { + log.G(ctx).WithField("snapshots.Info", i).Info("walking") + return nil + }) + var wg sync.WaitGroup + wg.Add(1) + go func() { + if err = c.snGrpcServer.Serve(c.snListener); err != nil { + log.G(c.ctx).WithError(err).Errorf("failed to serve snapshotter") + return + } + }() + + wg.Wait() +} diff --git a/cmd/snapshotter/snapshot.go b/cmd/snapshotter/snapshot.go new file mode 100644 index 0000000..1f7a672 --- /dev/null +++ b/cmd/snapshotter/snapshot.go @@ -0,0 +1,800 @@ +// Copied from https://raw.githubusercontent.com/containerd/stargz-snapshotter/main/snapshot/snapshot.go +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package main + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strings" + "syscall" + + "github.com/containerd/containerd/errdefs" + "github.com/containerd/containerd/log" + "github.com/containerd/containerd/mount" + "github.com/containerd/containerd/snapshots" + "github.com/containerd/containerd/snapshots/overlay/overlayutils" + "github.com/containerd/containerd/snapshots/storage" + "github.com/containerd/continuity/fs" + "github.com/moby/sys/mountinfo" + sns "github.com/naoki9911/fuse-diff-containerd/pkg/snapshotter" + "github.com/sirupsen/logrus" + "golang.org/x/sync/errgroup" +) + +const ( + targetSnapshotLabel = "containerd.io/snapshot.ref" + remoteLabel = "containerd.io/snapshot/remote" + remoteLabelVal = "remote snapshot" + + // remoteSnapshotLogKey is a key for log line, which indicates whether + // `Prepare` method successfully prepared targeting remote snapshot or not, as + // defined in the following: + // - "true" : indicates the snapshot has been successfully prepared as a + // remote snapshot + // - "false" : indicates the snapshot failed to be prepared as a remote + // snapshot + // - null : undetermined + //remoteSnapshotLogKey = "remote-snapshot-prepared" + //prepareSucceeded = "true" + //prepareFailed = "false" +) + +// FileSystem is a backing filesystem abstraction. +// +// Mount() tries to mount a remote snapshot to the specified mount point +// directory. If succeed, the mountpoint directory will be treated as a layer +// snapshot. If Mount() fails, the mountpoint directory MUST be cleaned up. +// Check() is called to check the connectibity of the existing layer snapshot +// every time the layer is used by containerd. +// Unmount() is called to unmount a remote snapshot from the specified mount point +// directory. +type FileSystem interface { + Mount(ctx context.Context, mountpoint string, labels map[string]string) error + Check(ctx context.Context, mountpoint string, labels map[string]string) error + Unmount(ctx context.Context, mountpoint string) error +} + +// SnapshotterConfig is used to configure the remote snapshotter instance +type SnapshotterConfig struct { + asyncRemove bool + noRestore bool + allowInvalidMountsOnRestart bool +} + +// Opt is an option to configure the remote snapshotter +type Opt func(config *SnapshotterConfig) error + +// AsynchronousRemove defers removal of filesystem content until +// the Cleanup method is called. Removals will make the snapshot +// referred to by the key unavailable and make the key immediately +// available for re-use. +func AsynchronousRemove(config *SnapshotterConfig) error { + config.asyncRemove = true + return nil +} + +func NoRestore(config *SnapshotterConfig) error { + config.noRestore = true + return nil +} + +func AllowInvalidMountsOnRestart(config *SnapshotterConfig) error { + config.allowInvalidMountsOnRestart = true + return nil +} + +type snapshotter struct { + root string + ms *storage.MetaStore + asyncRemove bool + + // fs is a filesystem that this snapshotter recognizes. + fs FileSystem + userxattr bool // whether to enable "userxattr" mount option + noRestore bool + allowInvalidMountsOnRestart bool +} + +// NewSnapshotter returns a Snapshotter which can use unpacked remote layers +// as snapshots. This is implemented based on the overlayfs snapshotter, so +// diffs are stored under the provided root and a metadata file is stored under +// the root as same as overlayfs snapshotter. +func NewSnapshotter(ctx context.Context, root string, targetFs FileSystem, opts ...Opt) (*snapshotter, error) { + if targetFs == nil { + return nil, fmt.Errorf("specify filesystem to use") + } + + var config SnapshotterConfig + for _, opt := range opts { + if err := opt(&config); err != nil { + return nil, err + } + } + + if err := os.MkdirAll(root, 0700); err != nil { + return nil, err + } + supportsDType, err := fs.SupportsDType(root) + if err != nil { + return nil, err + } + if !supportsDType { + return nil, fmt.Errorf("%s does not support d_type. If the backing filesystem is xfs, please reformat with ftype=1 to enable d_type support", root) + } + ms, err := storage.NewMetaStore(filepath.Join(root, "metadata.db")) + if err != nil { + return nil, err + } + + if err := os.Mkdir(filepath.Join(root, "snapshots"), 0700); err != nil && !os.IsExist(err) { + return nil, err + } + + userxattr, err := overlayutils.NeedsUserXAttr(root) + if err != nil { + logrus.WithError(err).Warnf("cannot detect whether \"userxattr\" option needs to be used, assuming to be %v", userxattr) + } + + o := &snapshotter{ + root: root, + ms: ms, + asyncRemove: config.asyncRemove, + fs: targetFs, + userxattr: userxattr, + noRestore: config.noRestore, + allowInvalidMountsOnRestart: config.allowInvalidMountsOnRestart, + } + + if err := o.restoreRemoteSnapshot(ctx); err != nil { + return nil, fmt.Errorf("failed to restore remote snapshot: %w", err) + } + + return o, nil +} + +// Stat returns the info for an active or committed snapshot by name or +// key. +// +// Should be used for parent resolution, existence checks and to discern +// the kind of snapshot. +func (o *snapshotter) Stat(ctx context.Context, key string) (snapshots.Info, error) { + log.G(ctx).WithFields(logrus.Fields{ + "key": key, + }).Debugf("snapshotter Stat() called") + ctx, t, err := o.ms.TransactionContext(ctx, false) + if err != nil { + return snapshots.Info{}, err + } + defer t.Rollback() + _, info, _, err := storage.GetInfo(ctx, key) + if err != nil { + return snapshots.Info{}, err + } + log.G(ctx).WithFields(logrus.Fields{ + "info": info, + }).Debugf("getinfo done") + + return info, nil +} + +func (o *snapshotter) Update(ctx context.Context, info snapshots.Info, fieldpaths ...string) (snapshots.Info, error) { + log.G(ctx).Info("snapshotter Update() called") + ctx, t, err := o.ms.TransactionContext(ctx, true) + if err != nil { + return snapshots.Info{}, err + } + + info, err = storage.UpdateInfo(ctx, info, fieldpaths...) + if err != nil { + t.Rollback() + return snapshots.Info{}, err + } + + if err := t.Commit(); err != nil { + return snapshots.Info{}, err + } + + return info, nil +} + +// Usage returns the resources taken by the snapshot identified by key. +// +// For active snapshots, this will scan the usage of the overlay "diff" (aka +// "upper") directory and may take some time. +// for remote snapshots, no scan will be held and recognise the number of inodes +// and these sizes as "zero". +// +// For committed snapshots, the value is returned from the metadata database. +func (o *snapshotter) Usage(ctx context.Context, key string) (snapshots.Usage, error) { + log.G(ctx).Info("snapshotter Usage() called") + ctx, t, err := o.ms.TransactionContext(ctx, false) + if err != nil { + return snapshots.Usage{}, err + } + id, info, usage, err := storage.GetInfo(ctx, key) + t.Rollback() // transaction no longer needed at this point. + + if err != nil { + return snapshots.Usage{}, err + } + + upperPath := o.upperPath(id) + + if info.Kind == snapshots.KindActive { + du, err := fs.DiskUsage(ctx, upperPath) + if err != nil { + // TODO(stevvooe): Consider not reporting an error in this case. + return snapshots.Usage{}, err + } + + usage = snapshots.Usage(du) + } + + return usage, nil +} + +func (o *snapshotter) Prepare(ctx context.Context, key, parent string, opts ...snapshots.Opt) ([]mount.Mount, error) { + log.G(ctx).WithFields(logrus.Fields{ + "key": key, + "parent": parent, + "opts": opts, + }).Info("snapshotter Prepare() called") + s, err := o.createSnapshot(ctx, snapshots.KindActive, key, parent, opts) + if err != nil { + return nil, err + } + + // Try to prepare the remote snapshot. If succeeded, we commit the snapshot now + // and return ErrAlreadyExists. + var base snapshots.Info + for _, opt := range opts { + if err := opt(&base); err != nil { + return nil, err + } + } + if target, ok := base.Labels[sns.SnapshotLabelRefUncompressed]; ok { + // NOTE: If passed labels include a target of the remote snapshot, `Prepare` + // must log whether this method succeeded to prepare that remote snapshot + // or not, using the key `remoteSnapshotLogKey` defined in the above. This + // log is used by tests in this project. + //lCtx := log.WithLogger(ctx, log.G(ctx).WithField("key", key).WithField("parent", parent)) + //if err := o.prepareRemoteSnapshot(lCtx, key, base.Labels); err != nil { + // log.G(lCtx).WithField(remoteSnapshotLogKey, prepareFailed). + // WithError(err).Warn("failed to prepare remote snapshot") + //} else { + // //base.Labels[remoteLabel] = remoteLabelVal // Mark this snapshot as remote + // //err := o.commit(ctx, true, target, key, append(opts, snapshots.WithLabels(base.Labels))...) + // if err == nil || errdefs.IsAlreadyExists(err) { + // // count also AlreadyExists as "success" + // log.G(lCtx).WithField(remoteSnapshotLogKey, prepareSucceeded).Debug("prepared remote snapshot") + // return nil, fmt.Errorf("target snapshot %q: %w", target, errdefs.ErrAlreadyExists) + // } + // log.G(lCtx).WithField(remoteSnapshotLogKey, prepareFailed). + // WithError(err).Warn("failed to internally commit remote snapshot") + // // Don't fallback here (= prohibit to use this key again) because the FileSystem + // // possible has done some work on this "upper" directory. + // return nil, err + //} + log.G(ctx).WithField("target", target).Info("prepare remote layer") + if err := o.prepareRemoteSnapshot(ctx, key, base.Labels); err != nil { + log.G(ctx).WithError(err).Error("failed to prepare remote layer") + return nil, err + } + } + mnt, err := o.mounts(ctx, s, parent) + if err != nil { + return nil, err + } + log.G(ctx).WithField("mnt", mnt).Debugf("prepare done") + return mnt, nil +} + +func (o *snapshotter) View(ctx context.Context, key, parent string, opts ...snapshots.Opt) ([]mount.Mount, error) { + log.G(ctx).Info("snapshotter View() called") + s, err := o.createSnapshot(ctx, snapshots.KindView, key, parent, opts) + if err != nil { + return nil, err + } + return o.mounts(ctx, s, parent) +} + +// Mounts returns the mounts for the transaction identified by key. Can be +// called on an read-write or readonly transaction. +// +// This can be used to recover mounts after calling View or Prepare. +func (o *snapshotter) Mounts(ctx context.Context, key string) ([]mount.Mount, error) { + log.G(ctx).Info("snapshotter Mount() called") + ctx, t, err := o.ms.TransactionContext(ctx, false) + if err != nil { + return nil, err + } + s, err := storage.GetSnapshot(ctx, key) + t.Rollback() + if err != nil { + return nil, fmt.Errorf("failed to get active mount: %w", err) + } + return o.mounts(ctx, s, key) +} + +func (o *snapshotter) Commit(ctx context.Context, name, key string, opts ...snapshots.Opt) error { + log.G(ctx).Info("snapshotter Commit() called") + return o.commit(ctx, false, name, key, opts...) +} + +func (o *snapshotter) commit(ctx context.Context, isRemote bool, name, key string, opts ...snapshots.Opt) error { + ctx, t, err := o.ms.TransactionContext(ctx, true) + if err != nil { + return err + } + + defer func() { + if err != nil { + if rerr := t.Rollback(); rerr != nil { + log.G(ctx).WithError(rerr).Warn("failed to rollback transaction") + } + } + }() + + // grab the existing id + id, _, usage, err := storage.GetInfo(ctx, key) + if err != nil { + return err + } + + if !isRemote { // skip diskusage for remote snapshots for allowing lazy preparation of nodes + du, err := fs.DiskUsage(ctx, o.upperPath(id)) + if err != nil { + return err + } + usage = snapshots.Usage(du) + } + + if _, err = storage.CommitActive(ctx, key, name, usage, opts...); err != nil { + return fmt.Errorf("failed to commit snapshot: %w", err) + } + + return t.Commit() +} + +// Remove abandons the snapshot identified by key. The snapshot will +// immediately become unavailable and unrecoverable. Disk space will +// be freed up on the next call to `Cleanup`. +func (o *snapshotter) Remove(ctx context.Context, key string) (err error) { + log.G(ctx).Info("snapshotter Remove() called") + ctx, t, err := o.ms.TransactionContext(ctx, true) + if err != nil { + return err + } + defer func() { + if err != nil { + if rerr := t.Rollback(); rerr != nil { + log.G(ctx).WithError(rerr).Warn("failed to rollback transaction") + } + } + }() + + _, _, err = storage.Remove(ctx, key) + if err != nil { + return fmt.Errorf("failed to remove: %w", err) + } + + if !o.asyncRemove { + var removals []string + const cleanupCommitted = false + removals, err = o.getCleanupDirectories(ctx, t, cleanupCommitted) + if err != nil { + return fmt.Errorf("unable to get directories for removal: %w", err) + } + + // Remove directories after the transaction is closed, failures must not + // return error since the transaction is committed with the removal + // key no longer available. + defer func() { + if err == nil { + for _, dir := range removals { + if err := o.cleanupSnapshotDirectory(ctx, dir); err != nil { + log.G(ctx).WithError(err).WithField("path", dir).Warn("failed to remove directory") + } + } + } + }() + + } + + return t.Commit() +} + +// Walk the snapshots. +func (o *snapshotter) Walk(ctx context.Context, fn snapshots.WalkFunc, fs ...string) error { + log.G(ctx).WithFields(logrus.Fields{ + "fn": fn, + "fs": fs, + }).Info("snapshotter Walk() called") + ctx, t, err := o.ms.TransactionContext(ctx, false) + if err != nil { + return err + } + defer t.Rollback() + return storage.WalkInfo(ctx, fn, fs...) +} + +// Cleanup cleans up disk resources from removed or abandoned snapshots +func (o *snapshotter) Cleanup(ctx context.Context) error { + log.G(ctx).Info("snapshotter Cleanup() called") + const cleanupCommitted = false + return o.cleanup(ctx, cleanupCommitted) +} + +func (o *snapshotter) cleanup(ctx context.Context, cleanupCommitted bool) error { + cleanup, err := o.cleanupDirectories(ctx, cleanupCommitted) + if err != nil { + return err + } + + log.G(ctx).Debugf("cleanup: dirs=%v", cleanup) + for _, dir := range cleanup { + if err := o.cleanupSnapshotDirectory(ctx, dir); err != nil { + log.G(ctx).WithError(err).WithField("path", dir).Warn("failed to remove directory") + } + } + + return nil +} + +func (o *snapshotter) cleanupDirectories(ctx context.Context, cleanupCommitted bool) ([]string, error) { + // Get a write transaction to ensure no other write transaction can be entered + // while the cleanup is scanning. + ctx, t, err := o.ms.TransactionContext(ctx, true) + if err != nil { + return nil, err + } + + defer t.Rollback() + return o.getCleanupDirectories(ctx, t, cleanupCommitted) +} + +func (o *snapshotter) getCleanupDirectories(ctx context.Context, t storage.Transactor, cleanupCommitted bool) ([]string, error) { + ids, err := storage.IDMap(ctx) + if err != nil { + return nil, err + } + + snapshotDir := filepath.Join(o.root, "snapshots") + fd, err := os.Open(snapshotDir) + if err != nil { + return nil, err + } + defer fd.Close() + + dirs, err := fd.Readdirnames(0) + if err != nil { + return nil, err + } + + cleanup := []string{} + for _, d := range dirs { + if !cleanupCommitted { + if _, ok := ids[d]; ok { + continue + } + } + + cleanup = append(cleanup, filepath.Join(snapshotDir, d)) + } + + return cleanup, nil +} + +func (o *snapshotter) cleanupSnapshotDirectory(ctx context.Context, dir string) error { + + // On a remote snapshot, the layer is mounted on the "fs" directory. + // We use Filesystem's Unmount API so that it can do necessary finalization + // before/after the unmount. + mp := filepath.Join(dir, "fs") + if err := o.fs.Unmount(ctx, mp); err != nil { + log.G(ctx).WithError(err).WithField("dir", mp).Debug("failed to unmount") + } + if err := os.RemoveAll(dir); err != nil { + return fmt.Errorf("failed to remove directory %q: %w", dir, err) + } + return nil +} + +func (o *snapshotter) createSnapshot(ctx context.Context, kind snapshots.Kind, key, parent string, opts []snapshots.Opt) (_ storage.Snapshot, err error) { + ctx, t, err := o.ms.TransactionContext(ctx, true) + if err != nil { + return storage.Snapshot{}, err + } + + var td, path string + defer func() { + if err != nil { + if td != "" { + if err1 := o.cleanupSnapshotDirectory(ctx, td); err1 != nil { + log.G(ctx).WithError(err1).Warn("failed to cleanup temp snapshot directory") + } + } + if path != "" { + if err1 := o.cleanupSnapshotDirectory(ctx, path); err1 != nil { + log.G(ctx).WithError(err1).WithField("path", path).Error("failed to reclaim snapshot directory, directory may need removal") + err = fmt.Errorf("failed to remove path: %v: %w", err1, err) + } + } + } + }() + + snapshotDir := filepath.Join(o.root, "snapshots") + td, err = o.prepareDirectory(ctx, snapshotDir, kind) + if err != nil { + if rerr := t.Rollback(); rerr != nil { + log.G(ctx).WithError(rerr).Warn("failed to rollback transaction") + } + return storage.Snapshot{}, fmt.Errorf("failed to create prepare snapshot dir: %w", err) + } + rollback := true + defer func() { + if rollback { + if rerr := t.Rollback(); rerr != nil { + log.G(ctx).WithError(rerr).Warn("failed to rollback transaction") + } + } + }() + + s, err := storage.CreateSnapshot(ctx, kind, key, parent, opts...) + if err != nil { + return storage.Snapshot{}, fmt.Errorf("failed to create snapshot: %w", err) + } + + if len(s.ParentIDs) > 0 { + st, err := os.Stat(o.upperPath(s.ParentIDs[0])) + if err != nil { + return storage.Snapshot{}, fmt.Errorf("failed to stat parent: %w", err) + } + + stat := st.Sys().(*syscall.Stat_t) + + if err := os.Lchown(filepath.Join(td, "fs"), int(stat.Uid), int(stat.Gid)); err != nil { + if rerr := t.Rollback(); rerr != nil { + log.G(ctx).WithError(rerr).Warn("failed to rollback transaction") + } + return storage.Snapshot{}, fmt.Errorf("failed to chown: %w", err) + } + } + + path = filepath.Join(snapshotDir, s.ID) + if err = os.Rename(td, path); err != nil { + return storage.Snapshot{}, fmt.Errorf("failed to rename: %w", err) + } + td = "" + + rollback = false + if err = t.Commit(); err != nil { + return storage.Snapshot{}, fmt.Errorf("commit failed: %w", err) + } + + return s, nil +} + +func (o *snapshotter) prepareDirectory(ctx context.Context, snapshotDir string, kind snapshots.Kind) (string, error) { + td, err := os.MkdirTemp(snapshotDir, "new-") + if err != nil { + return "", fmt.Errorf("failed to create temp dir: %w", err) + } + + if err := os.Mkdir(filepath.Join(td, "fs"), 0755); err != nil { + return td, err + } + + if kind == snapshots.KindActive { + if err := os.Mkdir(filepath.Join(td, "work"), 0711); err != nil { + return td, err + } + } + + return td, nil +} + +func (o *snapshotter) mounts(ctx context.Context, s storage.Snapshot, checkKey string) ([]mount.Mount, error) { + // Make sure that all layers lower than the target layer are available + if checkKey != "" && !o.checkAvailability(ctx, checkKey) { + return nil, fmt.Errorf("layer %q unavailable: %w", s.ID, errdefs.ErrUnavailable) + } + + if len(s.ParentIDs) == 0 { + // if we only have one layer/no parents then just return a bind mount as overlay + // will not work + roFlag := "rw" + if s.Kind == snapshots.KindView { + roFlag = "ro" + } + + return []mount.Mount{ + { + Source: o.upperPath(s.ID), + Type: "bind", + Options: []string{ + roFlag, + "rbind", + }, + }, + }, nil + } + var options []string + + if s.Kind == snapshots.KindActive { + options = append(options, + fmt.Sprintf("workdir=%s", o.workPath(s.ID)), + fmt.Sprintf("upperdir=%s", o.upperPath(s.ID)), + ) + } else if len(s.ParentIDs) == 1 { + return []mount.Mount{ + { + Source: o.upperPath(s.ParentIDs[0]), + Type: "bind", + Options: []string{ + "ro", + "rbind", + }, + }, + }, nil + } + + parentPaths := make([]string, len(s.ParentIDs)) + for i := range s.ParentIDs { + parentPaths[i] = o.upperPath(s.ParentIDs[i]) + } + + options = append(options, fmt.Sprintf("lowerdir=%s", strings.Join(parentPaths, ":"))) + if o.userxattr { + options = append(options, "userxattr") + } + return []mount.Mount{ + { + Type: "overlay", + Source: "overlay", + Options: options, + }, + }, nil + +} + +func (o *snapshotter) upperPath(id string) string { + return filepath.Join(o.root, "snapshots", id, "fs") +} + +func (o *snapshotter) workPath(id string) string { + return filepath.Join(o.root, "snapshots", id, "work") +} + +// Close closes the snapshotter +func (o *snapshotter) Close() error { + log.G(context.TODO()).Info("snapshotter Close() called") + // unmount all mounts including Committed + const cleanupCommitted = true + ctx := context.Background() + if err := o.cleanup(ctx, cleanupCommitted); err != nil { + log.G(ctx).WithError(err).Warn("failed to cleanup") + } + return o.ms.Close() +} + +// prepareRemoteSnapshot tries to prepare the snapshot as a remote snapshot +// using filesystems registered in this snapshotter. +func (o *snapshotter) prepareRemoteSnapshot(ctx context.Context, key string, labels map[string]string) error { + ctx, t, err := o.ms.TransactionContext(ctx, false) + if err != nil { + return err + } + defer t.Rollback() + id, _, _, err := storage.GetInfo(ctx, key) + if err != nil { + return err + } + + mountpoint := o.upperPath(id) + log.G(ctx).Infof("preparing filesystem mount at mountpoint=%v", mountpoint) + + return o.fs.Mount(ctx, mountpoint, labels) +} + +// checkAvailability checks avaiability of the specified layer and all lower +// layers using filesystem's checking functionality. +func (o *snapshotter) checkAvailability(ctx context.Context, key string) bool { + ctx = log.WithLogger(ctx, log.G(ctx).WithField("key", key)) + log.G(ctx).Debug("checking layer availability") + + ctx, t, err := o.ms.TransactionContext(ctx, false) + if err != nil { + log.G(ctx).WithError(err).Warn("failed to get transaction") + return false + } + defer t.Rollback() + + eg, egCtx := errgroup.WithContext(ctx) + for cKey := key; cKey != ""; { + id, info, _, err := storage.GetInfo(ctx, cKey) + if err != nil { + log.G(ctx).WithError(err).Warnf("failed to get info of %q", cKey) + return false + } + mp := o.upperPath(id) + lCtx := log.WithLogger(ctx, log.G(ctx).WithField("mount-point", mp)) + if _, ok := info.Labels[remoteLabel]; ok { + eg.Go(func() error { + log.G(lCtx).Debug("checking mount point") + if err := o.fs.Check(egCtx, mp, info.Labels); err != nil { + log.G(lCtx).WithError(err).Warn("layer is unavailable") + return err + } + return nil + }) + } else { + log.G(lCtx).Debug("layer is normal snapshot(overlayfs)") + } + cKey = info.Parent + } + if err := eg.Wait(); err != nil { + return false + } + return true +} + +func (o *snapshotter) restoreRemoteSnapshot(ctx context.Context) error { + mounts, err := mountinfo.GetMounts(nil) + if err != nil { + return err + } + for _, m := range mounts { + if strings.HasPrefix(m.Mountpoint, filepath.Join(o.root, "snapshots")) { + if err := syscall.Unmount(m.Mountpoint, syscall.MNT_FORCE); err != nil { + return fmt.Errorf("failed to unmount %s: %w", m.Mountpoint, err) + } + } + } + + if o.noRestore { + return nil + } + + var task []snapshots.Info + if err := o.Walk(ctx, func(ctx context.Context, info snapshots.Info) error { + if _, ok := info.Labels[remoteLabel]; ok { + task = append(task, info) + } + return nil + }); err != nil && !errdefs.IsNotFound(err) { + return err + } + for _, info := range task { + if err := o.prepareRemoteSnapshot(ctx, info.Name, info.Labels); err != nil { + if o.allowInvalidMountsOnRestart { + logrus.WithError(err).Warnf("failed to restore remote snapshot %s; remove this snapshot manually", info.Name) + // This snapshot mount is invalid but allow this. + // NOTE: snapshotter.Mount() will fail to return the mountpoint of these invalid snapshots so + // containerd cannot use them anymore. User needs to manually remove the snapshots from + // containerd's metadata store using ctr (e.g. `ctr snapshot rm`). + continue + } + return fmt.Errorf("failed to prepare remote snapshot: %s: %w", info.Name, err) + } + } + + return nil +} diff --git a/cmd/stats/main.go b/cmd/stats/main.go new file mode 100644 index 0000000..f9b0de0 --- /dev/null +++ b/cmd/stats/main.go @@ -0,0 +1,318 @@ +package main + +import ( + "bytes" + "compress/gzip" + "crypto/sha256" + "fmt" + "io" + "io/fs" + "io/ioutil" + "os" + "path" + "strings" + + "github.com/icedream/go-bsdiff" +) + +var gzipUnpackDiff = true + +func generateFileHash(filePath string) ([]byte, error) { + file, err := os.Open(filePath) + if err != nil { + return nil, err + } + defer file.Close() + + h := sha256.New() + _, err = io.Copy(h, file) + if err != nil { + return nil, err + } + + return h.Sum(nil), nil +} + +// return true if files are same. +func compareFile(fileAPath, fileBPath string) (bool, error) { + fileAHash, err := generateFileHash(fileAPath) + if err != nil { + return false, err + } + fileBHash, err := generateFileHash(fileBPath) + if err != nil { + return false, err + } + + for i := range fileAHash { + if fileAHash[i] != fileBHash[i] { + return false, nil + } + } + + return true, nil +} + +func generateFileDiff(baseFilePath, newFilePath string) ([]byte, error) { + baseFile, err := os.Open(baseFilePath) + if err != nil { + return nil, err + } + defer baseFile.Close() + newFile, err := os.Open(newFilePath) + if err != nil { + return nil, err + } + defer newFile.Close() + writer := new(bytes.Buffer) + err = bsdiff.Diff(baseFile, newFile, writer) + if err != nil { + return nil, err + } + + return writer.Bytes(), nil +} + +func generateFileDiffFromGzip(baseFilePath, newFilePath string) ([]byte, error) { + baseFile, err := os.Open(baseFilePath) + if err != nil { + return nil, err + } + defer baseFile.Close() + gzipBaseReader, err := gzip.NewReader(baseFile) + if err != nil { + return nil, err + } + newFile, err := os.Open(newFilePath) + if err != nil { + return nil, err + } + defer newFile.Close() + gzipNewReader, err := gzip.NewReader(newFile) + if err != nil { + return nil, err + } + writer := new(bytes.Buffer) + err = bsdiff.Diff(gzipBaseReader, gzipNewReader, writer) + if err != nil { + return nil, err + } + + return writer.Bytes(), nil +} + +func compressWithGzipFromFile(path string) ([]byte, error) { + file, err := os.Open(path) + if err != nil { + return nil, err + } + fileBytes, err := ioutil.ReadAll(file) + if err != nil { + return nil, err + } + + return compressWithGzip(fileBytes) +} + +func compressWithGzip(src []byte) ([]byte, error) { + writer := new(bytes.Buffer) + gWriter := gzip.NewWriter(writer) + _, err := gWriter.Write(src) + if err != nil { + return nil, err + } + err = gWriter.Close() + if err != nil { + return nil, err + } + + return writer.Bytes(), nil +} + +type DiffStats = struct { + numDir int + numFile int + numSymlink int + numNewDir int + numNewFile int + numNewSymlink int + numSameDir int + numSameFile int + numSameSymlink int + numDiffDir int + numDiffFile int + numDiffSymlink int + diffFileStats []DiffFileStats +} + +type DiffFileStats = struct { + path string + newFileSize int64 + diffSize int + newFileGzipSize int + diffGzipSize int +} + +func accumulateStats(baseDir, newDir string, stats *DiffStats) error { + baseFiles := map[string]fs.DirEntry{} + if _, err := os.Stat(baseDir); err == nil { + baseEntries, err := os.ReadDir(baseDir) + if err != nil { + return err + } + for i := range baseEntries { + baseFiles[baseEntries[i].Name()] = baseEntries[i] + } + } + newEntries, err := os.ReadDir(newDir) + if err != nil { + return err + } + + for _, entry := range newEntries { + fName := entry.Name() + baseFilePath := path.Join(baseDir, fName) + newFilePath := path.Join(newDir, fName) + fileInfo, err := entry.Info() + if err != nil { + return err + } + + isDir := entry.IsDir() + isSymlink := fileInfo.Mode()&os.ModeSymlink == os.ModeSymlink + if isDir { + stats.numDir += 1 + } else if isSymlink { + stats.numSymlink += 1 + } else { + stats.numFile += 1 + } + + baseEntry, ok := baseFiles[fName] + if !ok { + if isDir { + stats.numNewDir += 1 + } else if isSymlink { + stats.numNewSymlink += 1 + } else { + stats.numNewFile += 1 + } + } else { + baseFI, err := baseEntry.Info() + isBaseDir := baseEntry.IsDir() + isBaseSymlink := baseFI.Mode()&os.ModeSymlink == os.ModeSymlink + if err != nil { + return err + } + if isDir { + if isBaseDir { + stats.numSameDir += 1 + } else { + stats.numDiffDir += 1 + } + } else if isSymlink { + if isBaseSymlink { + baseRealPath, err := os.Readlink(baseFilePath) + if err != nil { + return err + } + + newRealPath, err := os.Readlink(newFilePath) + if err != nil { + return err + } + + if baseRealPath == newRealPath { + stats.numSameSymlink += 1 + } else { + stats.numDiffSymlink += 1 + } + } else { + stats.numDiffSymlink += 1 + } + } else { + if !isBaseDir && !isBaseSymlink { + isSame, err := compareFile(baseFilePath, newFilePath) + if err != nil { + return err + } + if isSame { + stats.numSameFile += 1 + } else { + stats.numDiffFile += 1 + var diffBytes []byte + if strings.Contains(baseFilePath, ".gz") && gzipUnpackDiff { + diffBytes, err = generateFileDiffFromGzip(baseFilePath, newFilePath) + if err != nil { + return err + } + } else { + diffBytes, err = generateFileDiff(baseFilePath, newFilePath) + if err != nil { + return err + } + } + gzipNewFile, err := compressWithGzipFromFile(newFilePath) + if err != nil { + return err + } + gzipDiffBytes, err := compressWithGzip(diffBytes) + if err != nil { + return err + } + stats.diffFileStats = append(stats.diffFileStats, + DiffFileStats{ + path: newFilePath, + newFileSize: fileInfo.Size(), + diffSize: len(diffBytes), + newFileGzipSize: len(gzipNewFile), + diffGzipSize: len(gzipDiffBytes), + }) + } + } else { + stats.numDiffFile += 1 + } + } + } + if isDir { + err = accumulateStats(baseFilePath, newFilePath, stats) + if err != nil { + return err + } + } + } + return nil +} + +func main() { + if len(os.Args) < 3 { + fmt.Println("diff base-dir new-dir") + os.Exit(1) + } + baseDir := os.Args[1] + newDir := os.Args[2] + stats := &DiffStats{ + diffFileStats: make([]DiffFileStats, 0), + } + err := accumulateStats(baseDir, newDir, stats) + if err != nil { + panic(err) + } + + fmt.Printf("numDir: %d\n", stats.numDir) + fmt.Printf("numFile: %d\n", stats.numFile) + fmt.Printf("numSynlink: %d\n", stats.numSymlink) + fmt.Printf("numNewDir: %d\n", stats.numNewDir) + fmt.Printf("numNewFile: %d\n", stats.numNewFile) + fmt.Printf("numNewSymlink: %d\n", stats.numNewSymlink) + fmt.Printf("numSameDir: %d\n", stats.numSameDir) + fmt.Printf("numSameFile: %d\n", stats.numSameFile) + fmt.Printf("numSameSymlink: %d\n", stats.numSameSymlink) + fmt.Printf("numDiffDir: %d\n", stats.numDiffDir) + fmt.Printf("numDiffFile: %d\n", stats.numDiffFile) + fmt.Printf("numDiffSymlink: %d\n", stats.numDiffSymlink) + fmt.Printf("path, newFileSize(bytes), diffSize(bytes), newFileGzipSize(bytes), diffGzipSize(bytes)\n") + for _, diff := range stats.diffFileStats { + fmt.Printf("\"%s\",%d,%d,%d,%d\n", diff.path, diff.newFileSize, diff.diffSize, diff.newFileGzipSize, diff.diffGzipSize) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f0c2b85 --- /dev/null +++ b/go.mod @@ -0,0 +1,68 @@ +module github.com/naoki9911/fuse-diff-containerd + +go 1.19 + +require ( + github.com/containerd/containerd v1.6.12 + github.com/containerd/continuity v0.3.0 + github.com/moby/sys/mountinfo v0.5.0 + github.com/opencontainers/go-digest v1.0.0 + github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 + github.com/pkg/errors v0.9.1 + github.com/sirupsen/logrus v1.8.1 + github.com/urfave/cli/v2 v2.23.7 + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c + google.golang.org/grpc v1.47.0 +) + +require ( + github.com/Microsoft/go-winio v0.5.2 // indirect + github.com/Microsoft/hcsshim v0.9.5 // indirect + github.com/containerd/cgroups v1.0.3 // indirect + github.com/containerd/fifo v1.0.0 // indirect + github.com/containerd/ttrpc v1.1.0 // indirect + github.com/containerd/typeurl v1.0.2 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect + github.com/dsnet/compress v0.0.1 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/gin-gonic/gin v1.8.2 // indirect + github.com/go-playground/locales v0.14.0 // indirect + github.com/go-playground/universal-translator v0.18.0 // indirect + github.com/go-playground/validator/v10 v10.11.1 // indirect + github.com/goccy/go-json v0.9.11 // indirect + github.com/gogo/googleapis v1.4.0 // 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.2 // indirect + github.com/google/uuid v1.2.0 // indirect + github.com/hanwen/go-fuse/v2 v2.2.0 // indirect + github.com/hashicorp/go-version v1.6.0 // indirect + github.com/icedream/go-bsdiff v1.0.1 // indirect + github.com/jinzhu/copier v0.3.5 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.15.13 // indirect + github.com/leodido/go-urn v1.2.1 // indirect + github.com/mattn/go-isatty v0.0.16 // indirect + github.com/moby/locker v1.0.1 // indirect + github.com/moby/sys/signal v0.6.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/opencontainers/runc v1.1.2 // indirect + github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417 // indirect + github.com/opencontainers/selinux v1.10.1 // indirect + github.com/otiai10/copy v1.9.0 // indirect + github.com/pelletier/go-toml/v2 v2.0.6 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/ugorji/go/codec v1.2.8 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect + go.etcd.io/bbolt v1.3.6 // indirect + go.opencensus.io v0.23.0 // indirect + golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd // indirect + golang.org/x/net v0.4.0 // indirect + golang.org/x/sys v0.3.0 // indirect + golang.org/x/text v0.5.0 // indirect + google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21 // indirect + google.golang.org/protobuf v1.28.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..9781bce --- /dev/null +++ b/go.sum @@ -0,0 +1,1169 @@ +bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw= +github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg= +github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= +github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= +github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= +github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= +github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= +github.com/Microsoft/go-winio v0.4.16-0.20201130162521-d1ffc52c7331/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= +github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= +github.com/Microsoft/go-winio v0.4.17-0.20210211115548-6eac466e5fa3/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= +github.com/Microsoft/go-winio v0.4.17-0.20210324224401-5516f17a5958/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= +github.com/Microsoft/go-winio v0.4.17/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= +github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= +github.com/Microsoft/hcsshim v0.8.7-0.20190325164909-8abdbb8205e4/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= +github.com/Microsoft/hcsshim v0.8.7/go.mod h1:OHd7sQqRFrYd3RmSgbgji+ctCwkbq2wbEYNSzOYtcBQ= +github.com/Microsoft/hcsshim v0.8.9/go.mod h1:5692vkUqntj1idxauYlpoINNKeqCiG6Sg38RRsjT5y8= +github.com/Microsoft/hcsshim v0.8.14/go.mod h1:NtVKoYxQuTLx6gEq0L96c9Ju4JbRJ4nY2ow3VK6a9Lg= +github.com/Microsoft/hcsshim v0.8.15/go.mod h1:x38A4YbHbdxJtc0sF6oIz+RG0npwSCAvn69iY6URG00= +github.com/Microsoft/hcsshim v0.8.16/go.mod h1:o5/SZqmR7x9JNKsW3pu+nqHm0MF8vbA+VxGOoXdC600= +github.com/Microsoft/hcsshim v0.8.21/go.mod h1:+w2gRZ5ReXQhFOrvSQeNfhrYB/dg3oDwTOcER2fw4I4= +github.com/Microsoft/hcsshim v0.9.5 h1:AbV+VPfTrIVffukazHcpxmz/sRiE6YaMDzHWR9BXZHo= +github.com/Microsoft/hcsshim v0.9.5/go.mod h1:7pLA8lDk46WKDWlVsENo92gC0XFa8rbKfyFRBqxEbCc= +github.com/Microsoft/hcsshim/test v0.0.0-20201218223536-d3e5debf77da/go.mod h1:5hlzMzRKMLyo42nCZ9oml8AdTlq/0cvIaBv6tK1RehU= +github.com/Microsoft/hcsshim/test v0.0.0-20210227013316-43a75bb4edd3/go.mod h1:mw7qgWloBUl75W/gVH3cQszUg1+gUITj7D6NY7ywVnY= +github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= +github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +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/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= +github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= +github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= +github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= +github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= +github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= +github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= +github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw= +github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M= +github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg= +github.com/cilium/ebpf v0.0.0-20200702112145-1c8d4c9ef775/go.mod h1:7cR51M8ViRLIdUjrmSXlK9pkrsDlLHbO8jiB8X8JnOc= +github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs= +github.com/cilium/ebpf v0.4.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= +github.com/cilium/ebpf v0.6.2/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= +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/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/containerd/aufs v0.0.0-20200908144142-dab0cbea06f4/go.mod h1:nukgQABAEopAHvB6j7cnP5zJ+/3aVcE7hCYqvIwAHyE= +github.com/containerd/aufs v0.0.0-20201003224125-76a6863f2989/go.mod h1:AkGGQs9NM2vtYHaUen+NljV0/baGCAPELGm2q9ZXpWU= +github.com/containerd/aufs v0.0.0-20210316121734-20793ff83c97/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU= +github.com/containerd/aufs v1.0.0/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU= +github.com/containerd/btrfs v0.0.0-20201111183144-404b9149801e/go.mod h1:jg2QkJcsabfHugurUvvPhS3E08Oxiuh5W/g1ybB4e0E= +github.com/containerd/btrfs v0.0.0-20210316141732-918d888fb676/go.mod h1:zMcX3qkXTAi9GI50+0HOeuV8LU2ryCE/V2vG/ZBiTss= +github.com/containerd/btrfs v1.0.0/go.mod h1:zMcX3qkXTAi9GI50+0HOeuV8LU2ryCE/V2vG/ZBiTss= +github.com/containerd/cgroups v0.0.0-20190717030353-c4b9ac5c7601/go.mod h1:X9rLEHIqSf/wfK8NsPqxJmeZgW4pcfzdXITDrUSJ6uI= +github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko= +github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59/go.mod h1:pA0z1pT8KYB3TCXK/ocprsh7MAkoW8bZVzPdih9snmM= +github.com/containerd/cgroups v0.0.0-20200710171044-318312a37340/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo= +github.com/containerd/cgroups v0.0.0-20200824123100-0b889c03f102/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo= +github.com/containerd/cgroups v0.0.0-20210114181951-8a68de567b68/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE= +github.com/containerd/cgroups v1.0.1/go.mod h1:0SJrPIenamHDcZhEcJMNBB85rHcUsw4f25ZfBiPYRkU= +github.com/containerd/cgroups v1.0.3 h1:ADZftAkglvCiD44c77s5YmMqaP2pzVCFZvBmAlBdAP4= +github.com/containerd/cgroups v1.0.3/go.mod h1:/ofk34relqNjSGyqPrmEULrO4Sc8LJhvJmWbUCUKqj8= +github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= +github.com/containerd/console v0.0.0-20181022165439-0650fd9eeb50/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= +github.com/containerd/console v0.0.0-20191206165004-02ecf6a7291e/go.mod h1:8Pf4gM6VEbTNRIT26AyyU7hxdQU3MvAvxVI0sc00XBE= +github.com/containerd/console v1.0.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw= +github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ= +github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= +github.com/containerd/containerd v1.2.10/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.3.0-beta.2.0.20190828155532-0293cbd26c69/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.3.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.3.1-0.20191213020239-082f7e3aed57/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.4.0-beta.2.0.20200729163537-40b22ef07410/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.4.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.4.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.5.0-beta.1/go.mod h1:5HfvG1V2FsKesEGQ17k5/T7V960Tmcumvqn8Mc+pCYQ= +github.com/containerd/containerd v1.5.0-beta.3/go.mod h1:/wr9AVtEM7x9c+n0+stptlo/uBBoBORwEx6ardVcmKU= +github.com/containerd/containerd v1.5.0-beta.4/go.mod h1:GmdgZd2zA2GYIBZ0w09ZvgqEq8EfBp/m3lcVZIvPHhI= +github.com/containerd/containerd v1.5.0-rc.0/go.mod h1:V/IXoMqNGgBlabz3tHD2TWDoTJseu1FGOKuoA4nNb2s= +github.com/containerd/containerd v1.5.1/go.mod h1:0DOxVqwDy2iZvrZp2JUx/E+hS0UNTVn7dJnIOwtYR4g= +github.com/containerd/containerd v1.5.7/go.mod h1:gyvv6+ugqY25TiXxcZC3L5yOeYgEw0QMhscqVp1AR9c= +github.com/containerd/containerd v1.6.12 h1:kJ9b3mOFKf8yqo05Ob+tMoxvt1pbVWhnB0re9Y+k+8c= +github.com/containerd/containerd v1.6.12/go.mod h1:K4Bw7gjgh4TnkmQY+py/PYQGp4e7xgnHAeg87VeWb3A= +github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= +github.com/containerd/continuity v0.0.0-20190815185530-f2a389ac0a02/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= +github.com/containerd/continuity v0.0.0-20191127005431-f65d91d395eb/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= +github.com/containerd/continuity v0.0.0-20200710164510-efbc4488d8fe/go.mod h1:cECdGN1O8G9bgKTlLhuPJimka6Xb/Gg7vYzCTNVxhvo= +github.com/containerd/continuity v0.0.0-20201208142359-180525291bb7/go.mod h1:kR3BEg7bDFaEddKm54WSmrol1fKWDU1nKYkgrcgZT7Y= +github.com/containerd/continuity v0.0.0-20210208174643-50096c924a4e/go.mod h1:EXlVlkqNba9rJe3j7w3Xa924itAMLgZH4UD/Q4PExuQ= +github.com/containerd/continuity v0.1.0/go.mod h1:ICJu0PwR54nI0yPEnJ6jcS+J7CZAUXrLh8lPo2knzsM= +github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg= +github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM= +github.com/containerd/fifo v0.0.0-20180307165137-3d5202aec260/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= +github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= +github.com/containerd/fifo v0.0.0-20200410184934-f15a3290365b/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0= +github.com/containerd/fifo v0.0.0-20201026212402-0724c46b320c/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0= +github.com/containerd/fifo v0.0.0-20210316144830-115abcc95a1d/go.mod h1:ocF/ME1SX5b1AOlWi9r677YJmCPSwwWnQ9O123vzpE4= +github.com/containerd/fifo v1.0.0 h1:6PirWBr9/L7GDamKr+XM0IeUFXu5mf3M/BPpH9gaLBU= +github.com/containerd/fifo v1.0.0/go.mod h1:ocF/ME1SX5b1AOlWi9r677YJmCPSwwWnQ9O123vzpE4= +github.com/containerd/go-cni v1.0.1/go.mod h1:+vUpYxKvAF72G9i1WoDOiPGRtQpqsNW/ZHtSlv++smU= +github.com/containerd/go-cni v1.0.2/go.mod h1:nrNABBHzu0ZwCug9Ije8hL2xBCYh/pjfMb1aZGrrohk= +github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= +github.com/containerd/go-runc v0.0.0-20190911050354-e029b79d8cda/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= +github.com/containerd/go-runc v0.0.0-20200220073739-7016d3ce2328/go.mod h1:PpyHrqVs8FTi9vpyHwPwiNEGaACDxT/N/pLcvMSRA9g= +github.com/containerd/go-runc v0.0.0-20201020171139-16b287bc67d0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok= +github.com/containerd/go-runc v1.0.0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok= +github.com/containerd/imgcrypt v1.0.1/go.mod h1:mdd8cEPW7TPgNG4FpuP3sGBiQ7Yi/zak9TYCG3juvb0= +github.com/containerd/imgcrypt v1.0.4-0.20210301171431-0ae5c75f59ba/go.mod h1:6TNsg0ctmizkrOgXRNQjAPFWpMYRWuiB6dSF4Pfa5SA= +github.com/containerd/imgcrypt v1.1.1-0.20210312161619-7ed62a527887/go.mod h1:5AZJNI6sLHJljKuI9IHnw1pWqo/F0nGDOuR9zgTs7ow= +github.com/containerd/imgcrypt v1.1.1/go.mod h1:xpLnwiQmEUJPvQoAapeb2SNCxz7Xr6PJrXQb0Dpc4ms= +github.com/containerd/nri v0.0.0-20201007170849-eb1350a75164/go.mod h1:+2wGSDGFYfE5+So4M5syatU0N0f0LbWpuqyMi4/BE8c= +github.com/containerd/nri v0.0.0-20210316161719-dbaa18c31c14/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY= +github.com/containerd/nri v0.1.0/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY= +github.com/containerd/stargz-snapshotter/estargz v0.4.1/go.mod h1:x7Q9dg9QYb4+ELgxmo4gBUeJB0tl5dqH1Sdz0nJU1QM= +github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= +github.com/containerd/ttrpc v0.0.0-20190828172938-92c8520ef9f8/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= +github.com/containerd/ttrpc v0.0.0-20191028202541-4f1b8fe65a5c/go.mod h1:LPm1u0xBw8r8NOKoOdNMeVHSawSsltak+Ihv+etqsE8= +github.com/containerd/ttrpc v1.0.1/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y= +github.com/containerd/ttrpc v1.0.2/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y= +github.com/containerd/ttrpc v1.1.0 h1:GbtyLRxb0gOLR0TYQWt3O6B0NvT8tMdorEHqIQo/lWI= +github.com/containerd/ttrpc v1.1.0/go.mod h1:XX4ZTnoOId4HklF4edwc4DcqskFZuvXB1Evzy5KFQpQ= +github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc= +github.com/containerd/typeurl v0.0.0-20190911142611-5eb25027c9fd/go.mod h1:GeKYzf2pQcqv7tJ0AoCuuhtnqhva5LNU3U+OyKxxJpk= +github.com/containerd/typeurl v1.0.1/go.mod h1:TB1hUtrpaiO88KEK56ijojHS1+NeF0izUACaJW2mdXg= +github.com/containerd/typeurl v1.0.2 h1:Chlt8zIieDbzQFzXzAeBEF92KhExuE4p9p92/QmY7aY= +github.com/containerd/typeurl v1.0.2/go.mod h1:9trJWW2sRlGub4wZJRTW83VtbOLS6hwcDZXTn6oPz9s= +github.com/containerd/zfs v0.0.0-20200918131355-0a33824f23a2/go.mod h1:8IgZOBdv8fAgXddBT4dBXJPtxyRsejFIpXoklgxgEjw= +github.com/containerd/zfs v0.0.0-20210301145711-11e8f1707f62/go.mod h1:A9zfAbMlQwE+/is6hi0Xw8ktpL+6glmqZYtevJgaB8Y= +github.com/containerd/zfs v0.0.0-20210315114300-dde8f0fda960/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= +github.com/containerd/zfs v0.0.0-20210324211415-d5c4544f0433/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= +github.com/containerd/zfs v1.0.0/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= +github.com/containernetworking/cni v0.7.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= +github.com/containernetworking/cni v0.8.0/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= +github.com/containernetworking/cni v0.8.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= +github.com/containernetworking/plugins v0.8.6/go.mod h1:qnw5mN19D8fIwkqW7oHHYDHVlzhJpcY6TQxn/fUyDDM= +github.com/containernetworking/plugins v0.9.1/go.mod h1:xP/idU2ldlzN6m4p5LmGiwRDjeJr6FLK6vuiUwoH7P8= +github.com/containers/ocicrypt v1.0.1/go.mod h1:MeJDzk1RJHv89LjsH0Sp5KTY3ZYkjXO/C+bKAeWFIrc= +github.com/containers/ocicrypt v1.1.0/go.mod h1:b8AOe0YR67uU8OqfVNcznfFpAzu3rdgUV4GP9qXPfu4= +github.com/containers/ocicrypt v1.1.1/go.mod h1:Dm55fwWm1YZAjYRaJ94z2mfZikIyIN4B0oB3dj3jFxY= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= +github.com/coreos/go-iptables v0.5.0/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= +github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20161114122254-48702e0da86b/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= +github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= +github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ= +github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s= +github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5/go.mod h1:Eo87+Kg/IX2hfWJfwxMzLyuSZyxSoAug2nGa1G2QAi8= +github.com/d2g/hardwareaddr v0.0.0-20190221164911-e7d9fbe030e4/go.mod h1:bMl4RjIciD2oAxI7DmWRx6gbeqrkoLqv3MV0vzNad+I= +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/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0= +github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= +github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY= +github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-events v0.0.0-20170721190031-9461782956ad/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= +github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= +github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= +github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI= +github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= +github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q= +github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo= +github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= +github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +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/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +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.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA= +github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= +github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.8.2 h1:UzKToD9/PoFj/V4rvlKqTRKnQYyz8Sc1MJlv4JHPtvY= +github.com/gin-gonic/gin v1.8.2/go.mod h1:qw5AYuDrzRTnhvusDsrov+fDIxp9Dleuu12h8nfB398= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= +github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= +github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= +github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= +github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= +github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= +github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= +github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= +github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= +github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= +github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= +github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ= +github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk= +github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= +github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= +github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= +github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +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/googleapis v1.2.0/go.mod h1:Njal3psf3qN6dwBtQfUmBZh2ybovJ0tlu3o/AC7HYjU= +github.com/gogo/googleapis v1.4.0 h1:zgVt4UpGxcqVOw97aRGxT4svlcmdK35fynLNctY32zI= +github.com/gogo/googleapis v1.4.0/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +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/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +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.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +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.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +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.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +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.1/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.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-containerregistry v0.5.1/go.mod h1:Ct15B4yir3PLOP5jsy0GNeYVaIZs/MK/Jz5any1wFW0= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= +github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= +github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hanwen/go-fuse/v2 v2.2.0 h1:jo5QZYmBLNcl9ovypWaQ5yXMSSV+Ch68xoC3rtZvvBM= +github.com/hanwen/go-fuse/v2 v2.2.0/go.mod h1:B1nGE/6RBFyBRC1RRnf23UpwCdyJ31eukw34oAKukAc= +github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= +github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/icedream/go-bsdiff v1.0.1 h1:4I4BE/7e567PvnxBfp6cYRLBHf2gsc0EGGJWBd4ucek= +github.com/icedream/go-bsdiff v1.0.1/go.mod h1:gGhdZZqlpfmaUUEzntr3mDeO3dy0tvlIJ7F+a4z/vxY= +github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA= +github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg= +github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= +github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/joefitzgerald/rainbow-reporter v0.1.0/go.mod h1:481CNgqmVHQZzdIbN52CupLJyoVwB10FQ/IQlF1pdL8= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +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/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +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.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.11.13 h1:eSvu8Tmq6j2psUJqJrLcWH6K3w5Dwc+qipbaA6eVEN4= +github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.15.13 h1:NFn1Wr8cfnenSJSA46lLq4wHCcBzKTSjnBIexDMMOV0= +github.com/klauspost/compress v1.15.13/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= +github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= +github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q95whyfWQpmGZTu3gk3v2YkMi05HEzl7Tf7YEo= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= +github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88Jz2VyhSmden33/aXg4oVIY= +github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4= +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/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A= +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.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= +github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= +github.com/moby/sys/mountinfo v0.5.0 h1:2Ks8/r6lopsxWi9m58nlwjaeSzUX9iiL1vj5qB/9ObI= +github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU= +github.com/moby/sys/signal v0.6.0 h1:aDpY94H8VlhTGa9sNYUFCFsMZIUh5wm0B6XkIoJj/iY= +github.com/moby/sys/signal v0.6.0/go.mod h1:GQ6ObYZfqacOwTtlXvcmh9A26dVRul/hbOZn88Kg8Tg= +github.com/moby/sys/symlink v0.1.0/go.mod h1:GGDODQmbFOjFsXvfLVn3+ZRxkch54RkSiGqsZeMYowQ= +github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= +github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/onsi/ginkgo v0.0.0-20151202141238-7f8ab55aaf3b/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/gomega v0.0.0-20151007035656-2152b45fa28a/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= +github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= +github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/go-digest v1.0.0-rc1.0.20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +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.0.0/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 h1:rc3tiVYb5z54aKaDfakKn0dDjIyPpTtszkjuMzyt7ec= +github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runc v1.0.0-rc8.0.20190926000215-3e425f80a8c9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runc v1.0.0-rc9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runc v1.0.0-rc93/go.mod h1:3NOsor4w32B2tC0Zbl8Knk4Wg84SM2ImC1fxBuqJ/H0= +github.com/opencontainers/runc v1.0.2/go.mod h1:aTaHFFwQXuA71CiyxOdFFIorAoemI04suvGRQFzWTD0= +github.com/opencontainers/runc v1.1.2 h1:2VSZwLx5k/BfsBxMMipG/LYUnmqOD/BPkIVgQUcTlLw= +github.com/opencontainers/runc v1.1.2/go.mod h1:Tj1hFw6eFWp/o33uxGf5yF2BX5yz2Z6iptFpuvbbKqc= +github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.0.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.0.2-0.20190207185410-29686dbc5559/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.0.3-0.20200929063507-e6143ca7d51d/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417 h1:3snG66yBm59tKhhSPQrQ/0bCrv1LQbKt40LnUPiUxdc= +github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-tools v0.0.0-20181011054405-1d69bd0f9c39/go.mod h1:r3f7wjNzSs2extwzU3Y+6pKfobzPh+kKFJ3ofN+3nfs= +github.com/opencontainers/selinux v1.6.0/go.mod h1:VVGKuOLlE7v4PJyT6h7mNWvq1rzqiriPsEqVhc+svHE= +github.com/opencontainers/selinux v1.8.0/go.mod h1:RScLhm78qiWa2gbVCcGkC7tCGdgk3ogry1nUQF8Evvo= +github.com/opencontainers/selinux v1.8.2/go.mod h1:MUIHuUEvKB1wtJjQdOyYRgOnLD2xAPP8dBsCoU0KuF8= +github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI= +github.com/opencontainers/selinux v1.10.1 h1:09LIPVRP3uuZGQvgR+SgMSNBd1Eb3vlRbGqQpoHsF8w= +github.com/opencontainers/selinux v1.10.1/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI= +github.com/otiai10/copy v1.9.0 h1:7KFNiCgZ91Ru4qW4CWPf/7jqtxLagGRmIxWldPP9VY4= +github.com/otiai10/copy v1.9.0/go.mod h1:hsfX19wcn0UWIHUQ3/4fHuehhk2UyArQ9dVFAn3FczI= +github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= +github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= +github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= +github.com/otiai10/mint v1.4.0/go.mod h1:gifjb2MYOoULtKLqUAEILUG/9KONW6f7YsJ6vQLTlFI= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= +github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ= +github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU= +github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/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/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= +github.com/prometheus/client_golang v0.0.0-20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +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/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.0-20190522114515-bc1a522cf7b1/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U= +github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo= +github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/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.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= +github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +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.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8= +github.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +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 v0.0.0-20180303142811-b89eecf5ca5d/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +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 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ugorji/go v1.1.4 h1:j4s+tAvLfL3bZyefP2SEWmhBzmuIlH/eqNuPdFPgngw= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/ugorji/go/codec v1.2.8 h1:sgBJS6COt0b/P40VouWKdseidkDgHxYGm0SAglUHfP0= +github.com/ugorji/go/codec v1.2.8/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= +github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/cli/v2 v2.23.7 h1:YHDQ46s3VghFHFf1DdF+Sh7H4RqhcM+t0TmZRJx4oJY= +github.com/urfave/cli/v2 v2.23.7/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= +github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk= +github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= +github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= +github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI= +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/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= +github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +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/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= +github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= +github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= +go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= +go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= +go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg= +go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +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.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd h1:XcWmESyNjXJMLahc3mqVQJcgSTDxFxhETVlfk9uGc38= +golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +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-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/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/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-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/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-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/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-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/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-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/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-20200222125558-5a598a2470a0/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-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU= +golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +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-20190227155943-e225da77a7e6/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-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/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 h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/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-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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190522044717-8097e1b27ff5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190812073006-9eafafc0a87e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200817155316-9781c653f443/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200922070232-aee5d888a860/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201117170446-d9b008d0a637/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210324051608-47abb6519492/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-20210426230700-d19ff857e887/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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/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-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-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/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-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190706070813-72ffa07ba3db/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200616133436-c1934b75d054/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200916195026-c9a70fc28ce3/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= +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.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +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 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +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/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190522204451-c2c4e71fbf69/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200117163144-32f20d992d24/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200527145253-8367513e4ece/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21 h1:hrbNEivu7Zn1pxvHk6MBrq9iE22woVILTHqexqBxe6I= +google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +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.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.47.0 h1:9n77onPX5F3qfFCqjy9dhn8PbNQsIKeVU04J9G7umt8= +google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +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.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +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.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= +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/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +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.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/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= +gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= +gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= +gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +k8s.io/api v0.20.1/go.mod h1:KqwcCVogGxQY3nBlRpwt+wpAMF/KjaCc7RpywacvqUo= +k8s.io/api v0.20.4/go.mod h1:++lNL1AJMkDymriNniQsWRkMDzRaX2Y/POTUi8yvqYQ= +k8s.io/api v0.20.6/go.mod h1:X9e8Qag6JV/bL5G6bU8sdVRltWKmdHsFUGS3eVndqE8= +k8s.io/apimachinery v0.20.1/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= +k8s.io/apimachinery v0.20.4/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= +k8s.io/apimachinery v0.20.6/go.mod h1:ejZXtW1Ra6V1O5H8xPBGz+T3+4gfkTCeExAHKU57MAc= +k8s.io/apiserver v0.20.1/go.mod h1:ro5QHeQkgMS7ZGpvf4tSMx6bBOgPfE+f52KwvXfScaU= +k8s.io/apiserver v0.20.4/go.mod h1:Mc80thBKOyy7tbvFtB4kJv1kbdD0eIH8k8vianJcbFM= +k8s.io/apiserver v0.20.6/go.mod h1:QIJXNt6i6JB+0YQRNcS0hdRHJlMhflFmsBDeSgT1r8Q= +k8s.io/client-go v0.20.1/go.mod h1:/zcHdt1TeWSd5HoUe6elJmHSQ6uLLgp4bIJHVEuy+/Y= +k8s.io/client-go v0.20.4/go.mod h1:LiMv25ND1gLUdBeYxBIwKpkSC5IsozMMmOOeSJboP+k= +k8s.io/client-go v0.20.6/go.mod h1:nNQMnOvEUEsOzRRFIIkdmYOjAZrC8bgq0ExboWSU1I0= +k8s.io/code-generator v0.19.7/go.mod h1:lwEq3YnLYb/7uVXLorOJfxg+cUu2oihFhHZ0n9NIla0= +k8s.io/component-base v0.20.1/go.mod h1:guxkoJnNoh8LNrbtiQOlyp2Y2XFCZQmrcg2n/DeYNLk= +k8s.io/component-base v0.20.4/go.mod h1:t4p9EdiagbVCJKrQ1RsA5/V4rFQNDfRlevJajlGwgjI= +k8s.io/component-base v0.20.6/go.mod h1:6f1MPBAeI+mvuts3sIdtpjljHWBQ2cIy38oBIWMYnrM= +k8s.io/cri-api v0.17.3/go.mod h1:X1sbHmuXhwaHs9xxYffLqJogVsnI+f6cPRcgPel7ywM= +k8s.io/cri-api v0.20.1/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI= +k8s.io/cri-api v0.20.4/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI= +k8s.io/cri-api v0.20.6/go.mod h1:ew44AjNXwyn1s0U4xCKGodU7J1HzBeZ1MpGrpa5r8Yc= +k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/gengo v0.0.0-20200428234225-8167cfdcfc14/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/gengo v0.0.0-20201113003025-83324d819ded/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= +k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= +k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= +k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= +k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6/go.mod h1:UuqjUnNftUyPE5H64/qeyjQoUZhGpeFDVdxjTeEVN2o= +k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM= +k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= +k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.14/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.15/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= +sigs.k8s.io/structured-merge-diff/v4 v4.0.1/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/structured-merge-diff/v4 v4.0.3/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/install_snapshotter.sh b/install_snapshotter.sh new file mode 100755 index 0000000..3ff20e1 --- /dev/null +++ b/install_snapshotter.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +cat < /dev/null +[proxy_plugins] + [proxy_plugins.di3fs] + type = "snapshot" + address = "/run/di3fs/snapshotter.sock" +EOT diff --git a/pkg/algorithm/dijkstra.go b/pkg/algorithm/dijkstra.go new file mode 100644 index 0000000..f920cbc --- /dev/null +++ b/pkg/algorithm/dijkstra.go @@ -0,0 +1,205 @@ +// This code is copied from https://blog.amedama.jp/entry/2015/10/20/202245 +// Thanks so much! + +package algorithm + +import ( + "errors" + "fmt" +) + +// ノード +type Node struct { + name string // ノード名 + edges []*Edge // 次に移動できるエッジ + done bool // 処理済みかを表すフラグ + cost int // このノードにたどり着くのに必要だったコスト + prev *Node // このノードにたどりつくのに使われたノード +} + +func NewNode(name string) *Node { + node := &Node{name, []*Edge{}, false, -1, nil} + return node +} + +// ノードに次の接続先を示したエッジを追加する +func (self *Node) AddEdge(edge *Edge) { + self.edges = append(self.edges, edge) +} + +func (self *Node) GetName() string { + return self.name +} + +// エッジ +type Edge struct { + next *Node // 次に移動できるノード + cost int // 移動にかかるコスト +} + +func NewEdge(next *Node, cost int) *Edge { + edge := &Edge{next, cost} + return edge +} + +// 有向グラフ +type DirectedGraph struct { + nodes map[string]*Node +} + +func NewDirectedGraph() *DirectedGraph { + return &DirectedGraph{ + map[string]*Node{}} +} + +func (self *DirectedGraph) Print() { + for k, v := range self.nodes { + fmt.Printf("k=%v v=%v\n", k, v) + } +} + +// グラフの要素を追加する (接続元ノード名、接続先ノード名、移動にかかるコスト) +func (self *DirectedGraph) Add(src, dst string, cost int) { + // ノードが既にある場合は追加しない + srcNode, ok := self.nodes[src] + if !ok { + srcNode = NewNode(src) + self.nodes[src] = srcNode + } + + dstNode, ok := self.nodes[dst] + if !ok { + dstNode = NewNode(dst) + self.nodes[dst] = dstNode + } + + // ノードをエッジでつなぐ + edge := NewEdge(dstNode, cost) + srcNode.AddEdge(edge) +} + +// スタートとゴールを指定して最短経路を求める +func (self *DirectedGraph) ShortestPath(start string, goal string) (ret []*Node, err error) { + // 名前からスタート地点のノードを取得する + startNode := self.nodes[start] + + // スタートのコストを 0 に設定することで処理対象にする + startNode.cost = 0 + + for { + // 次の処理対象のノードを取得する + node, err := self.nextNode() + + // 次に処理するノードが見つからなければ終了 + if err != nil { + return nil, errors.New("Goal not found") + } + + // ゴールまで到達した + if node.name == goal { + break + } + + // 取得したノードを処理する + self.calc(node) + } + + // ゴールから逆順にスタートまでノードをたどっていく + n := self.nodes[goal] + ret_rev := make([]*Node, 0) + for { + ret_rev = append(ret_rev, n) + if n.name == start { + break + } + n = n.prev + } + + for i := range ret_rev { + ret = append(ret, ret_rev[len(ret_rev)-i-1]) + } + + return ret, nil +} + +// つながっているノードのコストを計算する +func (self *DirectedGraph) calc(node *Node) { + // ノードにつながっているエッジを取得する + for _, edge := range node.edges { + nextNode := edge.next + + // 既に処理済みのノードならスキップする + if nextNode.done { + continue + } + + // このノードに到達するのに必要なコストを計算する + cost := node.cost + edge.cost + if nextNode.cost == -1 || cost < nextNode.cost { + // 既に見つかっている経路よりもコストが小さければ処理中のノードを遷移元として記録する + nextNode.cost = cost + nextNode.prev = node + } + } + + // つながっているノードのコスト計算がおわったらこのノードは処理済みをマークする + node.done = true +} + +func (self *DirectedGraph) nextNode() (next *Node, err error) { + // グラフに含まれるノードを線形探索する + for _, node := range self.nodes { + + // 処理済みのノードは対象外 + if node.done { + continue + } + + // コストが初期値 (-1) になっているノードはまだそのノードまでの最短経路が判明していないので処理できない + if node.cost == -1 { + continue + } + + // 最初に見つかったものは問答無用で次の処理対象の候補になる + if next == nil { + next = node + } + + // 既に見つかったノードよりもコストの小さいものがあればそちらを先に処理しなければいけない + if next.cost > node.cost { + next = node + } + } + + // 次の処理対象となるノードが見つからなかったときはエラー + if next == nil { + return nil, errors.New("Untreated node not found") + } + + return +} + +func main() { + // 有向グラフを作る + g := NewDirectedGraph() + + // グラフを定義していく + g.Add("1.23.1", "1.23.2", 1) + g.Add("1.23.2", "1.23.3", 1) + g.Add("1.23.3", "1.23.4", 1) + g.Add("1.23.1", "1.23.3", 1) + + // "s" ノードから "z" ノードへの最短経路を得る + path, err := g.ShortestPath("1.23.1", "1.23.4") + + // 経路が見つからなければ終了 + if err != nil { + fmt.Println("Goal not found") + return + } + + // 見つかった経路からノードとコストを表示する + for _, node := range path { + fmt.Printf("ノード: %v, コスト: %v\n", node.name, node.cost) + } +} diff --git a/pkg/benchmark/benchmark.go b/pkg/benchmark/benchmark.go new file mode 100644 index 0000000..9bc27ae --- /dev/null +++ b/pkg/benchmark/benchmark.go @@ -0,0 +1,57 @@ +package benchmark + +import ( + "fmt" + "os" + "time" +) + +type Benchmark struct { + logFile *os.File +} + +type Metric struct { + TaskName string + Timestamp time.Time + ElapsedMilli int + Labels []string +} + +func NewBenchmark(path string) (*Benchmark, error) { + file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR|os.O_APPEND, os.ModePerm) + if err != nil { + return nil, err + } + b := &Benchmark{ + logFile: file, + } + + return b, nil +} + +func (b *Benchmark) Close() error { + err := b.logFile.Close() + if err != nil { + return err + } + + return nil +} + +func (b *Benchmark) AppendResult(m Metric) error { + m.Timestamp = time.Now() + col := fmt.Sprintf("%s,%s,%d", m.TaskName, m.Timestamp.Format(time.RFC3339), m.ElapsedMilli) + for _, l := range m.Labels { + col += fmt.Sprintf(",%s", l) + } + col += "\n" + _, err := b.logFile.Write([]byte(col)) + if err != nil { + return err + } + err = b.logFile.Sync() + if err != nil { + return err + } + return nil +} diff --git a/pkg/di3fs/di3fs.go b/pkg/di3fs/di3fs.go new file mode 100644 index 0000000..40cd688 --- /dev/null +++ b/pkg/di3fs/di3fs.go @@ -0,0 +1,461 @@ +package di3fs + +import ( + "bytes" + "context" + "fmt" + "io" + "os" + "path" + "path/filepath" + "strings" + "syscall" + "time" + + "github.com/hanwen/go-fuse/v2/fs" + "github.com/hanwen/go-fuse/v2/fuse" + "github.com/icedream/go-bsdiff" + "github.com/klauspost/compress/zstd" + log "github.com/sirupsen/logrus" +) + +type Di3fsNodeID uint64 + +type Di3fsNode struct { + fs.Inode + + basePath []string + patchPath string + meta *FileEntry + baseMeta []*FileEntry + file *os.File + data []byte + root *Di3fsRoot +} + +var _ = (fs.NodeGetattrer)((*Di3fsNode)(nil)) +var _ = (fs.NodeOpener)((*Di3fsNode)(nil)) +var _ = (fs.NodeReader)((*Di3fsNode)(nil)) +var _ = (fs.NodeReaddirer)((*Di3fsNode)(nil)) +var _ = (fs.NodeReadlinker)((*Di3fsNode)(nil)) + +func (dn *Di3fsNode) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno { + log.Traceln("Getattr started") + defer log.Traceln("Getattr finished") + + out.Mode = dn.meta.Mode & 0777 + out.Nlink = 1 + out.Mtime = uint64(time.Now().Unix()) + out.Atime = out.Mtime + out.Ctime = out.Mtime + out.Size = uint64(dn.meta.Size) + out.Uid = dn.meta.UID + out.Gid = dn.meta.GID + const bs = 512 + out.Blksize = bs + out.Blocks = (out.Size + bs - 1) / bs + return 0 +} + +func (dn *Di3fsNode) readBaseFiles() ([]byte, error) { + diffIdxs := make([]int, 0) + for i := 0; i < len(dn.baseMeta); i++ { + baseMeta := dn.baseMeta[i] + baseImageOffset := dn.baseMeta[i].Offset + baseImage := dn.root.baseImage[i] + baseImageBodyOffset := dn.root.baseImageBodyOffset[i] + if baseMeta.Type == FILE_ENTRY_OPAQUE { + return nil, nil + } + if baseMeta.IsSame() { + continue + } + if baseMeta.IsNew() { + zstdBytes := make([]byte, baseMeta.CompressedSize) + _, err := baseImage.ReadAt(zstdBytes, baseImageBodyOffset+baseImageOffset) + if err != nil { + return nil, fmt.Errorf("failed to read from image: %v", err) + } + zstdReader, err := zstd.NewReader(bytes.NewBuffer(zstdBytes)) + if err != nil { + return nil, err + } + defer zstdReader.Close() + data, err := io.ReadAll(zstdReader) + if err != nil { + return nil, err + } + if len(diffIdxs) == 0 { + return data, nil + } + for j := len(diffIdxs) - 1; j >= 0; j -= 1 { + diffIdx := diffIdxs[j] + patchBytes := make([]byte, dn.baseMeta[diffIdx].CompressedSize) + _, err := dn.root.baseImage[diffIdx].ReadAt(patchBytes, dn.root.baseImageBodyOffset[diffIdx]+dn.baseMeta[diffIdx].Offset) + if err != nil { + fmt.Println(err) + return nil, err + } + patchReader := bytes.NewBuffer(patchBytes) + baseReader := bytes.NewBuffer(data) + + writer := new(bytes.Buffer) + err = bsdiff.Patch(baseReader, writer, patchReader) + if err != nil { + return nil, err + } + data = writer.Bytes() + } + return data, nil + } + diffIdxs = append(diffIdxs, i) + } + return nil, fmt.Errorf("not implemented") +} + +func (dn *Di3fsNode) openFileInImage(ctx context.Context, flags uint32, baseImgIdx int) (fs.FileHandle, uint32, syscall.Errno) { + if dn.file != nil || len(dn.data) != 0 { + } else if dn.meta.IsNew() { + patchBytes := make([]byte, dn.meta.CompressedSize) + offset := dn.root.diffImageBodyOffset + dn.meta.Offset + _, err := dn.root.diffImage.ReadAt(patchBytes, dn.root.diffImageBodyOffset+dn.meta.Offset) + if err != nil { + log.Errorf("failed to read from diffImage offset=%d err=%s", offset, err) + return 0, 0, syscall.EIO + } + patchBuf := bytes.NewBuffer(patchBytes) + patchReader, err := zstd.NewReader(patchBuf) + if err != nil { + log.Errorf("failed to create zstd Reader err=%s", err) + return 0, 0, syscall.EIO + } + defer patchReader.Close() + dn.data, err = io.ReadAll(patchReader) + if err != nil { + log.Errorf("failed to read with zstd Reader err=%s", err) + return 0, 0, syscall.EIO + } + } else if dn.meta.IsSame() { + data, err := dn.readBaseFiles() + if err != nil { + log.Errorf("failed to read from base: %v", err) + return 0, 0, syscall.EIO + } + dn.data = data + } else if dn.meta.Type == FILE_ENTRY_OPAQUE { + dn.file = nil + dn.data = []byte{} + } else { + var patchReader io.Reader + patchBytes := make([]byte, dn.meta.CompressedSize) + offset := dn.root.diffImageBodyOffset + dn.meta.Offset + _, err := dn.root.diffImage.ReadAt(patchBytes, offset) + if err != nil { + log.Errorf("failed to read from diffImage offset=%d len=%d err=%s", offset, len(patchBytes), err) + return 0, 0, syscall.EIO + } + patchReader = bytes.NewBuffer(patchBytes) + baseData, err := dn.readBaseFiles() + if err != nil { + log.Errorf("failed to read from base: %v", err) + return 0, 0, syscall.EIO + } + + baseReader := bytes.NewBuffer(baseData) + + writer := new(bytes.Buffer) + err = bsdiff.Patch(baseReader, writer, patchReader) + if err != nil { + log.Errorf("Open failed(bsdiff) err=%v", err) + return 0, 0, syscall.EIO + } + dn.data = writer.Bytes() + log.Debugf("Successfully patched %s", dn.meta.Name) + } + return nil, fuse.FOPEN_KEEP_CACHE | fuse.FOPEN_CACHE_DIR, 0 +} + +func (dn *Di3fsNode) Open(ctx context.Context, flags uint32) (fs.FileHandle, uint32, syscall.Errno) { + log.Traceln("Open started") + defer log.Traceln("Open finished") + isImage := dn.root.diffImage != nil + if isImage { + return dn.openFileInImage(ctx, flags, 0) + } + + if dn.file != nil || len(dn.data) != 0 { + } else if dn.meta.IsNew() { + file, err := os.OpenFile(dn.patchPath, os.O_RDONLY, 0) + if err != nil { + log.Errorf("Open failed(new=%s) err=%v", dn.patchPath, err) + return 0, 0, syscall.ENOENT + } + log.Debugf("Successfully opened new file=%s", dn.patchPath) + dn.file = file + } else if dn.meta.IsSame() { + file, err := os.OpenFile(dn.basePath[0], os.O_RDONLY, 0) + if err != nil { + log.Errorf("Open failed(same=%s) err=%v", dn.basePath, err) + return 0, 0, syscall.ENOENT + } + log.Debugf("Successfully opened same file=%s", dn.basePath) + dn.file = file + } else if dn.meta.Type == FILE_ENTRY_OPAQUE { + dn.file = nil + dn.data = []byte{} + } else { + patchFile, err := os.OpenFile(dn.patchPath, os.O_RDONLY, 0) + if err != nil { + log.Errorf("Open failed(patch=%s) err=%v", dn.patchPath, err) + return 0, 0, syscall.ENOENT + } + defer patchFile.Close() + patchReader := patchFile + + baseFile, err := os.OpenFile(dn.basePath[0], os.O_RDONLY, 0) + if err != nil { + log.Errorf("Open failed(base=%s) err=%v", dn.basePath, err) + return 0, 0, syscall.ENOENT + } + defer baseFile.Close() + + writer := new(bytes.Buffer) + err = bsdiff.Patch(baseFile, writer, patchReader) + if err != nil { + log.Errorf("Open failed(bsdiff) err=%v", err) + return 0, 0, syscall.ENOENT + } + dn.data = writer.Bytes() + log.Debugf("Successfully patched %s", dn.meta.Name) + } + return nil, fuse.FOPEN_KEEP_CACHE | fuse.FOPEN_CACHE_DIR, 0 +} + +func (dn *Di3fsNode) Read(ctx context.Context, f fs.FileHandle, data []byte, off int64) (fuse.ReadResult, syscall.Errno) { + log.Traceln("Read started") + defer log.Traceln("Read finished") + + end := int64(off) + int64(len(data)) + log.Debugf("READ STARTED file=%s offset=%d len=%d", dn.meta.Name, off, len(data)) + + if dn.file != nil { + readBuf := make([]byte, end-off) + if dn.meta.IsNew() { + log.Debugf("READ reading from %s", dn.patchPath) + } else { + log.Debugf("READ reading from %s", dn.basePath) + } + readSize, err := dn.file.ReadAt(readBuf, off) + if err != nil { + if err != io.EOF { + log.Errorf("READ failed err=%v", err) + return nil, syscall.EIO + } + } + log.Debugf("READ FINISHED file=%s offset=%d len=%d", dn.meta.Name, off, readSize) + return fuse.ReadResultData(readBuf[0:readSize]), 0 + } else { + if end > int64(len(dn.data)) { + end = int64(len(dn.data)) + } + log.Debugf("READ FINISHED file=%s offset=%d len=%d", dn.meta.Name, off, (end - off)) + return fuse.ReadResultData(dn.data[off:end]), 0 + } + +} + +func (dn *Di3fsNode) Readlink(ctx context.Context) ([]byte, syscall.Errno) { + log.Traceln("Readlink started") + defer log.Traceln("Readlink finished") + return []byte(dn.meta.RealPath), 0 +} + +func generateOpaqueFileEntry(dir *FileEntry) (*FileEntry, error) { + fe := &FileEntry{ + Name: ".wh..wh..opq", + Size: int(0), + Mode: uint32(dir.Mode), + DiffName: ".wh..wh..opq", + Type: FILE_ENTRY_OPAQUE, + RealPath: "", + Childs: []FileEntry{}, + } + + return fe, nil +} + +func (dn *Di3fsNode) Readdir(ctx context.Context) (fs.DirStream, syscall.Errno) { + log.Traceln("Readdir started") + defer log.Traceln("Readdir finished") + + r := []fuse.DirEntry{} + for k, ch := range dn.Children() { + r = append(r, fuse.DirEntry{Mode: ch.Mode(), + Name: k, + Ino: ch.StableAttr().Ino}) + } + return fs.NewListDirStream(r), 0 +} +func (dr *Di3fsNode) OnAdd(ctx context.Context) { + log.Traceln("OnAdd started") + defer log.Traceln("OnAdd finished") + + isImage := dr.root.diffImage != nil + if isImage && dr.root.IsBase() && !dr.meta.IsDir() && !dr.meta.IsSymlink() && !dr.meta.IsNew() { + log.Fatalf("invalid base image %q", dr.patchPath) + } + // here, rootNode is initialized + log.Debugf("base=%s patch=%s", dr.basePath, dr.patchPath) + if dr.meta.IsDir() { + for _, o := range dr.meta.OaqueFiles { + if strings.Contains(o, "/") { + continue + } + fe, err := generateOpaqueFileEntry(dr.meta) + if err != nil { + log.Fatalf("failed to generate opaqueFileEntry: %v", err) + } + n := newNode(dr.basePath, dr.patchPath, fe, nil, dr.root) + stableAttr := fs.StableAttr{} + cNode := dr.NewPersistentInode(ctx, n, stableAttr) + dr.AddChild(n.meta.Name, cNode, false) + } + } + for i := range dr.meta.Childs { + c := dr.meta.Childs[i] + var childBaseFEs []*FileEntry = make([]*FileEntry, 0) + for baseMetaIdx := range dr.baseMeta { + for baseIdx := range dr.baseMeta[baseMetaIdx].Childs { + baseChild := dr.baseMeta[baseMetaIdx].Childs[baseIdx] + if baseChild.Name == c.Name { + childBaseFEs = append(childBaseFEs, &baseChild) + } + } + } + n := newNode(dr.basePath, dr.patchPath, &c, childBaseFEs, dr.root) + stableAttr := fs.StableAttr{} + if c.IsDir() { + stableAttr.Mode = fuse.S_IFDIR + } else if c.IsSymlink() { + stableAttr.Mode = fuse.S_IFLNK + } + cNode := dr.NewPersistentInode(ctx, n, stableAttr) + dr.AddChild(n.meta.Name, cNode, false) + } +} + +type Di3fsRoot struct { + baseImage []*os.File + baseImageBodyOffset []int64 + diffImage *os.File + diffImageBodyOffset int64 + RootNode *Di3fsNode +} + +func (dr *Di3fsRoot) IsBase() bool { + return dr.baseImage == nil +} + +func newNode(diffBaseDirPath []string, patchDirPath string, fe *FileEntry, baseFE []*FileEntry, root *Di3fsRoot) *Di3fsNode { + node := &Di3fsNode{ + meta: fe, + baseMeta: baseFE, + root: root, + basePath: []string{}, + } + name := node.meta.Name + if fe.IsDir() || fe.IsNew() { + node.patchPath = path.Join(patchDirPath, name) + } else { + node.patchPath = path.Join(patchDirPath, name+".diff") + } + + for i := range diffBaseDirPath { + node.basePath = append(node.basePath, path.Join(diffBaseDirPath[i], name)) + } + return node +} + +func NewDi3fsRoot(opts *fs.Options, diffBase []string, patchBase string, fileEntry *FileEntry, baseFileEntry []*FileEntry, baseImage []*os.File, baseImageBodyOffset []int64, diffImage *os.File, diffImageBodyOffset int64) (Di3fsRoot, error) { + rootNode := newNode(diffBase, patchBase, fileEntry, baseFileEntry, nil) + root := Di3fsRoot{ + baseImage: baseImage, + baseImageBodyOffset: baseImageBodyOffset, + diffImage: diffImage, + diffImageBodyOffset: diffImageBodyOffset, + RootNode: rootNode, + } + rootNode.root = &root + + return root, nil +} + +func Do(diffImagePath, mountPath string) error { + start := time.Now() + customFormatter := new(log.TextFormatter) + customFormatter.TimestampFormat = "2006-01-02 15:04:05" + customFormatter.FullTimestamp = true + log.SetFormatter(customFormatter) + log.SetLevel(log.InfoLevel) + + // Scans the arg list and sets up flags + diffImagePathAbs, err := filepath.Abs(diffImagePath) + if err != nil { + panic(err) + } + + imageBodyOffset := int64(0) + var baseImageFiles []*os.File = make([]*os.File, 0) + var baseImageBodyOffsets []int64 = make([]int64, 0) + var baseMetaJsons []*FileEntry = make([]*FileEntry, 0) + var baseImagePaths []string = make([]string, 0) + + imageHeader, imageFile, imageBodyOffset, err := LoadImage(diffImagePathAbs) + if err != nil { + panic(err) + } + + baseImageId := imageHeader.BaseId + fmt.Println(baseImageId) + for baseImageId != "" { + imageStore, _ := filepath.Split(diffImagePathAbs) + baseImagePath := filepath.Join(imageStore, baseImageId+".dimg") + baseImageHeader, baseImageFile, baseImageBodyOffset, err := LoadImage(baseImagePath) + if err != nil { + panic(err) + } + baseImageFiles = append(baseImageFiles, baseImageFile) + baseImageBodyOffsets = append(baseImageBodyOffsets, baseImageBodyOffset) + baseMetaJsons = append(baseMetaJsons, &baseImageHeader.FileEntry) + baseImagePaths = append(baseImagePaths, baseImagePath) + + baseImageId = baseImageHeader.BaseId + log.Infof("baseImage %s is loaded", baseImageId) + } + + sec := time.Second + opts := &fs.Options{ + AttrTimeout: &sec, + EntryTimeout: &sec, + } + + opts.MountOptions.Options = append(opts.MountOptions.Options, "ro") + opts.MountOptions.Options = append(opts.MountOptions.Options, "fsname=fuse-diff") + opts.MountOptions.Name = "fuse-diff" + opts.NullPermissions = true + + di3fsRoot, err := NewDi3fsRoot(opts, baseImagePaths, diffImagePathAbs, &imageHeader.FileEntry, baseMetaJsons, baseImageFiles, baseImageBodyOffsets, imageFile, imageBodyOffset) + if err != nil { + log.Fatalf("creating Di3fsRoot failed: %v\n", err) + } + + server, err := fs.Mount(mountPath, di3fsRoot.RootNode, opts) + if err != nil { + log.Fatalf("Mount fail: %v\n", err) + } + log.Infof("Mounted!") + fmt.Printf("elapsed = %v\n", (time.Since(start).Milliseconds())) + server.Wait() + + return nil +} diff --git a/pkg/di3fs/file.go b/pkg/di3fs/file.go new file mode 100644 index 0000000..9d4b7b7 --- /dev/null +++ b/pkg/di3fs/file.go @@ -0,0 +1,235 @@ +package di3fs + +import ( + "bytes" + "encoding/binary" + "encoding/json" + "fmt" + "io" + "os" + "strings" + "syscall" + + "github.com/klauspost/compress/zstd" +) + +type EntryType int + +const ( + FILE_ENTRY_FILE_NEW EntryType = iota + FILE_ENTRY_FILE_SAME + FILE_ENTRY_FILE_DIFF + FILE_ENTRY_DIR + FILE_ENTRY_DIR_NEW + FILE_ENTRY_SYMLINK + FILE_ENTRY_OPAQUE +) + +func UnmarshalJsonFromCompressed[T any](b []byte) (*T, error) { + buf := bytes.NewBuffer(b) + reader, err := zstd.NewReader(buf) + if err != nil { + return nil, err + } + defer reader.Close() + + jsonBytes, err := io.ReadAll(reader) + if err != nil { + return nil, err + } + + var res T + err = json.Unmarshal(jsonBytes, &res) + if err != nil { + return nil, err + } + + return &res, nil +} + +type ImageHeader struct { + BaseId string `json:"baseID"` + FileEntry FileEntry `json:"fileEntry"` +} + +type FileEntry struct { + Name string `json:"name"` + Size int `json:"size"` + Mode uint32 `json:"mode"` + UID uint32 `json:"uid"` + GID uint32 `json:"gid"` + DiffName string `json:"diffName,omitempty"` + Type EntryType `json:"type"` + OaqueFiles []string `json:"opaqueFiles,omitempty"` + UncompressedGz bool `json:"uncompressedGz"` + RealPath string `json:"realPath,omitempty"` + Childs []FileEntry `json:"childs" copier:"-"` + CompressedSize int64 `json:"compressedSize,omitempty"` + Offset int64 `json:"offset,omitempty"` +} + +func CompressWithZstd(src []byte) ([]byte, error) { + out := &bytes.Buffer{} + z, err := zstd.NewWriter(out) + if err != nil { + return nil, err + } + + _, err = z.Write(src) + if err != nil { + return nil, err + } + + err = z.Close() + if err != nil { + return nil, err + } + + return out.Bytes(), nil +} + +func compressFileWithZstd(path string) ([]byte, error) { + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + + fileBytes, err := io.ReadAll(f) + if err != nil { + return nil, err + } + + res, err := CompressWithZstd(fileBytes) + if err != nil { + return nil, err + } + + return res, nil + +} + +func PackFile(srcFilePath string, out io.Writer) (int64, error) { + compressed, err := compressFileWithZstd(srcFilePath) + if err != nil { + return 0, err + } + writtenSize, err := out.Write(compressed) + if err != nil { + return 0, err + } + + return int64(writtenSize), err +} + +// int64: imageBodyOffset +func LoadImage(dimgPath string) (*ImageHeader, *os.File, int64, error) { + imageFile, err := os.Open(dimgPath) + if err != nil { + return nil, nil, 0, err + } + + curOffset := int64(0) + bs := make([]byte, 4) + _, err = imageFile.ReadAt(bs, curOffset) + if err != nil { + return nil, nil, 0, err + } + curOffset += 4 + + compressedHeaderSize := binary.LittleEndian.Uint32(bs) + compressedHeader := make([]byte, compressedHeaderSize) + _, err = imageFile.ReadAt(compressedHeader, curOffset) + if err != nil { + return nil, nil, 0, err + } + curOffset += int64(compressedHeaderSize) + imageHeader, err := UnmarshalJsonFromCompressed[ImageHeader](compressedHeader) + if err != nil { + return nil, nil, 0, err + } + + return imageHeader, imageFile, curOffset, nil +} + +func NewFileEntry() *FileEntry { + return &FileEntry{ + Childs: make([]FileEntry, 0), + } +} + +func (fe FileEntry) Print(prefix string, isLast bool) { + r := "├" + if isLast { + r = "└" + } + state := "updated" + if fe.Type == FILE_ENTRY_FILE_NEW { + state = "new" + } else if fe.Type == FILE_ENTRY_FILE_SAME { + state = "same" + } + fmt.Printf("%v %s %s (%s, size=%d)\n", prefix, r, fe.Name, state, fe.Size) + if len(fe.Childs) > 0 { + for i, c := range fe.Childs { + c.Print(prefix+" ", i == len(fe.Childs)-1) + } + } +} + +func (fe FileEntry) IsDir() bool { + return fe.Type == FILE_ENTRY_DIR || fe.Type == FILE_ENTRY_DIR_NEW +} + +func (fe FileEntry) IsNew() bool { + return fe.Type == FILE_ENTRY_FILE_NEW || + fe.Type == FILE_ENTRY_OPAQUE || + fe.Type == FILE_ENTRY_DIR_NEW +} + +func (fe FileEntry) IsSame() bool { + return fe.Type == FILE_ENTRY_FILE_SAME +} + +func (fe FileEntry) IsSymlink() bool { + return fe.Type == FILE_ENTRY_SYMLINK +} + +func (fe *FileEntry) SetUGID(path string) error { + fileInfo, err := os.Stat(path) + if err != nil { + return err + } + stat, ok := fileInfo.Sys().(*syscall.Stat_t) + if !ok { + return fmt.Errorf("this supports only linux") + } + fe.UID = stat.Uid + fe.GID = stat.Gid + + return nil +} + +func (fe *FileEntry) Lookup(path string) (*FileEntry, error) { + paths := strings.Split(path, "/") + if paths[0] == "" { + paths = paths[1:] + } + return fe.lookupImpl(paths) +} + +func (fe *FileEntry) lookupImpl(paths []string) (*FileEntry, error) { + // it must be file + if len(paths) == 0 { + return fe, nil + } + + for idx := range fe.Childs { + child := fe.Childs[idx] + if child.Name == paths[0] { + return child.lookupImpl(paths[1:]) + } + } + + return nil, fmt.Errorf("not found") +} diff --git a/pkg/di3fs/merge.go b/pkg/di3fs/merge.go new file mode 100644 index 0000000..a9bb5b5 --- /dev/null +++ b/pkg/di3fs/merge.go @@ -0,0 +1,1065 @@ +package di3fs + +import ( + "bytes" + "encoding/binary" + "encoding/json" + "fmt" + "io" + "os" + "path" + + "github.com/icedream/go-bsdiff" + "github.com/jinzhu/copier" + "github.com/klauspost/compress/zstd" + cp "github.com/otiai10/copy" + log "github.com/sirupsen/logrus" + + "github.com/dsnet/compress/bzip2" + + "github.com/pkg/errors" +) + +type DiffBlock = struct { + oldPos int64 + newPos int64 + addBytes []byte + insertBytes []byte +} + +func NewDiffBlock(oldPos, newPos int64) DiffBlock { + res := DiffBlock{} + res.oldPos = oldPos + res.newPos = newPos + res.addBytes = make([]byte, 0) + res.insertBytes = make([]byte, 0) + return res +} + +var ErrInvalidMagic = errors.New("Invalid magic") +var sizeEncoding = binary.BigEndian +var magicText = []byte("ENDSLEY/BSDIFF43") + +func ReadHeader(r io.Reader) (size uint64, err error) { + magicBuf := make([]byte, len(magicText)) + n, err := r.Read(magicBuf) + if err != nil { + return + } + if n < len(magicText) { + err = ErrInvalidMagic + return + } + + err = binary.Read(r, sizeEncoding, &size) + + return +} + +func WriteHeader(w io.Writer, size uint64) error { + _, err := w.Write(magicText) + if err != nil { + return err + } + + err = binary.Write(w, sizeEncoding, size) + if err != nil { + return err + } + + return err +} + +func readPatch(reader io.Reader) ([]DiffBlock, uint64, error) { + newLen, err := ReadHeader(reader) + if err != nil { + return nil, 0, err + } + + // Decompression + bz2Reader, err := bzip2.NewReader(reader, nil) + if err != nil { + return nil, 0, err + } + defer bz2Reader.Close() + + content, err := io.ReadAll(bz2Reader) + if err != nil { + return nil, 0, err + } + + lowerBlocks, err := readContent(newLen, bytes.NewReader(content)) + if err != nil { + return nil, 0, err + } + + return lowerBlocks, newLen, nil +} + +func writePatch(w io.Writer, size uint64, blocks []DiffBlock) error { + err := WriteHeader(w, size) + if err != nil { + return err + } + + bz2Writer, err := bzip2.NewWriter(w, nil) + if err != nil { + return err + } + defer bz2Writer.Close() + + for i, b := range blocks { + ctrl0 := int64(len(b.addBytes)) + err = writeInt64(bz2Writer, ctrl0) + if err != nil { + return err + } + + ctrl1 := int64(len(b.insertBytes)) + err = writeInt64(bz2Writer, ctrl1) + if err != nil { + return err + } + + ctrl2 := int64(0) + if i != len(blocks)-1 { + ctrl2 = blocks[i+1].oldPos - blocks[i].oldPos - ctrl0 + } + err = writeInt64(bz2Writer, ctrl2) + if err != nil { + return err + } + + _, err = bz2Writer.Write(b.addBytes) + if err != nil { + return err + } + + _, err = bz2Writer.Write(b.insertBytes) + if err != nil { + return err + } + + } + + return nil +} + +func readInt64(reader io.Reader) (int64, error) { + buf := make([]byte, 8) + readSize, err := reader.Read(buf) + if err != nil { + return 0, err + } + if readSize != 8 { + return 0, fmt.Errorf("invalid size") + } + + isNegative := (buf[7]&0x80 > 0) + buf[7] = buf[7] & 0x7F + res := binary.LittleEndian.Uint64(buf) + if isNegative { + return -int64(res), nil + } else { + return int64(res), nil + } +} + +func writeInt64(writer io.Writer, len int64) error { + buf := make([]byte, 8) + if len < 0 { + binary.LittleEndian.PutUint64(buf, uint64(-len)) + buf[7] |= 0x80 + } else { + binary.LittleEndian.PutUint64(buf, uint64(len)) + } + + _, err := writer.Write(buf) + if err != nil { + return err + } + + return err +} + +func readContent(newSize uint64, reader io.Reader) ([]DiffBlock, error) { + newPos := int64(0) + oldPos := int64(0) + + blocks := []DiffBlock{} + for newPos < int64(newSize) { + ctrl0, err := readInt64(reader) + if err != nil { + return nil, err + } + if ctrl0 < 0 { + return nil, fmt.Errorf("ctrl0 negative") + } + ctrl1, err := readInt64(reader) + if err != nil { + return nil, err + } + ctrl2, err := readInt64(reader) + if err != nil { + return nil, err + } + + if uint64(newPos+ctrl0) > newSize { + return nil, fmt.Errorf("newPos + ctrl0 exceeds newSize") + } + //fmt.Printf("newPos=%d oldPos=%d\n", newPos, oldPos) + //fmt.Printf("ctrl0=%d ctrl1=%d ctrl2=%d\n", ctrl0, ctrl1, ctrl2) + + diff := make([]byte, ctrl0) + diffSize, err := reader.Read(diff) + if err != nil { + return nil, err + } + if int(ctrl0) != diffSize { + return nil, fmt.Errorf("invalid size expected=%d actual=%d", ctrl0, diffSize) + } + + block := DiffBlock{ + oldPos: oldPos, + newPos: newPos, + addBytes: diff, + } + + newPos += ctrl0 + oldPos += ctrl0 + + insert := make([]byte, ctrl1) + if ctrl1 != 0 { + insertSize, err := reader.Read(insert) + if err != nil { + return nil, err + } + if int(ctrl1) != insertSize { + return nil, fmt.Errorf("invalid size expected=%d actual=%d", ctrl1, insertSize) + } + } + + block.insertBytes = insert + blocks = append(blocks, block) + + newPos += ctrl1 + oldPos += ctrl2 + } + + return blocks, nil +} + +func min(a, b int) int { + if a < b { + return a + } else { + return b + } +} + +func getBlock(newPos int64, blocks []DiffBlock) *DiffBlock { + start_idx := int(0) + end_idx := int(len(blocks)) + for { + idx := int((start_idx + end_idx) / 2) + b := blocks[idx] + //log.Tracef("start_idx=%d end_idx=%d", start_idx, end_idx) + //log.Tracef("b start=%d end=%d req=%d", b.newPos, b.newPos+int64(len(b.addBytes)+len(b.insertBytes)), newPos) + if newPos >= b.newPos && newPos < b.newPos+int64(len(b.addBytes))+int64(len(b.insertBytes)) { + return &b + } + if start_idx == end_idx { + return nil + } + if newPos < b.newPos { + end_idx = idx + } else { + start_idx = idx + } + } +} + +func mergeBlocks(lower, upper []DiffBlock, base, updated *os.File) ([]DiffBlock, error) { + var merged = []DiffBlock{} + lowerLastBlock := lower[len(lower)-1] + lowerSize := lowerLastBlock.newPos + int64(len(lowerLastBlock.addBytes)) + int64(len(lowerLastBlock.insertBytes)) + log.Tracef("lowerSize=%d", lowerSize) + + for _, upperBlock := range upper { + upperInsertPos := upperBlock.newPos + int64(len(upperBlock.addBytes)) + + cur := int64(0) + // state = 0: new block + // state = 1: already add + // state = 2: already insert + state := 0 + mergeBlock := NewDiffBlock(0, upperBlock.newPos) + for cur < int64(len(upperBlock.addBytes))+int64(len(upperBlock.insertBytes)) { + log.Tracef("upperOldPos=%d upperNewPos=%d", upperBlock.oldPos+cur, upperBlock.newPos+cur) + if upperBlock.oldPos+cur >= lowerSize { + if upperBlock.newPos+cur < upperInsertPos { + return nil, fmt.Errorf("overlapped offset") + } + upperInsertBytesBegin := upperBlock.newPos + cur - upperInsertPos + insertLen := len(upperBlock.insertBytes[upperInsertBytesBegin:]) + insertBytes := make([]byte, insertLen) + copy(insertBytes, upperBlock.insertBytes[upperInsertBytesBegin:]) + + if state == 0 { + log.Panicf("HOGEHOGE") + } + state = 2 + mergeBlock.insertBytes = append(mergeBlock.insertBytes, insertBytes...) + + cur += int64(insertLen) + continue + } + lowerBlock := getBlock(upperBlock.oldPos+cur, lower) + if lowerBlock == nil { + return nil, fmt.Errorf("invalid lower blocks") + } + if state == 1 { + merged = append(merged, mergeBlock) + mergeBlock = NewDiffBlock(0, 0) + state = 0 + } + log.Tracef("Got lowerBlock") + lowerInsertPos := lowerBlock.newPos + int64(len(lowerBlock.addBytes)) + + log.Tracef("lowerBlock oldPos=%d newPos=%d add=%d insert=%d\n", lowerBlock.oldPos, lowerBlock.newPos, len(lowerBlock.addBytes), len(lowerBlock.insertBytes)) + log.Tracef("upperBlock oldPos=%d newPos=%d add=%d insert=%d\n", upperBlock.oldPos, upperBlock.newPos, len(upperBlock.addBytes), len(upperBlock.insertBytes)) + for cur < int64(len(upperBlock.addBytes))+int64(len(upperBlock.insertBytes)) && + upperBlock.oldPos+cur < lowerBlock.newPos+int64(len(lowerBlock.addBytes))+int64(len(lowerBlock.insertBytes)) { + if upperBlock.oldPos+cur < lowerInsertPos { + // lower ADD + lowerAddBytesBegin := int(upperBlock.oldPos + cur - lowerBlock.newPos) + lowerAddRestBytesLen := len(lowerBlock.addBytes[lowerAddBytesBegin:]) + + if upperBlock.newPos+cur < upperInsertPos { + // upper ADD + //upperAddBytesBegin := int(upperNewPos - upperBlock.newPos) + upperAddRestBytesLen := len(upperBlock.addBytes[cur:]) + + addLen := min(lowerAddRestBytesLen, upperAddRestBytesLen) + log.Tracef("lower=ADD upper=ADD len=%d\n", addLen) + if state == 2 { + merged = append(merged, mergeBlock) + mergeBlock = NewDiffBlock(0, 0) + state = 0 + } + + if state == 0 { + mergeBlock.oldPos = lowerBlock.oldPos + int64(lowerAddBytesBegin) + mergeBlock.newPos = upperBlock.newPos + cur + } + + addBytes := make([]byte, addLen) + for i := 0; i < addLen; i++ { + addBytes[i] = lowerBlock.addBytes[lowerAddBytesBegin+i] + upperBlock.addBytes[cur+int64(i)] + } + mergeBlock.addBytes = append(mergeBlock.addBytes, addBytes...) + cur += int64(addLen) + + state = 1 + } else { + log.Tracef("lower=ADD upper=INSERT\n") + // upper INSERT + upperInsertBytesBegin := cur - int64(len(upperBlock.addBytes)) + upperInsertRestBytesLen := len(upperBlock.insertBytes[upperInsertBytesBegin:]) + + insertLen := min(lowerAddRestBytesLen, upperInsertRestBytesLen) + insertBytes := make([]byte, insertLen) + + if state == 0 { + mergeBlock.newPos = upperBlock.newPos + cur + } + + copy(insertBytes, upperBlock.insertBytes[upperInsertBytesBegin:]) + mergeBlock.insertBytes = append(mergeBlock.insertBytes, insertBytes...) + state = 2 + + cur += int64(insertLen) + } + } else { + // lower INSERT + lowerInsertBytesBegin := int(upperBlock.oldPos + cur - lowerInsertPos) + lowerInsertRestBytesLen := len(lowerBlock.insertBytes[lowerInsertBytesBegin:]) + + if state == 0 { + mergeBlock.newPos = upperBlock.newPos + cur + } + if upperBlock.newPos+cur < upperInsertPos { + log.Tracef("lower=INSERT upper=ADD\n") + // upper ADD + upperAddRestBytesLen := len(upperBlock.addBytes[cur:]) + insertLen := min(lowerInsertRestBytesLen, upperAddRestBytesLen) + + insertBytes := make([]byte, insertLen) + for i := 0; i < insertLen; i++ { + insertBytes[i] = lowerBlock.insertBytes[lowerInsertBytesBegin+i] + upperBlock.addBytes[cur+int64(i)] + } + mergeBlock.insertBytes = append(mergeBlock.insertBytes, insertBytes...) + state = 2 + + cur += int64(insertLen) + } else { + // upper INSERT + upperInsertBytesBegin := cur - int64(len(upperBlock.addBytes)) + upperInsertRestBytesLen := len(upperBlock.insertBytes[upperInsertBytesBegin:]) + + insertLen := min(lowerInsertRestBytesLen, upperInsertRestBytesLen) + insertBytes := make([]byte, insertLen) + + log.Tracef("lower=INSERT upper=INSERT Len=%d\n", insertLen) + copy(insertBytes, upperBlock.insertBytes[upperInsertBytesBegin:]) + mergeBlock.insertBytes = append(mergeBlock.insertBytes, insertBytes...) + state = 2 + + cur += int64(insertLen) + } + } + } + if base != nil && updated != nil { + baseAddBytes := make([]byte, len(mergeBlock.addBytes)) + patchedBytes := make([]byte, len(mergeBlock.addBytes)+len(mergeBlock.insertBytes)) + updatedBytes := make([]byte, len(mergeBlock.addBytes)+len(mergeBlock.insertBytes)) + _, err := base.ReadAt(baseAddBytes, mergeBlock.oldPos) + if err != nil { + return nil, err + } + _, err = updated.ReadAt(updatedBytes, mergeBlock.newPos) + if err != nil { + return nil, err + } + + for i := 0; i < len(baseAddBytes); i++ { + patchedBytes[i] = baseAddBytes[i] + mergeBlock.addBytes[i] + } + for i := 0; i < len(mergeBlock.insertBytes); i++ { + patchedBytes[i+len(mergeBlock.addBytes)] = mergeBlock.insertBytes[i] + } + if !bytes.Equal(updatedBytes, patchedBytes) { + fmt.Printf("ans=%v\n", updatedBytes) + fmt.Printf("patched=%v\n", patchedBytes) + log.Panicf("Coruppted! oldPos=%d newPos=%d len=%d", mergeBlock.oldPos, mergeBlock.newPos, len(updatedBytes)) + } else { + fmt.Printf("VERIFY OK\n") + } + } + + //if !merging { + // merged = append(merged, mergeBlock) + //} else { + // merged[len(merged)-1] = mergeBlock + //} + } + if state == 1 || state == 2 { + merged = append(merged, mergeBlock) + } + //fmt.Println(merged) + } + + return merged, nil +} + +func DeltaMerging(lowerDiff, upperDiff, mergedDiff, lowerFile, upperFile string) error { + //fmt.Println(upperDiff) + lowerPatch, err := os.Open(lowerDiff) + if err != nil { + return err + } + defer lowerPatch.Close() + + upperPatch, err := os.Open(upperDiff) + if err != nil { + return err + } + defer upperPatch.Close() + + mergedPatch, err := os.Create(mergedDiff) + if err != nil { + return err + } + defer mergedPatch.Close() + + lowerBlocks, _, err := readPatch(lowerPatch) + if err != nil { + return err + } + upperBlocks, newLen, err := readPatch(upperPatch) + if err != nil { + return err + } + + var lowerF *os.File = nil + var upperF *os.File = nil + if lowerFile != "" { + lowerF, err = os.Open(lowerFile) + if err != nil { + return err + } + } + if upperFile != "" { + upperF, err = os.Open(upperFile) + if err != nil { + return err + } + } + + mergedBlocks, err := mergeBlocks(lowerBlocks, upperBlocks, lowerF, upperF) + if err != nil { + return err + } + + err = writePatch(mergedPatch, newLen, mergedBlocks) + if err != nil { + return err + } + + return nil +} + +func DeltaMergingBytes(lowerDiff, upperDiff io.Reader, mergedDiff io.Writer) error { + lowerBlocks, _, err := readPatch(lowerDiff) + if err != nil { + return err + } + upperBlocks, newLen, err := readPatch(upperDiff) + if err != nil { + return err + } + + mergedBlocks, err := mergeBlocks(lowerBlocks, upperBlocks, nil, nil) + if err != nil { + return err + } + + err = writePatch(mergedDiff, newLen, mergedBlocks) + if err != nil { + return err + } + + return nil +} + +var ErrUnexpected = fmt.Errorf("unexpected error") + +func applyFilePatch(baseFilePath, newFilePath, patchPath string) error { + baseFile, err := os.Open(baseFilePath) + if err != nil { + return err + } + defer baseFile.Close() + newFile, err := os.Create(newFilePath) + if err != nil { + return err + } + defer newFile.Close() + patchFile, err := os.Open(patchPath) + if err != nil { + return err + } + err = bsdiff.Patch(baseFile, newFile, patchFile) + if err != nil { + return err + } + + return nil +} + +func MergeDiff(lowerDiff, upperDiff, mergedDiff string, lowerEntry, upperEntry, mergeEntry *FileEntry) error { + os.Mkdir(mergedDiff, os.ModePerm) + lowerIdx := 0 + for upperIdx := range upperEntry.Childs { + upperChild := upperEntry.Childs[upperIdx] + log.Debugf("Processsing %s(diffPath = %s)", upperChild.Name, path.Join(upperDiff, upperChild.DiffName)) + upperDiffPath := path.Join(upperDiff, upperChild.DiffName) + mergeChild := NewFileEntry() + if upperChild.IsNew() { + log.Debugf("upperChild is New") + srcPath := path.Join(upperDiff, upperChild.Name) + dstPath := path.Join(mergedDiff, upperChild.Name) + log.Debugf("Copy src=%v dst=%v", srcPath, dstPath) + err := cp.Copy(srcPath, dstPath) + if err != nil { + return err + } + mergeChild = &upperChild + } else { + log.Debugf("upperChild is not New") + if upperChild.IsSymlink() { + log.Debugf("upperChild is symlink") + err := copier.Copy(mergeChild, upperChild) + if err != nil { + return err + } + mergeEntry.Childs = append(mergeEntry.Childs, *mergeChild) + continue + } + for lowerIdx < len(lowerEntry.Childs) && lowerEntry.Childs[lowerIdx].Name != upperChild.Name { + lowerIdx += 1 + } + lowerChild := lowerEntry.Childs[lowerIdx] + if lowerChild.Name != upperEntry.Childs[upperIdx].Name { + log.Errorf("lowerChild(%s) not found", path.Join(lowerDiff, upperChild.Name)) + return ErrUnexpected + } + lowerDiffPath := path.Join(lowerDiff, lowerChild.DiffName) + log.Debugf("lowerChild is found(diffPath = %s)", lowerDiffPath) + + if upperChild.IsDir() { + log.Debugf("upperChild is dir") + if lowerChild.IsDir() { + err := copier.Copy(mergeChild, upperChild) + if err != nil { + return err + } + err = MergeDiff(path.Join(lowerDiff, lowerChild.Name), path.Join(upperDiff, upperChild.Name), path.Join(mergedDiff, upperChild.Name), &lowerChild, &upperChild, mergeChild) + if err != nil { + return err + } + } else { + err := copier.Copy(mergeChild, upperChild) + if err != nil { + return err + } + log.Debugf("Copy src=%v dst=%v", upperDiff, mergedDiff) + err = cp.Copy(upperDiff, mergedDiff) + if err != nil { + return err + } + } + } else { + log.Debugf("upperChild is not dir") + if lowerChild.IsSymlink() { + log.Debugf("lowerChild is symlink") + if !upperChild.IsSymlink() { + err := copier.Copy(mergeChild, upperChild) + if err != nil { + return err + } + dstPath := path.Join(mergedDiff, mergeChild.DiffName) + log.Debugf("Copy src=%v dst=%v", upperDiffPath, dstPath) + err = cp.Copy(upperDiffPath, dstPath) + if err != nil { + return err + } + } else { + return ErrUnexpected + } + } else if lowerChild.IsSame() { + log.Debugf("lowerChild is same") + err := copier.Copy(mergeChild, upperChild) + if err != nil { + return err + } + if !upperChild.IsSame() { + // something diff + dstPath := path.Join(mergedDiff, upperChild.DiffName) + log.Debugf("Copy src=%v dst=%v", upperDiffPath, dstPath) + err = cp.Copy(upperDiffPath, dstPath) + if err != nil { + return err + } + } + } else if lowerChild.IsNew() { + log.Debugf("lowerChild is new") + if upperChild.IsSame() { + err := copier.Copy(mergeChild, lowerChild) + if err != nil { + return err + } + mergedDiffPath := path.Join(mergedDiff, lowerChild.DiffName) + log.Debugf("Copy src=%v dst=%v", lowerDiffPath, mergedDiffPath) + err = cp.Copy(lowerDiffPath, mergedDiffPath) + if err != nil { + return err + } + } else if !upperChild.IsNew() { + err := copier.Copy(mergeChild, upperChild) + if err != nil { + return err + } + mergeChild.Type = FILE_ENTRY_FILE_NEW + mergeChild.DiffName = upperChild.Name + dstPath := path.Join(mergedDiff, mergeChild.Name) + log.Debugf("Apply patch src=%v dst=%v", lowerDiffPath, dstPath) + err = applyFilePatch(lowerDiffPath, dstPath, upperDiffPath) + if err != nil { + return err + } + } else { + return ErrUnexpected + } + } else { + log.Debugf("lowerChild is diff") + if upperChild.IsSame() { + log.Debugf("upperChild is same") + err := copier.Copy(mergeChild, lowerChild) + if err != nil { + return err + } + dstPath := path.Join(mergedDiff, lowerChild.DiffName) + log.Debugf("Copy src=%v dst=%v", lowerDiffPath, dstPath) + err = cp.Copy(lowerDiffPath, dstPath) + if err != nil { + return err + } + } else if !upperChild.IsNew() { + log.Debugf("upperChild is diff") + // DeltaMerging + err := copier.Copy(mergeChild, upperChild) + if err != nil { + return err + } + mergedDiffPath := path.Join(mergedDiff, upperChild.DiffName) + err = DeltaMerging(lowerDiffPath, upperDiffPath, mergedDiffPath, "", "") + if err != nil { + return err + } + } else { + return ErrUnexpected + } + } + } + } + mergeEntry.Childs = append(mergeEntry.Childs, *mergeChild) + } + + return nil +} + +func copyDimg(entry *FileEntry, upperPath string, upperImgFile *os.File, upperOffset int64, mergeEntry *FileEntry, mergeOut *bytes.Buffer) error { + err := copier.Copy(mergeEntry, entry) + if err != nil { + return err + } + if entry.IsDir() { + for idx := range entry.Childs { + e := entry.Childs[idx] + mergeChild := NewFileEntry() + err = copyDimg(&e, path.Join(upperPath, e.Name), upperImgFile, upperOffset, mergeChild, mergeOut) + if err != nil { + return err + } + mergeEntry.Childs = append(mergeEntry.Childs, *mergeChild) + } + } else { + log.Debugf("Copy %s from upper", upperPath) + upperBytes := make([]byte, entry.CompressedSize) + _, err := upperImgFile.ReadAt(upperBytes, upperOffset+entry.Offset) + if err != nil { + return err + } + mergeEntry.Offset = int64(len(mergeOut.Bytes())) + _, err = mergeOut.Write(upperBytes) + if err != nil { + return err + } + } + return nil + +} + +func MergeDiffDimg(lowerEntry, upperEntry *FileEntry, lowerDiff, upperDiff string, lowerImgFile, upperImgFile *os.File, lowerOffset, upperOffset int64, mergeEntry *FileEntry, mergeOut *bytes.Buffer) error { + lowerIdx := 0 + for upperIdx := range upperEntry.Childs { + upperChild := upperEntry.Childs[upperIdx] + log.Debugf("Processsing %s", path.Join(upperDiff, upperChild.Name)) + upperDiffPath := path.Join(upperDiff, upperChild.Name) + mergeChild := NewFileEntry() + if upperChild.IsNew() { + log.Debugf("upperChild is New") + err := copyDimg(&upperChild, upperDiffPath, upperImgFile, upperOffset, mergeChild, mergeOut) + if err != nil { + return err + } + } else { + log.Debugf("upperChild is not New") + if upperChild.IsSymlink() { + log.Debugf("upperChild is symlink") + err := copier.Copy(mergeChild, upperChild) + if err != nil { + return err + } + mergeEntry.Childs = append(mergeEntry.Childs, *mergeChild) + continue + } + for lowerIdx < len(lowerEntry.Childs) && lowerEntry.Childs[lowerIdx].Name != upperChild.Name { + lowerIdx += 1 + } + lowerChild := lowerEntry.Childs[lowerIdx] + if lowerChild.Name != upperEntry.Childs[upperIdx].Name { + log.Errorf("lowerChild(%s) not found", path.Join(lowerDiff, upperChild.Name)) + return ErrUnexpected + } + lowerDiffPath := path.Join(lowerDiff, lowerChild.Name) + log.Debugf("lowerChild is found(%s)", lowerDiffPath) + + if upperChild.IsDir() { + log.Debugf("upperChild is dir") + if lowerChild.IsDir() { + err := copier.Copy(mergeChild, upperChild) + if err != nil { + return err + } + err = MergeDiffDimg(&lowerChild, &upperChild, lowerDiffPath, upperDiffPath, lowerImgFile, upperImgFile, lowerOffset, upperOffset, mergeChild, mergeOut) + if err != nil { + return err + } + } else { + err := copier.Copy(mergeChild, upperChild) + if err != nil { + return err + } + log.Debugf("Copy %v from upper", upperDiffPath) + upperBytes := make([]byte, upperChild.CompressedSize) + _, err = upperImgFile.ReadAt(upperBytes, upperOffset+upperChild.Offset) + if err != nil { + return err + } + mergeChild.Offset = int64(len(mergeOut.Bytes())) + _, err = mergeOut.Write(upperBytes) + if err != nil { + return err + } + } + } else { + log.Debugf("upperChild is not dir") + if lowerChild.IsSymlink() { + log.Debugf("lowerChild is symlink") + if !upperChild.IsSymlink() { + err := copier.Copy(mergeChild, upperChild) + if err != nil { + return err + } + log.Debugf("Copy %q from upper", upperDiffPath) + upperBytes := make([]byte, upperChild.CompressedSize) + _, err = upperImgFile.ReadAt(upperBytes, upperOffset+upperChild.Offset) + if err != nil { + return err + } + mergeChild.Offset = int64(len(mergeOut.Bytes())) + _, err = mergeOut.Write(upperBytes) + if err != nil { + return err + } + } else { + return ErrUnexpected + } + } else if lowerChild.IsSame() { + log.Debugf("lowerChild is same") + err := copier.Copy(mergeChild, upperChild) + if err != nil { + return err + } + if !upperChild.IsSame() { + // something diff + err := copier.Copy(mergeChild, upperChild) + if err != nil { + return err + } + log.Debugf("Copy %v from upper", upperDiffPath) + upperBytes := make([]byte, upperChild.CompressedSize) + _, err = upperImgFile.ReadAt(upperBytes, upperOffset+upperChild.Offset) + if err != nil { + return err + } + mergeChild.Offset = int64(len(mergeOut.Bytes())) + _, err = mergeOut.Write(upperBytes) + if err != nil { + return err + } + } + } else if lowerChild.IsNew() { + log.Debugf("lowerChild is new") + if upperChild.IsSame() { + err := copier.Copy(mergeChild, lowerChild) + if err != nil { + return err + } + log.Debugf("Copy %v from lower", lowerDiffPath) + lowerBytes := make([]byte, lowerChild.CompressedSize) + _, err = lowerImgFile.ReadAt(lowerBytes, lowerOffset+lowerChild.Offset) + if err != nil { + return err + } + mergeChild.Offset = int64(len(mergeOut.Bytes())) + _, err = mergeOut.Write(lowerBytes) + if err != nil { + return err + } + } else if !upperChild.IsNew() { + log.Debugf("Apply patch %v to %v", lowerDiffPath, upperDiffPath) + err := copier.Copy(mergeChild, upperChild) + if err != nil { + return err + } + mergeChild.Type = FILE_ENTRY_FILE_NEW + + lowerBytes := make([]byte, lowerChild.CompressedSize) + upperBytes := make([]byte, upperChild.CompressedSize) + _, err = lowerImgFile.ReadAt(lowerBytes, lowerOffset+lowerChild.Offset) + if err != nil { + return err + } + baseBuf := bytes.NewBuffer(nil) + baseReader, err := zstd.NewReader(bytes.NewBuffer(lowerBytes)) + if err != nil { + return err + } + defer baseReader.Close() + _, err = io.Copy(baseBuf, baseReader) + if err != nil { + return err + } + + _, err = upperImgFile.ReadAt(upperBytes, upperOffset+upperChild.Offset) + if err != nil { + return err + } + mergeBytes := bytes.NewBuffer(nil) + err = bsdiff.Patch(bytes.NewBuffer(baseBuf.Bytes()), mergeBytes, bytes.NewBuffer(upperBytes)) + if err != nil { + return err + } + + mergeCompressed, err := CompressWithZstd(mergeBytes.Bytes()) + if err != nil { + return err + } + mergeChild.Offset = int64(len(mergeOut.Bytes())) + mergeChild.CompressedSize = int64(len(mergeCompressed)) + _, err = mergeOut.Write(mergeCompressed) + if err != nil { + return err + } + } else { + return ErrUnexpected + } + } else { + log.Debugf("lowerChild is diff") + if upperChild.IsSame() { + log.Debugf("upperChild is same") + err := copier.Copy(mergeChild, lowerChild) + if err != nil { + return err + } + log.Debugf("Copy %v from lower", lowerDiffPath) + lowerBytes := make([]byte, lowerChild.CompressedSize) + _, err = lowerImgFile.ReadAt(lowerBytes, lowerOffset+lowerChild.Offset) + if err != nil { + return err + } + mergeChild.Offset = int64(len(mergeOut.Bytes())) + _, err = mergeOut.Write(lowerBytes) + if err != nil { + return err + } + } else if !upperChild.IsNew() { + log.Debugf("upperChild is diff") + // DeltaMerging + err := copier.Copy(mergeChild, upperChild) + if err != nil { + return err + } + lowerBytes := make([]byte, lowerChild.CompressedSize) + upperBytes := make([]byte, upperChild.CompressedSize) + _, err = lowerImgFile.ReadAt(lowerBytes, lowerOffset+lowerChild.Offset) + if err != nil { + return err + } + _, err = upperImgFile.ReadAt(upperBytes, upperOffset+upperChild.Offset) + if err != nil { + return err + } + mergeBytes := bytes.NewBuffer(nil) + err = DeltaMergingBytes(bytes.NewBuffer(lowerBytes), bytes.NewBuffer(upperBytes), mergeBytes) + if err != nil { + return err + } + mergeChild.Offset = int64(len(mergeOut.Bytes())) + mergeChild.CompressedSize = int64(len(mergeBytes.Bytes())) + _, err = mergeOut.Write(mergeBytes.Bytes()) + if err != nil { + return err + } + } else { + return ErrUnexpected + } + } + } + } + mergeEntry.Childs = append(mergeEntry.Childs, *mergeChild) + } + + return nil +} + +func MergeDimg(lowerDimg, upperDimg string, merged io.Writer) error { + lowerImg, lowerImgFile, lowerOffset, err := LoadImage(lowerDimg) + if err != nil { + panic(err) + } + defer lowerImgFile.Close() + upperImg, upperImgFile, upperOffset, err := LoadImage(upperDimg) + if err != nil { + panic(err) + } + defer upperImgFile.Close() + tmp := bytes.Buffer{} + mergedEntry := NewFileEntry() + err = copier.Copy(mergedEntry, upperImg.FileEntry) + if err != nil { + panic(err) + } + err = MergeDiffDimg(&lowerImg.FileEntry, &upperImg.FileEntry, lowerImg.FileEntry.Name, upperImg.FileEntry.Name, lowerImgFile, upperImgFile, lowerOffset, upperOffset, mergedEntry, &tmp) + if err != nil { + panic(err) + } + + header := ImageHeader{ + BaseId: lowerImg.BaseId, + FileEntry: *mergedEntry, + } + + jsonBytes, err := json.Marshal(header) + if err != nil { + panic(err) + } + + // encode header + var headerZstdBuffer bytes.Buffer + headerZstd, err := zstd.NewWriter(&headerZstdBuffer) + if err != nil { + panic(err) + } + _, err = headerZstd.Write(jsonBytes) + if err != nil { + panic(err) + } + err = headerZstd.Close() + if err != nil { + panic(err) + } + + bs := make([]byte, 4) + binary.LittleEndian.PutUint32(bs, uint32(len(headerZstdBuffer.Bytes()))) + + // Image format + // [ length of compressed image header (4bit)] + // [ compressed image header ] + // [ content body ] + + _, err = merged.Write(append(bs, headerZstdBuffer.Bytes()...)) + if err != nil { + panic(err) + } + + _, err = io.Copy(merged, &tmp) + if err != nil { + panic(err) + } + + return nil +} diff --git a/pkg/image/diff.go b/pkg/image/diff.go new file mode 100644 index 0000000..d07eb20 --- /dev/null +++ b/pkg/image/diff.go @@ -0,0 +1,538 @@ +package image + +import ( + "bytes" + "crypto/sha256" + "encoding/binary" + "encoding/json" + "fmt" + "io" + "io/fs" + "os" + "path" + + "github.com/icedream/go-bsdiff" + "github.com/klauspost/compress/zstd" + "github.com/naoki9911/fuse-diff-containerd/pkg/di3fs" + "github.com/naoki9911/fuse-diff-containerd/pkg/utils" + cp "github.com/otiai10/copy" +) + +func copyFile(srcFile, dstFile string) error { + src, err := os.Open(srcFile) + if err != nil { + return err + } + defer src.Close() + + dst, err := os.Create(dstFile) + if err != nil { + return err + } + defer dst.Close() + + _, err = io.Copy(dst, src) + if err != nil { + return err + } + + return nil +} + +func getFileSize(path string) (int, error) { + fileInfo, err := os.Stat(path) + if err != nil { + return 0, err + } + + return int(fileInfo.Size()), nil +} + +func generateFileHash(filePath string) ([]byte, error) { + file, err := os.Open(filePath) + if err != nil { + return nil, err + } + defer file.Close() + + h := sha256.New() + _, err = io.Copy(h, file) + if err != nil { + return nil, err + } + + return h.Sum(nil), nil +} + +// return true if files are same. +func compareFile(fileAPath, fileBPath string) (bool, error) { + fileAHash, err := generateFileHash(fileAPath) + if err != nil { + return false, err + } + fileBHash, err := generateFileHash(fileBPath) + if err != nil { + return false, err + } + + for i := range fileAHash { + if fileAHash[i] != fileBHash[i] { + return false, nil + } + } + + return true, nil +} + +func generateFileDiff(baseFilePath, newFilePath, outFilePath string) error { + baseFile, err := os.Open(baseFilePath) + if err != nil { + return err + } + defer baseFile.Close() + newFile, err := os.Open(newFilePath) + if err != nil { + return err + } + defer newFile.Close() + outFile, err := os.Create(outFilePath) + if err != nil { + return err + } + defer outFile.Close() + err = bsdiff.Diff(baseFile, newFile, outFile) + if err != nil { + return err + } + + return nil +} + +// @return bool[0]: is new same as base? +// @return bool[1]: is entirly new ? +// @return bool[2]: is the dir containing opaque dir ? +func generateDiffFromDirImpl(basePath, newPath, outPath string, dirEntry *di3fs.FileEntry, isBinaryDiff, baseExists bool) (bool, bool, []string, error) { + entireSame := true + entireNew := true + baseOk := baseExists + + baseFiles := map[string]fs.DirEntry{} + if _, err := os.Stat(basePath); err == nil && baseOk { + baseEntries, _ := os.ReadDir(basePath) + for i := range baseEntries { + baseFiles[baseEntries[i].Name()] = baseEntries[i] + } + } else { + baseOk = false + entireSame = false + } + + logger.Debugf("newPath:%s\n", outPath) + newEntries, err := os.ReadDir(newPath) + if err != nil { + return false, false, nil, err + } + + if _, err := os.Stat(outPath); err != nil { + err = os.Mkdir(outPath, os.ModePerm) + if err != nil { + return false, false, nil, err + } + } + + childDirs := []fs.DirEntry{} + isOpaqueDir := false + for i, entry := range newEntries { + fName := entry.Name() + baseFilePath := path.Join(basePath, fName) + newFilePath := path.Join(newPath, fName) + outFilePath := path.Join(outPath, fName) + logger.Debugf("DIFF %s\n", newFilePath) + + if entry.IsDir() { + childDirs = append(childDirs, newEntries[i]) + continue + } + fileInfo, err := entry.Info() + if err != nil { + return false, false, nil, err + } + + // opaque directory + // https://www.madebymikal.com/interpreting-whiteout-files-in-docker-image-layers/ + // AUFS provided an “opaque directory” that ensured that the directory remained, but all of its previous content was hidden. + // TODO check filemode + // https://www.kernel.org/doc/html/latest/filesystems/overlayfs.html#whiteouts-and-opaque-directories + if fName == ".wh..wh..opq" { + isOpaqueDir = true + continue + } + + entry := di3fs.FileEntry{ + Name: fName, + Mode: uint32(fileInfo.Mode()), + Childs: []di3fs.FileEntry{}, + } + + if fileInfo.Mode()&os.ModeSymlink == os.ModeSymlink { + realPath, err := os.Readlink(newFilePath) + if err != nil { + return false, false, nil, err + } + entireSame = false + entry.Type = di3fs.FILE_ENTRY_SYMLINK + entry.RealPath = realPath + dirEntry.Childs = append(dirEntry.Childs, entry) + continue + } + + if fileInfo.Mode()&os.ModeCharDevice == os.ModeCharDevice { + logger.Infof("Ignore char device:%v\n", newFilePath) + continue + } + if fileInfo.Mode()&os.ModeNamedPipe == os.ModeNamedPipe { + logger.Infof("Ignore named pipe:%v\n", newFilePath) + continue + } + + err = entry.SetUGID(newFilePath) + if err != nil { + return false, false, nil, err + } + + entry.Size, err = getFileSize(newFilePath) + if err != nil { + return false, false, nil, err + } + // newFile is not dir, but baseFile is dir + // newFile must be newly created. + if baseFile, ok := baseFiles[fName]; !ok || baseFile.IsDir() { + logger.Debugf("NewFile Name:%v\n", newFilePath) + err = cp.Copy(newFilePath, outFilePath) + if err != nil { + return false, false, nil, err + } + entry.Type = di3fs.FILE_ENTRY_FILE_NEW + entry.DiffName = fName + entireSame = false + } else { + isSame, err := compareFile(baseFilePath, newFilePath) + if err != nil { + return false, false, nil, err + } + if isSame { + entireNew = false + entry.Type = di3fs.FILE_ENTRY_FILE_SAME + logger.Debugf("NotUpdatedFile Name:%v\n", fName) + } else { + baseStat, err := os.Stat(baseFilePath) + if err != nil { + return false, false, nil, err + } + entireSame = false + if baseStat.Size() == 0 || !isBinaryDiff { + err = copyFile(newFilePath, outFilePath) + if err != nil { + return false, false, nil, err + } + entry.Type = di3fs.FILE_ENTRY_FILE_NEW + entry.DiffName = fName + } else { + entireNew = false + entry.Type = di3fs.FILE_ENTRY_FILE_DIFF + outFilePath += ".diff" + entry.DiffName = path.Base(outFilePath) + logger.Debugf("UpdatedFile Name:%v\n", fName) + err = generateFileDiff(baseFilePath, newFilePath, outFilePath) + if err != nil { + return false, false, nil, err + } + logger.Debugf("CreatedDiffFile %v\n", outFilePath) + } + } + + } + dirEntry.Childs = append(dirEntry.Childs, entry) + } + + opaqueFiles := []string{} + if isOpaqueDir { + opaqueFiles = append(opaqueFiles, ".wh..wh..opq") + } + for _, childDir := range childDirs { + childBasePath := path.Join(basePath, childDir.Name()) + childNewPath := path.Join(newPath, childDir.Name()) + childOutPath := path.Join(outPath, childDir.Name()) + entry := di3fs.FileEntry{ + Name: childDir.Name(), + Childs: []di3fs.FileEntry{}, + } + same, new, opaque, err := generateDiffFromDirImpl(childBasePath, childNewPath, childOutPath, &entry, isBinaryDiff, baseOk) + if err != nil { + return false, false, nil, err + } + if !same { + entireSame = false + } + if !new { + entireNew = false + } + + for _, o := range opaque { + opaqueFiles = append(opaqueFiles, path.Join(childDir.Name(), o)) + } + dirEntry.Childs = append(dirEntry.Childs, entry) + } + fileInfo, err := os.Stat(newPath) + if err != nil { + return false, false, nil, nil + } + dirEntry.Size = int(fileInfo.Size()) + dirEntry.Mode = uint32(fileInfo.Mode()) + err = dirEntry.SetUGID(newPath) + if err != nil { + return false, false, nil, err + } + + if !entireSame && entireNew { + dirEntry.Type = di3fs.FILE_ENTRY_DIR_NEW + } else { + dirEntry.Type = di3fs.FILE_ENTRY_DIR + } + dirEntry.OaqueFiles = opaqueFiles + return entireSame, entireNew, nil, nil +} + +func GenerateDiffFromDir(basePath, newPath, outPath string, isBinaryDiff, baseExists bool) (*di3fs.FileEntry, error) { + entry := &di3fs.FileEntry{ + Name: "/", + } + _, _, _, err := generateDiffFromDirImpl(basePath, newPath, outPath, entry, isBinaryDiff, baseExists) + return entry, err +} + +func GenerateDiffFromDimg(oldDimgPath, newDimgPath, parentDimgPath, diffDimgPath string, isBinaryDiff bool) error { + oldImg, oldImgFile, oldOffset, err := di3fs.LoadImage(oldDimgPath) + if err != nil { + return err + } + defer oldImgFile.Close() + + newImg, newImgFile, newOffset, err := di3fs.LoadImage(newDimgPath) + if err != nil { + return err + } + defer newImgFile.Close() + + diffFile, err := os.Create(diffDimgPath) + if err != nil { + return err + } + defer diffFile.Close() + + diffOut := bytes.Buffer{} + _, err = generateDiffFromDimg(oldImgFile, oldOffset, &oldImg.FileEntry, newImgFile, newOffset, &newImg.FileEntry, &diffOut, isBinaryDiff) + if err != nil { + return err + } + baseId := "" + if parentDimgPath != "" { + h := sha256.New() + baseImg, err := os.Open(parentDimgPath) + if err != nil { + panic(err) + } + _, err = io.Copy(h, baseImg) + if err != nil { + panic(err) + } + baseId = fmt.Sprintf("sha256:%x", h.Sum(nil)) + } + header := di3fs.ImageHeader{ + BaseId: baseId, + FileEntry: newImg.FileEntry, + } + + jsonBytes, err := json.Marshal(header) + if err != nil { + panic(err) + } + + // encode header + var headerZstdBuffer bytes.Buffer + headerZstd, err := zstd.NewWriter(&headerZstdBuffer) + if err != nil { + panic(err) + } + _, err = headerZstd.Write(jsonBytes) + if err != nil { + panic(err) + } + err = headerZstd.Close() + if err != nil { + panic(err) + } + + bs := make([]byte, 4) + binary.LittleEndian.PutUint32(bs, uint32(len(headerZstdBuffer.Bytes()))) + + // Image format + // [ length of compressed image header (4bit)] + // [ compressed image header ] + // [ content body ] + + _, err = diffFile.Write(append(bs, headerZstdBuffer.Bytes()...)) + if err != nil { + return err + } + + _, err = io.Copy(diffFile, &diffOut) + if err != nil { + return err + } + + return nil +} + +// @return bool: is entirly new ? +func generateDiffFromDimg(oldImgFile *os.File, oldOffset int64, oldEntry *di3fs.FileEntry, newImgFile *os.File, newOffset int64, newEntry *di3fs.FileEntry, diffBody *bytes.Buffer, isBinaryDiff bool) (bool, error) { + entireNew := true + + for i := range newEntry.Childs { + newChildEntry := &newEntry.Childs[i] + if newChildEntry.Type == di3fs.FILE_ENTRY_FILE_SAME || + newChildEntry.Type == di3fs.FILE_ENTRY_FILE_DIFF { + return false, fmt.Errorf("invalid dimg") + } + + if newChildEntry.Type == di3fs.FILE_ENTRY_OPAQUE || + newChildEntry.Type == di3fs.FILE_ENTRY_SYMLINK || + newChildEntry.Size == 0 { + continue + } + + // newly created file or directory + if oldEntry == nil { + if newChildEntry.IsDir() { + _, err := generateDiffFromDimg(oldImgFile, oldOffset, nil, newImgFile, newOffset, &newEntry.Childs[i], diffBody, isBinaryDiff) + if err != nil { + return false, err + } + } else { + newBytes := make([]byte, newChildEntry.CompressedSize) + _, err := newImgFile.ReadAt(newBytes, newOffset+newChildEntry.Offset) + if err != nil { + return false, err + } + newChildEntry.Offset = int64(len(diffBody.Bytes())) + _, err = diffBody.Write(newBytes) + if err != nil { + return false, err + } + } + + continue + } + + oldChildIdx := 0 + for oldChildIdx < len(oldEntry.Childs) && oldEntry.Childs[oldChildIdx].Name != newChildEntry.Name { + oldChildIdx += 1 + } + oldChildEntry := oldEntry.Childs[oldChildIdx] + + // newly created file or directory including unmatched EntryType + if oldChildEntry.Name != newChildEntry.Name || + oldChildEntry.Type != newChildEntry.Type { + if newChildEntry.IsDir() { + _, err := generateDiffFromDimg(oldImgFile, oldOffset, nil, newImgFile, newOffset, &newEntry.Childs[i], diffBody, isBinaryDiff) + if err != nil { + return false, err + } + } else { + newBytes := make([]byte, newChildEntry.CompressedSize) + _, err := newImgFile.ReadAt(newBytes, newOffset+newChildEntry.Offset) + if err != nil { + return false, err + } + newChildEntry.Offset = int64(len(diffBody.Bytes())) + _, err = diffBody.Write(newBytes) + if err != nil { + return false, err + } + } + + continue + } + + // if both new and old are directory, recursively generate diff + if newChildEntry.IsDir() { + new, err := generateDiffFromDimg(oldImgFile, oldOffset, &oldEntry.Childs[oldChildIdx], newImgFile, newOffset, &newEntry.Childs[i], diffBody, isBinaryDiff) + if err != nil { + return false, err + } + if !new { + entireNew = false + } + + continue + } + + newCompressedBytes := make([]byte, newChildEntry.CompressedSize) + _, err := newImgFile.ReadAt(newCompressedBytes, newOffset+newChildEntry.Offset) + if err != nil { + return false, err + } + newBytes, err := utils.DecompressWithZstd(newCompressedBytes) + if err != nil { + return false, err + } + + oldCompressedBytes := make([]byte, oldChildEntry.CompressedSize) + _, err = oldImgFile.ReadAt(oldCompressedBytes, oldOffset+oldChildEntry.Offset) + if err != nil { + return false, err + } + oldBytes, err := utils.DecompressWithZstd(oldCompressedBytes) + if err != nil { + return false, err + } + isSame := bytes.Equal(newBytes, oldBytes) + if isSame { + entireNew = false + newChildEntry.Type = di3fs.FILE_ENTRY_FILE_SAME + continue + } + + if isBinaryDiff { + entireNew = false + diffWriter := new(bytes.Buffer) + err = bsdiff.Diff(bytes.NewBuffer(oldBytes), bytes.NewBuffer(newBytes), diffWriter) + if err != nil { + return false, err + } + newChildEntry.Offset = int64(len(diffBody.Bytes())) + newChildEntry.CompressedSize = int64(len(diffWriter.Bytes())) + _, err = diffBody.Write(diffWriter.Bytes()) + if err != nil { + return false, err + } + newChildEntry.Type = di3fs.FILE_ENTRY_FILE_DIFF + } else { + newBytes := make([]byte, newChildEntry.CompressedSize) + _, err := newImgFile.ReadAt(newBytes, newOffset+newChildEntry.Offset) + if err != nil { + return false, err + } + newChildEntry.Offset = int64(len(diffBody.Bytes())) + _, err = diffBody.Write(newBytes) + if err != nil { + return false, err + } + newChildEntry.Type = di3fs.FILE_ENTRY_FILE_NEW + } + } + if newEntry.IsDir() && entireNew { + newEntry.Type = di3fs.FILE_ENTRY_DIR_NEW + } + return entireNew, nil +} diff --git a/pkg/image/dimg.go b/pkg/image/dimg.go new file mode 100644 index 0000000..10d682b --- /dev/null +++ b/pkg/image/dimg.go @@ -0,0 +1,147 @@ +package image + +import ( + "bytes" + "crypto/sha256" + "encoding/binary" + "encoding/json" + "fmt" + "io" + "os" + "path/filepath" + + "github.com/klauspost/compress/zstd" + "github.com/naoki9911/fuse-diff-containerd/pkg/di3fs" + "github.com/sirupsen/logrus" +) + +// @return int64: written size +func packDimgContent(logger *logrus.Entry, diffDir string, entry *di3fs.FileEntry, offset int64, out io.Writer) (int64, error) { + if entry.IsSame() { + // no need to copy this + return 0, nil + } + + if entry.IsSymlink() { + // nothing to do + return 0, nil + } + + if entry.IsDir() { + curOffset := offset + for idx := range entry.Childs { + childDir := &entry.Childs[idx] + written, err := packDimgContent(logger, filepath.Join(diffDir, entry.Name), childDir, curOffset, out) + if err != nil { + return 0, err + } + curOffset += written + } + return curOffset - offset, nil + } + + var writtenSize int64 + var err error + if entry.IsNew() { + // comress from diffDir + filePath := filepath.Join(diffDir, entry.Name) + logger.Debugf("packing %q", filePath) + writtenSize, err = di3fs.PackFile(filePath, out) + if err != nil { + return 0, err + } + } else { + // read and write file + diffFilePath := filepath.Join(diffDir, entry.DiffName) + logger.Debugf("packing %q", diffFilePath) + diffFile, err := os.Open(diffFilePath) + if err != nil { + return 0, err + } + defer diffFile.Close() + writtenSize, err = io.Copy(out, diffFile) + if err != nil { + return 0, err + } + } + logger.Debugf("pack done(offset=%d size=%d)", offset, writtenSize) + + entry.Offset = offset + entry.CompressedSize = writtenSize + entry.DiffName = "" + return writtenSize, nil +} + +func PackDimg(logger *logrus.Entry, diffDir string, entry *di3fs.FileEntry, baseDImgPath string, outputPath string) error { + outFile, err := os.Create(outputPath) + if err != nil { + panic(err) + } + defer outFile.Close() + + //entry.print("", false) + var tmpBuf bytes.Buffer + + baseId := "" + if baseDImgPath != "" { + h := sha256.New() + baseImg, err := os.Open(baseDImgPath) + if err != nil { + panic(err) + } + _, err = io.Copy(h, baseImg) + if err != nil { + panic(err) + } + baseId = fmt.Sprintf("sha256:%x", h.Sum(nil)) + } + + _, err = packDimgContent(logger, diffDir, entry, 0, &tmpBuf) + if err != nil { + panic(err) + } + header := di3fs.ImageHeader{ + BaseId: baseId, + FileEntry: *entry, + } + + jsonBytes, err := json.Marshal(header) + if err != nil { + panic(err) + } + + // encode header + var headerZstdBuffer bytes.Buffer + headerZstd, err := zstd.NewWriter(&headerZstdBuffer) + if err != nil { + panic(err) + } + _, err = headerZstd.Write(jsonBytes) + if err != nil { + panic(err) + } + err = headerZstd.Close() + if err != nil { + panic(err) + } + + bs := make([]byte, 4) + binary.LittleEndian.PutUint32(bs, uint32(len(headerZstdBuffer.Bytes()))) + + // Image format + // [ length of compressed image header (4bytes)] + // [ compressed image header ] + // [ content body ] + + _, err = outFile.Write(append(bs, headerZstdBuffer.Bytes()...)) + if err != nil { + return err + } + + _, err = io.Copy(outFile, &tmpBuf) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/image/image.go b/pkg/image/image.go new file mode 100644 index 0000000..d223d0c --- /dev/null +++ b/pkg/image/image.go @@ -0,0 +1,325 @@ +package image + +import ( + "bytes" + "context" + "encoding/binary" + "encoding/json" + "io" + "os" + + "github.com/containerd/containerd/log" + "github.com/klauspost/compress/zstd" + "github.com/naoki9911/fuse-diff-containerd/pkg/utils" + "github.com/opencontainers/go-digest" + v1 "github.com/opencontainers/image-spec/specs-go/v1" +) + +var logger = log.G(context.TODO()) + +type Di3FSImageHeader struct { + ManifestSize int64 `json:"manifestSize"` + ManifestDigest digest.Digest `json:"manifestDigest"` + ConfigSize int64 `json:"configSize"` + DimgSize int64 `json:"dimgSize"` +} + +type Di3FSImage struct { + Header Di3FSImageHeader + ManifestBytes []byte + Manifest v1.Manifest + ConfigBytes []byte + Config v1.Image + DImgDigest digest.Digest + DImgOffset int64 + Image *os.File +} + +func (h *Di3FSImageHeader) pack() ([]byte, error) { + jsonBytes, err := json.Marshal(h) + if err != nil { + return nil, err + } + + res, err := compressWithZstd(jsonBytes) + if err != nil { + return nil, err + } + + return res, nil +} + +func compressWithZstd(src []byte) ([]byte, error) { + out := &bytes.Buffer{} + z, err := zstd.NewWriter(out) + if err != nil { + return nil, err + } + + _, err = z.Write(src) + if err != nil { + return nil, err + } + + err = z.Close() + if err != nil { + return nil, err + } + + return out.Bytes(), nil +} + +func compressFileWithZstd(path string) ([]byte, error) { + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + + fileBytes, err := io.ReadAll(f) + if err != nil { + return nil, err + } + + res, err := compressWithZstd(fileBytes) + if err != nil { + return nil, err + } + + return res, nil + +} + +func packBytes(b []byte, out *bytes.Buffer) (int64, error) { + compressed, err := compressWithZstd(b) + if err != nil { + return 0, err + } + writtenSize, err := out.Write(compressed) + if err != nil { + return 0, err + } + + return int64(writtenSize), err +} + +func packFile(srcFilePath string, out *bytes.Buffer) (int64, error) { + compressed, err := compressFileWithZstd(srcFilePath) + if err != nil { + return 0, err + } + writtenSize, err := out.Write(compressed) + if err != nil { + return 0, err + } + + return int64(writtenSize), err +} + +func loadConfigFromReader(r io.Reader) (*v1.Image, error) { + return utils.UnmarshalJsonFromReader[v1.Image](r) +} + +func PackCdimg(configPath, dimgPath, outPath string) error { + outFile, err := os.Create(outPath) + if err != nil { + return err + } + defer outFile.Close() + logger.Debugf("created outFile %q", outPath) + + configFile, err := os.Open(configPath) + if err != nil { + return err + } + defer configFile.Close() + logger.Debugf("opened configFile %q", configPath) + + dimgFile, err := os.Open(dimgPath) + if err != nil { + return err + } + defer dimgFile.Close() + logger.Debugf("opened dimgFile %q", dimgPath) + + dimgBytes, err := io.ReadAll(dimgFile) + if err != nil { + return err + } + + err = PackIo(configFile, dimgBytes, outFile) + if err != nil { + return err + } + + return nil +} + +func PackIo(configReader io.Reader, dimg []byte, out io.Writer) error { + header := Di3FSImageHeader{} + outBytes := bytes.Buffer{} + config, err := loadConfigFromReader(configReader) + if err != nil { + return err + } + dimgFileSize, dimgFilDigest, err := utils.GetSizeAndDigest(dimg) + if err != nil { + return err + } + + config.RootFS.DiffIDs = []digest.Digest{*dimgFilDigest} + configBytes, err := json.Marshal(config) + if err != nil { + return err + } + + configSize, configDigest, err := utils.GetSizeAndDigest(configBytes) + if err != nil { + return err + } + + manifest := v1.Manifest{ + MediaType: v1.MediaTypeImageManifest, + Config: v1.Descriptor{ + MediaType: v1.MediaTypeImageConfig, + Size: configSize, + Digest: *configDigest, + }, + Layers: []v1.Descriptor{ + { + MediaType: v1.MediaTypeImageLayer, + Size: dimgFileSize, + Digest: *dimgFilDigest, + }, + }, + } + manifest.SchemaVersion = 2 + manifestBytes, err := json.Marshal(manifest) + if err != nil { + return err + } + _, manifestDigest, err := utils.GetSizeAndDigest(manifestBytes) + if err != nil { + return err + } + header.ManifestDigest = *manifestDigest + + header.ManifestSize, err = packBytes(manifestBytes, &outBytes) + if err != nil { + return err + } + logger.Debugf("compressed manifest (size=%d)", header.ManifestSize) + + header.ConfigSize, err = packBytes(configBytes, &outBytes) + if err != nil { + return err + } + logger.Debugf("compressed config (size=%d)", header.ConfigSize) + + header.DimgSize = dimgFileSize + + headerCompressedBytes, err := header.pack() + if err != nil { + return err + } + bs := make([]byte, 4) + binary.LittleEndian.PutUint32(bs, uint32(len(headerCompressedBytes))) + + _, err = out.Write(append(bs, headerCompressedBytes...)) + if err != nil { + return err + } + logger.WithField("header", header).Debugf("written Di3FSImageHeader") + + _, err = io.Copy(out, &outBytes) + if err != nil { + return err + } + _, err = out.Write(dimg) + if err != nil { + return err + } + logger.Debugf("written contents") + + return nil +} + +func LoadHeader(r io.Reader) (*Di3FSImage, error) { + var image Di3FSImage + bs := make([]byte, 4) + _, err := r.Read(bs) + if err != nil { + return nil, err + } + headerSize := binary.LittleEndian.Uint32(bs) + curOffset := int64(len(bs)) + + headerBytes := make([]byte, headerSize) + _, err = r.Read(headerBytes) + if err != nil { + return nil, err + } + header, err := utils.UnmarshalJsonFromCompressed[Di3FSImageHeader](headerBytes) + if err != nil { + return nil, err + } + image.Header = *header + curOffset += int64(len(headerBytes)) + + // load manifest + manifestZstdBytes := make([]byte, header.ManifestSize) + _, err = r.Read(manifestZstdBytes) + if err != nil { + return nil, err + } + manifestBytes, err := utils.DecompressWithZstd(manifestZstdBytes) + if err != nil { + return nil, err + } + curOffset += int64(len(manifestZstdBytes)) + var manifest v1.Manifest + err = json.Unmarshal(manifestBytes, &manifest) + if err != nil { + return nil, err + } + image.ManifestBytes = manifestBytes + image.Manifest = manifest + + // load config + configZstdBytes := make([]byte, header.ConfigSize) + _, err = r.Read(configZstdBytes) + if err != nil { + return nil, err + } + configBytes, err := utils.DecompressWithZstd(configZstdBytes) + if err != nil { + return nil, err + } + curOffset += int64(len(configZstdBytes)) + var config v1.Image + err = json.Unmarshal(configBytes, &config) + if err != nil { + return nil, err + } + image.ConfigBytes = configBytes + image.Config = config + + image.DImgDigest = config.RootFS.DiffIDs[0] + image.DImgOffset = curOffset + return &image, nil +} + +func Load(dimgPath string) (*Di3FSImage, error) { + imgFile, err := os.Open(dimgPath) + if err != nil { + return nil, err + } + + image, err := LoadHeader(imgFile) + if err != nil { + return nil, err + } + image.Image = imgFile + + return image, nil +} diff --git a/pkg/snapshotter/client.go b/pkg/snapshotter/client.go new file mode 100644 index 0000000..712a7a0 --- /dev/null +++ b/pkg/snapshotter/client.go @@ -0,0 +1,33 @@ +package snapshotter + +import ( + "path/filepath" + + "github.com/containerd/containerd" + "github.com/containerd/containerd/snapshots" +) + +type Client struct { + CtrClient *containerd.Client + + SnClient snapshots.Snapshotter + SnRootPath string + SnImageStorePath string +} + +func NewClient() (*Client, error) { + ctr, err := containerd.New("/run/containerd/containerd.sock", containerd.WithDefaultNamespace("default")) + if err != nil { + return nil, err + } + + c := &Client{ + CtrClient: ctr, + SnRootPath: "/tmp/di3fs/sn", + } + + c.SnClient = c.CtrClient.SnapshotService("di3fs") + c.SnImageStorePath = filepath.Join(c.SnRootPath, "images") + + return c, nil +} diff --git a/pkg/snapshotter/consts.go b/pkg/snapshotter/consts.go new file mode 100644 index 0000000..a8105b9 --- /dev/null +++ b/pkg/snapshotter/consts.go @@ -0,0 +1,53 @@ +package snapshotter + +import ( + "context" + "fmt" + + "github.com/containerd/containerd/log" + "github.com/containerd/containerd/snapshots" + "github.com/naoki9911/fuse-diff-containerd/pkg/utils" + "github.com/opencontainers/go-digest" +) + +const ImageMediaTypeManifestV2 = "application/vnd.docker.distribution.manifest.v2+json" +const ContentLabelContainerdGC = "containerd.io/gc.ref.content" +const ImageLabelPuller = "puller.containerd.io" +const SnapshotLabelRefImage = "containerd.io/snapshot/di3fs.ref.image" +const SnapshotLabelRefUncompressed = "containerd.io/snapshot/di3fs.ref.uncompressed" +const SnapshotLabelRefImagePath = "containerd.io/snapshot/di3fs.ref.imagepath" +const SnapshotLabelRefLayer = "containerd.io/snapshot/di3fs.ref.layer" +const SnapshotLabelImageName = "containerd.io/snapshot/di3fs.image.name" +const SnapshotLabelImageVersion = "containerd.io/snapshot/di3fs.image.version" +const NerverGC = "containerd.io/gc.root" +const TargetSnapshotLabel = "containerd.io/snapshot.ref" + +func CreateSnapshot(ctx context.Context, ss snapshots.Snapshotter, manifestDigest, dimgDigest *digest.Digest) error { + opts := snapshots.WithLabels(map[string]string{ + NerverGC: "hogehoge", + SnapshotLabelRefImage: manifestDigest.String(), + SnapshotLabelRefLayer: fmt.Sprintf("%d", 0), + SnapshotLabelRefUncompressed: dimgDigest.String(), + //targetSnapshotLabel: chain.Hex(), + //remoteLabel: "true", + }) + + randId := utils.GetRandomId("di3fs") + err := ss.Remove(ctx, dimgDigest.String()) + if err != nil { + log.G(ctx).Errorf("failed to remove %q : %v", dimgDigest.String(), err) + } + + _, err = ss.Prepare(ctx, randId, "", opts) + if err != nil { + log.G(ctx).WithField("opts", opts).Error("failed to prepare") + return err + } + err = ss.Commit(ctx, dimgDigest.String(), randId, opts) + if err != nil { + log.G(ctx).Errorf("failed to commit snapshot :%w", err) + return err + } + log.G(ctx).Debug("commit done") + return nil +} diff --git a/pkg/update/update.go b/pkg/update/update.go new file mode 100644 index 0000000..eda0c7c --- /dev/null +++ b/pkg/update/update.go @@ -0,0 +1,25 @@ +package update + +type Image struct { + Name string `json:"name"` + Version string `json:"version"` +} + +type DiffData struct { + ImageName string `json:"imageName"` + FileName string `json:"fileName"` + ConfigPath string `json:"configPath"` + Version string `json:"version"` + BaseVersion string `json:"baseVersion"` +} + +type UpdateDataRequest struct { + RequestImage Image `json:"requestImage"` + LocalImages []Image `json:"localImages"` +} + +type UpdateDataResponse struct { + Name string `json:"name"` + Version string `json:"version"` + BaseVersion string `json:"baseVersion"` +} diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go new file mode 100644 index 0000000..8dd0612 --- /dev/null +++ b/pkg/utils/utils.go @@ -0,0 +1,123 @@ +package utils + +import ( + "bytes" + "crypto/sha256" + "encoding/json" + "fmt" + "io" + "math/rand" + "os" + "time" + + "github.com/klauspost/compress/zstd" + "github.com/opencontainers/go-digest" +) + +func UnmarshalJsonFromFile[T any](path string) (*T, error) { + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + jsonBytes, err := io.ReadAll(f) + if err != nil { + return nil, err + } + var res T + err = json.Unmarshal(jsonBytes, &res) + if err != nil { + return nil, err + } + + return &res, nil +} + +func UnmarshalJsonFromReader[T any](r io.Reader) (*T, error) { + jsonBytes, err := io.ReadAll(r) + if err != nil { + return nil, err + } + var res T + err = json.Unmarshal(jsonBytes, &res) + if err != nil { + return nil, err + } + + return &res, nil +} + +func UnmarshalJsonFromCompressed[T any](b []byte) (*T, error) { + buf := bytes.NewBuffer(b) + reader, err := zstd.NewReader(buf) + if err != nil { + return nil, err + } + defer reader.Close() + + jsonBytes, err := io.ReadAll(reader) + if err != nil { + return nil, err + } + + var res T + err = json.Unmarshal(jsonBytes, &res) + if err != nil { + return nil, err + } + + return &res, nil +} + +func DecompressWithZstd(b []byte) ([]byte, error) { + buf := bytes.NewBuffer(b) + reader, err := zstd.NewReader(buf) + if err != nil { + return nil, err + } + defer reader.Close() + + return io.ReadAll(reader) +} + +func GetFileSizeAndDigest(path string) (int64, *digest.Digest, error) { + file, err := os.Open(path) + if err != nil { + return 0, nil, err + } + defer file.Close() + h := sha256.New() + size, err := io.Copy(h, file) + if err != nil { + return 0, nil, err + } + d := digest.Digest("sha256:" + fmt.Sprintf("%x", h.Sum(nil))) + return size, &d, nil +} + +func GetSizeAndDigest(b []byte) (int64, *digest.Digest, error) { + h := sha256.New() + size, err := h.Write(b) + if err != nil { + return 0, nil, err + } + d := digest.Digest("sha256:" + fmt.Sprintf("%x", h.Sum(nil))) + return int64(size), &d, nil +} + +var ( + letters = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") +) + +func randSequence(n int) string { + rand.Seed(time.Now().UnixNano()) + b := make([]byte, n) + for i := range b { + b[i] = letters[rand.Intn(len(letters))] + } + return string(b) +} + +func GetRandomId(prefix string) string { + return fmt.Sprintf("%s-%s", prefix, randSequence(10)) +} diff --git a/push_nginx.sh b/push_nginx.sh new file mode 100755 index 0000000..6dab6f3 --- /dev/null +++ b/push_nginx.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +set -eux + +IMAGE_NAME="d4c-nginx" +IMAGE_LOWER="1.23.1" +IMAGE_MIDDLE="1.23.2" +IMAGE_UPPER="1.23.3" + +SERVER_HOST="localhost:8081" +IMAGE_PATH="./images" + +curl -XDELETE http://$SERVER_HOST/diffData/cleanup + +curl -XPOST http://$SERVER_HOST/diffData/add \ + -d @- < di3fs.csv +python ../bench-agg-patch.py patch_log.csv > patch.csv +python ../bench-agg-merge.py merge_log.csv > merge.csv +python ../bench-agg-diff.py diff_binary_log.csv > diff_binary.csv +python ../bench-agg-diff.py diff_file_log.csv > diff_file.csv diff --git a/tests/bench_impl.sh b/tests/bench_impl.sh new file mode 100755 index 0000000..ff6c3ce --- /dev/null +++ b/tests/bench_impl.sh @@ -0,0 +1,121 @@ +#!/bin/bash + +set -eu + + +source ./version.sh +RUN_NUM=$1 + +mkdir -p /tmp/fuse + +function err() { + fusermount3 -u /tmp/fuse + exit 1 +} + +trap err ERR + + +rm -f diff patch pack fuse-diff merge +go build ../../cmd/diff +go build ../../cmd/patch +go build ../../cmd/pack +go build ../../cmd/fuse-diff +go build ../../cmd/merge + +for ((i=0; i < ${#IMAGE_VERSIONS[@]}; i++));do + IMAGE=${IMAGE_VERSIONS[i]} + echo "Creating base image for $IMAGE" + ./diff "" $IMAGE $IMAGE-base $IMAGE-base.json binary-diff + ./pack $IMAGE-base $IMAGE-base.json "" $IMAGE-base.dimg + ./patch dimg "" $IMAGE-base-patched $IMAGE-base.dimg + diff -r $IMAGE $IMAGE-base-patched --no-dereference +done + + +for ((i=0; i < $(expr ${#IMAGE_VERSIONS[@]} - 1); i++));do + LOWER=${IMAGE_VERSIONS[i]} + UPPER=${IMAGE_VERSIONS[$(expr $i + 1)]} + DIFF_NAME=$LOWER-$UPPER + + # generating diff data with binary-diff + for ((j=0; j < $RUN_NUM; j++));do + NOW_COUNT=$(expr $j + 1) + echo "Benchmark diff $DIFF_NAME binary-diff ($NOW_COUNT/$RUN_NUM)" + ./diff $LOWER $UPPER diff_$DIFF_NAME diff_$DIFF_NAME.json binary-diff benchmark + done + + # packing diff data + ./pack diff_$DIFF_NAME diff_$DIFF_NAME.json $LOWER-base.dimg diff_$DIFF_NAME.dimg + ls -l diff_$DIFF_NAME.dimg + + # patching diff data + for ((j=0; j < $RUN_NUM; j++));do + NOW_COUNT=$(expr $j + 1) + echo "Benchmark patch $DIFF_NAME binary-diff ($NOW_COUNT/$RUN_NUM)" + ./patch dimg $LOWER $UPPER-patched diff_$DIFF_NAME.dimg benchmark + done + diff -r $UPPER $UPPER-patched --no-dereference + + # mount with di3fs + for ((j=0; j < $RUN_NUM; j++));do + NOW_COUNT=$(expr $j + 1) + echo "Benchmark di3fs $DIFF_NAME binary-diff ($NOW_COUNT/$RUN_NUM)" + ./fuse-diff --basedir=./$LOWER-base.dimg --patchdir=./diff_$DIFF_NAME.dimg --mode=dimg --benchmark=true /tmp/fuse >/dev/null 2>&1 & + sleep 1 + if [ $j -eq 0 ]; then + diff -r $UPPER /tmp/fuse --no-dereference + fi + fusermount3 -u /tmp/fuse + done + + # generating diff data with file-dff + for ((j=0; j < $RUN_NUM; j++));do + NOW_COUNT=$(expr $j + 1) + echo "Benchmark diff $DIFF_NAME file-diff ($NOW_COUNT/$RUN_NUM)" + ./diff $LOWER $UPPER diff_file_$DIFF_NAME diff_file_$DIFF_NAME.json file-diff benchmark + done + + # packing diff data and test it + echo "Testing packed $DIFF_NAME file-diff" + ./pack diff_file_$DIFF_NAME diff_file_$DIFF_NAME.json $LOWER-base.dimg diff_file_$DIFF_NAME.dimg + ls -l diff_file_$DIFF_NAME.dimg + ./patch dimg $LOWER $UPPER-patched diff_file_$DIFF_NAME.dimg + diff -r $UPPER $UPPER-patched --no-dereference +done + +MERGE_LOWER=$IMAGE_LOWER-$IMAGE_MIDDLE +MERGE_UPPER=$IMAGE_MIDDLE-$IMAGE_UPPER +MERGED=$IMAGE_LOWER-$IMAGE_UPPER +for ((j=0; j < $RUN_NUM; j++));do + NOW_COUNT=$(expr $j + 1) + echo "Benchmark merge $MERGE_LOWER and $MERGE_UPPER to $MERGED ($NOW_COUNT/$RUN_NUM)" + ./merge dimg diff_$MERGE_LOWER.dimg diff_$MERGE_UPPER.dimg diff_merged_$MERGED.dimg benchmark +done + +echo "Testing merged $MERGED" +./patch dimg $IMAGE_LOWER $IMAGE_UPPER-merged diff_merged_$MERGED.dimg +diff -r $IMAGE_UPPER $IMAGE_UPPER-merged --no-dereference +ls -l diff_merged_$MERGED.dimg +./fuse-diff --basedir=./$IMAGE_LOWER-base.dimg --patchdir=./diff_merged_$MERGED.dimg --mode=dimg /tmp/fuse >/dev/null 2>&1 & +sleep 1 +diff -r $IMAGE_UPPER /tmp/fuse --no-dereference +fusermount3 -u /tmp/fuse + +for ((j=0; j < $RUN_NUM; j++));do + NOW_COUNT=$(expr $j + 1) + echo "Benchmark regen-diff $MERGED binary-diff ($NOW_COUNT/$RUN_NUM)" + ./diff $IMAGE_LOWER $IMAGE_UPPER diff_$MERGED diff_$MERGED.json binary-diff benchmark +done +./pack diff_$MERGED diff_$MERGED.json $IMAGE_LOWER-base.dimg diff_$MERGED.dimg +ls -l diff_$MERGED.dimg + +for ((j=0; j < $RUN_NUM; j++));do + NOW_COUNT=$(expr $j + 1) + echo "Benchmark regen-diff $MERGED file-diff ($NOW_COUNT/$RUN_NUM)" + ./diff $IMAGE_LOWER $IMAGE_UPPER diff_file_$MERGED diff_file_$MERGED.json file-diff benchmark +done +./pack diff_file_$MERGED diff_file_$MERGED.json $IMAGE_LOWER-base.dimg diff_file_$MERGED.dimg +ls -l diff_file_$MERGED.dimg + +echo "Benchmark done" diff --git a/tests/bench_patch.sh b/tests/bench_patch.sh new file mode 100755 index 0000000..d361920 --- /dev/null +++ b/tests/bench_patch.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +set -eu +if [ $EUID -ne 0 ]; then + echo "root previlige required" + exit 1 +fi + +RESULT_DIR=benchmark_`date +%Y-%m-%d-%H%M` +mkdir -p $RESULT_DIR + +RUN_NUM=1 +PATH=$PATH:"/usr/local/go/bin" + +IMAGES=("apach-root" "mysql-root" "nginx-root" "postgres-root" "redis-root") +for IMAGE in "${IMAGES[@]}" +do + echo "Benchmarking $IMAGE" + cd $IMAGE + rm -f benchmark.log + ln -sf ../bench_patch_impl.sh ./bench_patch.sh + ./bench_patch.sh $RUN_NUM + cp benchmark.log ../$RESULT_DIR/$IMAGE-benchmark-patch.log + cd ../ +done + +cd $RESULT_DIR +python ../bench-log.py benchmark-patch +python ../bench-agg-di3fs.py di3fs_log.csv > di3fs.csv +python ../bench-agg-patch.py patch_log.csv > patch.csv diff --git a/tests/bench_patch_impl.sh b/tests/bench_patch_impl.sh new file mode 100755 index 0000000..43323ae --- /dev/null +++ b/tests/bench_patch_impl.sh @@ -0,0 +1,52 @@ +#!/bin/bash + +fusermount3 -u /tmp/fuse + +set -eu + +source ./version.sh +RUN_NUM=$1 + +mkdir -p /tmp/fuse + +function err() { + fusermount3 -u /tmp/fuse + exit 1 +} + +trap err ERR + +rm -f diff patch pack fuse-diff merge +go build ../../cmd/diff +go build ../../cmd/patch +go build ../../cmd/pack +go build ../../cmd/fuse-diff +go build ../../cmd/merge + +for ((i=0; i < $(expr ${#IMAGE_VERSIONS[@]} - 1); i++));do + LOWER=${IMAGE_VERSIONS[i]} + UPPER=${IMAGE_VERSIONS[$(expr $i + 1)]} + DIFF_NAME=$LOWER-$UPPER + + # patching diff data + for ((j=0; j < $RUN_NUM; j++));do + NOW_COUNT=$(expr $j + 1) + echo "Benchmark patch $DIFF_NAME binary-diff ($NOW_COUNT/$RUN_NUM)" + ./patch dimg $LOWER $UPPER-patched diff_$DIFF_NAME.dimg benchmark + done + diff -r $UPPER $UPPER-patched --no-dereference + + # mount with di3fs + for ((j=0; j < $RUN_NUM; j++));do + NOW_COUNT=$(expr $j + 1) + echo "Benchmark di3fs $DIFF_NAME binary-diff ($NOW_COUNT/$RUN_NUM)" + ./fuse-diff --basedir=./$LOWER-base.dimg --patchdir=./diff_$DIFF_NAME.dimg --mode=dimg --benchmark=true /tmp/fuse >/dev/null 2>&1 & + sleep 5 + if [ $j -eq 0 ]; then + diff -r $UPPER /tmp/fuse --no-dereference + fi + fusermount3 -u /tmp/fuse + sleep 2 + done +done + diff --git a/tests/bench_pull.sh b/tests/bench_pull.sh new file mode 100755 index 0000000..921472d --- /dev/null +++ b/tests/bench_pull.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +set -eu +#!/bin/bash + +set -eu + +if [ $EUID -ne 0 ]; then + echo "root previlige required" + exit 1 +fi + +SERVER_HOST="localhost:8081" +RUN_NUM=1 + +RESULT_DIR=benchmark_`date +%Y-%m-%d-%H%M` +mkdir -p $RESULT_DIR + +IMAGES=("apach-root" "mysql-root" "nginx-root" "postgres-root" "redis-root") +for IMAGE in "${IMAGES[@]}" +do + cd $IMAGE + sudo rm -f benchmark.log + ln -sf ../push_impl.sh ./push.sh + ln -sf ../bench_pull_impl.sh ./bench_pull.sh + + ./push.sh $SERVER_HOST `pwd` + ./bench_pull.sh $SERVER_HOST $RUN_NUM + cp benchmark.log ../$RESULT_DIR/$IMAGE-benchmark-pull.log + cd ../ +done + +cd $RESULT_DIR +python ../bench-log.py benchmark-pull +python ../bench-agg-pull.py pull_log.csv > pull.csv +python ../bench-agg-pull.py pull_download_log.csv > pull_download.csv diff --git a/tests/bench_pull_impl.sh b/tests/bench_pull_impl.sh new file mode 100755 index 0000000..9fdce56 --- /dev/null +++ b/tests/bench_pull_impl.sh @@ -0,0 +1,64 @@ +#!/bin/bash + +set -eu + +source ./version.sh +SERVER_HOST=$1 +RUN_NUM=$2 + +ctr image rm $IMAGE_NAME:$IMAGE_LOWER +ctr image rm $IMAGE_NAME:$IMAGE_MIDDLE +ctr image rm $IMAGE_NAME:$IMAGE_UPPER +ctr image rm $IMAGE_NAME-file:$IMAGE_LOWER +ctr image rm $IMAGE_NAME-file:$IMAGE_MIDDLE +ctr image rm $IMAGE_NAME-file:$IMAGE_UPPER + +go build ../../cmd/./ctr-cli + +./ctr-cli pull --image $IMAGE_NAME:$IMAGE_MIDDLE --host $SERVER_HOST +for ((j=0; j < $RUN_NUM; j++));do + NOW_COUNT=$(expr $j + 1) + echo "Benchmark pull $IMAGE_NAME:$IMAGE_UPPER ($NOW_COUNT/$RUN_NUM)" + ./ctr-cli pull --image $IMAGE_NAME:$IMAGE_UPPER --benchmark --host $SERVER_HOST + ctr image rm $IMAGE_NAME:$IMAGE_UPPER +done +ctr image rm $IMAGE_NAME:$IMAGE_MIDDLE + +./ctr-cli pull --image $IMAGE_NAME:$IMAGE_LOWER --host $SERVER_HOST +for ((j=0; j < $RUN_NUM; j++));do + NOW_COUNT=$(expr $j + 1) + echo "Benchmark pull $IMAGE_NAME:$IMAGE_MIDDLE ($NOW_COUNT/$RUN_NUM)" + ./ctr-cli pull --image $IMAGE_NAME:$IMAGE_MIDDLE --benchmark --host $SERVER_HOST + ctr image rm $IMAGE_NAME:$IMAGE_MIDDLE +done + +for ((j=0; j < $RUN_NUM; j++));do + NOW_COUNT=$(expr $j + 1) + echo "Benchmark pull $IMAGE_NAME:$IMAGE_UPPER ($NOW_COUNT/$RUN_NUM)" + ./ctr-cli pull --image $IMAGE_NAME:$IMAGE_UPPER --benchmark --host $SERVER_HOST + ctr image rm $IMAGE_NAME:$IMAGE_UPPER +done + +./ctr-cli pull --image $IMAGE_NAME-file:$IMAGE_MIDDLE --host $SERVER_HOST +for ((j=0; j < $RUN_NUM; j++));do + NOW_COUNT=$(expr $j + 1) + echo "Benchmark pull $IMAGE_NAME-file:$IMAGE_UPPER ($NOW_COUNT/$RUN_NUM)" + ./ctr-cli pull --image $IMAGE_NAME-file:$IMAGE_UPPER --benchmark --host $SERVER_HOST + ctr image rm $IMAGE_NAME-file:$IMAGE_UPPER +done +ctr image rm $IMAGE_NAME-file:$IMAGE_MIDDLE + +./ctr-cli pull --image $IMAGE_NAME-file:$IMAGE_LOWER --host $SERVER_HOST +for ((j=0; j < $RUN_NUM; j++));do + NOW_COUNT=$(expr $j + 1) + echo "Benchmark pull $IMAGE_NAME-file:$IMAGE_MIDDLE ($NOW_COUNT/$RUN_NUM)" + ./ctr-cli pull --image $IMAGE_NAME-file:$IMAGE_MIDDLE --benchmark --host $SERVER_HOST + ctr image rm $IMAGE_NAME-file:$IMAGE_MIDDLE +done + +for ((j=0; j < $RUN_NUM; j++));do + NOW_COUNT=$(expr $j + 1) + echo "Benchmark pull $IMAGE_NAME-file:$IMAGE_UPPER ($NOW_COUNT/$RUN_NUM)" + ./ctr-cli pull --image $IMAGE_NAME-file:$IMAGE_UPPER --benchmark --host $SERVER_HOST + ctr image rm $IMAGE_NAME-file:$IMAGE_UPPER +done \ No newline at end of file