github.com/unigraph-dev/dgraph@v1.1.1-0.20200923154953-8b52b426f765/ee/backup/tests/minio/backup_test.go (about) 1 /* 2 * Copyright 2018 Dgraph Labs, Inc. and Contributors * 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16 package main 17 18 import ( 19 "context" 20 "fmt" 21 "io/ioutil" 22 "math" 23 "net/http" 24 "net/url" 25 "os" 26 "strings" 27 "testing" 28 "time" 29 30 "github.com/dgraph-io/dgo" 31 "github.com/dgraph-io/dgo/protos/api" 32 minio "github.com/minio/minio-go" 33 "github.com/stretchr/testify/require" 34 "google.golang.org/grpc" 35 36 "github.com/dgraph-io/dgraph/ee/backup" 37 "github.com/dgraph-io/dgraph/testutil" 38 "github.com/dgraph-io/dgraph/x" 39 ) 40 41 var ( 42 backupDir = "./data/backups" 43 restoreDir = "./data/restore" 44 testDirs = []string{backupDir, restoreDir} 45 46 mc *minio.Client 47 bucketName = "dgraph-backup" 48 backupDestination = "minio://minio1:9001/dgraph-backup?secure=false" 49 50 alphaContainers = []string{ 51 "alpha1", 52 "alpha2", 53 "alpha3", 54 } 55 ) 56 57 func TestBackupMinio(t *testing.T) { 58 conn, err := grpc.Dial(testutil.SockAddr, grpc.WithInsecure()) 59 require.NoError(t, err) 60 dg := dgo.NewDgraphClient(api.NewDgraphClient(conn)) 61 mc, err = testutil.NewMinioClient() 62 require.NoError(t, err) 63 require.NoError(t, mc.MakeBucket(bucketName, "")) 64 65 // Add initial data. 66 ctx := context.Background() 67 require.NoError(t, dg.Alter(ctx, &api.Operation{DropAll: true})) 68 require.NoError(t, dg.Alter(ctx, &api.Operation{Schema: `movie: string .`})) 69 original, err := dg.NewTxn().Mutate(ctx, &api.Mutation{ 70 CommitNow: true, 71 SetNquads: []byte(` 72 <_:x1> <movie> "BIRDS MAN OR (THE UNEXPECTED VIRTUE OF IGNORANCE)" . 73 <_:x2> <movie> "Spotlight" . 74 <_:x3> <movie> "Moonlight" . 75 <_:x4> <movie> "THE SHAPE OF WATERLOO" . 76 <_:x5> <movie> "BLACK PUNTER" . 77 `), 78 }) 79 require.NoError(t, err) 80 t.Logf("--- Original uid mapping: %+v\n", original.Uids) 81 82 // Move tablet to group 1 to avoid messes later. 83 _, err = http.Get("http://" + testutil.SockAddrZeroHttp + "/moveTablet?tablet=movie&group=1") 84 require.NoError(t, err) 85 86 // After the move, we need to pause a bit to give zero a chance to quorum. 87 t.Log("Pausing to let zero move tablet...") 88 moveOk := false 89 for retry := 5; retry > 0; retry-- { 90 time.Sleep(3 * time.Second) 91 state, err := testutil.GetState() 92 require.NoError(t, err) 93 if _, ok := state.Groups["1"].Tablets["movie"]; ok { 94 moveOk = true 95 break 96 } 97 } 98 require.True(t, moveOk) 99 100 // Setup test directories. 101 dirSetup() 102 103 // Send backup request. 104 _ = runBackup(t, 3, 1) 105 restored := runRestore(t, backupDir, "", math.MaxUint64) 106 107 checks := []struct { 108 blank, expected string 109 }{ 110 {blank: "x1", expected: "BIRDS MAN OR (THE UNEXPECTED VIRTUE OF IGNORANCE)"}, 111 {blank: "x2", expected: "Spotlight"}, 112 {blank: "x3", expected: "Moonlight"}, 113 {blank: "x4", expected: "THE SHAPE OF WATERLOO"}, 114 {blank: "x5", expected: "BLACK PUNTER"}, 115 } 116 for _, check := range checks { 117 require.EqualValues(t, check.expected, restored[original.Uids[check.blank]]) 118 } 119 120 // Add more data for the incremental backup. 121 incr1, err := dg.NewTxn().Mutate(ctx, &api.Mutation{ 122 CommitNow: true, 123 SetNquads: []byte(fmt.Sprintf(` 124 <%s> <movie> "Birdman or (The Unexpected Virtue of Ignorance)" . 125 <%s> <movie> "The Shape of Waterloo" . 126 `, original.Uids["x1"], original.Uids["x4"])), 127 }) 128 t.Logf("%+v", incr1) 129 require.NoError(t, err) 130 131 // Perform first incremental backup. 132 _ = runBackup(t, 6, 2) 133 restored = runRestore(t, backupDir, "", incr1.Txn.CommitTs) 134 135 checks = []struct { 136 blank, expected string 137 }{ 138 {blank: "x1", expected: "Birdman or (The Unexpected Virtue of Ignorance)"}, 139 {blank: "x4", expected: "The Shape of Waterloo"}, 140 } 141 for _, check := range checks { 142 require.EqualValues(t, check.expected, restored[original.Uids[check.blank]]) 143 } 144 145 // Add more data for a second incremental backup. 146 incr2, err := dg.NewTxn().Mutate(ctx, &api.Mutation{ 147 CommitNow: true, 148 SetNquads: []byte(fmt.Sprintf(` 149 <%s> <movie> "The Shape of Water" . 150 <%s> <movie> "The Black Panther" . 151 `, original.Uids["x4"], original.Uids["x5"])), 152 }) 153 require.NoError(t, err) 154 155 // Perform second incremental backup. 156 _ = runBackup(t, 9, 3) 157 restored = runRestore(t, backupDir, "", incr2.Txn.CommitTs) 158 159 checks = []struct { 160 blank, expected string 161 }{ 162 {blank: "x4", expected: "The Shape of Water"}, 163 {blank: "x5", expected: "The Black Panther"}, 164 } 165 for _, check := range checks { 166 require.EqualValues(t, check.expected, restored[original.Uids[check.blank]]) 167 } 168 169 // Add more data for a second full backup. 170 incr3, err := dg.NewTxn().Mutate(ctx, &api.Mutation{ 171 CommitNow: true, 172 SetNquads: []byte(fmt.Sprintf(` 173 <%s> <movie> "El laberinto del fauno" . 174 <%s> <movie> "Black Panther 2" . 175 `, original.Uids["x4"], original.Uids["x5"])), 176 }) 177 require.NoError(t, err) 178 179 // Perform second full backup. 180 dirs := runBackupInternal(t, true, 12, 4) 181 restored = runRestore(t, backupDir, "", incr3.Txn.CommitTs) 182 183 // Check all the values were restored to their most recent value. 184 checks = []struct { 185 blank, expected string 186 }{ 187 {blank: "x1", expected: "Birdman or (The Unexpected Virtue of Ignorance)"}, 188 {blank: "x2", expected: "Spotlight"}, 189 {blank: "x3", expected: "Moonlight"}, 190 {blank: "x4", expected: "El laberinto del fauno"}, 191 {blank: "x5", expected: "Black Panther 2"}, 192 } 193 for _, check := range checks { 194 require.EqualValues(t, check.expected, restored[original.Uids[check.blank]]) 195 } 196 197 // Remove the full backup dirs and verify restore catches the error. 198 require.NoError(t, os.RemoveAll(dirs[0])) 199 require.NoError(t, os.RemoveAll(dirs[3])) 200 runFailingRestore(t, backupDir, "", incr3.Txn.CommitTs) 201 202 // Clean up test directories. 203 dirCleanup() 204 } 205 206 func runBackup(t *testing.T, numExpectedFiles, numExpectedDirs int) []string { 207 return runBackupInternal(t, false, numExpectedFiles, numExpectedDirs) 208 } 209 210 func runBackupInternal(t *testing.T, forceFull bool, numExpectedFiles, 211 numExpectedDirs int) []string { 212 forceFullStr := "false" 213 if forceFull { 214 forceFullStr = "true" 215 } 216 217 resp, err := http.PostForm("http://localhost:8180/admin/backup", url.Values{ 218 "destination": []string{backupDestination}, 219 "force_full": []string{forceFullStr}, 220 }) 221 require.NoError(t, err) 222 defer resp.Body.Close() 223 buf, err := ioutil.ReadAll(resp.Body) 224 require.NoError(t, err) 225 require.Contains(t, string(buf), "Backup completed.") 226 227 // Verify that the right amount of files and directories were created. 228 copyToLocalFs(t) 229 230 files := x.WalkPathFunc(backupDir, func(path string, isdir bool) bool { 231 return !isdir && strings.HasSuffix(path, ".backup") 232 }) 233 require.Equal(t, numExpectedFiles, len(files)) 234 235 dirs := x.WalkPathFunc(backupDir, func(path string, isdir bool) bool { 236 return isdir && strings.HasPrefix(path, "data/backups/dgraph.") 237 }) 238 require.Equal(t, numExpectedDirs, len(dirs)) 239 240 manifests := x.WalkPathFunc(backupDir, func(path string, isdir bool) bool { 241 return !isdir && strings.Contains(path, "manifest.json") 242 }) 243 require.Equal(t, numExpectedDirs, len(manifests)) 244 245 return dirs 246 } 247 248 func runRestore(t *testing.T, backupLocation, lastDir string, commitTs uint64) map[string]string { 249 // Recreate the restore directory to make sure there's no previous data when 250 // calling restore. 251 require.NoError(t, os.RemoveAll(restoreDir)) 252 require.NoError(t, os.MkdirAll(restoreDir, os.ModePerm)) 253 254 t.Logf("--- Restoring from: %q", backupLocation) 255 _, err := backup.RunRestore("./data/restore", backupLocation, lastDir) 256 require.NoError(t, err) 257 258 restored, err := testutil.GetPValues("./data/restore/p1", "movie", commitTs) 259 require.NoError(t, err) 260 t.Logf("--- Restored values: %+v\n", restored) 261 262 return restored 263 } 264 265 // runFailingRestore is like runRestore but expects an error during restore. 266 func runFailingRestore(t *testing.T, backupLocation, lastDir string, commitTs uint64) { 267 // Recreate the restore directory to make sure there's no previous data when 268 // calling restore. 269 require.NoError(t, os.RemoveAll(restoreDir)) 270 require.NoError(t, os.MkdirAll(restoreDir, os.ModePerm)) 271 272 _, err := backup.RunRestore("./data/restore", backupLocation, lastDir) 273 require.Error(t, err) 274 require.Contains(t, err.Error(), "expected a BackupNum value of 1") 275 } 276 277 func dirSetup() { 278 // Clean up data from previous runs. 279 dirCleanup() 280 281 for _, dir := range testDirs { 282 x.Check(os.MkdirAll(dir, os.ModePerm)) 283 } 284 } 285 286 func dirCleanup() { 287 x.Check(os.RemoveAll("./data")) 288 } 289 290 func copyToLocalFs(t *testing.T) { 291 // List all the folders in the bucket. 292 lsCh1 := make(chan struct{}) 293 defer close(lsCh1) 294 objectCh1 := mc.ListObjectsV2(bucketName, "", false, lsCh1) 295 for object := range objectCh1 { 296 require.NoError(t, object.Err) 297 dstDir := backupDir + "/" + object.Key 298 os.MkdirAll(dstDir, os.ModePerm) 299 300 // Get all the files in that folder and 301 lsCh2 := make(chan struct{}) 302 defer close(lsCh2) 303 objectCh2 := mc.ListObjectsV2(bucketName, "", true, lsCh2) 304 for object := range objectCh2 { 305 require.NoError(t, object.Err) 306 dstFile := backupDir + "/" + object.Key 307 mc.FGetObject(bucketName, object.Key, dstFile, minio.GetObjectOptions{}) 308 } 309 } 310 }