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 }