From 520357d9fc0353ffd08075da480fa3b99eea8ca7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Grill?= Date: Sat, 20 Jul 2024 06:02:45 +0200 Subject: [PATCH] working s3 integration --- .github/workflows/go.yml | 7 + cmd/potatos3/basepathfs.go | 255 ++++++++++++++++++ cmd/potatos3/main.go | 62 +++++ example/main.go | 4 +- filesystem.go => filesystem/filesystem.go | 24 +- .../filesystem_test.go | 8 +- go.mod | 8 +- go.sum | 42 +++ 8 files changed, 398 insertions(+), 12 deletions(-) create mode 100644 cmd/potatos3/basepathfs.go create mode 100644 cmd/potatos3/main.go rename filesystem.go => filesystem/filesystem.go (94%) rename filesystem_test.go => filesystem/filesystem_test.go (93%) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 67afd78..f03312f 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -26,3 +26,10 @@ jobs: - name: Test run: go test -v ./... + + - name: Build + run: go build -o potatos3.exe ./cmd/potatos3 + - uses: actions/upload-artifact@v4 + with: + name: potatos3.exe + path: potatos3.exe diff --git a/cmd/potatos3/basepathfs.go b/cmd/potatos3/basepathfs.go new file mode 100644 index 0000000..4cdbefd --- /dev/null +++ b/cmd/potatos3/basepathfs.go @@ -0,0 +1,255 @@ +package main + +import ( + "io/fs" + "os" + "path" + "runtime" + "strings" + "time" + + "github.com/spf13/afero" +) + +var ( + _ afero.Lstater = (*BasePathFs)(nil) + _ fs.ReadDirFile = (*BasePathFile)(nil) +) + +// The BasePathFs restricts all operations to a given path within an Fs. +// The given file name to the operations on this Fs will be prepended with +// the base path before calling the base Fs. +// Any file name (after filepath.Clean()) outside this base path will be +// treated as non existing file. +// +// Note that it does not clean the error messages on return, so you may +// reveal the real path on errors. +type BasePathFs struct { + source afero.Fs + path string +} + +type BasePathFile struct { + afero.File + path string +} + +type readDirFile struct { + afero.File +} + +var _ fs.ReadDirFile = readDirFile{} + +func (r readDirFile) ReadDir(n int) ([]fs.DirEntry, error) { + items, err := r.File.Readdir(n) + if err != nil { + return nil, err + } + + ret := make([]fs.DirEntry, len(items)) + for i := range items { + ret[i] = FileInfoDirEntry{FileInfo: items[i]} + } + + return ret, nil +} + +// FileInfoDirEntry provides an adapter from os.FileInfo to fs.DirEntry +type FileInfoDirEntry struct { + fs.FileInfo +} + +var _ fs.DirEntry = FileInfoDirEntry{} + +func (d FileInfoDirEntry) Type() fs.FileMode { return d.FileInfo.Mode().Type() } + +func (d FileInfoDirEntry) Info() (fs.FileInfo, error) { return d.FileInfo, nil } + +func (f *BasePathFile) Name() string { + sourcename := f.File.Name() + return strings.TrimPrefix(sourcename, path.Clean(f.path)) +} + +func (f *BasePathFile) ReadDir(n int) ([]fs.DirEntry, error) { + if rdf, ok := f.File.(fs.ReadDirFile); ok { + return rdf.ReadDir(n) + } + return readDirFile{f.File}.ReadDir(n) +} + +func NewBasePathFs(source afero.Fs, path string) afero.Fs { + return &BasePathFs{source: source, path: path} +} + +// on a file outside the base path it returns the given file name and an error, +// else the given file with the base path prepended +func (b *BasePathFs) RealPath(name string) (rpath string, err error) { + if err := validateBasePathName(name); err != nil { + return name, err + } + + bpath := path.Clean(b.path) + rpath = path.Clean(path.Join(bpath, name)) + if !strings.HasPrefix(rpath, bpath) { + return name, os.ErrNotExist + } + + return rpath, nil +} + +func validateBasePathName(name string) error { + if runtime.GOOS != "windows" { + // Not much to do here; + // the virtual file paths all look absolute on *nix. + return nil + } + + // On Windows a common mistake would be to provide an absolute OS path + // We could strip out the base part, but that would not be very portable. + if path.IsAbs(name) { + return os.ErrNotExist + } + + return nil +} + +func (b *BasePathFs) Chtimes(name string, atime, mtime time.Time) (err error) { + if name, err = b.RealPath(name); err != nil { + return &os.PathError{Op: "chtimes", Path: name, Err: err} + } + return b.source.Chtimes(name, atime, mtime) +} + +func (b *BasePathFs) Chmod(name string, mode os.FileMode) (err error) { + if name, err = b.RealPath(name); err != nil { + return &os.PathError{Op: "chmod", Path: name, Err: err} + } + return b.source.Chmod(name, mode) +} + +func (b *BasePathFs) Chown(name string, uid, gid int) (err error) { + if name, err = b.RealPath(name); err != nil { + return &os.PathError{Op: "chown", Path: name, Err: err} + } + return b.source.Chown(name, uid, gid) +} + +func (b *BasePathFs) Name() string { + return "BasePathFs" +} + +func (b *BasePathFs) Stat(name string) (fi os.FileInfo, err error) { + if name, err = b.RealPath(name); err != nil { + return nil, &os.PathError{Op: "stat", Path: name, Err: err} + } + return b.source.Stat(name) +} + +func (b *BasePathFs) Rename(oldname, newname string) (err error) { + if oldname, err = b.RealPath(oldname); err != nil { + return &os.PathError{Op: "rename", Path: oldname, Err: err} + } + if newname, err = b.RealPath(newname); err != nil { + return &os.PathError{Op: "rename", Path: newname, Err: err} + } + return b.source.Rename(oldname, newname) +} + +func (b *BasePathFs) RemoveAll(name string) (err error) { + if name, err = b.RealPath(name); err != nil { + return &os.PathError{Op: "remove_all", Path: name, Err: err} + } + return b.source.RemoveAll(name) +} + +func (b *BasePathFs) Remove(name string) (err error) { + if name, err = b.RealPath(name); err != nil { + return &os.PathError{Op: "remove", Path: name, Err: err} + } + return b.source.Remove(name) +} + +func (b *BasePathFs) OpenFile(name string, flag int, mode os.FileMode) (f afero.File, err error) { + if name, err = b.RealPath(name); err != nil { + return nil, &os.PathError{Op: "openfile", Path: name, Err: err} + } + sourcef, err := b.source.OpenFile(name, flag, mode) + if err != nil { + return nil, err + } + return &BasePathFile{sourcef, b.path}, nil +} + +func (b *BasePathFs) Open(name string) (f afero.File, err error) { + if name, err = b.RealPath(name); err != nil { + return nil, &os.PathError{Op: "open", Path: name, Err: err} + } + sourcef, err := b.source.Open(name) + if err != nil { + return nil, err + } + return &BasePathFile{File: sourcef, path: b.path}, nil +} + +func (b *BasePathFs) Mkdir(name string, mode os.FileMode) (err error) { + if name, err = b.RealPath(name); err != nil { + return &os.PathError{Op: "mkdir", Path: name, Err: err} + } + return b.source.Mkdir(name, mode) +} + +func (b *BasePathFs) MkdirAll(name string, mode os.FileMode) (err error) { + if name, err = b.RealPath(name); err != nil { + return &os.PathError{Op: "mkdir", Path: name, Err: err} + } + return b.source.MkdirAll(name, mode) +} + +func (b *BasePathFs) Create(name string) (f afero.File, err error) { + if name, err = b.RealPath(name); err != nil { + return nil, &os.PathError{Op: "create", Path: name, Err: err} + } + sourcef, err := b.source.Create(name) + if err != nil { + return nil, err + } + return &BasePathFile{File: sourcef, path: b.path}, nil +} + +func (b *BasePathFs) LstatIfPossible(name string) (os.FileInfo, bool, error) { + name, err := b.RealPath(name) + if err != nil { + return nil, false, &os.PathError{Op: "lstat", Path: name, Err: err} + } + if lstater, ok := b.source.(afero.Lstater); ok { + return lstater.LstatIfPossible(name) + } + fi, err := b.source.Stat(name) + return fi, false, err +} + +func (b *BasePathFs) SymlinkIfPossible(oldname, newname string) error { + oldname, err := b.RealPath(oldname) + if err != nil { + return &os.LinkError{Op: "symlink", Old: oldname, New: newname, Err: err} + } + newname, err = b.RealPath(newname) + if err != nil { + return &os.LinkError{Op: "symlink", Old: oldname, New: newname, Err: err} + } + if linker, ok := b.source.(afero.Linker); ok { + return linker.SymlinkIfPossible(oldname, newname) + } + return &os.LinkError{Op: "symlink", Old: oldname, New: newname, Err: afero.ErrNoSymlink} +} + +func (b *BasePathFs) ReadlinkIfPossible(name string) (string, error) { + name, err := b.RealPath(name) + if err != nil { + return "", &os.PathError{Op: "readlink", Path: name, Err: err} + } + if reader, ok := b.source.(afero.LinkReader); ok { + return reader.ReadlinkIfPossible(name) + } + return "", &os.PathError{Op: "readlink", Path: name, Err: afero.ErrNoReadlink} +} diff --git a/cmd/potatos3/main.go b/cmd/potatos3/main.go new file mode 100644 index 0000000..bac1b1f --- /dev/null +++ b/cmd/potatos3/main.go @@ -0,0 +1,62 @@ +package main + +import ( + "flag" + "log" + "os" + "os/signal" + "syscall" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/balazsgrill/projfero/filesystem" + s3 "github.com/fclairamb/afero-s3" +) + +func main() { + endpoint := flag.String("endpoint", "", "S3 endpoint") + accessKeyID := flag.String("keyid", "", "Access Key ID") + secretAccessKey := flag.String("secred", "", "Access Key Secret") + useSSL := flag.Bool("useSSL", false, "Use SSL encryption for S3 connection") + region := flag.String("region", "", "Region") + bucket := flag.String("bucket", "", "Bucket") + localpath := flag.String("localpath", "", "Local folder") + flag.Parse() + + sess, _ := session.NewSession(&aws.Config{ + Region: aws.String(*region), + Endpoint: aws.String(*endpoint), + DisableSSL: aws.Bool(!*useSSL), + S3ForcePathStyle: aws.Bool(true), + Credentials: credentials.NewStaticCredentials(*accessKeyID, *secretAccessKey, ""), + }) + + // Initialize the file system + fs := s3.NewFs(*bucket, sess) + fs.MkdirAll("root", 0777) + rootfs := NewBasePathFs(fs, "root") + + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + closer, err := filesystem.StartProjecting(*localpath, rootfs) + if err != nil { + log.Panic(err) + } + + t := time.NewTicker(30 * time.Second) + go func() { + for range t.C { + err = closer.PerformSynchronization() + if err != nil { + log.Panic(err) + } + } + }() + + <-c + t.Stop() + closer.Close() + os.Exit(1) +} diff --git a/example/main.go b/example/main.go index 5af5e1b..aa2d809 100644 --- a/example/main.go +++ b/example/main.go @@ -6,7 +6,7 @@ import ( "os/signal" "syscall" - "github.com/balazsgrill/projfero" + "github.com/balazsgrill/projfero/filesystem" "github.com/spf13/afero" ) @@ -14,7 +14,7 @@ func main() { fs := afero.NewBasePathFs(afero.NewOsFs(), "C:\\work\\vfsbase") c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt, syscall.SIGTERM) - closer, err := projfero.StartProjecting("C:\\work\\vfs", fs) + closer, err := filesystem.StartProjecting("C:\\work\\vfs", fs) if err != nil { log.Panic(err) } diff --git a/filesystem.go b/filesystem/filesystem.go similarity index 94% rename from filesystem.go rename to filesystem/filesystem.go index 189f4ae..84cb9b9 100644 --- a/filesystem.go +++ b/filesystem/filesystem.go @@ -1,4 +1,4 @@ -package projfero +package filesystem import ( "encoding/binary" @@ -134,6 +134,7 @@ func (instance *VirtualizationInstance) PerformSynchronization() error { func (instance *VirtualizationInstance) syncRemoteToLocal() error { return afero.Walk(instance.fs, "", func(path string, remoteinfo fs.FileInfo, err error) error { + log.Printf("Syncing remote file '%s'", path) if os.IsNotExist(err) { return nil } @@ -198,6 +199,7 @@ func (instance *VirtualizationInstance) localHash(remotepath string) ([]byte, er func (instance *VirtualizationInstance) syncLocalToRemote() error { return filepath.Walk(instance.rootPath, func(localpath string, localinfo fs.FileInfo, err error) error { + log.Printf("Syncing local file '%s'", localpath) if os.IsNotExist(err) { return nil } @@ -379,7 +381,7 @@ func (instance *VirtualizationInstance) streamLocalToRemote(filename string) err } defer file.Close() data := make([]byte, 1024*1024) - targetfile, err := instance.fs.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0x666) + targetfile, err := instance.fs.OpenFile(filename, os.O_WRONLY|os.O_CREATE, 0x666) if err != nil { return err } @@ -464,6 +466,7 @@ func (instance *VirtualizationInstance) GetDirectoryEnumeration(callbackData *pr } for _, file := range files[session.sentcount:] { + session.sentcount += 1 fname := filepath.Base(file.Name()) if strings.HasPrefix(fname, ".") { continue @@ -477,7 +480,6 @@ func (instance *VirtualizationInstance) GetDirectoryEnumeration(callbackData *pr } dirEntry := toBasicInfo(file) projfs.PrjFillDirEntryBuffer(file.Name(), &dirEntry, dirEntryBufferHandle) - session.sentcount += 1 } log.Printf("Sent %d entries", session.sentcount) return 0 @@ -531,7 +533,7 @@ func (instance *VirtualizationInstance) GetPlaceholderInfo(callbackData *projfs. func (instance *VirtualizationInstance) GetFileData(callbackData *projfs.PRJ_CALLBACK_DATA, byteOffset uint64, length uint32) uintptr { filename := instance.path_localToRemote(callbackData.GetFilePathName()) - log.Printf("GetFileData %s", filename) + log.Printf("GetFileData %s[%d]@%d", filename, length, byteOffset) file, err := instance.fs.Open(filename) if err != nil { log.Printf("Error opening file %s: %s", filename, err) @@ -539,7 +541,19 @@ func (instance *VirtualizationInstance) GetFileData(callbackData *projfs.PRJ_CAL } defer file.Close() buffer := make([]byte, length) - _, err = file.ReadAt(buffer, int64(byteOffset)) + + var n int + var count uint32 + for count < length { + n, err = file.ReadAt(buffer[count:], int64(byteOffset+uint64(count))) + count += uint32(n) + if err == io.EOF { + err = nil + break + } + } + + log.Printf("Read %d bytes", count) if err != nil { log.Printf("Error reading file %s: %s", filename, err) return uintptr(syscall.EIO) diff --git a/filesystem_test.go b/filesystem/filesystem_test.go similarity index 93% rename from filesystem_test.go rename to filesystem/filesystem_test.go index 32f7986..60791d6 100644 --- a/filesystem_test.go +++ b/filesystem/filesystem_test.go @@ -1,4 +1,4 @@ -package projfero_test +package filesystem_test import ( "bytes" @@ -10,7 +10,7 @@ import ( "testing" "time" - "github.com/balazsgrill/projfero" + "github.com/balazsgrill/projfero/filesystem" "github.com/spf13/afero" ) @@ -18,7 +18,7 @@ type testInstance struct { t *testing.T location string fs afero.Fs - closer projfero.Virtualization + closer filesystem.Virtualization closechan chan bool } @@ -38,7 +38,7 @@ func (i *testInstance) start() { started := make(chan bool) var err error go func() { - i.closer, err = projfero.StartProjecting(i.location, i.fs) + i.closer, err = filesystem.StartProjecting(i.location, i.fs) started <- true <-i.closechan i.closer.Close() diff --git a/go.mod b/go.mod index a6f47e2..b41b852 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,17 @@ module github.com/balazsgrill/projfero go 1.21.0 require ( + github.com/aws/aws-sdk-go v1.54.20 github.com/balazsgrill/projfs v0.0.2 + github.com/fclairamb/afero-s3 v0.3.1 github.com/google/uuid v1.6.0 github.com/spf13/afero v1.11.0 ) -require golang.org/x/text v0.14.0 // indirect +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + golang.org/x/text v0.14.0 // indirect +) //replace github.com/balazsgrill/projfs => ../projfs diff --git a/go.sum b/go.sum index 6572fb7..83f7b6b 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,50 @@ +github.com/aws/aws-sdk-go v1.42.9/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= +github.com/aws/aws-sdk-go v1.54.20 h1:FZ2UcXya7bUkvkpf7TaPmiL7EubK0go1nlXGLRwEsoo= +github.com/aws/aws-sdk-go v1.54.20/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/balazsgrill/projfs v0.0.2 h1:HyhDgJT1sgIdHd1j1OHr/YYeDauQINzd+irerpaBlUM= github.com/balazsgrill/projfs v0.0.2/go.mod h1:zxk3JTKjlt3AKYJ98gJQEpJ6ZQ+qtkyuqsgfMLb5mW4= +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/fclairamb/afero-s3 v0.3.1 h1:JLxcl42wseOjKAdXfVkz7GoeyNRrvxkZ1jBshuDSDgA= +github.com/fclairamb/afero-s3 v0.3.1/go.mod h1:VZ/bvRox6Bq3U+vTGa12uyDu+5UJb40M7tpIXlByKkc= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= +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/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=