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  }