diff --git a/src/internal/operations/backup.go b/src/internal/operations/backup.go index 80cd4055a..937e6733d 100644 --- a/src/internal/operations/backup.go +++ b/src/internal/operations/backup.go @@ -934,24 +934,49 @@ func (op *BackupOperation) createBackupModels( model.ServiceTag: op.Selectors.PathService().String(), } - // Add tags to mark this backup as either assist or merge. This is used to: + // Add tags to mark this backup as preview, assist, or merge. This is used to: // 1. Filter assist backups by tag during base selection process - // 2. Differentiate assist backups from merge backups - if isMergeBackup( + // 2. Differentiate assist backups, merge backups, and preview backups. + // + // model.BackupTypeTag has more info about how these tags are used. + switch { + case op.Options.ToggleFeatures.PreviewBackup: + // Preview backups need to be successful and without errors to be considered + // valid. Just reuse the merge base check for that since it has the same + // requirements. + if !isMergeBackup( + snapID, + ssid, + op.Options.FailureHandling, + op.Errors) { + return clues.New("failed preview backup").WithClues(ctx) + } + + tags[model.BackupTypeTag] = model.PreviewBackup + + case isMergeBackup( snapID, ssid, op.Options.FailureHandling, - op.Errors) { + op.Errors): tags[model.BackupTypeTag] = model.MergeBackup - } else if isAssistBackup( + + case isAssistBackup( opStats.hasNewDetailEntries, snapID, ssid, op.Options.FailureHandling, - op.Errors) { + op.Errors): tags[model.BackupTypeTag] = model.AssistBackup - } else { - return clues.New("backup is neither assist nor merge").WithClues(ctx) + + default: + return clues.New("unable to determine backup type due to operation errors"). + WithClues(ctx) + } + + // Additional defensive check to make sure we tag things as expected above. + if len(tags[model.BackupTypeTag]) == 0 { + return clues.New("empty backup type tag").WithClues(ctx) } ctx = clues.Add(ctx, model.BackupTypeTag, tags[model.BackupTypeTag]) diff --git a/src/internal/operations/backup_test.go b/src/internal/operations/backup_test.go index 26b8612c5..d8286c60e 100644 --- a/src/internal/operations/backup_test.go +++ b/src/internal/operations/backup_test.go @@ -1649,7 +1649,6 @@ func (suite *AssistBackupIntegrationSuite) TestBackupTypesForFailureModes() { var ( acct = tconfig.NewM365Account(suite.T()) tenantID = acct.Config[account.AzureTenantIDKey] - opts = control.DefaultOptions() osel = selectors.NewOneDriveBackup([]string{userID}) ) @@ -1667,6 +1666,7 @@ func (suite *AssistBackupIntegrationSuite) TestBackupTypesForFailureModes() { collFunc func() []data.BackupCollection injectNonRecoverableErr bool failurePolicy control.FailurePolicy + previewBackup bool expectRunErr assert.ErrorAssertionFunc expectBackupTag string expectFaults func(t *testing.T, errs *fault.Bus) @@ -1829,6 +1829,67 @@ func (suite *AssistBackupIntegrationSuite) TestBackupTypesForFailureModes() { assert.Greater(t, len(errs.Recovered()), 0, "recovered errors") }, }, + + { + name: "preview, fail after recovery, no errors", + collFunc: func() []data.BackupCollection { + bc := []data.BackupCollection{ + makeBackupCollection( + tmp, + locPath, + []dataMock.Item{ + makeMockItem("file1", nil, time.Now(), false, nil), + makeMockItem("file2", nil, time.Now(), false, nil), + }), + } + + return bc + }, + failurePolicy: control.FailAfterRecovery, + previewBackup: true, + expectRunErr: assert.NoError, + expectBackupTag: model.PreviewBackup, + expectFaults: func(t *testing.T, errs *fault.Bus) { + assert.NoError(t, errs.Failure(), clues.ToCore(errs.Failure())) + assert.Empty(t, errs.Recovered(), "recovered errors") + }, + }, + { + name: "preview, fail after recovery, non-recoverable errors", + collFunc: func() []data.BackupCollection { + return nil + }, + injectNonRecoverableErr: true, + failurePolicy: control.FailAfterRecovery, + previewBackup: true, + expectRunErr: assert.Error, + expectFaults: func(t *testing.T, errs *fault.Bus) { + assert.Error(t, errs.Failure(), clues.ToCore(errs.Failure())) + }, + }, + { + name: "preview, fail after recovery, recoverable errors", + collFunc: func() []data.BackupCollection { + bc := []data.BackupCollection{ + makeBackupCollection( + tmp, + locPath, + []dataMock.Item{ + makeMockItem("file1", nil, time.Now(), false, nil), + makeMockItem("file2", nil, time.Now(), false, assert.AnError), + }), + } + + return bc + }, + failurePolicy: control.FailAfterRecovery, + previewBackup: true, + expectRunErr: assert.Error, + expectFaults: func(t *testing.T, errs *fault.Bus) { + assert.Error(t, errs.Failure(), clues.ToCore(errs.Failure())) + assert.Greater(t, len(errs.Recovered()), 0, "recovered errors") + }, + }, } for _, test := range table { suite.Run(test.name, func() { @@ -1856,7 +1917,9 @@ func (suite *AssistBackupIntegrationSuite) TestBackupTypesForFailureModes() { cs = append(cs, mc) bp := opMock.NewMockBackupProducer(cs, data.CollectionStats{}, test.injectNonRecoverableErr) + opts := control.DefaultOptions() opts.FailureHandling = test.failurePolicy + opts.ToggleFeatures.PreviewBackup = test.previewBackup bo, err := NewBackupOperation( ctx, diff --git a/src/pkg/control/options.go b/src/pkg/control/options.go index 0f7d559aa..7db2d9038 100644 --- a/src/pkg/control/options.go +++ b/src/pkg/control/options.go @@ -86,4 +86,9 @@ type Toggles struct { // DisableConcurrencyLimiter removes concurrency limits when communicating with // graph API. This flag is only relevant for exchange backups for now DisableConcurrencyLimiter bool `json:"disableConcurrencyLimiter,omitempty"` + + // PreviewBackup denotes that this backup contains a subset of information for + // the protected resource. PreviewBackups are used to demonstrate value by + // being quick to create. + PreviewBackup bool `json:"previewBackup"` }