mirror of
https://github.com/vmware-tanzu/velero.git
synced 2026-06-10 00:03:10 +00:00
add repo snapshot operations
Signed-off-by: Lyndon-Li <lyonghui@vmware.com>
This commit is contained in:
@@ -25,12 +25,14 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/kopia/kopia/fs"
|
||||
"github.com/kopia/kopia/repo"
|
||||
"github.com/kopia/kopia/repo/compression"
|
||||
"github.com/kopia/kopia/repo/content/index"
|
||||
"github.com/kopia/kopia/repo/maintenance"
|
||||
"github.com/kopia/kopia/repo/manifest"
|
||||
"github.com/kopia/kopia/repo/object"
|
||||
"github.com/kopia/kopia/snapshot"
|
||||
"github.com/kopia/kopia/snapshot/snapshotmaintenance"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
@@ -446,19 +448,83 @@ func (kr *kopiaRepository) DeleteManifest(ctx context.Context, id udmrepo.ID) er
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO add implementation in following PRs
|
||||
func (kr *kopiaRepository) SaveSnapshot(ctx context.Context, snap udmrepo.Snapshot) (udmrepo.ID, error) {
|
||||
return "", errors.New("not supported")
|
||||
if kr.rawWriter == nil {
|
||||
return "", errors.New("repo writer is closed or not open")
|
||||
}
|
||||
|
||||
if snap.Source == "" {
|
||||
return "", errors.New("invalid snapshot source")
|
||||
}
|
||||
|
||||
rootObj, err := object.ParseID(string(snap.RootObject.ID))
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "error parsing root object ID %v", snap.RootObject.ID)
|
||||
}
|
||||
|
||||
manifest := snapshot.Manifest{
|
||||
Source: snapshot.SourceInfo{
|
||||
UserName: udmrepo.GetRepoUser(),
|
||||
Host: udmrepo.GetRepoDomain(),
|
||||
Path: snap.Source,
|
||||
},
|
||||
Description: snap.Description,
|
||||
StartTime: fs.UTCTimestampFromTime(snap.StartTime),
|
||||
EndTime: fs.UTCTimestampFromTime(snap.EndTime),
|
||||
RootEntry: &snapshot.DirEntry{
|
||||
Type: snapshot.EntryTypeDirectory,
|
||||
ObjectID: rootObj,
|
||||
ModTime: fs.UTCTimestampFromTime(snap.RootObject.ModTime),
|
||||
Permissions: snapshot.Permissions(snap.RootObject.Permissions),
|
||||
FileSize: snap.RootObject.Size,
|
||||
UserID: snap.RootObject.UserID,
|
||||
GroupID: snap.RootObject.GroupID,
|
||||
},
|
||||
Tags: snap.Tags,
|
||||
}
|
||||
|
||||
id, err := snapshot.SaveSnapshot(ctx, kr.rawWriter, &manifest)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "error saving snapshot")
|
||||
}
|
||||
|
||||
return udmrepo.ID(id), nil
|
||||
}
|
||||
|
||||
// TODO add implementation in following PRs
|
||||
func (kr *kopiaRepository) GetSnapshot(ctx context.Context, id udmrepo.ID) (udmrepo.Snapshot, error) {
|
||||
return udmrepo.Snapshot{}, errors.New("not supported")
|
||||
snap, err := snapshot.LoadSnapshot(ctx, kr.rawRepo, manifest.ID(id))
|
||||
if err != nil {
|
||||
return udmrepo.Snapshot{}, errors.Wrap(err, "error getting snapshot manifest")
|
||||
}
|
||||
|
||||
if snap.RootEntry == nil {
|
||||
return udmrepo.Snapshot{}, errors.Wrap(err, "invalid snapshot root entry")
|
||||
}
|
||||
|
||||
return udmrepo.Snapshot{
|
||||
Source: snap.Source.Path,
|
||||
Description: snap.Description,
|
||||
StartTime: snap.StartTime.ToTime(),
|
||||
EndTime: snap.EndTime.ToTime(),
|
||||
Tags: snap.Tags,
|
||||
RootObject: udmrepo.ObjectMetadata{
|
||||
ID: udmrepo.ID(snap.RootEntry.ObjectID.String()),
|
||||
Type: udmrepo.ObjectDataTypeMetadata,
|
||||
Size: snap.RootEntry.FileSize,
|
||||
ModTime: snap.RootEntry.ModTime.ToTime(),
|
||||
Permissions: int(snap.RootEntry.Permissions),
|
||||
UserID: snap.RootEntry.UserID,
|
||||
GroupID: snap.RootEntry.GroupID,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// TODO add implementation in following PRs
|
||||
func (kr *kopiaRepository) DeleteSnapshot(ctx context.Context, id udmrepo.ID) error {
|
||||
return errors.New("not supported")
|
||||
if _, err := kr.GetSnapshot(ctx, id); err != nil {
|
||||
return errors.Wrap(err, "error getting snapshot")
|
||||
}
|
||||
|
||||
return kr.DeleteManifest(ctx, id)
|
||||
}
|
||||
|
||||
func (kr *kopiaRepository) Flush(ctx context.Context) error {
|
||||
|
||||
@@ -18,14 +18,17 @@ package kopialib
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"math"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/kopia/kopia/fs"
|
||||
"github.com/kopia/kopia/repo"
|
||||
"github.com/kopia/kopia/repo/manifest"
|
||||
"github.com/kopia/kopia/repo/object"
|
||||
"github.com/kopia/kopia/snapshot"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -1282,3 +1285,268 @@ func TestIsReady(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveSnapshot(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
rawWriter *repomocks.MockRepositoryWriter
|
||||
snap udmrepo.Snapshot
|
||||
rawWriterRetErr error
|
||||
rawWriterRetID manifest.ID
|
||||
setWriterMock bool
|
||||
expectedErr string
|
||||
expectedID udmrepo.ID
|
||||
}{
|
||||
{
|
||||
name: "raw writer is nil",
|
||||
expectedErr: "repo writer is closed or not open",
|
||||
},
|
||||
{
|
||||
name: "invalid snapshot source",
|
||||
rawWriter: repomocks.NewMockRepositoryWriter(t),
|
||||
snap: udmrepo.Snapshot{
|
||||
Source: "",
|
||||
},
|
||||
expectedErr: "invalid snapshot source",
|
||||
},
|
||||
{
|
||||
name: "invalid root object id",
|
||||
rawWriter: repomocks.NewMockRepositoryWriter(t),
|
||||
snap: udmrepo.Snapshot{
|
||||
Source: "fake-source",
|
||||
RootObject: udmrepo.ObjectMetadata{ID: "fake-id"},
|
||||
},
|
||||
expectedErr: "error parsing root object ID fake-id: malformed content ID: \"fake-id\": invalid content prefix",
|
||||
},
|
||||
{
|
||||
name: "save snapshot fail",
|
||||
rawWriter: repomocks.NewMockRepositoryWriter(t),
|
||||
snap: udmrepo.Snapshot{
|
||||
Source: "fake-source",
|
||||
RootObject: udmrepo.ObjectMetadata{ID: "I123456"},
|
||||
},
|
||||
rawWriterRetErr: errors.New("fake-save-error"),
|
||||
setWriterMock: true,
|
||||
expectedErr: "error saving snapshot: error putting manifest: fake-save-error",
|
||||
},
|
||||
{
|
||||
name: "succeed",
|
||||
rawWriter: repomocks.NewMockRepositoryWriter(t),
|
||||
snap: udmrepo.Snapshot{
|
||||
Source: "fake-source",
|
||||
RootObject: udmrepo.ObjectMetadata{ID: "I123456"},
|
||||
},
|
||||
rawWriterRetID: manifest.ID("fake-manifest-id"),
|
||||
setWriterMock: true,
|
||||
expectedID: udmrepo.ID("fake-manifest-id"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
kr := &kopiaRepository{}
|
||||
|
||||
if tc.rawWriter != nil {
|
||||
if tc.setWriterMock {
|
||||
tc.rawWriter.On("PutManifest", mock.Anything, mock.Anything, mock.Anything).Return(tc.rawWriterRetID, tc.rawWriterRetErr)
|
||||
}
|
||||
kr.rawWriter = tc.rawWriter
|
||||
}
|
||||
|
||||
id, err := kr.SaveSnapshot(t.Context(), tc.snap)
|
||||
|
||||
if tc.expectedErr == "" {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.expectedID, id)
|
||||
} else {
|
||||
assert.EqualError(t, err, tc.expectedErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSnapshot(t *testing.T) {
|
||||
expectedTime := time.Now()
|
||||
rawObjID, _ := object.ParseID("I123456")
|
||||
|
||||
mockMani := &snapshot.Manifest{
|
||||
Source: snapshot.SourceInfo{Path: "fake-source"},
|
||||
Description: "fake-desc",
|
||||
StartTime: fs.UTCTimestampFromTime(expectedTime),
|
||||
EndTime: fs.UTCTimestampFromTime(expectedTime.Add(time.Minute)),
|
||||
RootEntry: &snapshot.DirEntry{
|
||||
ObjectID: rawObjID,
|
||||
},
|
||||
Tags: map[string]string{"tag1": "val1"},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
rawRepo *repomocks.MockRepository
|
||||
snapshotID udmrepo.ID
|
||||
rawRepoRetErr error
|
||||
setRepoMock bool
|
||||
expectedErr string
|
||||
expectedSnap udmrepo.Snapshot
|
||||
}{
|
||||
{
|
||||
name: "get snapshot fail",
|
||||
rawRepo: repomocks.NewMockRepository(t),
|
||||
snapshotID: udmrepo.ID("fake-id"),
|
||||
rawRepoRetErr: errors.New("fake-get-error"),
|
||||
setRepoMock: true,
|
||||
expectedErr: "error getting snapshot manifest: unable to find manifest entries: fake-get-error",
|
||||
},
|
||||
{
|
||||
name: "succeed",
|
||||
rawRepo: repomocks.NewMockRepository(t),
|
||||
snapshotID: udmrepo.ID("fake-id"),
|
||||
setRepoMock: true,
|
||||
expectedSnap: udmrepo.Snapshot{
|
||||
Source: "fake-source",
|
||||
Description: "fake-desc",
|
||||
StartTime: mockMani.StartTime.ToTime(),
|
||||
EndTime: mockMani.EndTime.ToTime(),
|
||||
RootObject: udmrepo.ObjectMetadata{
|
||||
ID: udmrepo.ID("I123456"),
|
||||
Type: udmrepo.ObjectDataTypeMetadata,
|
||||
},
|
||||
Tags: map[string]string{"tag1": "val1"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
kr := &kopiaRepository{}
|
||||
|
||||
if tc.rawRepo != nil {
|
||||
if tc.setRepoMock {
|
||||
tc.rawRepo.On("GetManifest", mock.Anything, mock.Anything, mock.Anything).Return(&manifest.EntryMetadata{
|
||||
Labels: map[string]string{
|
||||
manifest.TypeLabelKey: snapshot.ManifestType,
|
||||
},
|
||||
}, tc.rawRepoRetErr).Run(func(args mock.Arguments) {
|
||||
if tc.rawRepoRetErr == nil {
|
||||
payload := args.Get(2)
|
||||
if ptr, ok := payload.(*snapshot.Manifest); ok {
|
||||
*ptr = *mockMani
|
||||
} else {
|
||||
b, _ := json.Marshal(mockMani)
|
||||
json.Unmarshal(b, payload)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
kr.rawRepo = tc.rawRepo
|
||||
}
|
||||
|
||||
snap, err := kr.GetSnapshot(t.Context(), tc.snapshotID)
|
||||
|
||||
if tc.expectedErr == "" {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.expectedSnap, snap)
|
||||
} else {
|
||||
assert.EqualError(t, err, tc.expectedErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteSnapshot(t *testing.T) {
|
||||
expectedTime := time.Now()
|
||||
rawObjID, _ := object.ParseID("I123456")
|
||||
|
||||
mockMani := &snapshot.Manifest{
|
||||
Source: snapshot.SourceInfo{Path: "fake-source"},
|
||||
Description: "fake-desc",
|
||||
StartTime: fs.UTCTimestampFromTime(expectedTime),
|
||||
EndTime: fs.UTCTimestampFromTime(expectedTime.Add(time.Minute)),
|
||||
RootEntry: &snapshot.DirEntry{
|
||||
ObjectID: rawObjID,
|
||||
},
|
||||
Tags: map[string]string{"tag1": "val1"},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
rawRepo *repomocks.MockRepository
|
||||
rawWriter *repomocks.MockRepositoryWriter
|
||||
snapshotID udmrepo.ID
|
||||
rawRepoRetErr error
|
||||
rawWriterRetErr error
|
||||
setRepoMock bool
|
||||
setWriterMock bool
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "get snapshot fail",
|
||||
rawRepo: repomocks.NewMockRepository(t),
|
||||
snapshotID: udmrepo.ID("fake-id"),
|
||||
rawRepoRetErr: errors.New("fake-get-error"),
|
||||
setRepoMock: true,
|
||||
expectedErr: "error getting snapshot: error getting snapshot manifest: unable to find manifest entries: fake-get-error",
|
||||
},
|
||||
{
|
||||
name: "delete manifest fail",
|
||||
rawRepo: repomocks.NewMockRepository(t),
|
||||
rawWriter: repomocks.NewMockRepositoryWriter(t),
|
||||
snapshotID: udmrepo.ID("fake-id"),
|
||||
rawWriterRetErr: errors.New("fake-delete-error"),
|
||||
setRepoMock: true,
|
||||
setWriterMock: true,
|
||||
expectedErr: "error to delete manifest: fake-delete-error",
|
||||
},
|
||||
{
|
||||
name: "succeed",
|
||||
rawRepo: repomocks.NewMockRepository(t),
|
||||
rawWriter: repomocks.NewMockRepositoryWriter(t),
|
||||
snapshotID: udmrepo.ID("fake-id"),
|
||||
setRepoMock: true,
|
||||
setWriterMock: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
kr := &kopiaRepository{}
|
||||
|
||||
if tc.rawRepo != nil {
|
||||
if tc.setRepoMock {
|
||||
tc.rawRepo.On("GetManifest", mock.Anything, mock.Anything, mock.Anything).Return(&manifest.EntryMetadata{
|
||||
Labels: map[string]string{
|
||||
manifest.TypeLabelKey: snapshot.ManifestType,
|
||||
},
|
||||
}, tc.rawRepoRetErr).Run(func(args mock.Arguments) {
|
||||
if tc.rawRepoRetErr == nil {
|
||||
payload := args.Get(2)
|
||||
if ptr, ok := payload.(*snapshot.Manifest); ok {
|
||||
*ptr = *mockMani
|
||||
} else {
|
||||
b, _ := json.Marshal(mockMani)
|
||||
json.Unmarshal(b, payload)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
kr.rawRepo = tc.rawRepo
|
||||
}
|
||||
|
||||
if tc.rawWriter != nil {
|
||||
if tc.setWriterMock {
|
||||
tc.rawWriter.On("DeleteManifest", mock.Anything, mock.Anything).Return(tc.rawWriterRetErr)
|
||||
}
|
||||
kr.rawWriter = tc.rawWriter
|
||||
}
|
||||
|
||||
err := kr.DeleteSnapshot(t.Context(), tc.snapshotID)
|
||||
|
||||
if tc.expectedErr == "" {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
assert.EqualError(t, err, tc.expectedErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -77,15 +77,18 @@ type AdvancedFeatureInfo struct {
|
||||
}
|
||||
|
||||
type ObjectMetadata struct {
|
||||
ID ID
|
||||
Type int // OBJECT_DATA_TYPE_*
|
||||
Size int64
|
||||
ID ID
|
||||
Type int // OBJECT_DATA_TYPE_*
|
||||
Size int64
|
||||
ModTime time.Time
|
||||
Permissions int
|
||||
UserID uint32
|
||||
GroupID uint32
|
||||
}
|
||||
|
||||
type Metadata struct {
|
||||
SubObjects []ObjectMetadata // For dir metadata only, the sub objects in this dir.
|
||||
ExtraDataLen int // Extra data associated to this metadata.
|
||||
ExtraData []byte
|
||||
SubObjects []ObjectMetadata
|
||||
Summary string
|
||||
}
|
||||
|
||||
type Snapshot struct {
|
||||
@@ -94,7 +97,7 @@ type Snapshot struct {
|
||||
StartTime time.Time
|
||||
EndTime time.Time
|
||||
Tags map[string]string
|
||||
RootObject ID
|
||||
RootObject ObjectMetadata
|
||||
}
|
||||
|
||||
// BackupRepoService is used to initialize, open or maintain a backup repository
|
||||
|
||||
Reference in New Issue
Block a user