Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 25 additions & 48 deletions pkg/scyllaclient/client_rclone.go
Original file line number Diff line number Diff line change
Expand Up @@ -736,10 +736,11 @@ func (c *Client) RcloneListDirIter(ctx context.Context, host, remotePath string,
// PermissionCheckOpts describes permission check options.
type PermissionCheckOpts struct {
// CheckRetention controls if the permission related to given retention mode is granted.
// Empty value is translated to RetentionLockDisabled.
CheckRetention RetentionLockMode
CheckRetention RetentionMode
// CheckOverrideRetentionLock controls if the permission to override unlocked retention lock is granted.
CheckOverrideRetentionLock bool
// CheckEventBasedHold controls if the permissions to set and clear event based hold are granted.
CheckEventBasedHold bool
}

// RcloneCheckPermissions checks if location is available for listing, getting,
Expand All @@ -749,11 +750,7 @@ func (c *Client) RcloneCheckPermissions(ctx context.Context, host, remotePath st
if err != nil {
return err
}
if opts.CheckRetention == "" {
opts.CheckRetention = RetentionLockDisabled
}
modeParam, holdParam, err := retentionLockModeToParam(opts.CheckRetention)
if err != nil {
if err := opts.CheckRetention.validate(); err != nil {
return err
}

Expand All @@ -762,9 +759,9 @@ func (c *Client) RcloneCheckPermissions(ctx context.Context, host, remotePath st
Options: &models.CheckPermissionsOptions{
Fs: fs,
Remote: remote,
RetentionMode: modeParam,
RetentionMode: string(opts.CheckRetention),
OverrideUnlocked: opts.CheckOverrideRetentionLock,
EventBasedHold: holdParam,
EventBasedHold: opts.CheckEventBasedHold,
},
}
_, err = c.agentOps.OperationsCheckPermissions(&p)
Expand Down Expand Up @@ -806,54 +803,39 @@ func (c *Client) RclonePut(ctx context.Context, host, remotePath string, body *b
return nil
}

// RetentionLockMode describes the object retention lock mode for backup files.
type RetentionLockMode string
// RetentionMode describes the object retention lock mode.
type RetentionMode string

const (
// RetentionLockDisabled means that no retention lock is applied to backup files.
RetentionLockDisabled RetentionLockMode = "disabled"
// RetentionLockUnlocked means that retention lock is applied but can be overridden with special permissions.
RetentionLockUnlocked RetentionLockMode = "unlocked"
// RetentionLockLocked means that retention lock is applied and cannot be overridden.
RetentionLockLocked RetentionLockMode = "locked"
// RetentionLockEventBasedHold means that backup files are protected with default
// event based hold and bucket retention policy. SM is responsible for clearing
// the hold when it's no longer needed which starts bucket retention timer.
RetentionLockEventBasedHold RetentionLockMode = "event-based-hold"
// RetentionModeNone means that no retention lock is applied.
RetentionModeNone RetentionMode = ""
// RetentionModeUnlocked means that retention lock is applied but can be overridden with special permissions.
RetentionModeUnlocked RetentionMode = "unlocked"
// RetentionModeLocked means that retention lock is applied and cannot be overridden.
RetentionModeLocked RetentionMode = "locked"
)

func retentionLockModeToParam(mode RetentionLockMode) (modeParam string, holdParam bool, err error) {
switch mode {
case RetentionLockDisabled:
return "", false, nil
case RetentionLockUnlocked:
return "unlocked", false, nil
case RetentionLockLocked:
return "locked", false, nil
case RetentionLockEventBasedHold:
return "", true, nil
func (m RetentionMode) validate() error {
switch m {
case RetentionModeNone, RetentionModeUnlocked, RetentionModeLocked:
return nil
default:
return "", false, errors.Errorf("unknown retention lock mode: %s", mode)
return errors.Errorf("unknown retention mode: %s", m)
}
}

// RcloneBatchRetentionLock sets object retention locks on the specified paths.
// The remoteDir param format is "provider:bucket/path".
// Specified paths are relative to remoteDir.
// The mode arg can't be set to RetentionLockEventBasedHold - in such case, use RcloneBatchEventBasedHold.
func (c *Client) RcloneBatchRetentionLock(ctx context.Context, host, remoteDir string, paths []string, mode RetentionLockMode, until time.Time, overrideLock bool) (int64, error) {
func (c *Client) RcloneBatchRetentionLock(ctx context.Context, host, remoteDir string, paths []string, mode RetentionMode, until time.Time, overrideLock bool) (int64, error) {
fs, remote, err := rcloneSplitRemotePath(remoteDir)
if err != nil {
return 0, err
}
if paths == nil {
paths = make([]string, 0)
}
if mode == RetentionLockEventBasedHold {
return 0, errors.New("event based hold should be applied with dedicated method")
}
modeParam, _, err := retentionLockModeToParam(mode)
if err != nil {
if err := mode.validate(); err != nil {
return 0, err
}

Expand All @@ -863,7 +845,7 @@ func (c *Client) RcloneBatchRetentionLock(ctx context.Context, host, remoteDir s
Fs: fs,
Remote: remote,
Paths: paths,
RetentionMode: modeParam,
RetentionMode: string(mode),
RetainUntil: strfmt.DateTime(until),
OverrideUnlocked: overrideLock,
},
Expand All @@ -880,17 +862,12 @@ func (c *Client) RcloneBatchRetentionLock(ctx context.Context, host, remoteDir s

// RcloneRetentionLock synchronously sets retention lock on the specified object.
// The remotePath param format is "provider:bucket/path".
// The mode arg can't be set to RetentionLockEventBasedHold - in such case, use RcloneEventBasedHold.
func (c *Client) RcloneRetentionLock(ctx context.Context, host, remotePath string, mode RetentionLockMode, until time.Time, overrideLock bool) error {
func (c *Client) RcloneRetentionLock(ctx context.Context, host, remotePath string, mode RetentionMode, until time.Time, overrideLock bool) error {
fs, remote, err := rcloneSplitRemotePath(remotePath)
if err != nil {
return err
}
if mode == RetentionLockEventBasedHold {
return errors.New("event based hold should be applied with dedicated method")
}
modeParam, _, err := retentionLockModeToParam(mode)
if err != nil {
if err := mode.validate(); err != nil {
return err
}

Expand All @@ -900,7 +877,7 @@ func (c *Client) RcloneRetentionLock(ctx context.Context, host, remotePath strin
Fs: fs,
Remote: path.Dir(remote),
Paths: []string{path.Base(remote)},
RetentionMode: modeParam,
RetentionMode: string(mode),
RetainUntil: strfmt.DateTime(until),
OverrideUnlocked: overrideLock,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ func TestBackupEventBasedHoldInterceptorIntegration(t *testing.T) {
interceptor.now = func() time.Time { return baseTime.Add(10 * policy) }

Print("Then: file has retention lock and no event based hold")
assertObjectMetadata(t, h.Client, host, remoteFile, false, string(scyllaclient.RetentionLockLocked), baseTime.Add(policy))
assertObjectMetadata(t, h.Client, host, remoteFile, false, string(scyllaclient.RetentionModeLocked), baseTime.Add(policy))

Print("When: event based hold is re-applied")
if err := h.Client.RcloneEventBasedHold(t.Context(), host, remoteFile, true); err != nil {
Expand Down Expand Up @@ -310,20 +310,20 @@ func TestBackupRetentionLockCRUDIntegration(t *testing.T) {

Print("When: unlocked retention is applied")
until := timeutc.Now().Add(24 * time.Hour).Truncate(time.Second)
if err := h.Client.RcloneRetentionLock(t.Context(), host, remoteFile, scyllaclient.RetentionLockUnlocked, until, false); err != nil {
if err := h.Client.RcloneRetentionLock(t.Context(), host, remoteFile, scyllaclient.RetentionModeUnlocked, until, false); err != nil {
t.Fatal(err)
}

Print("Then: file has unlocked retention")
assertObjectMetadata(t, h.Client, host, remoteFile, false, string(scyllaclient.RetentionLockUnlocked), until)
assertObjectMetadata(t, h.Client, host, remoteFile, false, string(scyllaclient.RetentionModeUnlocked), until)

Print("When: retention is upgraded to locked")
if err := h.Client.RcloneRetentionLock(t.Context(), host, remoteFile, scyllaclient.RetentionLockLocked, until, true); err != nil {
if err := h.Client.RcloneRetentionLock(t.Context(), host, remoteFile, scyllaclient.RetentionModeLocked, until, true); err != nil {
t.Fatal(err)
}

Print("Then: file has locked retention")
assertObjectMetadata(t, h.Client, host, remoteFile, false, string(scyllaclient.RetentionLockLocked), until)
assertObjectMetadata(t, h.Client, host, remoteFile, false, string(scyllaclient.RetentionModeLocked), until)
}

func TestBackupEventBasedHoldIntegration(t *testing.T) {
Expand Down Expand Up @@ -367,7 +367,7 @@ func TestBackupEventBasedHoldIntegration(t *testing.T) {
if err != nil {
t.Fatal(err)
}
target.RetentionLockMode = scyllaclient.RetentionLockEventBasedHold
target.RetentionLockMode = backup.RetentionLockEventBasedHold
// To skip not interesting schema sstables
target.Units = []backup.Unit{{Keyspace: testKeyspace}}

Expand Down Expand Up @@ -405,7 +405,7 @@ func TestBackupEventBasedHoldIntegration(t *testing.T) {
t.Fatal(err)
}
}
assertObjectMetadataAll(t, h.Client, host, location, tagATOCFiles, false, string(scyllaclient.RetentionLockLocked), baseTime.Add(policy))
assertObjectMetadataAll(t, h.Client, host, location, tagATOCFiles, false, string(scyllaclient.RetentionModeLocked), baseTime.Add(policy))

// The second backup should contain both deduplicated and new sstables
Print("And: small amount of new data is inserted")
Expand Down Expand Up @@ -440,7 +440,7 @@ func TestBackupEventBasedHoldIntegration(t *testing.T) {
}

Print("And: files from non-current snapshots don't have event based hold")
assertObjectMetadataAll(t, h.Client, host, location, tagAOnlyFiles, false, string(scyllaclient.RetentionLockLocked), baseTime.Add(policy))
assertObjectMetadataAll(t, h.Client, host, location, tagAOnlyFiles, false, string(scyllaclient.RetentionModeLocked), baseTime.Add(policy))
}

func assertObjectMetadataAll(t *testing.T, client *scyllaclient.Client, host string, location backupspec.Location, files []string, hold bool, retentionMode string, retainUntil time.Time) {
Expand Down
73 changes: 65 additions & 8 deletions pkg/service/backup/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,52 @@ import (
"sync"
"time"

"github.com/scylladb/go-set/strset"
"github.com/scylladb/scylla-manager/backupspec"
"github.com/scylladb/scylla-manager/v3/pkg/scyllaclient"
"github.com/scylladb/scylla-manager/v3/pkg/util/timeutc"
"github.com/scylladb/scylla-manager/v3/pkg/util2/slices"

"github.com/scylladb/scylla-manager/v3/pkg/util/uuid"
)

// listManifestsInAllLocations returns manifests for all nodes of a in all
// locations specified in hosts.
type remoteManifestInfo struct {
*backupspec.ManifestInfo

RetentionMode scyllaclient.RetentionMode
RetainUntil time.Time
EventBasedHold bool
}

func (m remoteManifestInfo) Protected(now time.Time) bool {
return m.EventBasedHold || (m.RetentionMode != scyllaclient.RetentionModeNone && m.RetainUntil.After(now))
}

func protectedTags(manifests []remoteManifestInfo) *strset.Set {
tags := strset.New()
now := timeutc.Now()
for _, m := range manifests {
if m.Protected(now) {
tags.Add(m.SnapshotTag)
}
}
return tags
}

// listManifestsInAllLocations is a type wrapper for listRemoteManifestsInAllLocations.
func listManifestsInAllLocations(ctx context.Context, client *scyllaclient.Client, hosts []hostInfo, clusterID uuid.UUID) ([]*backupspec.ManifestInfo, error) {
manifests, err := listRemoteManifestsInAllLocations(ctx, client, hosts, clusterID)
if err != nil {
return nil, err
}
return manifestInfos(manifests), nil
}

// listRemoteManifestsInAllLocations returns remote manifests for locations specified in hosts.
func listRemoteManifestsInAllLocations(ctx context.Context, client *scyllaclient.Client, hosts []hostInfo, clusterID uuid.UUID) ([]remoteManifestInfo, error) {
var (
locations = make(map[backupspec.Location]struct{})
manifests []*backupspec.ManifestInfo
manifests []remoteManifestInfo
)

for i := range hosts {
Expand All @@ -29,7 +63,7 @@ func listManifestsInAllLocations(ctx context.Context, client *scyllaclient.Clien
}
locations[hosts[i].Location] = struct{}{}

lm, err := listManifests(ctx, client, hosts[i].IP, hosts[i].Location, clusterID)
lm, err := listRemoteManifests(ctx, client, hosts[i].IP, hosts[i].Location, clusterID)
if err != nil {
return nil, err
}
Expand All @@ -39,10 +73,10 @@ func listManifestsInAllLocations(ctx context.Context, client *scyllaclient.Clien
return manifests, nil
}

// listManifests returns manifests for all nodes of a given cluster in the location.
// listRemoteManifests returns remote manifests for all nodes of a given cluster in the location.
// Manifests are sorted deterministically by their ClusterID, TaskID, SnapshotTag and NodeID.
// If cluster is uuid.Nil then it returns manifests for all clusters it can find.
func listManifests(ctx context.Context, client *scyllaclient.Client, host string, location backupspec.Location, clusterID uuid.UUID) ([]*backupspec.ManifestInfo, error) {
func listRemoteManifests(ctx context.Context, client *scyllaclient.Client, host string, location backupspec.Location, clusterID uuid.UUID) ([]remoteManifestInfo, error) {
baseDir := backupspec.RemoteMetaClusterDCDir(clusterID)
if clusterID == uuid.Nil {
baseDir = path.Join("backup", string(backupspec.MetaDirKind))
Expand All @@ -52,11 +86,20 @@ func listManifests(ctx context.Context, client *scyllaclient.Client, host string
FilesOnly: true,
Recurse: true,
}
if location.Provider == backupspec.GCS {
opts.ShowRetentionInfo = true
opts.ShowEventBasedHold = true
}
Comment thread
Michal-Leszczynski marked this conversation as resolved.

var manifests []*backupspec.ManifestInfo
var manifests []remoteManifestInfo
err := client.RcloneListDirIter(ctx, host, location.RemotePath(baseDir), &opts, func(f *scyllaclient.RcloneListDirItem) {
p := path.Join(baseDir, f.Path)
m := &backupspec.ManifestInfo{}
m := remoteManifestInfo{
ManifestInfo: &backupspec.ManifestInfo{},
RetentionMode: scyllaclient.RetentionMode(f.RetentionMode),
RetainUntil: time.Time(f.RetainUntil),
EventBasedHold: f.EventBasedHold,
}
if err := m.ParsePath(p); err != nil {
return
}
Expand All @@ -83,6 +126,12 @@ func listManifests(ctx context.Context, client *scyllaclient.Client, host string
return manifests, nil
}

func manifestInfos(manifests []remoteManifestInfo) []*backupspec.ManifestInfo {
return slices.Map(manifests, func(m remoteManifestInfo) *backupspec.ManifestInfo {
return m.ManifestInfo
})
}

// ListFilter specifies manifest listing criteria.
type ListFilter struct {
ClusterID uuid.UUID `json:"cluster_id"`
Expand Down Expand Up @@ -197,6 +246,14 @@ func groupManifestsByTask(manifests []*backupspec.ManifestInfo) map[uuid.UUID][]
return v
}

func groupManifestsByLocation(manifests []*backupspec.ManifestInfo) map[backupspec.Location][]*backupspec.ManifestInfo {
v := map[backupspec.Location][]*backupspec.ManifestInfo{}
for _, m := range manifests {
v[m.Location] = append(v[m.Location], m)
}
return v
}

// popNodeIDManifestsForLocation returns a function that for a given location
// finds next node and it's manifests from that location.
func popNodeIDManifestsForLocation(manifests []*backupspec.ManifestInfo) func(h hostInfo) (string, []*backupspec.ManifestInfo) {
Expand Down
3 changes: 2 additions & 1 deletion pkg/service/backup/list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,14 @@ func TestListManifests(t *testing.T) {
ctx := context.Background()

t.Run("one cluster", func(t *testing.T) {
manifests, error := listManifests(ctx, client, scyllaclienttest.TestHost,
remoteManifests, error := listRemoteManifests(ctx, client, scyllaclienttest.TestHost,
backupspec.Location{Provider: "testdata", Path: "list"},
uuid.MustParse("2e4ac82f-a7b5-4b6d-ab5e-0a1553a50a21"),
)
if error != nil {
t.Fatal("listManifests() error", error)
}
manifests := manifestInfos(remoteManifests)
testutils.SaveGoldenJSONFileIfNeeded(t, manifests)
var golden []*backupspec.ManifestInfo
testutils.LoadGoldenJSONFile(t, &golden)
Expand Down
Loading
Loading