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
36 changes: 28 additions & 8 deletions cmd/pbm/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type listOpts struct {
unbacked bool
full bool
size int
profile string
rsMap string
}

Expand Down Expand Up @@ -113,7 +114,7 @@ func runList(ctx context.Context, conn connect.Client, pbm *sdk.Client, l *listO
return restoreList(ctx, conn, pbm, int64(l.size))
}

return backupList(ctx, conn, l.size, l.full, l.unbacked, rsMap)
return backupList(ctx, conn, l.size, l.full, l.unbacked, l.profile, rsMap)
}

func findLock(ctx context.Context, pbm *sdk.Client) (*sdk.OpLock, error) {
Expand Down Expand Up @@ -192,11 +193,13 @@ func restoreList(ctx context.Context, conn connect.Client, pbm *sdk.Client, limi

type backupListOut struct {
Snapshots []snapshotStat `json:"snapshots"`
PITR struct {
On bool `json:"on"`
Ranges []pitrRange `json:"ranges"`
RsRanges map[string][]pitrRange `json:"rsRanges,omitempty"`
} `json:"pitr"`
PITR *pitrListOut `json:"pitr,omitempty"`
}

type pitrListOut struct {
On bool `json:"on"`
Ranges []pitrRange `json:"ranges"`
RsRanges map[string][]pitrRange `json:"rsRanges,omitempty"`
}

func (bl backupListOut) String() string {
Expand All @@ -218,6 +221,13 @@ func (bl backupListOut) String() string {
}
s += fmt.Sprintf(" %s <%s> [restore_to_time: %s]\n", b.Name, t, fmtTS(int64(b.RestoreTS)))
}

// if not set, skip PITR information
if bl.PITR == nil {
return s
}

// include also PITR information
if bl.PITR.On {
s += fmt.Sprintln("\nPITR <on>:")
} else {
Expand Down Expand Up @@ -249,15 +259,24 @@ func backupList(
conn connect.Client,
size int,
full, unbacked bool,
profile string,
rsMap map[string]string,
) (backupListOut, error) {
var list backupListOut
var err error

list.Snapshots, err = getSnapshotList(ctx, conn, size, rsMap)
list.Snapshots, err = getSnapshotList(ctx, conn, profile, size, rsMap)
if err != nil {
return list, errors.Wrap(err, "get snapshots")
}

// for profile skip PITR information
if profile != "" {
return list, nil
}

// for main profile include PITR information
list.PITR = &pitrListOut{}
list.PITR.Ranges, list.PITR.RsRanges, err = getPitrList(ctx, conn, size, full, unbacked, rsMap)
if err != nil {
return list, errors.Wrap(err, "get PITR ranges")
Expand All @@ -274,10 +293,11 @@ func backupList(
func getSnapshotList(
ctx context.Context,
conn connect.Client,
profile string,
size int,
rsMap map[string]string,
) ([]snapshotStat, error) {
bcps, err := backup.BackupsList(ctx, conn, int64(size))
bcps, err := backup.BackupsList(ctx, conn, profile, int64(size))
if err != nil {
return nil, errors.Wrap(err, "unable to get backups list")
}
Expand Down
9 changes: 8 additions & 1 deletion cmd/pbm/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -699,7 +699,10 @@ func (app *pbmApp) buildListCmd() *cobra.Command {
listCmd.Flags().BoolVar(&listOptions.unbacked, "unbacked", false, "Show unbacked oplog ranges")
listCmd.Flags().BoolVarP(&listOptions.full, "full", "f", false, "Show extended restore info")
listCmd.Flags().IntVar(&listOptions.size, "size", 0, "Show last N backups")

listCmd.Flags().StringVar(
&listOptions.profile, "profile", "",
"Name of the PBM profile used to filter the backup list. By default all profiles are listed.",
)
listCmd.Flags().StringVar(&listOptions.rsMap, RSMappingFlag, "", RSMappingDoc)
_ = viper.BindPFlag(RSMappingFlag, listCmd.Flags().Lookup(RSMappingFlag))
_ = viper.BindEnv(RSMappingFlag, RSMappingEnvVar)
Expand Down Expand Up @@ -938,6 +941,10 @@ func (app *pbmApp) buildStatusCmd() *cobra.Command {
}),
}

statusCmd.Flags().StringVar(
&statusOpts.profile, "profile", "",
"Name of the PBM profile used to filter the backup list. By default all profiles are listed.",
)
statusCmd.Flags().StringVar(&statusOpts.rsMap, RSMappingFlag, "", RSMappingDoc)
_ = viper.BindPFlag(RSMappingFlag, statusCmd.Flags().Lookup(RSMappingFlag))
_ = viper.BindEnv(RSMappingFlag, RSMappingEnvVar)
Expand Down
17 changes: 11 additions & 6 deletions cmd/pbm/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
)

type statusOptions struct {
profile string
rsMap string
sections []string
priority bool
Expand Down Expand Up @@ -123,7 +124,7 @@
{
"backups", "Backups", nil,
func(ctx context.Context, conn connect.Client) (fmt.Stringer, error) {
return getStorageStat(ctx, conn, pbm, rsMap)
return getStorageStat(ctx, conn, pbm, opts.profile, rsMap)
},
},
},
Expand All @@ -131,7 +132,7 @@
}

var sfilter map[string]bool
if opts.sections != nil && len(opts.sections) > 0 {

Check failure on line 135 in cmd/pbm/status.go

View workflow job for this annotation

GitHub Actions / runner / golangci-lint

S1009: should omit nil check; len() for nil slices is defined as zero (staticcheck)
sfilter = make(map[string]bool)
for _, s := range opts.sections {
sfilter[s] = true
Expand Down Expand Up @@ -500,7 +501,7 @@
ret += fmt.Sprintf(" %s %s <%s> %s %s\n", ss.Name, storage.PrettySize(ss.Size), t, ss.PrintStatus, status)
}

if len(s.PITR.Ranges) == 0 {
if s.PITR == nil || len(s.PITR.Ranges) == 0 {
return ret
}

Expand Down Expand Up @@ -530,6 +531,7 @@
ctx context.Context,
conn connect.Client,
pbm *sdk.Client,
profile string,
rsMap map[string]string,
) (fmt.Stringer, error) {
var s storageStat
Expand All @@ -543,7 +545,7 @@
s.Region = cfg.Storage.Region()
s.Path = cfg.Storage.Path()

bcps, err := pbm.GetAllBackups(ctx)
bcps, err := backup.BackupsList(ctx, conn, profile, 0)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This filtered backup list doesn't contain base backups for PITR, and then PBM shows something like:

pbm s --profile s3

 Snapshots:
    2025-12-05T13:29:01Z 270.03KB <logical, s3> success [restore_to_time: 2025-12-05T13:29:05]   
  PITR chunks [7.51MB]:
    2025-12-05T13:25:32 - 2025-12-05T16:00:52
    2025-12-05T13:24:29 - 2025-12-05T13:25:31 (no base snapshot) <--

The best should be to omit the list of PITR in case of --profile, so the same as we do for list. Or pass down the full list of backups, so that it's possible to make that resolution.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, best is imho to remove the PITR section to align with list. I overlooked that PITR info is actually also here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

if err != nil {
return s, errors.Wrap(err, "get backups list")
}
Expand Down Expand Up @@ -630,9 +632,12 @@
s.Snapshot = append(s.Snapshot, snpsht)
}

s.PITR, err = getPITRranges(ctx, conn, bcps, rsMap)
if err != nil {
return s, errors.Wrap(err, "get PITR chunks")
// for main profile also fetch PITR chunks
if profile == "" {
s.PITR, err = getPITRranges(ctx, conn, bcps, rsMap)
if err != nil {
return s, errors.Wrap(err, "get PITR chunks")
}
}

return s, nil
Expand Down
2 changes: 1 addition & 1 deletion e2e-tests/pkg/pbm/mongo_pbm.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func (m *MongoPBM) SendCmd(ctx context.Context, cmd ctrl.Cmd) error {
}

func (m *MongoPBM) BackupsList(ctx context.Context, limit int64) ([]backup.BackupMeta, error) {
return backup.BackupsList(ctx, m.conn, limit)
return backup.BackupsList(ctx, m.conn, "", limit)
}

func (m *MongoPBM) GetBackupMeta(ctx context.Context, bcpName string) (*backup.BackupMeta, error) {
Expand Down
73 changes: 70 additions & 3 deletions pbm/backup/backup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,24 @@ package backup
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"sort"
"testing"
"time"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/modules/minio"

"github.com/percona/percona-backup-mongodb/pbm/defs"
stds3 "github.com/percona/percona-backup-mongodb/pbm/storage/s3"
"github.com/stretchr/testify/assert"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/modules/minio"
)

func TestMetadataEncodeDecodeWithMinio(t *testing.T) {
Expand Down Expand Up @@ -113,3 +116,67 @@ func getMetaDoc(t *testing.T) *BackupMeta {

return doc
}

func TestBackupsList(t *testing.T) {
TestEnv.Reset(t)
now := time.Date(2025, 1, 15, 10, 0, 0, 0, time.UTC)

backups := map[string][]bcp{
"": {
{Name: "a1", LWT: now.Add(-20 * time.Minute)},
{Name: "a2", LWT: now.Add(-15 * time.Minute)},
{Name: "a3", LWT: now.Add(-10 * time.Minute)},
},
"other": {
{Name: "b1", LWT: now.Add(-15 * time.Minute)},
{Name: "b2", LWT: now.Add(-10 * time.Minute)},
},
}
stgs := stgsFromTestBackups(t, backups)

expected := make([]BackupMeta, 0)
for profile, bcps := range backups {
for _, bcp := range bcps {
meta := insertTestBcpMeta(t, TestEnv, stgs[profile], bcp)
expected = append(expected, meta)
}
}
sort.Slice(expected, func(i, j int) bool {
return expected[i].StartTS < expected[j].StartTS
})
expectedNamesAll, expectedNamesByProfile := bcpNames(expected)

for profile := range backups {
tName := profile
if profile == "" {
tName = "default"
}

t.Run(fmt.Sprintf("List backups in profile %q", tName), func(t *testing.T) {
var expectedNames []string
if profile == "" {
expectedNames = expectedNamesAll
} else {
expectedNames = expectedNamesByProfile[profile]
}

actual, err := BackupsList(t.Context(), TestEnv.Client, profile, 0)
assert.NoError(t, err)
actualNames, _ := bcpNames(actual)
assert.ElementsMatchf(t, expectedNames, actualNames,
"Expectged backups %v, got %v, for profile %q", expectedNamesAll, actualNames, profile,
)
})
}

}

func bcpNames(backups []BackupMeta) ([]string, map[string][]string) {
var all = make([]string, len(backups))
var byProfile = make(map[string][]string)
for i, b := range backups {
all[i] = b.Name
byProfile[b.Store.Name] = append(byProfile[b.Store.Name], b.Name)
}
return all, byProfile
}
53 changes: 0 additions & 53 deletions pbm/backup/delete_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,15 @@ import (
"github.com/percona/percona-backup-mongodb/pbm/compress"
"github.com/percona/percona-backup-mongodb/pbm/config"
"github.com/percona/percona-backup-mongodb/pbm/connect"
"github.com/percona/percona-backup-mongodb/pbm/ctrl"
"github.com/percona/percona-backup-mongodb/pbm/defs"
"github.com/percona/percona-backup-mongodb/pbm/oplog"
"github.com/percona/percona-backup-mongodb/pbm/storage"
"github.com/percona/percona-backup-mongodb/pbm/topo"
"github.com/percona/percona-backup-mongodb/pbm/version"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
)

type bcp struct {
Name string
LWT time.Time
Expected bool
BcpType defs.BackupType
}

type chunk struct {
From time.Time
To time.Time
Expand Down Expand Up @@ -654,16 +644,6 @@ func assertBackupList(t *testing.T, expectedBackups []BackupMeta, actualBackups
)
}

func stgsFromTestBackups(t *testing.T, backups map[string][]bcp) map[string]Storage {
storages := make(map[string]Storage, len(backups))

for profile := range backups {
storages[profile] = TempStorageProfile(t, profile)
}

return storages
}

func insertTestBackupsStorage(t *testing.T, env *TestEnvironment, stg Storage, bcps []bcp) []BackupMeta {
inserted := make([]BackupMeta, len(bcps))
for i, b := range bcps {
Expand Down Expand Up @@ -708,39 +688,6 @@ func insertTestChunks(t *testing.T, env *TestEnvironment, chunks []chunk) []oplo
return expected
}

func insertTestBcpMeta(t *testing.T, env *TestEnvironment, stg Storage, b bcp) BackupMeta {
t.Helper()

firstWrite := b.LWT.Add(-10 * time.Minute)
if b.BcpType == "" {
b.BcpType = defs.LogicalBackup
}

meta := BackupMeta{
Type: b.BcpType,
OPID: ctrl.OPID(primitive.NilObjectID).String(),
Name: b.Name,
Namespaces: make([]string, 0),
Compression: compress.CompressionTypeS2,
Store: stg,
StartTS: time.Now().Unix(),
Status: defs.StatusDone,
Replsets: []BackupReplset{},
LastWriteTS: primitive.Timestamp{T: uint32(b.LWT.Unix())},
FirstWriteTS: primitive.Timestamp{T: uint32(firstWrite.Unix())},
PBMVersion: version.Current().Version,
MongoVersion: env.Brief.Version.String(),
Nomination: []BackupRsNomination{},
BalancerStatus: topo.BalancerModeOff,
Hb: primitive.Timestamp{T: uint32(b.LWT.Unix())},
}

_, err := env.Client.BcpCollection().InsertOne(t.Context(), meta)
require.NoError(t, err)

return meta
}

func insertTestChunkMeta(t *testing.T, env *TestEnvironment, c chunk) oplog.OplogChunk {
t.Helper()

Expand Down
Loading
Loading