github.com/mckael/restic@v0.8.3/internal/backend/s3/s3_test.go (about)

     1  package s3_test
     2  
     3  import (
     4  	"context"
     5  	"crypto/rand"
     6  	"encoding/hex"
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"net"
    11  	"net/http"
    12  	"os"
    13  	"os/exec"
    14  	"path/filepath"
    15  	"testing"
    16  	"time"
    17  
    18  	"github.com/restic/restic/internal/backend"
    19  	"github.com/restic/restic/internal/backend/s3"
    20  	"github.com/restic/restic/internal/backend/test"
    21  	"github.com/restic/restic/internal/restic"
    22  	rtest "github.com/restic/restic/internal/test"
    23  )
    24  
    25  func mkdir(t testing.TB, dir string) {
    26  	err := os.MkdirAll(dir, 0700)
    27  	if err != nil {
    28  		t.Fatal(err)
    29  	}
    30  }
    31  
    32  func runMinio(ctx context.Context, t testing.TB, dir, key, secret string) func() {
    33  	mkdir(t, filepath.Join(dir, "config"))
    34  	mkdir(t, filepath.Join(dir, "root"))
    35  
    36  	cmd := exec.CommandContext(ctx, "minio",
    37  		"server",
    38  		"--address", "127.0.0.1:9000",
    39  		"--config-dir", filepath.Join(dir, "config"),
    40  		filepath.Join(dir, "root"))
    41  	cmd.Env = append(os.Environ(),
    42  		"MINIO_ACCESS_KEY="+key,
    43  		"MINIO_SECRET_KEY="+secret,
    44  	)
    45  	cmd.Stderr = os.Stderr
    46  
    47  	err := cmd.Start()
    48  	if err != nil {
    49  		t.Fatal(err)
    50  	}
    51  
    52  	// wait until the TCP port is reachable
    53  	var success bool
    54  	for i := 0; i < 100; i++ {
    55  		time.Sleep(200 * time.Millisecond)
    56  
    57  		c, err := net.Dial("tcp", "localhost:9000")
    58  		if err == nil {
    59  			success = true
    60  			if err := c.Close(); err != nil {
    61  				t.Fatal(err)
    62  			}
    63  			break
    64  		}
    65  	}
    66  
    67  	if !success {
    68  		t.Fatal("unable to connect to minio server")
    69  		return nil
    70  	}
    71  
    72  	return func() {
    73  		err = cmd.Process.Kill()
    74  		if err != nil {
    75  			t.Fatal(err)
    76  		}
    77  
    78  		// ignore errors, we've killed the process
    79  		_ = cmd.Wait()
    80  	}
    81  }
    82  
    83  func newRandomCredentials(t testing.TB) (key, secret string) {
    84  	buf := make([]byte, 10)
    85  	_, err := io.ReadFull(rand.Reader, buf)
    86  	if err != nil {
    87  		t.Fatal(err)
    88  	}
    89  	key = hex.EncodeToString(buf)
    90  
    91  	_, err = io.ReadFull(rand.Reader, buf)
    92  	if err != nil {
    93  		t.Fatal(err)
    94  	}
    95  	secret = hex.EncodeToString(buf)
    96  
    97  	return key, secret
    98  }
    99  
   100  type MinioTestConfig struct {
   101  	s3.Config
   102  
   103  	tempdir       string
   104  	removeTempdir func()
   105  	stopServer    func()
   106  }
   107  
   108  func createS3(t testing.TB, cfg MinioTestConfig, tr http.RoundTripper) (be restic.Backend, err error) {
   109  	for i := 0; i < 10; i++ {
   110  		be, err = s3.Create(cfg.Config, tr)
   111  		if err != nil {
   112  			t.Logf("s3 open: try %d: error %v", i, err)
   113  			time.Sleep(500 * time.Millisecond)
   114  			continue
   115  		}
   116  
   117  		break
   118  	}
   119  
   120  	return be, err
   121  }
   122  
   123  func newMinioTestSuite(ctx context.Context, t testing.TB) *test.Suite {
   124  	tr, err := backend.Transport(backend.TransportOptions{})
   125  	if err != nil {
   126  		t.Fatalf("cannot create transport for tests: %v", err)
   127  	}
   128  
   129  	return &test.Suite{
   130  		// NewConfig returns a config for a new temporary backend that will be used in tests.
   131  		NewConfig: func() (interface{}, error) {
   132  			cfg := MinioTestConfig{}
   133  
   134  			cfg.tempdir, cfg.removeTempdir = rtest.TempDir(t)
   135  			key, secret := newRandomCredentials(t)
   136  			cfg.stopServer = runMinio(ctx, t, cfg.tempdir, key, secret)
   137  
   138  			cfg.Config = s3.NewConfig()
   139  			cfg.Config.Endpoint = "localhost:9000"
   140  			cfg.Config.Bucket = "restictestbucket"
   141  			cfg.Config.Prefix = fmt.Sprintf("test-%d", time.Now().UnixNano())
   142  			cfg.Config.UseHTTP = true
   143  			cfg.Config.KeyID = key
   144  			cfg.Config.Secret = secret
   145  			return cfg, nil
   146  		},
   147  
   148  		// CreateFn is a function that creates a temporary repository for the tests.
   149  		Create: func(config interface{}) (restic.Backend, error) {
   150  			cfg := config.(MinioTestConfig)
   151  
   152  			be, err := createS3(t, cfg, tr)
   153  			if err != nil {
   154  				return nil, err
   155  			}
   156  
   157  			exists, err := be.Test(context.TODO(), restic.Handle{Type: restic.ConfigFile})
   158  			if err != nil {
   159  				return nil, err
   160  			}
   161  
   162  			if exists {
   163  				return nil, errors.New("config already exists")
   164  			}
   165  
   166  			return be, nil
   167  		},
   168  
   169  		// OpenFn is a function that opens a previously created temporary repository.
   170  		Open: func(config interface{}) (restic.Backend, error) {
   171  			cfg := config.(MinioTestConfig)
   172  			return s3.Open(cfg.Config, tr)
   173  		},
   174  
   175  		// CleanupFn removes data created during the tests.
   176  		Cleanup: func(config interface{}) error {
   177  			cfg := config.(MinioTestConfig)
   178  			if cfg.stopServer != nil {
   179  				cfg.stopServer()
   180  			}
   181  			if cfg.removeTempdir != nil {
   182  				cfg.removeTempdir()
   183  			}
   184  			return nil
   185  		},
   186  	}
   187  }
   188  
   189  func TestBackendMinio(t *testing.T) {
   190  	defer func() {
   191  		if t.Skipped() {
   192  			rtest.SkipDisallowed(t, "restic/backend/s3.TestBackendMinio")
   193  		}
   194  	}()
   195  
   196  	// try to find a minio binary
   197  	_, err := exec.LookPath("minio")
   198  	if err != nil {
   199  		t.Skip(err)
   200  		return
   201  	}
   202  
   203  	ctx, cancel := context.WithCancel(context.Background())
   204  	defer cancel()
   205  
   206  	newMinioTestSuite(ctx, t).RunTests(t)
   207  }
   208  
   209  func BenchmarkBackendMinio(t *testing.B) {
   210  	// try to find a minio binary
   211  	_, err := exec.LookPath("minio")
   212  	if err != nil {
   213  		t.Skip(err)
   214  		return
   215  	}
   216  
   217  	ctx, cancel := context.WithCancel(context.Background())
   218  	defer cancel()
   219  
   220  	newMinioTestSuite(ctx, t).RunBenchmarks(t)
   221  }
   222  
   223  func newS3TestSuite(t testing.TB) *test.Suite {
   224  	tr, err := backend.Transport(backend.TransportOptions{})
   225  	if err != nil {
   226  		t.Fatalf("cannot create transport for tests: %v", err)
   227  	}
   228  
   229  	return &test.Suite{
   230  		// do not use excessive data
   231  		MinimalData: true,
   232  
   233  		// NewConfig returns a config for a new temporary backend that will be used in tests.
   234  		NewConfig: func() (interface{}, error) {
   235  			s3cfg, err := s3.ParseConfig(os.Getenv("RESTIC_TEST_S3_REPOSITORY"))
   236  			if err != nil {
   237  				return nil, err
   238  			}
   239  
   240  			cfg := s3cfg.(s3.Config)
   241  			cfg.KeyID = os.Getenv("RESTIC_TEST_S3_KEY")
   242  			cfg.Secret = os.Getenv("RESTIC_TEST_S3_SECRET")
   243  			cfg.Prefix = fmt.Sprintf("test-%d", time.Now().UnixNano())
   244  			return cfg, nil
   245  		},
   246  
   247  		// CreateFn is a function that creates a temporary repository for the tests.
   248  		Create: func(config interface{}) (restic.Backend, error) {
   249  			cfg := config.(s3.Config)
   250  
   251  			be, err := s3.Create(cfg, tr)
   252  			if err != nil {
   253  				return nil, err
   254  			}
   255  
   256  			exists, err := be.Test(context.TODO(), restic.Handle{Type: restic.ConfigFile})
   257  			if err != nil {
   258  				return nil, err
   259  			}
   260  
   261  			if exists {
   262  				return nil, errors.New("config already exists")
   263  			}
   264  
   265  			return be, nil
   266  		},
   267  
   268  		// OpenFn is a function that opens a previously created temporary repository.
   269  		Open: func(config interface{}) (restic.Backend, error) {
   270  			cfg := config.(s3.Config)
   271  			return s3.Open(cfg, tr)
   272  		},
   273  
   274  		// CleanupFn removes data created during the tests.
   275  		Cleanup: func(config interface{}) error {
   276  			cfg := config.(s3.Config)
   277  
   278  			be, err := s3.Open(cfg, tr)
   279  			if err != nil {
   280  				return err
   281  			}
   282  
   283  			return be.Delete(context.TODO())
   284  		},
   285  	}
   286  }
   287  
   288  func TestBackendS3(t *testing.T) {
   289  	defer func() {
   290  		if t.Skipped() {
   291  			rtest.SkipDisallowed(t, "restic/backend/s3.TestBackendS3")
   292  		}
   293  	}()
   294  
   295  	vars := []string{
   296  		"RESTIC_TEST_S3_KEY",
   297  		"RESTIC_TEST_S3_SECRET",
   298  		"RESTIC_TEST_S3_REPOSITORY",
   299  	}
   300  
   301  	for _, v := range vars {
   302  		if os.Getenv(v) == "" {
   303  			t.Skipf("environment variable %v not set", v)
   304  			return
   305  		}
   306  	}
   307  
   308  	t.Logf("run tests")
   309  	newS3TestSuite(t).RunTests(t)
   310  }
   311  
   312  func BenchmarkBackendS3(t *testing.B) {
   313  	vars := []string{
   314  		"RESTIC_TEST_S3_KEY",
   315  		"RESTIC_TEST_S3_SECRET",
   316  		"RESTIC_TEST_S3_REPOSITORY",
   317  	}
   318  
   319  	for _, v := range vars {
   320  		if os.Getenv(v) == "" {
   321  			t.Skipf("environment variable %v not set", v)
   322  			return
   323  		}
   324  	}
   325  
   326  	t.Logf("run tests")
   327  	newS3TestSuite(t).RunBenchmarks(t)
   328  }