From a6b01aad01eaca539c47a6796b53d9e08d462c78 Mon Sep 17 00:00:00 2001 From: Nathan Baulch Date: Sun, 19 Jan 2025 12:01:37 +1100 Subject: [PATCH] Integration testing with testcontainers --- testcontainers/atmoz.go | 52 ++ testcontainers/azurite.go | 52 ++ testcontainers/backend_integration_test.go | 863 +++++++++++++++++++++ testcontainers/doc.go | 5 + testcontainers/gcsserver.go | 69 ++ testcontainers/go.mod | 149 ++++ testcontainers/go.sum | 369 +++++++++ testcontainers/io_integration_test.go | 518 +++++++++++++ testcontainers/localstack.go | 51 ++ testcontainers/minio.go | 46 ++ testcontainers/vsftpd.go | 48 ++ 11 files changed, 2222 insertions(+) create mode 100644 testcontainers/atmoz.go create mode 100644 testcontainers/azurite.go create mode 100644 testcontainers/backend_integration_test.go create mode 100644 testcontainers/doc.go create mode 100644 testcontainers/gcsserver.go create mode 100644 testcontainers/go.mod create mode 100644 testcontainers/go.sum create mode 100644 testcontainers/io_integration_test.go create mode 100644 testcontainers/localstack.go create mode 100644 testcontainers/minio.go create mode 100644 testcontainers/vsftpd.go diff --git a/testcontainers/atmoz.go b/testcontainers/atmoz.go new file mode 100644 index 00000000..04e9d8f7 --- /dev/null +++ b/testcontainers/atmoz.go @@ -0,0 +1,52 @@ +package testcontainers + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" + "golang.org/x/crypto/ssh" + + "github.com/c2fo/vfs/v7/backend" + "github.com/c2fo/vfs/v7/backend/sftp" +) + +const ( + atmozPort = "22/tcp" + atmozUsername = "dummy" + atmozPassword = "dummy" +) + +func registerAtmoz(t *testing.T) string { + ctx := context.Background() + is := require.New(t) + + req := testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ + Name: "vfs-atmoz-sftp", + Image: "atmoz/sftp:alpine", + Env: map[string]string{"SFTP_USERS": fmt.Sprintf("%s:%s:::upload", atmozUsername, atmozPassword)}, + WaitingFor: wait.ForListeningPort(atmozPort), + }, + Started: true, + } + ctr, err := testcontainers.GenericContainer(ctx, req) + testcontainers.CleanupContainer(t, ctr) + is.NoError(err) + + host, err := ctr.Host(ctx) + is.NoError(err) + + port, err := ctr.MappedPort(ctx, atmozPort) + is.NoError(err) + + authority := fmt.Sprintf("sftp://%s@%s:%s/upload/", atmozUsername, host, port.Port()) + backend.Register(authority, sftp.NewFileSystem(sftp.WithOptions(sftp.Options{ + Password: vsftpdPassword, + KnownHostsCallback: ssh.InsecureIgnoreHostKey(), //nolint:gosec + }))) + return authority +} diff --git a/testcontainers/azurite.go b/testcontainers/azurite.go new file mode 100644 index 00000000..7067bf7b --- /dev/null +++ b/testcontainers/azurite.go @@ -0,0 +1,52 @@ +package testcontainers + +import ( + "context" + "net/url" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob" + "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/modules/azure/azurite" + + "github.com/c2fo/vfs/v7/backend" + "github.com/c2fo/vfs/v7/backend/azure" +) + +func registerAzurite(t *testing.T) string { + ctx := context.Background() + is := require.New(t) + + ctr, err := azurite.Run(ctx, "mcr.microsoft.com/azure-storage/azurite:latest", + testcontainers.WithName("vfs-azurite"), + azurite.WithEnabledServices(azurite.BlobService), + ) + testcontainers.CleanupContainer(t, ctr) + is.NoError(err) + + ep, err := ctr.BlobServiceURL(ctx) + is.NoError(err) + + cred, err := azblob.NewSharedKeyCredential(azurite.AccountName, azurite.AccountKey) + is.NoError(err) + + u, err := url.JoinPath(ep, azurite.AccountName) + is.NoError(err) + + cli, err := azblob.NewClientWithSharedKeyCredential(u, cred, nil) + is.NoError(err) + + _, err = cli.CreateContainer(ctx, "azurite", nil) + is.NoError(err) + + c, err := azure.NewClient(&azure.Options{ + ServiceURL: u, + AccountName: azurite.AccountName, + AccountKey: azurite.AccountKey, + }) + is.NoError(err) + + backend.Register("https://azurite/", azure.NewFileSystem(azure.WithClient(c))) + return "https://azurite/" +} diff --git a/testcontainers/backend_integration_test.go b/testcontainers/backend_integration_test.go new file mode 100644 index 00000000..c1bc0907 --- /dev/null +++ b/testcontainers/backend_integration_test.go @@ -0,0 +1,863 @@ +package testcontainers + +import ( + "fmt" + "io" + "net/url" + "os" + "path" + "path/filepath" + "regexp" + "strconv" + "strings" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/suite" + + "github.com/c2fo/vfs/v7" + "github.com/c2fo/vfs/v7/utils" + "github.com/c2fo/vfs/v7/vfssimple" +) + +type vfsTestSuite struct { + suite.Suite + testLocations map[string]vfs.Location +} + +func buildExpectedURI(fs vfs.FileSystem, authorityStr, p string) string { + return fmt.Sprintf("%s://%s%s", fs.Scheme(), authorityStr, p) +} + +func (s *vfsTestSuite) SetupSuite() { + registers := []func(*testing.T) string{ + registerMem, + registerOS, + registerAtmoz, + registerAzurite, + registerGCSServer, + registerLocalStack, + registerMinio, + registerVSFTPD, + } + uris := make([]string, len(registers)) + var wg sync.WaitGroup + for i := range registers { + wg.Add(1) + go func() { + uris[i] = registers[i](s.T()) + wg.Done() + }() + } + wg.Wait() + + s.testLocations = make(map[string]vfs.Location) + for _, u := range uris { + l, err := vfssimple.NewLocation(u) + s.Require().NoError(err) + s.testLocations[l.FileSystem().Scheme()] = l + } +} + +func registerMem(*testing.T) string { + return "mem://test/" +} + +func registerOS(t *testing.T) string { + return fmt.Sprintf("file://%s/", filepath.ToSlash(t.TempDir())) +} + +// Test Scheme +func (s *vfsTestSuite) TestScheme() { + for scheme, location := range s.testLocations { + s.Run(scheme, func() { + fmt.Printf("************** TESTING scheme: %s **************\n", scheme) + s.FileSystem(location) + s.Location(location) + s.File(location) + }) + } +} + +// Test FileSystem +func (s *vfsTestSuite) FileSystem(baseLoc vfs.Location) { + fmt.Println("****** testing vfs.FileSystem ******") + + // setup FileSystem + fs := baseLoc.FileSystem() + // NewFile initializes a File on the specified authority string at path 'absFilePath'. + // + // * Accepts authority and an absolute file path. + // * Upon success, a vfs.File, representing the file's new path (location path + file relative path), will be returned. + // * On error, nil is returned for the file. + // * Note that not all file systems will have an "authority" and will therefore be "": + // file:///path/to/file has an authority of "" and name /path/to/file + // whereas + // s3://mybucket/path/to/file has an authority of "mybucket and name /path/to/file + // results in /tmp/dir1/newerdir/file.txt for the final vfs.File path. + // * The file may or may not already exist. + filepaths := map[string]bool{ + "/path/to/file.txt": true, + "/path/./to/file.txt": true, + "/path/../to/file.txt": true, + "path/to/file.txt": false, + "./path/to/file.txt": false, + "../path/to/": false, + "/path/to/": false, + "": false, + } + for name, validates := range filepaths { + file, err := fs.NewFile(baseLoc.Authority().String(), name) + if validates { + s.Require().NoError(err, "there should be no error") + expected := buildExpectedURI(fs, baseLoc.Authority().String(), path.Clean(name)) + s.Equal(expected, file.URI(), "uri's should match") + } else { + s.Require().Error(err, "should have validation error for scheme[%s] and name[%s]", fs.Scheme(), name) + } + } + + // NewLocation initializes a Location on the specified authority with the given path. + // + // * Accepts authority and an absolute location path. + // * The file may or may not already exist. Note that on key-store file systems like S3 or GCS, paths never truly exist. + // * On error, nil is returned for the location. + // + // See NewFile for note on authority. + locpaths := map[string]bool{ + "/path/to/": true, + "/path/./to/": true, + "/path/../to/": true, + "path/to/": false, + "./path/to/": false, + "../path/to/": false, + "/path/to/file.txt": false, + "": false, + } + for name, validates := range locpaths { + loc, err := fs.NewLocation(baseLoc.Authority().String(), name) + if validates { + s.Require().NoError(err, "there should be no error") + expected := buildExpectedURI(fs, baseLoc.Authority().String(), utils.EnsureTrailingSlash(path.Clean(name))) + s.Equal(expected, loc.URI(), "uri's should match") + } else { + s.Require().Error(err, "should have validation error for scheme[%s] and name[%s]", fs.Scheme(), name) + } + } +} + +// Test Location +func (s *vfsTestSuite) Location(baseLoc vfs.Location) { + fmt.Println("****** testing vfs.Location ******") + + srcLoc, err := baseLoc.NewLocation("locTestSrc/") + s.Require().NoError(err, "there should be no error") + defer func() { + // clean up srcLoc after test for OS + if srcLoc.FileSystem().Scheme() == "file" { + exists, err := srcLoc.Exists() + s.Require().NoError(err) + if exists { + s.Require().NoError(os.RemoveAll(srcLoc.Path()), "failed to clean up location test srcLoc") + } + } + }() + + // NewLocation is an initializer for a new Location relative to the existing one. + // + // Given location: + // loc := fs.NewLocation(:s3://mybucket/some/path/to/") + // calling: + // newLoc := loc.NewLocation("../../") + // would return a new vfs.Location representing: + // s3://mybucket/some/ + // + // * Accepts a relative location path. + locpaths := map[string]bool{ + "/path/to/": false, + "/path/./to/": false, + "/path/../to/": false, + "path/to/": true, + "./path/to/": true, + "../path/to/": true, + "/path/to/file.txt": false, + "": false, + } + for name, validates := range locpaths { + loc, err := srcLoc.NewLocation(name) + if validates { + s.Require().NoError(err, "there should be no error") + expected := buildExpectedURI(srcLoc.FileSystem(), baseLoc.Authority().String(), + utils.EnsureTrailingSlash(path.Clean(path.Join(srcLoc.Path(), name)))) + s.Equal(expected, loc.URI(), "uri's should match") + } else { + s.Require().Error(err, "should have validation error for scheme and name: %s : %s", srcLoc.FileSystem().Scheme(), name) + } + } + + // NewFile will instantiate a vfs.File instance at or relative to the current location's path. + // + // * Accepts a relative file path. + // * In the case of an error, nil is returned for the file. + // * Resultant File path will be the shortest path name equivalent of combining the Location path and relative path, if any. + // ie, /tmp/dir1/ as location and relFilePath "newdir/./../newerdir/file.txt" + // results in /tmp/dir1/newerdir/file.txt for the final vfs.File path. + // * Upon success, a vfs.File, representing the file's new path (location path + file relative path), will be returned. + // * The file may or may not already exist. + filepaths := map[string]bool{ + "/path/to/file.txt": false, + "/path/./to/file.txt": false, + "/path/../to/file.txt": false, + "path/to/file.txt": true, + "./path/to/file.txt": true, + "../path/to/": false, + "../path/to/file.txt": true, + "/path/to/": false, + "": false, + } + for name, validates := range filepaths { + file, err := srcLoc.NewFile(name) + if validates { + s.Require().NoError(err, "there should be no error") + expected := buildExpectedURI(srcLoc.FileSystem(), srcLoc.Authority().String(), path.Clean(path.Join(srcLoc.Path(), name))) + s.Equal(expected, file.URI(), "uri's should match") + } else { + s.Require().Error(err, "should have validation error for scheme and name: %s : +%s+", srcLoc.FileSystem().Scheme(), name) + } + } + + // ChangeDir updates the existing Location's path to the provided relative location path. + + // Given location: + // loc := fs.NewLocation("file:///some/path/to/") + // calling: + // loc.ChangeDir("../../") + // would update the current location instance to + // file:///some/. + // + // * ChangeDir accepts a relative location path. + + // setup test + cdTestLoc, err := srcLoc.NewLocation("chdirTest/") + s.Require().NoError(err) + + _, err = cdTestLoc.NewLocation("") + s.Require().Error(err, "empty string should error") + _, err = cdTestLoc.NewLocation("/home/") + s.Require().Error(err, "absolute path should error") + _, err = cdTestLoc.NewLocation("file.txt") + s.Require().Error(err, "file should error") + cdTestLoc, err = cdTestLoc.NewLocation("l1dir1/./l2dir1/../l2dir2/") + s.Require().NoError(err, "should be no error for relative path") + + // Path returns absolute location path, ie /some/path/to/. + // ==== Path() string + s.True(strings.HasSuffix(cdTestLoc.Path(), "locTestSrc/chdirTest/l1dir1/l2dir2/"), "should end with dot dirs resolved") + s.True(strings.HasPrefix(cdTestLoc.Path(), "/"), "should start with slash (abs path)") + + // URI returns the fully qualified URI for the Location. IE, s3://bucket/some/path/ + // + // URI's for locations must always end with a separator character. + s.True(strings.HasSuffix(cdTestLoc.URI(), "locTestSrc/chdirTest/l1dir1/l2dir2/"), "should end with dot dirs resolved") + prefix := cdTestLoc.FileSystem().Scheme() + "://" + s.True(strings.HasPrefix(cdTestLoc.URI(), prefix), "should start with schema and abs slash") + + /* Exists returns boolean if the location exists on the file system. Returns an error if any. + + TODO: ************************************************************************************************************* + note that Exists is not consistent among implementations. GCSs and S3 always return true if the bucket exist. + Fundamentally, why one wants to know if location exists is to know whether you're able to write there. But + this feels unintuitive. + ************************************************************************************************************* + + Consider: + + // CREATE LOCATION INSTANCE + loc, _ := vfssimple.NewLocation("scheme://vol/path/") + + // DO EXISTS CHECK ON LOCATION + if !loc.Exists() { + // CREATE LOCATION ON OS + } + + // CREATE FILE IN LOCATION AND DO WORK + myfile, _ := loc.NewFile("myfile.txt") + myfile.Write("write some text") + myfile.Close() + + Now consider if the context is os/sftp OR gcs/s3/mem. + + ==== Exists() (bool, error) + */ + exists, err := baseLoc.Exists() + s.Require().NoError(err) + s.True(exists, "baseLoc location exists check") + + // setup list tests + f1, err := srcLoc.NewFile("file1.txt") + s.Require().NoError(err) + _, err = f1.Write([]byte("this is a test file")) + s.Require().NoError(err) + s.Require().NoError(f1.Close()) + + f2, err := srcLoc.NewFile("file2.txt") + s.Require().NoError(err) + s.Require().NoError(f1.CopyToFile(f2)) + s.Require().NoError(f1.Close()) + + f3, err := srcLoc.NewFile("self.txt") + s.Require().NoError(err) + s.Require().NoError(f1.CopyToFile(f3)) + s.Require().NoError(f1.Close()) + + subLoc, err := srcLoc.NewLocation("somepath/") + s.Require().NoError(err) + + f4, err := subLoc.NewFile("that.txt") + s.Require().NoError(err) + s.Require().NoError(f1.CopyToFile(f4)) + s.Require().NoError(f1.Close()) + + // List returns a slice of strings representing the base names of the files found at the Location. + // + // * All implementations are expected to return ([]string{}, nil) in the case of a non-existent directory/prefix/location. + // * If the user cares about the distinction between an empty location and a non-existent one, Location.Exists() should + // be checked first. + // ==== List() ([]string, error) + + files, err := srcLoc.List() + s.Require().NoError(err) + s.Len(files, 3, "list srcLoc location") + + files, err = subLoc.List() + s.Require().NoError(err) + s.Len(files, 1, "list subLoc location") + s.Equal("that.txt", files[0], "returned basename") + + files, err = cdTestLoc.List() + s.Require().NoError(err) + s.Empty(files, "non-existent location") + + // ListByPrefix returns a slice of strings representing the base names of the files found in Location whose filenames + // match the given prefix. + // + // * All implementations are expected to return ([]string{}, nil) in the case of a non-existent directory/prefix/location. + // * "relative" prefixes are allowed, ie, ListByPrefix() from location "/some/path/" with prefix "to/somepattern" + // is the same as location "/some/path/to/" with prefix of "somepattern" + // * If the user cares about the distinction between an empty location and a non-existent one, Location.Exists() should + // be checked first. + // ==== ListByPrefix(prefix string) ([]string, error) + + files, err = srcLoc.ListByPrefix("file") + s.Require().NoError(err) + s.Len(files, 2, "list srcLoc location matching prefix") + + files, err = srcLoc.ListByPrefix("s") + s.Require().NoError(err) + s.Len(files, 1, "list srcLoc location") + s.Equal("self.txt", files[0], "returned only file basename, not subdir matching prefix") + + files, err = srcLoc.ListByPrefix("somepath/t") + s.Require().NoError(err) + s.Len(files, 1, "list 'somepath' location relative to srcLoc") + s.Equal("that.txt", files[0], "returned only file basename, using relative prefix") + + files, err = cdTestLoc.List() + s.Require().NoError(err) + s.Empty(files, "non-existent location") + + // ListByRegex returns a slice of strings representing the base names of the files found in the Location that matched the + // given regular expression. + // + // * All implementations are expected to return ([]string{}, nil) in the case of a non-existent directory/prefix/location. + // * If the user cares about the distinction between an empty location and a non-existent one, Location.Exists() should + // be checked first. + // ==== ListByRegex(regex *regexp.Regexp) ([]string, error) + + files, err = srcLoc.ListByRegex(regexp.MustCompile("^f")) + s.Require().NoError(err) + s.Len(files, 2, "list srcLoc location matching prefix") + + files, err = srcLoc.ListByRegex(regexp.MustCompile(`.txt$`)) + s.Require().NoError(err) + s.Len(files, 3, "list srcLoc location matching prefix") + + files, err = srcLoc.ListByRegex(regexp.MustCompile(`Z`)) + s.Require().NoError(err) + s.Empty(files, "list srcLoc location matching prefix") + + // DeleteFile deletes the file of the given name at the location. + // + // This is meant to be a short cut for instantiating a new file and calling delete on that, with all the necessary + // error handling overhead. + // + // * Accepts relative file path. + // + // ==== DeleteFile(fileName string) error + s.Require().NoError(srcLoc.DeleteFile(f1.Name()), "deleteFile file1") + s.Require().NoError(srcLoc.DeleteFile(f2.Name()), "deleteFile file2") + s.Require().NoError(srcLoc.DeleteFile(f3.Name()), "deleteFile self.txt") + s.Require().NoError(srcLoc.DeleteFile("somepath/that.txt"), "deleted relative path") + + // should error if file doesn't exist + s.Require().Error(srcLoc.DeleteFile(f1.Path()), "deleteFile trying to delete a file already deleted") +} + +// Test File +func (s *vfsTestSuite) File(baseLoc vfs.Location) { + fmt.Println("****** testing vfs.File ******") + srcLoc, err := baseLoc.NewLocation("fileTestSrc/") + s.Require().NoError(err) + defer func() { + // clean up srcLoc after test for OS + if srcLoc.FileSystem().Scheme() == "file" { + exists, err := srcLoc.Exists() + s.Require().NoError(err) + if exists { + s.Require().NoError(os.RemoveAll(srcLoc.Path()), "failed to clean up file test srcLoc") + } + } + }() + + // setup srcFile + srcFile, err := srcLoc.NewFile("srcFile.txt") + s.Require().NoError(err) + + /* + Location returns the vfs.Location for the File. + + Location() Location + */ + + /* + io.Writer + */ + sz, err := srcFile.Write([]byte("this is a test\n")) + s.Require().NoError(err) + s.Equal(15, sz) + sz, err = srcFile.Write([]byte("and more text")) + s.Require().NoError(err) + s.Equal(13, sz) + + /* + io.Closer + */ + err = srcFile.Close() + s.Require().NoError(err) + + /* + Exists returns boolean if the file exists on the file system. Also returns an error if any. + + Exists() (bool, error) + */ + exists, err := srcFile.Exists() + s.Require().NoError(err) + s.True(exists, "file exists") + + /* + Name returns the base name of the file path. For file:///some/path/to/file.txt, it would return file.txt + + Name() string + */ + s.Equal("srcFile.txt", srcFile.Name(), "name test") + + /* + Path returns absolute path (with leading slash) including filename, ie /some/path/to/file.txt + + Path() string + */ + s.Equal(path.Join(baseLoc.Path(), "fileTestSrc/srcFile.txt"), srcFile.Path(), "path test") + + /* + URI returns the fully qualified URI for the File. IE, s3://bucket/some/path/to/file.txt + + URI() string + */ + s.Equal(baseLoc.URI()+"fileTestSrc/srcFile.txt", srcFile.URI(), "uri test") + + /* + String() must be implemented to satisfy the stringer interface. This ends up simply calling URI(). + + fmt.Stringer + */ + s.Equal(baseLoc.URI()+"fileTestSrc/srcFile.txt", srcFile.String(), "string(er) explicit test") + s.Equal(baseLoc.URI()+"fileTestSrc/srcFile.txt", fmt.Sprintf("%s", srcFile), "string(er) implicit test") //nolint:gocritic,staticcheck + + /* + Size returns the size of the file in bytes. + + Size() (uint64, error) + */ + b, err := srcFile.Size() + s.Require().NoError(err) + s.Equal(uint64(28), b) + + /* + LastModified returns the timestamp the file was last modified (as *time.Time). + + LastModified() (*time.Time, error) + */ + t, err := srcFile.LastModified() + s.Require().NoError(err) + s.IsType((*time.Time)(nil), t, "last modified returned *time.Time") + + /* + Exists returns boolean if the file exists on the file system. Also returns an error if any. + + Exists() (bool, error) + */ + exists, err = srcFile.Exists() + s.Require().NoError(err) + s.True(exists, "file exists") + + /* + io.Reader and io.Seeker + */ + str, err := io.ReadAll(srcFile) + s.Require().NoError(err) + s.Equal("this is a test\nand more text", string(str), "read was successful") + + offset, err := srcFile.Seek(3, 0) + s.Require().NoError(err) + s.Equal(int64(3), offset, "seek was successful") + + str, err = io.ReadAll(srcFile) + s.Require().NoError(err) + s.Equal("s is a test\nand more text", string(str), "read after seek") + err = srcFile.Close() + s.Require().NoError(err) + + for _, testLoc := range s.testLocations { + // setup dstLoc + dstLoc, err := testLoc.NewLocation("dstLoc/") + s.Require().NoError(err) + fmt.Printf("** location %s **\n", dstLoc) + if dstLoc.FileSystem().Scheme() == "file" { + s.T().Cleanup(func() { + // clean up dstLoc after test for OS + exists, err := dstLoc.Exists() + s.Require().NoError(err) + if exists { + s.Require().NoError(os.RemoveAll(dstLoc.Path()), "failed to clean up file test dstLoc") + } + }) + } + + // CopyToLocation will copy the current file to the provided location. + // + // * Upon success, a vfs.File, representing the file at the new location, will be returned. + // * In the case of an error, nil is returned for the file. + // * CopyToLocation should use native functions when possible within the same scheme. + // * If the file already exists at the location, the contents will be overwritten with the current file's contents. + _, err = srcFile.Seek(0, 0) + s.Require().NoError(err) + dst, err := srcFile.CopyToLocation(dstLoc) + s.Require().NoError(err) + exists, err := dst.Exists() + s.Require().NoError(err) + s.True(exists, "dst file should now exist") + exists, err = srcFile.Exists() + s.Require().NoError(err) + s.True(exists, "src file should still exist") + + // CopyToFile will copy the current file to the provided file instance. + // + // * In the case of an error, nil is returned for the file. + // * CopyToLocation should use native functions when possible within the same scheme. + // * If the file already exists, the contents will be overwritten with the current file's contents. + + // setup dstFile + dstFile1, err := dstLoc.NewFile("dstFile1.txt") + s.Require().NoError(err) + exists, err = dstFile1.Exists() + s.Require().NoError(err) + s.False(exists, "dstFile1 file should not yet exist") + _, err = srcFile.Seek(0, 0) + s.Require().NoError(err) + err = srcFile.CopyToFile(dstFile1) + s.Require().NoError(err) + exists, err = dstFile1.Exists() + s.Require().NoError(err) + s.True(exists, "dstFile1 file should now exist") + exists, err = srcFile.Exists() + s.Require().NoError(err) + s.True(exists, "src file should still exist") + + /* + io.Copy + */ + // create a local copy from srcFile with io.Copy + copyFile1, err := srcLoc.NewFile("copyFile1.txt") + s.Require().NoError(err) + // should not exist + exists, err = copyFile1.Exists() + s.Require().NoError(err) + s.False(exists, "copyFile1 should not yet exist locally") + // do copy + // skip this test for ftp files + buffer := make([]byte, utils.TouchCopyMinBufferSize) + + if srcLoc.FileSystem().Scheme() != "ftp" { + _, err = srcFile.Seek(0, 0) + s.Require().NoError(err) + b1, err := io.CopyBuffer(copyFile1, srcFile, buffer) + s.Require().NoError(err) + s.Equal(int64(28), b1) + err = copyFile1.Close() + s.Require().NoError(err) + + // should now exist + exists, err = copyFile1.Exists() + s.Require().NoError(err) + s.True(exists, "%s should now exist locally", copyFile1) + err = copyFile1.Close() + s.Require().NoError(err) + } else { + // else still have to ensure copyFile1 exists for later tests + err = copyFile1.Touch() + s.Require().NoError(err) + } + + // create another local copy from srcFile with io.Copy + copyFile2, err := srcLoc.NewFile("copyFile2.txt") + s.Require().NoError(err) + // should not exist + exists, err = copyFile2.Exists() + s.Require().NoError(err) + s.False(exists, "copyFile2 should not yet exist locally") + // do copy + // skip this test for ftp files + if srcLoc.FileSystem().Scheme() != "ftp" { + _, err = srcFile.Seek(0, 0) + s.Require().NoError(err) + buffer = make([]byte, utils.TouchCopyMinBufferSize) + b2, err := io.CopyBuffer(copyFile2, srcFile, buffer) + s.Require().NoError(err) + s.Equal(int64(28), b2) + + err = copyFile2.Close() + s.Require().NoError(err) + // should now exist + exists, err = copyFile2.Exists() + s.Require().NoError(err) + s.True(exists, "copyFile2 should now exist locally") + err = copyFile2.Close() + s.Require().NoError(err) + } else { + // else still have to ensure copyFile1 exists for later tests + err = copyFile2.Touch() + s.Require().NoError(err) + } + + // MoveToLocation will move the current file to the provided location. + // + // * If the file already exists at the location, the contents will be overwritten with the current file's contents. + // * If the location does not exist, an attempt will be made to create it. + // * Upon success, a vfs.File, representing the file at the new location, will be returned. + // * In the case of an error, nil is returned for the file. + // * When moving within the same Scheme, native move/rename should be used where possible. + // * If the file already exists, the contents will be overwritten with the current file's contents. + fileForNew, err := srcLoc.NewFile("fileForNew.txt") + s.Require().NoError(err) + + // skip this test for ftp files + if srcLoc.FileSystem().Scheme() != "ftp" { + _, err = srcFile.Seek(0, 0) + s.Require().NoError(err) + buffer = make([]byte, utils.TouchCopyMinBufferSize) + _, err = io.CopyBuffer(fileForNew, srcFile, buffer) + s.Require().NoError(err) + err = fileForNew.Close() + s.Require().NoError(err) + + newLoc, err := dstLoc.NewLocation("doesnotexist/") + s.Require().NoError(err) + dstCopyNew, err := fileForNew.MoveToLocation(newLoc) + s.Require().NoError(err) + exists, err = dstCopyNew.Exists() + s.Require().NoError(err) + s.True(exists) + s.Require().NoError(dstCopyNew.Delete()) // clean up file + + s.Require().NoError(srcFile.Close()) + } + + dstCopy1, err := copyFile1.MoveToLocation(dstLoc) + s.Require().NoError(err) + // destination file should now exist + exists, err = dstCopy1.Exists() + s.Require().NoError(err) + s.True(exists, "dstCopy1 file should now exist") + // local copy should no longer exist + exists, err = copyFile1.Exists() + s.Require().NoError(err) + s.False(exists, "copyFile1 should no longer exist locally") + + // MoveToFile will move the current file to the provided file instance. + // + // * If the file already exists, the contents will be overwritten with the current file's contents. + // * The current instance of the file will be removed. + dstCopy2, err := dstLoc.NewFile("dstFile2.txt") + s.Require().NoError(err) + // destination file should not exist + exists, err = dstCopy2.Exists() + s.Require().NoError(err) + s.False(exists, "dstCopy2 file should not yet exist") + // do move file + err = copyFile2.MoveToFile(dstCopy2) + s.Require().NoError(err) + // local copy should no longer exist + exists, err = copyFile2.Exists() + s.Require().NoError(err) + s.False(exists, "copyFile2 should no longer exist locally") + // destination file should now exist + exists, err = dstCopy2.Exists() + s.Require().NoError(err) + s.True(exists, "dstCopy2 file should now exist") + + // clean up files + err = dst.Delete() + s.Require().NoError(err) + err = dstFile1.Delete() + s.Require().NoError(err) + err = dstCopy1.Delete() + s.Require().NoError(err) + err = dstCopy2.Delete() + s.Require().NoError(err) + + // ensure that MoveToFile() works for files with spaces + type moveSpaceTest struct { + Path, Filename string + } + tests := []moveSpaceTest{ + {Path: "file", Filename: "has space.txt"}, + {Path: "file", Filename: "has%20encodedSpace.txt"}, + {Path: "path has", Filename: "space.txt"}, + {Path: "path%20has", Filename: "encodedSpace.txt"}, + } + + for i, test := range tests { + s.Run(strconv.Itoa(i), func() { + // setup src + srcSpaces, err := srcLoc.NewFile(path.Join(test.Path, test.Filename)) + s.Require().NoError(err) + b, err := srcSpaces.Write([]byte("something")) + s.Require().NoError(err) + s.Equal(9, b, "byte count is correct") + err = srcSpaces.Close() + s.Require().NoError(err) + + testDestLoc, err := dstLoc.NewLocation(test.Path + "/") + s.Require().NoError(err) + + dstSpaces, err := srcSpaces.MoveToLocation(testDestLoc) + s.Require().NoError(err) + exists, err := dstSpaces.Exists() + s.Require().NoError(err) + s.True(exists, "dstSpaces should now exist") + exists, err = srcSpaces.Exists() + s.Require().NoError(err) + s.False(exists, "srcSpaces should no longer exist") + s.True( + strings.HasSuffix(dstSpaces.URI(), path.Join(test.Path, test.Filename)) || + strings.HasSuffix(dstSpaces.URI(), path.Join(url.PathEscape(test.Path), url.PathEscape(test.Filename))), + "destination file %s ends with source string for %s", dstSpaces.URI(), path.Join(test.Path, test.Filename), + ) + + newSrcSpaces, err := dstSpaces.MoveToLocation(srcSpaces.Location()) + s.Require().NoError(err) + exists, err = newSrcSpaces.Exists() + s.Require().NoError(err) + s.True(exists, "newSrcSpaces should now exist") + exists, err = dstSpaces.Exists() + s.Require().NoError(err) + s.False(exists, "dstSpaces should no longer exist") + hasSuffix := strings.HasSuffix(newSrcSpaces.URI(), path.Join(test.Path, test.Filename)) || + strings.HasSuffix(newSrcSpaces.URI(), path.Join(url.PathEscape(test.Path), url.PathEscape(test.Filename))) + s.True(hasSuffix, "destination file %s ends with source string for %s", dstSpaces.URI(), path.Join(test.Path, test.Filename)) + + err = newSrcSpaces.Delete() + s.Require().NoError(err) + exists, err = newSrcSpaces.Exists() + s.Require().NoError(err) + s.False(exists, "newSrcSpaces should now exist") + }) + } + } + + // Touch creates a zero-length file on the vfs.File if no File exists. Update File's last modified timestamp. + // Returns error if unable to touch File. + + touchedFile, err := srcLoc.NewFile("touch.txt") + s.Require().NoError(err) + defer func() { _ = touchedFile.Delete() }() + exists, err = touchedFile.Exists() + s.Require().NoError(err) + s.False(exists, "%s shouldn't yet exist", touchedFile) + + err = touchedFile.Touch() + s.Require().NoError(err) + exists, err = touchedFile.Exists() + s.Require().NoError(err) + s.True(exists, "%s now exists", touchedFile) + + size, err := touchedFile.Size() + s.Require().NoError(err) + s.Zero(size, "%s should be empty", touchedFile) + + // capture last modified + modified, err := touchedFile.LastModified() + s.Require().NoError(err) + modifiedDeRef := *modified + // wait for eventual consistency + if baseLoc.FileSystem().Scheme() == "ftp" { + time.Sleep(time.Duration(61-time.Now().Second()) * time.Second) + } else { + time.Sleep(time.Second) + } + err = touchedFile.Touch() + s.Require().NoError(err) + newModified, err := touchedFile.LastModified() + s.Require().NoError(err) + s.Greater(newModified.UnixNano(), modifiedDeRef.UnixNano(), "touch updated modified date for %s", touchedFile) + + /* + Delete unlinks the File on the file system. + + Delete() error + */ + err = srcFile.Delete() + s.Require().NoError(err) + exists, err = srcFile.Exists() + s.Require().NoError(err) + s.False(exists, "file no longer exists") + + // The following blocks test that an error is thrown when these operations are called on a non-existent file + srcFile, err = srcLoc.NewFile("thisFileDoesNotExist") + s.Require().NoError(err, "unexpected error creating file") + + exists, err = srcFile.Exists() + s.Require().NoError(err) + s.False(exists, "file should not exist") + + size, err = srcFile.Size() + s.Require().Error(err, "expected error because file does not exist") + s.Zero(size) + + _, err = srcFile.LastModified() + s.Require().Error(err, "expected error because file does not exist") + + seeked, err := srcFile.Seek(-1, 2) + s.Require().Error(err, "expected error because file does not exist") + s.Zero(seeked) + + _, err = srcFile.Read(make([]byte, 1)) + s.Require().Error(err, "expected error because file does not exist") + + // end existence tests +} + +func TestVFS(t *testing.T) { + suite.Run(t, new(vfsTestSuite)) +} diff --git a/testcontainers/doc.go b/testcontainers/doc.go new file mode 100644 index 00000000..0cc533c9 --- /dev/null +++ b/testcontainers/doc.go @@ -0,0 +1,5 @@ +/* +Package testcontainers is meant to be run by implementors of backends to ensure that the behaviors of their backend matches the +expected behavior of the interface. It uses the local Docker daemon to run servers that emulate popular storage services. +*/ +package testcontainers diff --git a/testcontainers/gcsserver.go b/testcontainers/gcsserver.go new file mode 100644 index 00000000..a2aba2ea --- /dev/null +++ b/testcontainers/gcsserver.go @@ -0,0 +1,69 @@ +package testcontainers + +import ( + "context" + "crypto/tls" + "fmt" + "net/http" + "strings" + "testing" + + "cloud.google.com/go/storage" + "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" + "google.golang.org/api/option" + + "github.com/c2fo/vfs/v7/backend" + "github.com/c2fo/vfs/v7/backend/gs" +) + +const gcsServerPort = "4443/tcp" + +func registerGCSServer(t *testing.T) string { + ctx := context.Background() + is := require.New(t) + + req := testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ + Name: "vfs-fake-gcs-server", + Image: "fsouza/fake-gcs-server:latest", + Entrypoint: []string{"/bin/fake-gcs-server", "-backend", "memory"}, + WaitingFor: wait.ForHTTP("/_internal/healthcheck").WithTLS(true).WithAllowInsecure(true).WithPort(gcsServerPort), + }, + Started: true, + } + ctr, err := testcontainers.GenericContainer(ctx, req) + testcontainers.CleanupContainer(t, ctr) + is.NoError(err) + + host, err := ctr.Host(ctx) + is.NoError(err) + port, err := ctr.MappedPort(ctx, gcsServerPort) + is.NoError(err) + ep := fmt.Sprintf("https://%s:%s", host, port.Port()) + + hc := &http.Client{Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec + }} + configJSON := strings.NewReader(fmt.Sprintf(`{"publicHost":"%s:%s"}`, host, port.Port())) + hreq, err := http.NewRequest(http.MethodPut, ep+"/_internal/config", configJSON) + is.NoError(err) + res, err := hc.Do(hreq) + is.NoError(err) + _ = res.Body.Close() + is.Equal(http.StatusOK, res.StatusCode) + + cli, err := storage.NewClient(ctx, + option.WithHTTPClient(hc), + option.WithEndpoint(ep+"/storage/v1/"), + option.WithoutAuthentication(), + ) + is.NoError(err) + + err = cli.Bucket("gcsserver").Create(ctx, "", &storage.BucketAttrs{VersioningEnabled: true}) + is.NoError(err) + + backend.Register("gs://gcsserver/", gs.NewFileSystem(gs.WithClient(cli))) + return "gs://gcsserver/" +} diff --git a/testcontainers/go.mod b/testcontainers/go.mod new file mode 100644 index 00000000..9ded0d1c --- /dev/null +++ b/testcontainers/go.mod @@ -0,0 +1,149 @@ +module github.com/c2fo/vfs/testcontainers + +go 1.24.7 + +toolchain go1.24.8 + +replace github.com/c2fo/vfs/v7 => .. + +require ( + cloud.google.com/go/storage v1.57.1 + github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.3 + github.com/aws/aws-sdk-go-v2 v1.39.6 + github.com/aws/aws-sdk-go-v2/config v1.31.17 + github.com/aws/aws-sdk-go-v2/credentials v1.18.21 + github.com/aws/aws-sdk-go-v2/service/s3 v1.90.0 + github.com/c2fo/vfs/v7 v7.10.0 + github.com/stretchr/testify v1.11.1 + github.com/testcontainers/testcontainers-go v0.40.0 + github.com/testcontainers/testcontainers-go/modules/azure v0.40.0 + github.com/testcontainers/testcontainers-go/modules/localstack v0.40.0 + github.com/testcontainers/testcontainers-go/modules/minio v0.40.0 + golang.org/x/crypto v0.43.0 + google.golang.org/api v0.255.0 +) + +require ( + cel.dev/expr v0.25.0 // indirect + cloud.google.com/go v0.123.0 // indirect + cloud.google.com/go/auth v0.17.0 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect + cloud.google.com/go/iam v1.5.3 // indirect + cloud.google.com/go/monitoring v1.24.3 // indirect + dario.cat/mergo v1.0.2 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/data/aztables v1.4.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect + github.com/Azure/azure-sdk-for-go/sdk/storage/azqueue v1.0.1 // indirect + github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 // indirect + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.4 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.13 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 // indirect + github.com/aws/smithy-go v1.23.2 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cncf/xds/go v0.0.0-20251031190108-5cf4b1949528 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/creack/pty v1.1.24 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/docker v28.5.2+incompatible // indirect + github.com/docker/go-connections v0.6.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/ebitengine/purego v0.9.1 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect + github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect + github.com/google/s2a-go v0.1.9 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect + github.com/googleapis/gax-go/v2 v2.15.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/jlaffaye/ftp v0.2.1-0.20240214224549-4edb16bfcd0f // indirect + github.com/klauspost/compress v1.18.1 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/kr/fs v0.1.0 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect + github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect + github.com/magiconair/properties v1.8.10 // indirect + github.com/minio/crc64nvme v1.1.1 // indirect + github.com/minio/minio-go/v7 v7.0.95 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/go-archive v0.1.0 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/sys/sequential v0.6.0 // indirect + github.com/moby/sys/user v0.4.0 // indirect + github.com/moby/sys/userns v0.1.0 // indirect + github.com/moby/term v0.5.2 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pkg/sftp v1.13.10 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect + github.com/shirou/gopsutil/v4 v4.25.10 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect + github.com/tinylib/msgp v1.4.0 // indirect + github.com/tklauser/go-sysconf v0.3.15 // indirect + github.com/tklauser/numcpus v0.10.0 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.38.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/sdk v1.38.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.opentelemetry.io/proto/otlp v1.8.0 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/net v0.46.0 // indirect + golang.org/x/oauth2 v0.33.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.30.0 // indirect + golang.org/x/time v0.14.0 // indirect + google.golang.org/genproto v0.0.0-20251103181224-f26f9409b101 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251103181224-f26f9409b101 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 // indirect + google.golang.org/grpc v1.76.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/testcontainers/go.sum b/testcontainers/go.sum new file mode 100644 index 00000000..ea2b8a2b --- /dev/null +++ b/testcontainers/go.sum @@ -0,0 +1,369 @@ +cel.dev/expr v0.25.0 h1:qbCFvDJJthxLvf3TqeF9Ys7pjjWrO7LMzfYhpJUc30g= +cel.dev/expr v0.25.0/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= +cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= +cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4= +cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc= +cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU= +cloud.google.com/go/logging v1.13.1 h1:O7LvmO0kGLaHY/gq8cV7T0dyp6zJhYAOtZPX4TF3QtY= +cloud.google.com/go/logging v1.13.1/go.mod h1:XAQkfkMBxQRjQek96WLPNze7vsOmay9H5PqfsNYDqvw= +cloud.google.com/go/longrunning v0.7.0 h1:FV0+SYF1RIj59gyoWDRi45GiYUMM3K1qO51qoboQT1E= +cloud.google.com/go/longrunning v0.7.0/go.mod h1:ySn2yXmjbK9Ba0zsQqunhDkYi0+9rlXIwnoAf+h+TPY= +cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE= +cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI= +cloud.google.com/go/pubsub v1.50.1 h1:fzbXpPyJnSGvWXF1jabhQeXyxdbCIkXTpjXHy7xviBM= +cloud.google.com/go/pubsub/v2 v2.2.0 h1:UmvGMvIMhQOouXbVpIFRHEUHXsnaGjSTo2XAqk85u2s= +cloud.google.com/go/pubsub/v2 v2.2.0/go.mod h1:O5f0KHG9zDheZAd3z5rlCRhxt2JQtB+t/IYLKK3Bpvw= +cloud.google.com/go/storage v1.57.1 h1:gzao6odNJ7dR3XXYvAgPK+Iw4fVPPznEPPyNjbaVkq8= +cloud.google.com/go/storage v1.57.1/go.mod h1:329cwlpzALLgJuu8beyJ/uvQznDHpa2U5lGjWednkzg= +cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U= +cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 h1:JXg2dwJUmPB9JmtVmdEB16APJ7jurfbY5jnfXpJoRMc= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.0 h1:KpMC6LFL7mqpExyMC9jVOYRiVhLmamjeZfRsUpB7l4s= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.0/go.mod h1:J7MUC/wtRpfGVbQ5sIItY5/FuVWmvzlY21WAOfQnq/I= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= +github.com/Azure/azure-sdk-for-go/sdk/data/aztables v1.4.0 h1:mXlQ+2C8A4KpXTIIYYxgFYqSivjGTBQidq/b0xxZLuk= +github.com/Azure/azure-sdk-for-go/sdk/data/aztables v1.4.0/go.mod h1:K//Ck7MUa+r9jpV69WLeWnnju5WJx5120AFsEzvumII= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1 h1:/Zt+cDPnpC3OVDm/JKLOs7M2DKmLRIIp3XIx9pHHiig= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1/go.mod h1:Ng3urmn6dYe8gnbCMoHHVl5APYz2txho3koEkV2o2HA= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.3 h1:ZJJNFaQ86GVKQ9ehwqyAFE6pIfyicpuJ8IkVaPBc6/4= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.3/go.mod h1:URuDvhmATVKqHBH9/0nOiNKk0+YcwfQ3WkK5PqHKxc8= +github.com/Azure/azure-sdk-for-go/sdk/storage/azqueue v1.0.1 h1:qvrrnQ2mIjwY7IVlQuNB0ma43Nr74+9ZTZJ60KlmlV4= +github.com/Azure/azure-sdk-for-go/sdk/storage/azqueue v1.0.1/go.mod h1:FkF/Az07vR3S4sBdjCuisznWfFWOD8u6Ibm/g/oyDAk= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= +github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= +github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 h1:lhhYARPUu3LmHysQ/igznQphfzynnqI3D75oUyw1HXk= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0/go.mod h1:l9rva3ApbBpEJxSNYnwT9N4CDLrWgtq3u8736C5hyJw= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0 h1:xfK3bbi6F2RDtaZFtUdKO3osOBIhNb+xTs8lFW6yx9o= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 h1:s0WlVbf9qpvkh1c/uDAPElam0WrL7fHRIidgZJ7UqZI= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/aws/aws-sdk-go-v2 v1.39.6 h1:2JrPCVgWJm7bm83BDwY5z8ietmeJUbh3O2ACnn+Xsqk= +github.com/aws/aws-sdk-go-v2 v1.39.6/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 h1:DHctwEM8P8iTXFxC/QK0MRjwEpWQeM9yzidCRjldUz0= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3/go.mod h1:xdCzcZEtnSTKVDOmUZs4l/j3pSV6rpo1WXl5ugNsL8Y= +github.com/aws/aws-sdk-go-v2/config v1.31.17 h1:QFl8lL6RgakNK86vusim14P2k8BFSxjvUkcWLDjgz9Y= +github.com/aws/aws-sdk-go-v2/config v1.31.17/go.mod h1:V8P7ILjp/Uef/aX8TjGk6OHZN6IKPM5YW6S78QnRD5c= +github.com/aws/aws-sdk-go-v2/credentials v1.18.21 h1:56HGpsgnmD+2/KpG0ikvvR8+3v3COCwaF4r+oWwOeNA= +github.com/aws/aws-sdk-go-v2/credentials v1.18.21/go.mod h1:3YELwedmQbw7cXNaII2Wywd+YY58AmLPwX4LzARgmmA= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 h1:T1brd5dR3/fzNFAQch/iBKeX07/ffu/cLu+q+RuzEWk= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13/go.mod h1:Peg/GBAQ6JDt+RoBf4meB1wylmAipb7Kg2ZFakZTlwk= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.4 h1:2fjfz3/G9BRvIKuNZ655GwzpklC2kEH0cowZQGO7uBg= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.4/go.mod h1:Ymws824lvMypLFPwyyUXM52SXuGgxpu0+DISLfKvB+c= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 h1:a+8/MLcWlIxo1lF9xaGt3J/u3yOZx+CdSveSNwjhD40= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13/go.mod h1:oGnKwIYZ4XttyU2JWxFrwvhF6YKiK/9/wmE3v3Iu9K8= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 h1:HBSI2kDkMdWz4ZM7FjwE7e/pWDEZ+nR95x8Ztet1ooY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13/go.mod h1:YE94ZoDArI7awZqJzBAZ3PDD2zSfuP7w6P2knOzIn8M= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.13 h1:eg/WYAa12vqTphzIdWMzqYRVKKnCboVPRlvaybNCqPA= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.13/go.mod h1:/FDdxWhz1486obGrKKC1HONd7krpk38LBt+dutLcN9k= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 h1:x2Ibm/Af8Fi+BH+Hsn9TXGdT+hKbDd5XOTZxTMxDk7o= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.4 h1:NvMjwvv8hpGUILarKw7Z4Q0w1H9anXKsesMxtw++MA4= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.4/go.mod h1:455WPHSwaGj2waRSpQp7TsnpOnBfw8iDfPfbwl7KPJE= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 h1:kDqdFvMY4AtKoACfzIGD8A0+hbT41KTKF//gq7jITfM= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13/go.mod h1:lmKuogqSU3HzQCwZ9ZtcqOc5XGMqtDK7OIc2+DxiUEg= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13 h1:zhBJXdhWIFZ1acfDYIhu4+LCzdUS2Vbcum7D01dXlHQ= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13/go.mod h1:JaaOeCE368qn2Hzi3sEzY6FgAZVCIYcC2nwbro2QCh8= +github.com/aws/aws-sdk-go-v2/service/s3 v1.90.0 h1:ef6gIJR+xv/JQWwpa5FYirzoQctfSJm7tuDe3SZsUf8= +github.com/aws/aws-sdk-go-v2/service/s3 v1.90.0/go.mod h1:+wArOOrcHUevqdto9k1tKOF5++YTe9JEcPSc9Tx2ZSw= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 h1:0JPwLz1J+5lEOfy/g0SURC9cxhbQ1lIMHMa+AHZSzz0= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.1/go.mod h1:fKvyjJcz63iL/ftA6RaM8sRCtN4r4zl4tjL3qw5ec7k= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 h1:OWs0/j2UYR5LOGi88sD5/lhN6TDLG6SfA7CqsQO9zF0= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5/go.mod h1:klO+ejMvYsB4QATfEOIXk8WAEwN4N0aBfJpvC+5SZBo= +github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 h1:mLlUgHn02ue8whiR4BmxxGJLR2gwU6s6ZzJ5wDamBUs= +github.com/aws/aws-sdk-go-v2/service/sts v1.39.1/go.mod h1:E19xDjpzPZC7LS2knI9E6BaRFDK43Eul7vd6rSq2HWk= +github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM= +github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cncf/xds/go v0.0.0-20251031190108-5cf4b1949528 h1:/LeN/a7nXz/nkJkihmSFToTx0L8fvolwdEjwv1GygXE= +github.com/cncf/xds/go v0.0.0-20251031190108-5cf4b1949528/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= +github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd h1:l+vLbuxptsC6VQyQsfD7NnEC8BZuFpz45PgY+pH8YTg= +github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd/go.mod h1:7I+3Pe2o/YSU88W0hWlm9S22W7XI1JFNJ86U0zPKMf8= +github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 h1:DilThiXje0z+3UQ5YjYiSRRzVdtamFpvBQXKwMglWqw= +github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349/go.mod h1:4GC5sXji84i/p+irqghpPFZBF8tRN/Q7+700G0/DLe8= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A= +github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= +github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= +github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g= +github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= +github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= +github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fsouza/fake-gcs-server v1.52.3 h1:hXddOPMGDKq5ENmttw6xkodVJy0uVhf7HhWvQgAOH6g= +github.com/fsouza/fake-gcs-server v1.52.3/go.mod h1:A0XtSRX+zz5pLRAt88j9+Of0omQQW+RMqipFbvdNclQ= +github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= +github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= +github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= +github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= +github.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg= +github.com/google/renameio/v2 v2.0.0/go.mod h1:BtmJXm5YlszgC+TD4HOEEUFgkJP3nLxehU6hfe7jRt4= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= +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/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ= +github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= +github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= +github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= +github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/jlaffaye/ftp v0.2.1-0.20240214224549-4edb16bfcd0f h1:u9Rqt4DbfQ1xc7syxtnWFNU1OjcXJeVYGsiU1q3QAI4= +github.com/jlaffaye/ftp v0.2.1-0.20240214224549-4edb16bfcd0f/go.mod h1:4p8lUl4vQ80L598CygL+3IFtm+3nggvvW/palOlViwE= +github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= +github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= +github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= +github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k= +github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI= +github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= +github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= +github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= +github.com/minio/minio-go/v7 v7.0.95 h1:ywOUPg+PebTMTzn9VDsoFJy32ZuARN9zhB+K3IYEvYU= +github.com/minio/minio-go/v7 v7.0.95/go.mod h1:wOOX3uxS334vImCNRVyIDdXX9OsXDm89ToynKgqUKlo= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= +github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= +github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= +github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU= +github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA= +github.com/pkg/xattr v0.4.12 h1:rRTkSyFNTRElv6pkA3zpjHpQ90p/OdHQC1GmGh1aTjM= +github.com/pkg/xattr v0.4.12/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/shirou/gopsutil/v4 v4.25.10 h1:at8lk/5T1OgtuCp+AwrDofFRjnvosn0nkN2OLQ6g8tA= +github.com/shirou/gopsutil/v4 v4.25.10/go.mod h1:+kSwyC8DRUD9XXEHCAFjK+0nuArFJM0lva+StQAcskM= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= +github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+aMnb6JfVz1mxk7OeMU= +github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY= +github.com/testcontainers/testcontainers-go/modules/azure v0.40.0 h1:a4Qn4UEgL3uzpY1Hhuzh2c87u/CuSoTaV12timQfHQU= +github.com/testcontainers/testcontainers-go/modules/azure v0.40.0/go.mod h1:047cjSoIxghqTQt8OVeLwLO918jOTrRnKYSEG5L6paQ= +github.com/testcontainers/testcontainers-go/modules/localstack v0.40.0 h1:b+lN2Ch4J/6EwqB+Af+QQbSfv4sFGetHlBHpXi+1yJU= +github.com/testcontainers/testcontainers-go/modules/localstack v0.40.0/go.mod h1:8LuTSboTo2MJKFKV5xH6z4ZH1s3jhRJWwvtPJzKogj4= +github.com/testcontainers/testcontainers-go/modules/minio v0.40.0 h1:M+Ib1mIXq/hEcH8tyEvBnOZ7NJi03zY+P1gYO5GGp6o= +github.com/testcontainers/testcontainers-go/modules/minio v0.40.0/go.mod h1:ON0MxxS/pME0SJOKLImw/D9R1L7apYsxIZrM/uEqORA= +github.com/tinylib/msgp v1.4.0 h1:SYOeDRiydzOw9kSiwdYp9UcBgPFtLU2WDHaJXyHruf8= +github.com/tinylib/msgp v1.4.0/go.mod h1:cvjFkb4RiC8qSBOPMGPSzSAx47nAsfhLVTCZZNuHv5o= +github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= +github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= +github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= +github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/detectors/gcp v1.38.0 h1:ZoYbqX7OaA/TAikspPl3ozPI6iY6LiIY9I8cUfm+pJs= +go.opentelemetry.io/contrib/detectors/gcp v1.38.0/go.mod h1:SU+iU7nu5ud4oCb3LQOhIZ3nRLj6FNVrKgtflbaf2ts= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 h1:wm/Q0GAAykXv83wzcKzGGqAnnfLFyFe7RslekZuv+VI= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0/go.mod h1:ra3Pa40+oKjvYh+ZD3EdxFZZB0xdMfuileHAm4nNN7w= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/proto/otlp v1.8.0 h1:fRAZQDcAFHySxpJ1TwlA1cJ4tvcrw7nXl9xWWC8N5CE= +go.opentelemetry.io/proto/otlp v1.8.0/go.mod h1:tIeYOeNBU4cvmPqpaji1P+KbB4Oloai8wN4rWzRrFF0= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= +golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/api v0.255.0 h1:OaF+IbRwOottVCYV2wZan7KUq7UeNUQn1BcPc4K7lE4= +google.golang.org/api v0.255.0/go.mod h1:d1/EtvCLdtiWEV4rAEHDHGh2bCnqsWhw+M8y2ECN4a8= +google.golang.org/genproto v0.0.0-20251103181224-f26f9409b101 h1:MgBTzgUJFAmp2PlyqKJecSpZpjFxkYL3nDUIeH/6Q30= +google.golang.org/genproto v0.0.0-20251103181224-f26f9409b101/go.mod h1:bbWg36d7wp3knc0hIlmJAnW5R/CQ2rzpEVb72eH4ex4= +google.golang.org/genproto/googleapis/api v0.0.0-20251103181224-f26f9409b101 h1:vk5TfqZHNn0obhPIYeS+cxIFKFQgser/M2jnI+9c6MM= +google.golang.org/genproto/googleapis/api v0.0.0-20251103181224-f26f9409b101/go.mod h1:E17fc4PDhkr22dE3RgnH2hEubUaky6ZwW4VhANxyspg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 h1:tRPGkdGHuewF4UisLzzHHr1spKw92qLM98nIzxbC0wY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= +google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= diff --git a/testcontainers/io_integration_test.go b/testcontainers/io_integration_test.go new file mode 100644 index 00000000..27fce2b1 --- /dev/null +++ b/testcontainers/io_integration_test.go @@ -0,0 +1,518 @@ +package testcontainers + +import ( + "errors" + "io" + "os" + "path" + "regexp" + "strconv" + "strings" + "sync" + "testing" + + "github.com/stretchr/testify/suite" + + "github.com/c2fo/vfs/v7" + "github.com/c2fo/vfs/v7/options" + "github.com/c2fo/vfs/v7/vfssimple" +) + +type osWrapper struct { + filename string + file *os.File + exists bool + seekCalled bool +} + +func newOSWrapper(absPath string) *osWrapper { + return &osWrapper{ + filename: absPath, + exists: fileExists(absPath), + } +} + +func fileExists(filename string) bool { + info, err := os.Stat(filename) + if os.IsNotExist(err) { + return false + } + return !info.IsDir() +} + +func (o *osWrapper) Read(b []byte) (int, error) { + if !o.exists { + return 0, errors.New("file not found") + } + if o.file == nil { + file, err := os.OpenFile(o.filename, os.O_RDWR, 0o600) + if err != nil { + return 0, err + } + o.file = file + } + return o.file.Read(b) +} + +func (o *osWrapper) Write(b []byte) (int, error) { + if o.file == nil { + flags := os.O_RDWR | os.O_CREATE | os.O_TRUNC + if o.seekCalled { + flags = os.O_RDWR | os.O_CREATE + } + file, err := os.OpenFile(o.filename, flags, 0o600) //nolint:gosec + if err != nil { + return 0, err + } + o.file = file + o.exists = true + } + + return o.file.Write(b) +} + +func (o *osWrapper) Seek(offset int64, whence int) (int64, error) { + if !o.exists { + return 0, errors.New("file not found") + } + + if o.file == nil { + file, err := os.OpenFile(o.filename, os.O_RDWR, 0o600) + if err != nil { + return 0, err + } + o.file = file + } + o.seekCalled = true + return o.file.Seek(offset, whence) +} + +func (o *osWrapper) Close() error { + if !o.exists { + return nil + } + err := o.file.Close() + if err != nil { + return err + } + o.file = nil + return nil +} + +func (o *osWrapper) Name() string { + return path.Base(o.filename) +} + +func (o *osWrapper) URI() string { + return o.filename +} + +func (o *osWrapper) Delete(_ ...options.DeleteOption) error { + return os.Remove(o.URI()) +} + +type readWriteSeekCloseDeleter interface { + io.ReadWriteSeeker + io.Closer + Delete(opts ...options.DeleteOption) error +} + +type ioTestSuite struct { + suite.Suite + testLocations map[string]vfs.Location + localDir string +} + +func (s *ioTestSuite) SetupSuite() { + registers := []func(*testing.T) string{ + registerMem, + registerOS, + registerAtmoz, + registerAzurite, + registerGCSServer, + registerLocalStack, + registerMinio, + registerVSFTPD, + } + uris := make([]string, len(registers)) + var wg sync.WaitGroup + for i := range registers { + wg.Add(1) + go func() { + uris[i] = registers[i](s.T()) + wg.Done() + }() + } + wg.Wait() + + s.testLocations = make(map[string]vfs.Location) + for _, u := range uris { + if strings.HasPrefix(u, "/") { + s.localDir = u + } else { + l, err := vfssimple.NewLocation(u) + s.Require().NoError(err) + s.testLocations[l.FileSystem().Scheme()] = l + } + } +} + +func (s *ioTestSuite) TestFileOperations() { + if s.localDir != "" { + s.Run("local", func() { + s.testFileOperations(s.localDir) + }) + } + for scheme, location := range s.testLocations { + s.Run(scheme, func() { + s.testFileOperations(location.URI()) + }) + } +} + +// unless seek or read is called first, writes should replace a file (not edit) + +func (s *ioTestSuite) testFileOperations(testPath string) { + testCases := []struct { + description string + sequence string + fileAlreadyExists bool + expectFailure bool + expectedResults string + }{ + // Read, Close file + { + "Read, Close, file exists", + "R(all);C()", + true, + false, + "some text", + }, + { + "Read, Close, file does not exist", + "R(all);C()", + false, + true, + "", + }, + + // Read, Seek, Read, Close + { + "Read, Seek, Read, Close, file exists", + "R(4);S(0,0);R(4);C()", + true, + false, + "some text", + }, + + // Write, Close + { + "Write, Close, file does not exist", + "W(abc);C()", + false, + false, + "abc", + }, + { + "Write, Close, file exists", + "W(abc);C()", + true, + false, + "abc", + }, + + // Write, Seek, Write, Close + { + "Write, Seek, Write, Close, file does not exist", + "W(this and that);S(0,0);W(that);C()", + false, + false, + "that and that", + }, + { + "Write, Seek, Write, Close, file exists", + "W(this and that);S(0,0);W(that);C()", + true, + false, + "that and that", + }, + + // Seek + { + "Seek, Close, file does not exist", + "S(2,0);C()", + false, + true, + "", + }, + { + "Seek, Close, file exists", + "S(2,0);C()", + true, + false, + "some text", + }, + { + "Seek, Write, Close, file exists", + "S(5,0);W(new text);C()", + true, + false, + "some new text", + }, + + // Seek, Read, Close + { + "Seek, Read, Close, file does not exist", + "S(5,0);R(4);C()", + false, + true, + "", + }, + { + "Seek, Read, Close, file exists", + "S(5,0);R(4);C()", + true, + false, + "some text", + }, + + // Read, Write, Close + { + "Read, Write, Close, file does not exist", + "R(5);W(new text);C()", + false, + true, + "", + }, + { + "Read, Write, Close, file exists", + "R(5);W(new text);C()", + true, + false, + "some new text", + }, + + // Read, Seek, Write, Close + { + "Read, Seek, Write, Close, file does not exist", + "R(2);S(3,1);W(new text);C()", + false, + true, + "", + }, + { + "Read, Seek, Write, Close, file exists", + "R(2);S(3,1);W(new text);C()", + true, + false, + "some new text", + }, + + // Write, Seek, Read, Close + { + "Write, Seek, Read, Close, file does not exist", + "W(new text);S(0,0);R(5);C()", + false, + false, + "new text", + }, + { + "Write, Seek, Read, Close, file exists", + "W(new text);S(0,0);R(5);C()", + true, + false, + "new text", + }, + } + + defer s.teardownTestLocation(testPath) + for _, tc := range testCases { + s.Run(tc.description, func() { + testFileName := "testfile.txt" + + // run in a closure so we can defer teardown + func() { + // Setup vfs environment + file, err := s.setupTestFile(tc.fileAlreadyExists, testPath, testFileName) // Implement this setup function + defer func() { + if file != nil { + _ = file.Close() + _ = file.Delete() + } + }() + s.Require().NoError(err) + + // Use vfs to execute the sequence of operations described by the description + actualContents, err := executeSequence(s.T(), file, tc.sequence) // Implement this function + + // Assert expected outcomes + if tc.expectFailure { + s.Require().Error(err, "%s: expected failure but got success", tc.description) + } else { + s.Require().NoError(err, "%s: expected success but got failure: %v", tc.description, err) + } + + s.Equal(tc.expectedResults, actualContents, "%s: expected results %s but got %s", tc.description, tc.expectedResults, actualContents) + }() + }) + } +} + +//nolint:gocyclo +func executeSequence(t *testing.T, file readWriteSeekCloseDeleter, sequence string) (string, error) { + commands := strings.Split(sequence, ";") + var commandErr error +SEQ: + for _, command := range commands { + // parse command + commandName, commandArgs := parseCommand(t, command) + + switch commandName { + case "R": + if commandArgs[0] == "all" { + // Read entire file + _, commandErr = io.ReadAll(file) + if commandErr != nil { + break SEQ + } + } else { + // convert arg 0 to uint64 + bytesize, err := strconv.ParseUint(commandArgs[0], 10, 64) + if err != nil { + t.Fatalf("invalid bytesize: %s", commandArgs[0]) + } + + // Read file + b := make([]byte, bytesize) + _, commandErr = file.Read(b) + if commandErr != nil { + break SEQ + } + } + case "W": + // Write to file + _, commandErr = file.Write([]byte(commandArgs[0])) + if commandErr != nil { + break SEQ + } + case "S": + // expect 2 args for offset and whence + if len(commandArgs) != 2 { + t.Fatalf("invalid number of args for Seek: %d", len(commandArgs)) + } + // convert args + offset, err := strconv.ParseInt(commandArgs[0], 10, 64) + if err != nil { + t.Fatalf("invalid offset: %s", commandArgs[0]) + } + whence, err := strconv.Atoi(commandArgs[1]) + if err != nil { + t.Fatalf("invalid whence: %s", commandArgs[1]) + } + // Seek + _, commandErr = file.Seek(offset, whence) + if commandErr != nil { + break SEQ + } + case "C": + // Close + commandErr = file.Close() + if commandErr != nil { + break SEQ + } + } + } + // success so compare file contents to expected results + if commandErr != nil { + return "", commandErr + } + + var f io.ReadCloser + + switch assertedFile := file.(type) { + case *osWrapper: + var err error + f, err = os.Open(assertedFile.URI()) + if err != nil { + t.Fatalf("error opening file: %s", err.Error()) + } + case vfs.File: + var err error + f, err = assertedFile.Location().NewFile(assertedFile.Name()) + if err != nil { + t.Fatalf("error opening file: %s", err.Error()) + } + } + defer func() { _ = f.Close() }() + // Read entire file + contents, err := io.ReadAll(f) + if err != nil { + t.Fatalf("error reading file: %s", err.Error()) + } + return string(contents), nil +} + +var commandArgsRegex = regexp.MustCompile(`^([a-zA-Z0-9]+)\((.*)\)$`) + +// takes command string in the form of () and returns the command name and args +func parseCommand(t *testing.T, command string) (string, []string) { + // parse command string + results := commandArgsRegex.FindStringSubmatch(command) + if len(results) != 3 { + t.Fatalf("invalid command string: %s", command) + } + + // split args by comma + args := strings.Split(results[2], ",") + + return results[1], args +} + +func (s *ioTestSuite) setupTestFile(existsBefore bool, loc, filename string) (readWriteSeekCloseDeleter, error) { + var f readWriteSeekCloseDeleter + var err error + // Create file + if strings.HasPrefix(loc, "/") { + f = newOSWrapper(loc + filename) + } else { + scheme := strings.Split(loc, ":")[0] + // Write something to the file + f, err = s.testLocations[scheme].NewFile(filename) + if err != nil { + return nil, err + } + } + if existsBefore { + _, err = f.Write([]byte("some text")) + if err != nil { + return nil, err + } + err = f.Close() + if err != nil { + return nil, err + } + } + + return f, nil +} + +func (s *ioTestSuite) teardownTestLocation(testPath string) { + if strings.HasPrefix(testPath, "/") { + err := os.RemoveAll(testPath) + s.Require().NoError(err) + } else { + scheme := strings.Split(testPath, ":")[0] + // Write something to the file + loc := s.testLocations[scheme] + files, err := loc.List() + s.Require().NoError(err) + for _, file := range files { + err := loc.DeleteFile(file) + s.Require().NoError(err) + } + } +} + +func TestIOTestSuite(t *testing.T) { + suite.Run(t, new(ioTestSuite)) +} diff --git a/testcontainers/localstack.go b/testcontainers/localstack.go new file mode 100644 index 00000000..6968994b --- /dev/null +++ b/testcontainers/localstack.go @@ -0,0 +1,51 @@ +package testcontainers + +import ( + "context" + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials" + awss3 "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/modules/localstack" + + "github.com/c2fo/vfs/v7/backend" + "github.com/c2fo/vfs/v7/backend/s3" +) + +const ( + localStackPort = "4566/tcp" + localStackRegion = "dummy" + localStackKey = "dummy" + localStackSecret = "dummy" +) + +func registerLocalStack(t *testing.T) string { + ctx := context.Background() + is := require.New(t) + + ctr, err := localstack.Run(ctx, "localstack/localstack:latest", testcontainers.WithName("vfs-localstack")) + testcontainers.CleanupContainer(t, ctr) + is.NoError(err) + + ep, err := ctr.PortEndpoint(ctx, localStackPort, "http") + is.NoError(err) + + cfg, err := config.LoadDefaultConfig(ctx) + is.NoError(err) + + cli := awss3.NewFromConfig(cfg, func(opts *awss3.Options) { + opts.Region = localStackRegion + opts.UsePathStyle = true + opts.BaseEndpoint = aws.String(ep) + opts.Credentials = credentials.NewStaticCredentialsProvider(localStackKey, localStackSecret, "") + }) + _, err = cli.CreateBucket(ctx, &awss3.CreateBucketInput{Bucket: aws.String("localstack")}) + is.NoError(err) + + backend.Register("s3://localstack/", s3.NewFileSystem(s3.WithClient(cli))) + return "s3://localstack/" +} diff --git a/testcontainers/minio.go b/testcontainers/minio.go new file mode 100644 index 00000000..54b2c7c1 --- /dev/null +++ b/testcontainers/minio.go @@ -0,0 +1,46 @@ +package testcontainers + +import ( + "context" + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials" + awss3 "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/modules/minio" + + "github.com/c2fo/vfs/v7/backend" + "github.com/c2fo/vfs/v7/backend/s3" +) + +const minioRegion = "dummy" + +func registerMinio(t *testing.T) string { + ctx := context.Background() + is := require.New(t) + + ctr, err := minio.Run(ctx, "minio/minio:latest", testcontainers.WithName("vfs-minio")) + testcontainers.CleanupContainer(t, ctr) + is.NoError(err) + + ep, err := ctr.ConnectionString(ctx) + is.NoError(err) + + cfg, err := config.LoadDefaultConfig(ctx) + is.NoError(err) + + cli := awss3.NewFromConfig(cfg, func(opts *awss3.Options) { + opts.Region = minioRegion + opts.UsePathStyle = true + opts.BaseEndpoint = aws.String("http://" + ep) + opts.Credentials = credentials.NewStaticCredentialsProvider(ctr.Username, ctr.Password, "") + }) + _, err = cli.CreateBucket(ctx, &awss3.CreateBucketInput{Bucket: aws.String("miniobucket")}) + is.NoError(err) + + backend.Register("s3://miniobucket/", s3.NewFileSystem(s3.WithClient(cli), s3.WithOptions(s3.Options{DisableServerSideEncryption: true}))) + return "s3://miniobucket/" +} diff --git a/testcontainers/vsftpd.go b/testcontainers/vsftpd.go new file mode 100644 index 00000000..c0b7b537 --- /dev/null +++ b/testcontainers/vsftpd.go @@ -0,0 +1,48 @@ +package testcontainers + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" + + "github.com/c2fo/vfs/v7/backend" + "github.com/c2fo/vfs/v7/backend/ftp" +) + +const ( + vsftpdPort = "21/tcp" + vsftpdPassword = "dummy" +) + +func registerVSFTPD(t *testing.T) string { + ctx := context.Background() + is := require.New(t) + + req := testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ + Name: "vfs-vsftpd", + Image: "fauria/vsftpd:latest", + ExposedPorts: []string{"21", "21100-21110:21100-21110"}, + Env: map[string]string{"FTP_PASS": vsftpdPassword}, + WaitingFor: wait.ForListeningPort(vsftpdPort), + }, + Started: true, + } + ctr, err := testcontainers.GenericContainer(ctx, req) + testcontainers.CleanupContainer(t, ctr) + is.NoError(err) + + host, err := ctr.Host(ctx) + is.NoError(err) + + port, err := ctr.MappedPort(ctx, vsftpdPort) + is.NoError(err) + + authority := fmt.Sprintf("ftp://admin@%s:%s/", host, port.Port()) + backend.Register(authority, ftp.NewFileSystem(ftp.WithOptions(ftp.Options{Password: vsftpdPassword}))) + return authority +}