diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f148d36..a3b030dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Fixed +- Various compliance fixes (to be discussed). ## [v7.13.0] - 2025-01-26 ### Added diff --git a/backend/azure/file.go b/backend/azure/file.go index a5764942..03c4eb1a 100644 --- a/backend/azure/file.go +++ b/backend/azure/file.go @@ -70,7 +70,7 @@ func (f *File) Close() error { // the file is created and read operations are performed against that. The temp file is closed and flushed to Azure // when f.Close() is called. func (f *File) Read(p []byte) (n int, err error) { - if err := f.checkTempFile(); err != nil { + if err := f.checkTempFile(false); err != nil { return 0, utils.WrapReadError(err) } read, err := f.tempFile.Read(p) @@ -91,7 +91,7 @@ func (f *File) Read(p []byte) (n int, err error) { // the file is created and operations are performed against that. The temp file is closed and flushed to Azure // when f.Close() is called. func (f *File) Seek(offset int64, whence int) (int64, error) { - if err := f.checkTempFile(); err != nil { + if err := f.checkTempFile(false); err != nil { return 0, utils.WrapSeekError(err) } pos, err := f.tempFile.Seek(offset, whence) @@ -104,7 +104,7 @@ func (f *File) Seek(offset int64, whence int) (int64, error) { // Write implements the io.Writer interface. Writes are performed against a temporary local file. The temp file is // closed and flushed to Azure with f.Close() is called. func (f *File) Write(p []byte) (int, error) { - if err := f.checkTempFile(); err != nil { + if err := f.checkTempFile(true); err != nil { return 0, utils.WrapWriteError(err) } @@ -350,7 +350,7 @@ func (f *File) URI() string { return utils.GetFileURI(f) } -func (f *File) checkTempFile() error { +func (f *File) checkTempFile(isWrite bool) error { if f.tempFile == nil { client, err := f.location.fileSystem.Client() if err != nil { @@ -361,23 +361,23 @@ func (f *File) checkTempFile() error { if err != nil { return err } - if !exists { - tf, tfErr := os.CreateTemp("", fmt.Sprintf("%s.%d", path.Base(f.Name()), time.Now().UnixNano())) - if tfErr != nil { - return tfErr + + tf, tfErr := os.CreateTemp("", fmt.Sprintf("%s.%d", path.Base(f.Name()), time.Now().UnixNano())) + if tfErr != nil { + return tfErr + } + f.tempFile = tf + + if !isWrite { + if !exists { + return os.ErrNotExist } - f.tempFile = tf - } else { + reader, dlErr := client.Download(f) if dlErr != nil { return dlErr } - tf, tfErr := os.CreateTemp("", fmt.Sprintf("%s.%d", path.Base(f.Name()), time.Now().UnixNano())) - if tfErr != nil { - return tfErr - } - buffer := make([]byte, utils.TouchCopyMinBufferSize) if _, err := io.CopyBuffer(tf, reader, buffer); err != nil { return err @@ -386,8 +386,6 @@ func (f *File) checkTempFile() error { if _, err := tf.Seek(0, 0); err != nil { return err } - - f.tempFile = tf } } return nil diff --git a/backend/azure/file_test.go b/backend/azure/file_test.go index 1997cb94..998303f5 100644 --- a/backend/azure/file_test.go +++ b/backend/azure/file_test.go @@ -91,7 +91,6 @@ func (s *FileTestSuite) TestWrite() { s.NotNil(f) s.Require().NoError(err) client.EXPECT().Properties("test-container", "/foo.txt").Return(&BlobProperties{}, nil) - client.EXPECT().Download(mock.Anything).Return(io.NopCloser(strings.NewReader("Hello World!")), nil) n, err := f.Write([]byte(" Aaaaand, Goodbye!")) s.Require().NoError(err) s.Equal(18, n) @@ -375,7 +374,7 @@ func (s *FileTestSuite) TestCheckTempFile() { s.Nil(azureFile.tempFile, "No calls to checkTempFile have occurred so we expect tempFile to be nil") client.EXPECT().Properties("test-container", "/foo.txt").Return(&BlobProperties{}, nil) client.EXPECT().Download(mock.Anything).Return(io.NopCloser(strings.NewReader("Hello World!")), nil) - err = azureFile.checkTempFile() + err = azureFile.checkTempFile(false) s.Require().NoError(err, "Check temp file should create a local temp file so no error is expected") s.NotNil(azureFile.tempFile, "After the call to checkTempFile we should have a non-nil tempFile") @@ -397,7 +396,7 @@ func (s *FileTestSuite) TestCheckTempFile_FileDoesNotExist() { s.Nil(azureFile.tempFile, "No calls to checkTempFile have occurred so we expect tempFile to be nil") client.EXPECT().Properties("test-container", "/foo.txt").Return(nil, errBlobNotFound) - err = azureFile.checkTempFile() + err = azureFile.checkTempFile(true) s.Require().NoError(err, "Check temp file should create a local temp file so no error is expected") s.NotNil(azureFile.tempFile, "After the call to checkTempFile we should have a non-nil tempFile") @@ -420,7 +419,7 @@ func (s *FileTestSuite) TestCheckTempFile_DownloadError() { s.Nil(azureFile.tempFile, "No calls to checkTempFile have occurred so we expect tempFile to be nil") client.EXPECT().Properties("test-container", "/foo.txt").Return(&BlobProperties{}, nil) client.EXPECT().Download(mock.Anything).Return(nil, errors.New("i always error")) - err = azureFile.checkTempFile() + err = azureFile.checkTempFile(false) s.Require().Error(err, "The call to client.Download() errors so we expect to get an error") } diff --git a/backend/ftp/location.go b/backend/ftp/location.go index 2db13605..c901edac 100644 --- a/backend/ftp/location.go +++ b/backend/ftp/location.go @@ -152,6 +152,10 @@ func (l *Location) Path() string { // Exists returns true if the remote FTP directory exists. func (l *Location) Exists() (bool, error) { + if l.path == "/" { + return true, nil + } + dc, err := l.fileSystem.DataConn(context.TODO(), l.Authority(), types.SingleOp, nil) if err != nil { return false, err diff --git a/backend/ftp/location_test.go b/backend/ftp/location_test.go index 4bd3ae6c..f8eafc7e 100644 --- a/backend/ftp/location_test.go +++ b/backend/ftp/location_test.go @@ -411,21 +411,6 @@ func (lt *locationTestSuite) TestExists() { // location exists locPath := "/" - entries := []*_ftp.Entry{ - { - Name: "file.txt", - Target: "", - Type: _ftp.EntryTypeFile, - Time: time.Now().UTC(), - }, - { - Name: locPath, - Target: "", - Type: _ftp.EntryTypeFolder, - Time: time.Now().UTC(), - }, - } - lt.client.EXPECT().List(locPath).Return(entries, nil).Once() loc, err := lt.ftpfs.NewLocation(authorityStr, locPath) lt.Require().NoError(err) exists, err := loc.Exists() @@ -434,7 +419,7 @@ func (lt *locationTestSuite) TestExists() { // locations does not exist locPath = "/my/dir/" - entries = []*_ftp.Entry{ + entries := []*_ftp.Entry{ { Name: "file.txt", Target: "", diff --git a/testcontainers/CHANGELOG.md b/testcontainers/CHANGELOG.md new file mode 100644 index 00000000..ef88a6b9 --- /dev/null +++ b/testcontainers/CHANGELOG.md @@ -0,0 +1,7 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] 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..cbd709cb --- /dev/null +++ b/testcontainers/azurite.go @@ -0,0 +1,53 @@ +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.WithCmdArgs("--skipApiVersionCheck"), + ) + 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..9265f1c1 --- /dev/null +++ b/testcontainers/backend_integration_test.go @@ -0,0 +1,97 @@ +//go:build linux + +package testcontainers + +import ( + "fmt" + "os" + "path/filepath" + "sync" + "testing" + + "github.com/stretchr/testify/suite" + + "github.com/c2fo/vfs/v7" + "github.com/c2fo/vfs/v7/vfssimple" +) + +type vfsTestSuite struct { + suite.Suite + testLocations map[string]vfs.Location +} + +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 + wg.Add(len(registers)) + for i := range registers { + go func() { + uris[i] = registers[i](s.T()) + wg.Done() + }() + } + wg.Wait() + + s.testLocations = make(map[string]vfs.Location) + for _, loc := range uris { + l, err := vfssimple.NewLocation(loc) + s.Require().NoError(err) + + // For file:// locations, ensure directory exists + if l.FileSystem().Scheme() == "file" { + exists, err := l.Exists() + if err != nil { + panic(err) + } + if !exists { + err := os.Mkdir(l.Path(), 0750) + if err != nil { + panic(err) + } + } + } + + // Store location by scheme - no type assertion needed + 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())) +} + +// TestScheme runs conformance tests for each configured backend +func (s *vfsTestSuite) TestScheme() { + for scheme, location := range s.testLocations { + fmt.Printf("************** TESTING scheme: %s **************\n", scheme) + + // Determine conformance options based on scheme + opts := ConformanceOptions{ + SkipFTPSpecificTests: scheme == "ftp", + SkipTouchTimestampTest: scheme == "ftp", + } + + // Run the exported conformance tests + s.Run(scheme, func() { + RunConformanceTests(s.T(), location, opts) + }) + } +} + +func TestVFS(t *testing.T) { + suite.Run(t, new(vfsTestSuite)) +} diff --git a/testcontainers/conformance.go b/testcontainers/conformance.go new file mode 100644 index 00000000..2945b5d4 --- /dev/null +++ b/testcontainers/conformance.go @@ -0,0 +1,646 @@ +// Package testcontainers provides conformance tests for VFS backend implementations. +// +// These tests can be imported by any backend (core or contrib) to verify +// correct implementation of the vfs.FileSystem, vfs.Location, and vfs.File interfaces. +// +// Usage: +// +// //go:build vfsintegration +// +// package mybackend +// +// import ( +// "testing" +// "github.com/c2fo/vfs/v7/testcontainers" +// ) +// +// func TestConformance(t *testing.T) { +// fs := NewFileSystem(/* options */) +// loc, _ := fs.NewLocation("", "/test-path/") +// testcontainers.RunConformanceTests(t, loc) +// } +package testcontainers + +import ( + "fmt" + "io" + "net/url" + "os" + "path" + "regexp" + "strconv" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/c2fo/vfs/v7" + "github.com/c2fo/vfs/v7/utils" +) + +// ConformanceOptions configures conformance test behavior +type ConformanceOptions struct { + // SkipTouchTimestampTest skips the Touch timestamp update assertion. + // Some backends (e.g., Dropbox) may not update timestamps when content is unchanged. + SkipTouchTimestampTest bool + + // SkipFTPSpecificTests skips tests that don't work well with FTP + SkipFTPSpecificTests bool +} + +// RunConformanceTests runs all conformance tests against the provided location. +// This is the main entry point for backend conformance testing. +func RunConformanceTests(t *testing.T, baseLoc vfs.Location, opts ...ConformanceOptions) { + t.Helper() + opt := ConformanceOptions{} + if len(opts) > 0 { + opt = opts[0] + } + + t.Run("FileSystem", func(t *testing.T) { + RunFileSystemTests(t, baseLoc) + }) + + t.Run("Location", func(t *testing.T) { + RunLocationTests(t, baseLoc) + }) + + t.Run("File", func(t *testing.T) { + RunFileTests(t, baseLoc, opt) + }) +} + +// RunFileSystemTests tests vfs.FileSystem interface conformance +func RunFileSystemTests(t *testing.T, baseLoc vfs.Location) { + t.Helper() + fs := baseLoc.FileSystem() + + // NewFile initializes a File on the specified Authority string at path 'absFilePath'. + 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 { + require.NoError(t, err, "there should be no error") + expected := buildExpectedURI(fs, baseLoc.Authority().String(), path.Clean(name)) + assert.Equal(t, expected, file.URI(), "uri's should match") + } else { + require.Error(t, 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. + 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 { + require.NoError(t, err, "there should be no error") + expected := buildExpectedURI(fs, baseLoc.Authority().String(), utils.EnsureTrailingSlash(path.Clean(name))) + assert.Equal(t, expected, loc.URI(), "uri's should match") + } else { + require.Error(t, err, "should have validation error for scheme[%s] and name[%s]", fs.Scheme(), name) + } + } +} + +// RunLocationTests tests vfs.Location interface conformance +func RunLocationTests(t *testing.T, baseLoc vfs.Location) { + t.Helper() + + srcLoc, err := baseLoc.NewLocation("locTestSrc/") + require.NoError(t, err, "there should be no error") + defer func() { + // clean up srcLoc after test for OS + if srcLoc.FileSystem().Scheme() == "file" { + exists, err := srcLoc.Exists() + require.NoError(t, err) + if exists { + require.NoError(t, os.RemoveAll(srcLoc.Path()), "failed to clean up location test srcLoc") + } + } + }() + + // NewLocation is an initializer for a new Location relative to the existing one. + 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 { + require.NoError(t, err, "there should be no error") + expected := buildExpectedURI(srcLoc.FileSystem(), baseLoc.Authority().String(), + utils.EnsureTrailingSlash(path.Clean(path.Join(srcLoc.Path(), name)))) + assert.Equal(t, expected, loc.URI(), "uri's should match") + } else { + require.Error(t, 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. + 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 { + require.NoError(t, err, "there should be no error") + expected := buildExpectedURI(srcLoc.FileSystem(), srcLoc.Authority().String(), path.Clean(path.Join(srcLoc.Path(), name))) + assert.Equal(t, expected, file.URI(), "uri's should match") + } else { + require.Error(t, err, "should have validation error for scheme and name: %s : +%s+", srcLoc.FileSystem().Scheme(), name) + } + } + + // ChangeDir / NewLocation tests + cdTestLoc, err := srcLoc.NewLocation("chdirTest/") + require.NoError(t, err) + + _, err = cdTestLoc.NewLocation("") + require.Error(t, err, "empty string should error") + _, err = cdTestLoc.NewLocation("/home/") + require.Error(t, err, "absolute path should error") + _, err = cdTestLoc.NewLocation("file.txt") + require.Error(t, err, "file should error") + cdTestLoc, err = cdTestLoc.NewLocation("l1dir1/./l2dir1/../l2dir2/") + require.NoError(t, err, "should be no error for relative path") + + // Path returns absolute location path + assert.True(t, strings.HasSuffix(cdTestLoc.Path(), "locTestSrc/chdirTest/l1dir1/l2dir2/"), "should end with dot dirs resolved") + assert.True(t, strings.HasPrefix(cdTestLoc.Path(), "/"), "should start with slash (abs path)") + + // URI returns the fully qualified URI for the Location + assert.True(t, strings.HasSuffix(cdTestLoc.URI(), "locTestSrc/chdirTest/l1dir1/l2dir2/"), "should end with dot dirs resolved") + prefix := cdTestLoc.FileSystem().Scheme() + "://" + assert.True(t, strings.HasPrefix(cdTestLoc.URI(), prefix), "should start with schema and abs slash") + + // Exists + exists, err := baseLoc.Exists() + require.NoError(t, err) + assert.True(t, exists, "baseLoc location exists check") + + // setup list tests + f1, err := srcLoc.NewFile("file1.txt") + require.NoError(t, err) + _, err = f1.Write([]byte("this is a test file")) + require.NoError(t, err) + require.NoError(t, f1.Close()) + + f2, err := srcLoc.NewFile("file2.txt") + require.NoError(t, err) + require.NoError(t, f1.CopyToFile(f2)) + require.NoError(t, f1.Close()) + + f3, err := srcLoc.NewFile("self.txt") + require.NoError(t, err) + require.NoError(t, f1.CopyToFile(f3)) + require.NoError(t, f1.Close()) + + subLoc, err := srcLoc.NewLocation("somepath/") + require.NoError(t, err) + + f4, err := subLoc.NewFile("that.txt") + require.NoError(t, err) + require.NoError(t, f1.CopyToFile(f4)) + require.NoError(t, f1.Close()) + + // List + files, err := srcLoc.List() + require.NoError(t, err) + assert.Len(t, files, 3, "list srcLoc location") + + files, err = subLoc.List() + require.NoError(t, err) + assert.Len(t, files, 1, "list subLoc location") + assert.Equal(t, "that.txt", files[0], "returned basename") + + files, err = cdTestLoc.List() + require.NoError(t, err) + assert.Empty(t, files, "non-existent location") + + // ListByPrefix + files, err = srcLoc.ListByPrefix("file") + require.NoError(t, err) + assert.Len(t, files, 2, "list srcLoc location matching prefix") + + files, err = srcLoc.ListByPrefix("s") + require.NoError(t, err) + assert.Len(t, files, 1, "list srcLoc location") + assert.Equal(t, "self.txt", files[0], "returned only file basename, not subdir matching prefix") + + files, err = srcLoc.ListByPrefix("somepath/t") + require.NoError(t, err) + assert.Len(t, files, 1, "list 'somepath' location relative to srcLoc") + assert.Equal(t, "that.txt", files[0], "returned only file basename, using relative prefix") + + files, err = cdTestLoc.List() + require.NoError(t, err) + assert.Empty(t, files, "non-existent location") + + // ListByRegex + files, err = srcLoc.ListByRegex(regexp.MustCompile("^f")) + require.NoError(t, err) + assert.Len(t, files, 2, "list srcLoc location matching prefix") + + files, err = srcLoc.ListByRegex(regexp.MustCompile(`.txt$`)) + require.NoError(t, err) + assert.Len(t, files, 3, "list srcLoc location matching prefix") + + files, err = srcLoc.ListByRegex(regexp.MustCompile(`Z`)) + require.NoError(t, err) + assert.Empty(t, files, "list srcLoc location matching prefix") + + // DeleteFile + require.NoError(t, srcLoc.DeleteFile(f1.Name()), "deleteFile file1") + require.NoError(t, srcLoc.DeleteFile(f2.Name()), "deleteFile file2") + require.NoError(t, srcLoc.DeleteFile(f3.Name()), "deleteFile self.txt") + require.NoError(t, srcLoc.DeleteFile("somepath/that.txt"), "deleted relative path") + + // should error if file doesn't exist + require.Error(t, srcLoc.DeleteFile(f1.Path()), "deleteFile trying to delete a file already deleted") +} + +// RunFileTests tests vfs.File interface conformance +func RunFileTests(t *testing.T, baseLoc vfs.Location, opts ConformanceOptions) { + t.Helper() + + srcLoc, err := baseLoc.NewLocation("fileTestSrc/") + require.NoError(t, err) + defer func() { + // clean up srcLoc after test for OS + if srcLoc.FileSystem().Scheme() == "file" { + exists, err := srcLoc.Exists() + require.NoError(t, err) + if exists { + require.NoError(t, os.RemoveAll(srcLoc.Path()), "failed to clean up file test srcLoc") + } + } + }() + + // setup srcFile + srcFile, err := srcLoc.NewFile("srcFile.txt") + require.NoError(t, err) + + // io.Writer + sz, err := srcFile.Write([]byte("this is a test\n")) + require.NoError(t, err) + assert.Equal(t, 15, sz) + sz, err = srcFile.Write([]byte("and more text")) + require.NoError(t, err) + assert.Equal(t, 13, sz) + + // io.Closer + err = srcFile.Close() + require.NoError(t, err) + + // Exists + exists, err := srcFile.Exists() + require.NoError(t, err) + assert.True(t, exists, "file exists") + + // Name + assert.Equal(t, "srcFile.txt", srcFile.Name(), "name test") + + // Path + assert.Equal(t, path.Join(baseLoc.Path(), "fileTestSrc/srcFile.txt"), srcFile.Path(), "path test") + + // URI + assert.Equal(t, baseLoc.URI()+"fileTestSrc/srcFile.txt", srcFile.URI(), "uri test") + + // fmt.Stringer + assert.Equal(t, baseLoc.URI()+"fileTestSrc/srcFile.txt", srcFile.String(), "string(er) explicit test") + var stringer fmt.Stringer = srcFile + assert.Equal(t, baseLoc.URI()+"fileTestSrc/srcFile.txt", stringer.String(), "string(er) implicit test") + + // Size + b, err := srcFile.Size() + require.NoError(t, err) + assert.Equal(t, uint64(28), b) + + // LastModified + tm, err := srcFile.LastModified() + require.NoError(t, err) + assert.IsType(t, (*time.Time)(nil), tm, "last modified returned *time.Time") + + // Exists (again) + exists, err = srcFile.Exists() + require.NoError(t, err) + assert.True(t, exists, "file exists") + + // io.Reader and io.Seeker + str, err := io.ReadAll(srcFile) + require.NoError(t, err) + assert.Equal(t, "this is a test\nand more text", string(str), "read was successful") + + offset, err := srcFile.Seek(3, 0) + require.NoError(t, err) + assert.Equal(t, int64(3), offset, "seek was successful") + + str, err = io.ReadAll(srcFile) + require.NoError(t, err) + assert.Equal(t, "s is a test\nand more text", string(str), "read after seek") + err = srcFile.Close() + require.NoError(t, err) + + // CopyToLocation - test copying to same location + dstLoc, err := baseLoc.NewLocation("dstLoc/") + require.NoError(t, err) + if dstLoc.FileSystem().Scheme() == "file" { + t.Cleanup(func() { + exists, err := dstLoc.Exists() + require.NoError(t, err) + if exists { + require.NoError(t, os.RemoveAll(dstLoc.Path()), "failed to clean up file test dstLoc") + } + }) + } + + _, err = srcFile.Seek(0, 0) + require.NoError(t, err) + dst, err := srcFile.CopyToLocation(dstLoc) + require.NoError(t, err) + exists, err = dst.Exists() + require.NoError(t, err) + assert.True(t, exists, "dst file should now exist") + exists, err = srcFile.Exists() + require.NoError(t, err) + assert.True(t, exists, "src file should still exist") + + // CopyToFile + dstFile1, err := dstLoc.NewFile("dstFile1.txt") + require.NoError(t, err) + exists, err = dstFile1.Exists() + require.NoError(t, err) + assert.False(t, exists, "dstFile1 file should not yet exist") + _, err = srcFile.Seek(0, 0) + require.NoError(t, err) + err = srcFile.CopyToFile(dstFile1) + require.NoError(t, err) + exists, err = dstFile1.Exists() + require.NoError(t, err) + assert.True(t, exists, "dstFile1 file should now exist") + exists, err = srcFile.Exists() + require.NoError(t, err) + assert.True(t, exists, "src file should still exist") + + // io.Copy tests (skip for FTP) + buffer := make([]byte, utils.TouchCopyMinBufferSize) + copyFile1, err := srcLoc.NewFile("copyFile1.txt") + require.NoError(t, err) + + if !opts.SkipFTPSpecificTests && srcLoc.FileSystem().Scheme() != "ftp" { + exists, err = copyFile1.Exists() + require.NoError(t, err) + assert.False(t, exists, "copyFile1 should not yet exist locally") + + _, err = srcFile.Seek(0, 0) + require.NoError(t, err) + b1, err := io.CopyBuffer(copyFile1, srcFile, buffer) + require.NoError(t, err) + assert.Equal(t, int64(28), b1) + err = copyFile1.Close() + require.NoError(t, err) + + exists, err = copyFile1.Exists() + require.NoError(t, err) + assert.Truef(t, exists, "%s should now exist locally", copyFile1) + err = copyFile1.Close() + require.NoError(t, err) + } else { + // ensure copyFile1 exists for later tests + err = copyFile1.Touch() + require.NoError(t, err) + } + + copyFile2, err := srcLoc.NewFile("copyFile2.txt") + require.NoError(t, err) + + if !opts.SkipFTPSpecificTests && srcLoc.FileSystem().Scheme() != "ftp" { + exists, err = copyFile2.Exists() + require.NoError(t, err) + assert.False(t, exists, "copyFile2 should not yet exist locally") + + _, err = srcFile.Seek(0, 0) + require.NoError(t, err) + buffer = make([]byte, utils.TouchCopyMinBufferSize) + b2, err := io.CopyBuffer(copyFile2, srcFile, buffer) + require.NoError(t, err) + assert.Equal(t, int64(28), b2) + + err = copyFile2.Close() + require.NoError(t, err) + exists, err = copyFile2.Exists() + require.NoError(t, err) + assert.True(t, exists, "copyFile2 should now exist locally") + err = copyFile2.Close() + require.NoError(t, err) + } else { + err = copyFile2.Touch() + require.NoError(t, err) + } + + // MoveToLocation tests + fileForNew, err := srcLoc.NewFile("fileForNew.txt") + require.NoError(t, err) + + if !opts.SkipFTPSpecificTests && srcLoc.FileSystem().Scheme() != "ftp" { + _, err = srcFile.Seek(0, 0) + require.NoError(t, err) + buffer = make([]byte, utils.TouchCopyMinBufferSize) + _, err = io.CopyBuffer(fileForNew, srcFile, buffer) + require.NoError(t, err) + err = fileForNew.Close() + require.NoError(t, err) + + newLoc, err := dstLoc.NewLocation("doesnotexist/") + require.NoError(t, err) + dstCopyNew, err := fileForNew.MoveToLocation(newLoc) + require.NoError(t, err) + exists, err = dstCopyNew.Exists() + require.NoError(t, err) + assert.True(t, exists) + require.NoError(t, dstCopyNew.Delete()) + } + + dstCopy1, err := copyFile1.MoveToLocation(dstLoc) + require.NoError(t, err) + exists, err = dstCopy1.Exists() + require.NoError(t, err) + assert.True(t, exists, "dstCopy1 file should now exist") + exists, err = copyFile1.Exists() + require.NoError(t, err) + assert.False(t, exists, "copyFile1 should no longer exist locally") + + // MoveToFile + dstCopy2, err := dstLoc.NewFile("dstFile2.txt") + require.NoError(t, err) + exists, err = dstCopy2.Exists() + require.NoError(t, err) + assert.False(t, exists, "dstCopy2 file should not yet exist") + err = copyFile2.MoveToFile(dstCopy2) + require.NoError(t, err) + exists, err = copyFile2.Exists() + require.NoError(t, err) + assert.False(t, exists, "copyFile2 should no longer exist locally") + exists, err = dstCopy2.Exists() + require.NoError(t, err) + assert.True(t, exists, "dstCopy2 file should now exist") + + // clean up files + require.NoError(t, dst.Delete()) + require.NoError(t, dstFile1.Delete()) + require.NoError(t, dstCopy1.Delete()) + require.NoError(t, dstCopy2.Delete()) + + // MoveToFile with spaces in path + tests := []struct { + Path, Filename string + }{ + {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 { + t.Run(strconv.Itoa(i), func(t *testing.T) { + srcSpaces, err := srcLoc.NewFile(path.Join(test.Path, test.Filename)) + require.NoError(t, err) + b, err := srcSpaces.Write([]byte("something")) + require.NoError(t, err) + assert.Equal(t, 9, b, "byte count is correct") + err = srcSpaces.Close() + require.NoError(t, err) + + testDestLoc, err := dstLoc.NewLocation(test.Path) + require.NoError(t, err) + + dstSpaces, err := srcSpaces.MoveToLocation(testDestLoc) + require.NoError(t, err) + exists, err := dstSpaces.Exists() + require.NoError(t, err) + assert.True(t, exists, "dstSpaces should now exist") + exists, err = srcSpaces.Exists() + require.NoError(t, err) + assert.False(t, exists, "srcSpaces should no longer exist") + // HACK: ftp and sftp are the only backends that encode special characters + hasSuffix := strings.HasSuffix(dstSpaces.URI(), path.Join(test.Path, test.Filename)) || + strings.HasSuffix(dstSpaces.URI(), strings.ReplaceAll(url.PathEscape(path.Join(test.Path, test.Filename)), "%2F", "/")) + assert.True(t, hasSuffix, "destination file %s ends with source string for %s", dstSpaces.URI(), path.Join(test.Path, test.Filename)) + + newSrcSpaces, err := dstSpaces.MoveToLocation(srcSpaces.Location()) + require.NoError(t, err) + exists, err = newSrcSpaces.Exists() + require.NoError(t, err) + assert.True(t, exists, "newSrcSpaces should now exist") + exists, err = dstSpaces.Exists() + require.NoError(t, err) + assert.False(t, exists, "dstSpaces should no longer exist") + // HACK: ftp and sftp are the only backends that encode special characters + hasSuffix = strings.HasSuffix(newSrcSpaces.URI(), path.Join(test.Path, test.Filename)) || + strings.HasSuffix(newSrcSpaces.URI(), strings.ReplaceAll(url.PathEscape(path.Join(test.Path, test.Filename)), "%2F", "/")) + assert.True(t, hasSuffix, "destination file %s ends with source string for %s", dstSpaces.URI(), path.Join(test.Path, test.Filename)) + + require.NoError(t, newSrcSpaces.Delete()) + exists, err = newSrcSpaces.Exists() + require.NoError(t, err) + assert.False(t, exists, "newSrcSpaces should now exist") + }) + } + + // Touch tests + touchedFile, err := srcLoc.NewFile("touch.txt") + require.NoError(t, err) + defer func() { _ = touchedFile.Delete() }() + exists, err = touchedFile.Exists() + require.NoError(t, err) + assert.Falsef(t, exists, "%s shouldn't yet exist", touchedFile) + + err = touchedFile.Touch() + require.NoError(t, err) + exists, err = touchedFile.Exists() + require.NoError(t, err) + assert.Truef(t, exists, "%s now exists", touchedFile) + + size, err := touchedFile.Size() + require.NoError(t, err) + assert.Zerof(t, size, "%s should be empty", touchedFile) + + // Touch timestamp update test (optional) + if !opts.SkipTouchTimestampTest { + modified, err := touchedFile.LastModified() + require.NoError(t, err) + modifiedDeRef := *modified + time.Sleep(2 * time.Second) + err = touchedFile.Touch() + require.NoError(t, err) + newModified, err := touchedFile.LastModified() + require.NoError(t, err) + assert.Greaterf(t, *newModified, modifiedDeRef, "touch updated modified date for %s", touchedFile) + } + + // Delete + require.NoError(t, srcFile.Delete()) + exists, err = srcFile.Exists() + require.NoError(t, err) + assert.False(t, exists, "file no longer exists") + + // Operations on non-existent file should error + srcFile, err = srcLoc.NewFile("thisFileDoesNotExist") + require.NoError(t, err, "unexpected error creating file") + + exists, err = srcFile.Exists() + require.NoError(t, err) + assert.False(t, exists, "file should not exist") + + size, err = srcFile.Size() + require.Error(t, err, "expected error because file does not exist") + assert.Zero(t, size) + + _, err = srcFile.LastModified() + require.Error(t, err, "expected error because file does not exist") + + seeked, err := srcFile.Seek(-1, 2) + require.Error(t, err, "expected error because file does not exist") + assert.Zero(t, seeked) + + _, err = srcFile.Read(make([]byte, 1)) + require.Error(t, err, "expected error because file does not exist") +} + +func buildExpectedURI(fs vfs.FileSystem, authorityStr, p string) string { + return fmt.Sprintf("%s://%s%s", fs.Scheme(), authorityStr, p) +} diff --git a/testcontainers/doc.go b/testcontainers/doc.go new file mode 100644 index 00000000..96d9ed3a --- /dev/null +++ b/testcontainers/doc.go @@ -0,0 +1,5 @@ +/* +Package testcontainers provides conformance tests for VFS backend implementations. +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..bf218ccd --- /dev/null +++ b/testcontainers/go.mod @@ -0,0 +1,147 @@ +module github.com/c2fo/vfs/testcontainers + +go 1.24.11 + +replace github.com/c2fo/vfs/v7 => .. + +require ( + cloud.google.com/go/storage v1.59.1 + github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4 + github.com/aws/aws-sdk-go-v2 v1.41.1 + github.com/aws/aws-sdk-go-v2/config v1.32.7 + github.com/aws/aws-sdk-go-v2/credentials v1.19.7 + github.com/aws/aws-sdk-go-v2/service/s3 v1.95.1 + github.com/c2fo/vfs/v7 v7.13.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.47.0 + google.golang.org/api v0.262.0 +) + +require ( + cel.dev/expr v0.25.1 // indirect + cloud.google.com/go v0.123.0 // indirect + cloud.google.com/go/auth v0.18.1 // 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.21.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 // 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.31.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 // indirect + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.21.0 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 // 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.17 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect + github.com/aws/smithy-go v1.24.0 // 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-20260121142036-a486691bba94 // 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.3.0 // 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.11 // indirect + github.com/googleapis/gax-go/v2 v2.16.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.3 // 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.2.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.1.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.12 // indirect + github.com/sirupsen/logrus v1.9.4 // 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.16 // indirect + github.com/tklauser/numcpus v0.11.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.39.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect + go.opentelemetry.io/otel v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect + go.opentelemetry.io/otel/metric v1.39.0 // indirect + go.opentelemetry.io/otel/sdk v1.39.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.39.0 // indirect + go.opentelemetry.io/otel/trace v1.39.0 // indirect + go.opentelemetry.io/proto/otlp v1.8.0 // indirect + golang.org/x/mod v0.32.0 // indirect + golang.org/x/net v0.49.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/text v0.33.0 // indirect + golang.org/x/time v0.14.0 // indirect + google.golang.org/genproto v0.0.0-20260126211449-d11affda4bed // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260126211449-d11affda4bed // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260126211449-d11affda4bed // indirect + google.golang.org/grpc v1.78.0 // indirect + google.golang.org/protobuf v1.36.11 // 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..f33d646d --- /dev/null +++ b/testcontainers/go.sum @@ -0,0 +1,364 @@ +cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= +cel.dev/expr v0.25.1/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.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs= +cloud.google.com/go/auth v0.18.1/go.mod h1:GfTYoS9G3CWpRA3Va9doKN9mjPGRS+v41jmZAhBzbrA= +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.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8= +cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk= +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.3.0 h1:DgAN907x+sP0nScYfBzneRiIhWoXcpCD8ZAut8WX9vs= +cloud.google.com/go/pubsub/v2 v2.3.0/go.mod h1:O5f0KHG9zDheZAd3z5rlCRhxt2JQtB+t/IYLKK3Bpvw= +cloud.google.com/go/storage v1.59.1 h1:DXAZLcTimtiXdGqDSnebROVPd9QvRsFVVlptz02Wk58= +cloud.google.com/go/storage v1.59.1/go.mod h1:cMWbtM+anpC74gn6qjLh+exqYcfmB9Hqe5z6adx+CLI= +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.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0= +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.4 h1:jWQK1GI+LeGGUKBADtcH2rRqPxYB1Ljwms5gFA2LqrM= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4/go.mod h1:8mwH4klAm9DUgR2EEHyEEAQlRDvLPyg5fQry3y+cDew= +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.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0 h1:7t/qx5Ost0s0wbA/VDrByOooURhp+ikYwv20i9Y07TQ= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 h1:0s6TxfCu2KHkkZPnBfsQ2y5qia0jl3MMrmBhu3nCOYk= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.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.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU= +github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4= +github.com/aws/aws-sdk-go-v2/config v1.32.7 h1:vxUyWGUwmkQ2g19n7JY/9YL8MfAIl7bTesIUykECXmY= +github.com/aws/aws-sdk-go-v2/config v1.32.7/go.mod h1:2/Qm5vKUU/r7Y+zUk/Ptt2MDAEKAfUtKc1+3U1Mo3oY= +github.com/aws/aws-sdk-go-v2/credentials v1.19.7 h1:tHK47VqqtJxOymRrNtUXN5SP/zUTvZKeLx4tH6PGQc8= +github.com/aws/aws-sdk-go-v2/credentials v1.19.7/go.mod h1:qOZk8sPDrxhf+4Wf4oT2urYJrYt3RejHSzgAquYeppw= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.21.0 h1:pQZGI0qQXeCHZHMeWzhwPu+4jkWrdrIb2dgpG4OKmco= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.21.0/go.mod h1:XGq5kImVqQT4HUNbbG+0Y8O74URsPNH7CGPg1s1HW5E= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17/go.mod h1:5M5CI3D12dNOtH3/mk6minaRwI2/37ifCURZISxA/IQ= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 h1:WWLqlh79iO48yLkj1v3ISRNiv+3KdQoZ6JWyfcsyQik= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM= +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.17 h1:JqcdRG//czea7Ppjb+g/n4o8i/R50aTBHkA7vu0lK+k= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17/go.mod h1:CO+WeGmIdj/MlPel2KwID9Gt7CNq4M65HUfBW97liM0= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 h1:Z5EiPIzXKewUQK0QTMkutjiaPVeVYXX7KIqhXu/0fXs= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8/go.mod h1:FsTpJtvC4U1fyDXk7c71XoDv3HlRm8V3NiYLeYLh5YE= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17/go.mod h1:F2xxQ9TZz5gDWsclCtPQscGpP0VUOc8RqgFM3vDENmU= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 h1:bGeHBsGZx0Dvu/eJC0Lh9adJa3M1xREcndxLNZlve2U= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17/go.mod h1:dcW24lbU0CzHusTE8LLHhRLI42ejmINN8Lcr22bwh/g= +github.com/aws/aws-sdk-go-v2/service/s3 v1.95.1 h1:C2dUPSnEpy4voWFIq3JNd8gN0Y5vYGDo44eUE58a/p8= +github.com/aws/aws-sdk-go-v2/service/s3 v1.95.1/go.mod h1:5jggDlZ2CLQhwJBiZJb4vfk4f0GxWdEDruWKEJ1xOdo= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 h1:v6EiMvhEYBoHABfbGB4alOYmCIrcgyPPiBE1wZAEbqk= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.9/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 h1:gd84Omyu9JLriJVCbGApcLzVR3XtmC4ZDPcAI6Ftvds= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ= +github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= +github.com/aws/smithy-go v1.24.0/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-20260121142036-a486691bba94 h1:kkHPnzHm5Ln7WA0XYjrr2ITA0l9Vs6H++Ni//P+SZso= +github.com/cncf/xds/go v0.0.0-20260121142036-a486691bba94/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.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.5-0.20251024222203-75eaa193e329 h1:K+fnvUM0VZ7ZFJf0n4L/BRlnsb9pL/GuDG6FqaH+PwM= +github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329/go.mod h1:Alz8LEClvR7xKsrq3qzoc4N0guvVNSS8KmSChGYr9hs= +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.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4= +github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA= +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.1 h1:HyOM6qd9gF9sf15AvhbptGHUnaLTpEI9akAFFU3VyW0= +github.com/google/renameio/v2 v2.0.1/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.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao= +github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8= +github.com/googleapis/gax-go/v2 v2.16.0 h1:iHbQmKLLZrexmb0OSsNGTeSTS0HO4YvFOG8g5E4Zd0Y= +github.com/googleapis/gax-go/v2 v2.16.0/go.mod h1:o1vfQjjNZn4+dPnRdl/4ZD7S9414Y4xA+a/6Icj6l14= +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.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw= +github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +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.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8= +github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU= +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.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ= +github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw= +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.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.12 h1:e7PvW/0RmJ8p8vPGJH4jvNkOyLmbkXgXW4m6ZPic6CY= +github.com/shirou/gopsutil/v4 v4.25.12/go.mod h1:EivAfP5x2EhLp2ovdpKSozecVXn1TmuG7SMzs/Wh4PU= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +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.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= +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.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= +github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= +github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= +github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= +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.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE= +go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0 h1:RN3ifU8y4prNWeEnQp2kRRHz8UwonAEYZl8tUzHEXAk= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0/go.mod h1:habDz3tEWiFANTo6oUE99EmaFUrCNYAAg3wiVmusm70= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +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.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +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.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.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.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= +golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +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.262.0 h1:4B+3u8He2GwyN8St3Jhnd3XRHlIvc//sBmgHSp78oNY= +google.golang.org/api v0.262.0/go.mod h1:jNwmH8BgUBJ/VrUG6/lIl9YiildyLd09r9ZLHiQ6cGI= +google.golang.org/genproto v0.0.0-20260126211449-d11affda4bed h1:qZW022+WR7NN5TKrr24jcoT1rTS8Qc28YBPCYq7cxIU= +google.golang.org/genproto v0.0.0-20260126211449-d11affda4bed/go.mod h1:SpjiK7gGN2j/djoQMxLl3QOe/J/XxNzC5M+YLecVVWU= +google.golang.org/genproto/googleapis/api v0.0.0-20260126211449-d11affda4bed h1:3ip6+kOPIfzoQ5Gx9IOq79L1dEoarwV51IOs24iQvZE= +google.golang.org/genproto/googleapis/api v0.0.0-20260126211449-d11affda4bed/go.mod h1:p3MLuOwURrGBRoEyFHBT3GjUwaCQVKeNqqWxlcISGdw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260126211449-d11affda4bed h1:Yyog7dFpq0nVFnxj1NymkvC4RDIzc7KILL6vNAgLbCs= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260126211449-d11affda4bed/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= +google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/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.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_conformance.go b/testcontainers/io_conformance.go new file mode 100644 index 00000000..43db78c7 --- /dev/null +++ b/testcontainers/io_conformance.go @@ -0,0 +1,369 @@ +package testcontainers + +import ( + "errors" + "io" + "regexp" + "strconv" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/c2fo/vfs/v7" + "github.com/c2fo/vfs/v7/options" +) + +// IOOptions configures IO test behavior +type IOOptions struct { + // SkipFTPSpecificTests skips tests that don't work well with FTP + SkipFTPSpecificTests bool +} + +// ReadWriteSeekCloseURINamer interface for IO testing +type ReadWriteSeekCloseURINamer interface { + io.ReadWriteSeeker + io.Closer + Name() string + URI() string + Delete(opts ...options.DeleteOption) error +} + +// IOTestCase defines a single IO test scenario +type IOTestCase struct { + Description string + Sequence string + FileAlreadyExists bool + ExpectFailure bool + ExpectedResults string +} + +// DefaultIOTestCases returns the standard set of IO test cases +func DefaultIOTestCases() []IOTestCase { + return []IOTestCase{ + // Read, Close file + { + Description: "Read, Close, file exists", + Sequence: "R(all);C()", + FileAlreadyExists: true, + ExpectFailure: false, + ExpectedResults: "some text", + }, + { + Description: "Read, Close, file does not exist", + Sequence: "R(all);C()", + FileAlreadyExists: false, + ExpectFailure: true, + ExpectedResults: "", + }, + + // Read, Seek, Read, Close + { + Description: "Read, Seek, Read, Close, file exists", + Sequence: "R(4);S(0,0);R(4);C()", + FileAlreadyExists: true, + ExpectFailure: false, + ExpectedResults: "some text", + }, + + // Write, Close + { + Description: "Write, Close, file does not exist", + Sequence: "W(abc);C()", + FileAlreadyExists: false, + ExpectFailure: false, + ExpectedResults: "abc", + }, + { + Description: "Write, Close, file exists", + Sequence: "W(abc);C()", + FileAlreadyExists: true, + ExpectFailure: false, + ExpectedResults: "abc", + }, + + // Write, Seek, Write, Close + { + Description: "Write, Seek, Write, Close, file does not exist", + Sequence: "W(this and that);S(0,0);W(that);C()", + FileAlreadyExists: false, + ExpectFailure: false, + ExpectedResults: "that and that", + }, + { + Description: "Write, Seek, Write, Close, file exists", + Sequence: "W(this and that);S(0,0);W(that);C()", + FileAlreadyExists: true, + ExpectFailure: false, + ExpectedResults: "that and that", + }, + + // Seek + { + Description: "Seek, Close - file does not exist", + Sequence: "S(2,0);C()", + FileAlreadyExists: false, + ExpectFailure: true, + ExpectedResults: "", + }, + { + Description: "Seek, Close - file exists", + Sequence: "S(2,0);C()", + FileAlreadyExists: true, + ExpectFailure: false, + ExpectedResults: "some text", + }, + { + Description: "Seek, Write, Close, file exists", + Sequence: "S(5,0);W(new text);C()", + FileAlreadyExists: true, + ExpectFailure: false, + ExpectedResults: "some new text", + }, + + // Seek, Read, Close + { + Description: "Seek, Read, Close, file does not exist", + Sequence: "S(5,0);R(4);C()", + FileAlreadyExists: false, + ExpectFailure: true, + ExpectedResults: "", + }, + { + Description: "Seek, Read, Close, file exists", + Sequence: "S(5,0);R(4);C()", + FileAlreadyExists: true, + ExpectFailure: false, + ExpectedResults: "some text", + }, + + // Read, Write, Close + { + Description: "Read, Write, Close, file does not exist", + Sequence: "R(5);W(new text);C()", + FileAlreadyExists: false, + ExpectFailure: true, + ExpectedResults: "", + }, + { + Description: "Read, Write, Close, file exists", + Sequence: "R(5);W(new text);C()", + FileAlreadyExists: true, + ExpectFailure: false, + ExpectedResults: "some new text", + }, + + // Read, Seek, Write, Close + { + Description: "Read, Seek, Write, Close, file does not exist", + Sequence: "R(2);S(3,1);W(new text);C()", + FileAlreadyExists: false, + ExpectFailure: true, + ExpectedResults: "", + }, + { + Description: "Read, Seek, Write, Close, file exists", + Sequence: "R(2);S(3,1);W(new text);C()", + FileAlreadyExists: true, + ExpectFailure: false, + ExpectedResults: "some new text", + }, + + // Write, Seek, Read, Close + { + Description: "Write, Seek, Read, Close, file does not exist", + Sequence: "W(new text);S(0,0);R(5);C()", + FileAlreadyExists: false, + ExpectFailure: false, + ExpectedResults: "new text", + }, + { + Description: "Write, Seek, Read, Close, file exists", + Sequence: "W(new text);S(0,0);R(5);C()", + FileAlreadyExists: true, + ExpectFailure: false, + ExpectedResults: "new text", + }, + } +} + +// RunIOTests runs IO conformance tests against the provided location +func RunIOTests(t *testing.T, location vfs.Location, opts ...IOOptions) { + t.Helper() + opt := IOOptions{} + if len(opts) > 0 { + opt = opts[0] + } + + runIOTestsWithCases(t, location.URI(), location, DefaultIOTestCases(), opt) +} + +func runIOTestsWithCases(t *testing.T, testPath string, location vfs.Location, testCases []IOTestCase, opts IOOptions) { + t.Helper() + defer teardownTestLocation(t, testPath, location) + + for _, tc := range testCases { + t.Run(tc.Description, func(t *testing.T) { + testFileName := "testfile.txt" + + if opts.SkipFTPSpecificTests && strings.HasPrefix(tc.Description, "Write, Seek, Write") { + return + } + + func() { + file, err := setupTestFile(tc.FileAlreadyExists, location, testFileName) + defer func() { + if file != nil { + _ = file.Close() + _ = file.Delete() + } + }() + require.NoError(t, err) + + actualContents, err := ExecuteSequence(t, file, tc.Sequence) + + if tc.ExpectFailure && err == nil { + t.Fatalf("%s: expected failure but got success", tc.Description) + } + + if err != nil && !tc.ExpectFailure { + t.Fatalf("%s: expected success but got failure: %v", tc.Description, err) + } + + if tc.ExpectedResults != actualContents { + t.Fatalf("%s: expected results %s but got %s", tc.Description, tc.ExpectedResults, actualContents) + } + }() + }) + } +} + +func setupTestFile(existsBefore bool, location vfs.Location, filename string) (ReadWriteSeekCloseURINamer, error) { + f, err := location.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 teardownTestLocation(t *testing.T, _ string, location vfs.Location) { + t.Helper() + files, err := location.List() + if err != nil { + t.Logf("warning: error listing files for cleanup: %v", err) + return + } + for _, file := range files { + err := location.DeleteFile(file) + if err != nil { + t.Logf("warning: error deleting file %s: %v", file, err) + } + } +} + +// ExecuteSequence executes a sequence of IO operations and returns the final file contents +// +//nolint:gocyclo +func ExecuteSequence(t *testing.T, file ReadWriteSeekCloseURINamer, sequence string) (string, error) { + t.Helper() + commands := strings.Split(sequence, ";") + var commandErr error +SEQ: + for _, command := range commands { + commandName, commandArgs := parseCommand(t, command) + + switch commandName { + case "R": + if commandArgs[0] == "all" { + _, commandErr = io.ReadAll(file) + if commandErr != nil { + break SEQ + } + } else { + bytesize, err := strconv.ParseUint(commandArgs[0], 10, 64) + if err != nil { + t.Fatalf("invalid bytesize: %s", commandArgs[0]) + } + b := make([]byte, bytesize) + var n int + n, commandErr = file.Read(b) + if commandErr != nil { + if n > 0 && uint64(n) == bytesize && errors.Is(commandErr, io.EOF) { + commandErr = nil + } + break SEQ + } + } + case "W": + _, commandErr = file.Write([]byte(commandArgs[0])) + if commandErr != nil { + break SEQ + } + case "S": + if len(commandArgs) != 2 { + t.Fatalf("invalid number of args for Seek: %d", len(commandArgs)) + } + 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]) + } + _, commandErr = file.Seek(offset, whence) + if commandErr != nil { + break SEQ + } + case "C": + commandErr = file.Close() + if commandErr != nil { + break SEQ + } + } + } + + if commandErr != nil { + return "", commandErr + } + + vfsFile, ok := file.(vfs.File) + if !ok { + t.Fatalf("file must implement vfs.File") + } + f, err := vfsFile.Location().NewFile(vfsFile.Name()) + if err != nil { + t.Fatalf("error opening file: %s", err.Error()) + } + defer func() { _ = f.Close() }() + + 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]+)\((.*)\)$`) + +func parseCommand(t *testing.T, command string) (string, []string) { + t.Helper() + results := commandArgsRegex.FindStringSubmatch(command) + if len(results) != 3 { + t.Fatalf("invalid command string: %s", command) + } + args := strings.Split(results[2], ",") + return results[1], args +} diff --git a/testcontainers/io_integration_test.go b/testcontainers/io_integration_test.go new file mode 100644 index 00000000..823838cb --- /dev/null +++ b/testcontainers/io_integration_test.go @@ -0,0 +1,83 @@ +//go:build linux + +package testcontainers + +import ( + "os" + "sync" + "testing" + + "github.com/stretchr/testify/suite" + + "github.com/c2fo/vfs/v7" + "github.com/c2fo/vfs/v7/vfssimple" +) + +type ioTestSuite struct { + suite.Suite + testLocations map[string]vfs.Location +} + +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 + wg.Add(len(registers)) + for i := range registers { + go func() { + uris[i] = registers[i](s.T()) + wg.Done() + }() + } + wg.Wait() + + s.testLocations = make(map[string]vfs.Location) + for idx := range uris { + if uris[idx] == "" { + continue + } + l, err := vfssimple.NewLocation(uris[idx]) + s.Require().NoError(err) + + // For file:// locations, ensure directory exists + if l.FileSystem().Scheme() == "file" { + exists, err := l.Exists() + if err != nil { + panic(err) + } + if !exists { + err := os.Mkdir(l.Path(), 0750) + if err != nil { + panic(err) + } + } + } + + // Store location by scheme - no type assertion needed + s.testLocations[l.FileSystem().Scheme()] = l + } +} + +func (s *ioTestSuite) TestFileOperations() { + for scheme, location := range s.testLocations { + s.Run(scheme, func() { + opts := IOOptions{ + SkipFTPSpecificTests: scheme == "ftp", + } + RunIOTests(s.T(), location, opts) + }) + } +} + +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 +}