Skip to content

Commit 2a6b65b

Browse files
authored
PBM-1421: Enable mongorestore for cloning collection (#1040)
* Add restore's ns-from and ns-to options They are used for cloning existing collection from the backup (--ns-from) into the new collection with the different name (--ns-to). * Expand restore cmd with nsFrom/To * Pass nsFrom/To options to mongorestore * Fine tune mongorestore to support coll cloning. Following settings are applied: mongorestore has additional options: ns include for target collection; preserved UUID and dropping collections should be turned off for cloning collection. pbm restore should be run in selective mode, and selected ns should be cloning collection * Add tests for resolving ns and users&userAndRoles when cloning collections * Add CLI validation for --ns-from and --ns-to opts * Check whether the target cloning collection exists * Add index cloning support * Extract cloning to/from into separate type * Add tests for cloning validation * Add guard for cloning within sharded cluster Cloning feature is not supported in sharded cluster in this version. * Fix reviewdog warnings * Disable oplog apply when cloning collection * Add validation for incomplete ns
1 parent 53c6b3c commit 2a6b65b

File tree

7 files changed

+389
-32
lines changed

7 files changed

+389
-32
lines changed

cmd/pbm/main.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,12 @@ func main() {
246246
Int32Var(&restore.numParallelColls)
247247
restoreCmd.Flag("ns", `Namespaces to restore (e.g. "db1.*,db2.collection2"). If not set, restore all ("*.*")`).
248248
StringVar(&restore.ns)
249+
restoreCmd.Flag("ns-from", "Allows collection cloning (creating from the backup with different name) "+
250+
"and specifies source collection for cloning from.").
251+
StringVar(&restore.nsFrom)
252+
restoreCmd.Flag("ns-to", "Allows collection cloning (creating from the backup with different name) "+
253+
"and specifies destination collection for cloning to.").
254+
StringVar(&restore.nsTo)
249255
restoreCmd.Flag("with-users-and-roles", "Includes users and roles for selected database (--ns flag)").
250256
BoolVar(&restore.usersAndRoles)
251257
restoreCmd.Flag("wait", "Wait for the restore to finish.").

cmd/pbm/restore.go

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"time"
1212

1313
"github.com/mongodb/mongo-tools/common/db"
14+
"go.mongodb.org/mongo-driver/bson"
1415
"go.mongodb.org/mongo-driver/bson/primitive"
1516
"gopkg.in/yaml.v2"
1617

@@ -28,6 +29,15 @@ import (
2829
"github.com/percona/percona-backup-mongodb/sdk"
2930
)
3031

32+
var (
33+
ErrNSFromMissing = errors.New("--ns-from should be specified as the cloning source")
34+
ErrNSToMissing = errors.New("--ns-to should be specified as the cloning destination")
35+
ErrSelAndCloning = errors.New("cloning with selective restore is not possible (remove --ns option)")
36+
ErrCloningWithUAndR = errors.New("cloning with restoring users and rolles is not possible")
37+
ErrCloningWithPITR = errors.New("cloning with restore to the point-in-time is not possible")
38+
ErrCloningWithWildCards = errors.New("cloning with wild-cards is not possible")
39+
)
40+
3141
type restoreOpts struct {
3242
bcp string
3343
pitr string
@@ -36,6 +46,8 @@ type restoreOpts struct {
3646
waitTime time.Duration
3747
extern bool
3848
ns string
49+
nsFrom string
50+
nsTo string
3951
usersAndRoles bool
4052
rsMap string
4153
conf string
@@ -116,6 +128,9 @@ func runRestore(
116128
if err != nil {
117129
return nil, errors.Wrap(err, "parse --ns option")
118130
}
131+
if err := validateNSFromNSTo(o); err != nil {
132+
return nil, errors.Wrap(err, "parse --ns-from and --ns-to options")
133+
}
119134
if err := validateRestoreUsersAndRoles(o.usersAndRoles, nss); err != nil {
120135
return nil, errors.Wrap(err, "parse --with-users-and-roles option")
121136
}
@@ -139,7 +154,7 @@ func runRestore(
139154
}
140155
tdiff := time.Now().Unix() - int64(clusterTime.T)
141156

142-
m, err := doRestore(ctx, conn, o, numParallelColls, nss, rsMap, node, outf)
157+
m, err := doRestore(ctx, conn, o, numParallelColls, nss, o.nsFrom, o.nsTo, rsMap, node, outf)
143158
if err != nil {
144159
return nil, err
145160
}
@@ -283,6 +298,8 @@ func checkBackup(
283298
conn connect.Client,
284299
o *restoreOpts,
285300
nss []string,
301+
nsFrom string,
302+
nsTo string,
286303
) (string, defs.BackupType, error) {
287304
if o.extern && o.bcp == "" {
288305
return "", defs.ExternalBackup, nil
@@ -318,28 +335,65 @@ func checkBackup(
318335
if len(nss) != 0 && bcp.Type != defs.LogicalBackup {
319336
return "", "", errors.New("--ns flag is only allowed for logical restore")
320337
}
338+
if nsFrom != "" && nsTo != "" && bcp.Type != defs.LogicalBackup {
339+
return "", "", errors.New("--ns-from and ns-to flags are only allowed for logical restore")
340+
}
321341
if bcp.Status != defs.StatusDone {
322342
return "", "", errors.Errorf("backup '%s' didn't finish successfully", b)
323343
}
324344

325345
return bcp.Name, bcp.Type, nil
326346
}
327347

348+
// nsIsTaken returns error in case when specified namesapce is already in use (collection is created)
349+
// or when any other error ocurres within the checking process.
350+
func nsIsTaken(
351+
ctx context.Context,
352+
conn connect.Client,
353+
ns string,
354+
) error {
355+
ns = strings.TrimSpace(ns)
356+
db, coll, ok := strings.Cut(ns, ".")
357+
if !ok {
358+
return errors.Wrap(ErrInvalidNamespace, ns)
359+
}
360+
361+
collNames, err := conn.MongoClient().Database(db).ListCollectionNames(ctx, bson.D{{"name", coll}})
362+
if err != nil {
363+
return errors.Wrap(err, "list collection names for cloning target validation")
364+
}
365+
366+
if len(collNames) > 0 {
367+
return errors.New("cloning namespace (--ns-to) is already in use, specify another one that doesn't exist in database")
368+
}
369+
370+
return nil
371+
}
372+
328373
func doRestore(
329374
ctx context.Context,
330375
conn connect.Client,
331376
o *restoreOpts,
332377
numParallelColls *int32,
333378
nss []string,
379+
nsFrom string,
380+
nsTo string,
334381
rsMapping map[string]string,
335382
node string,
336383
outf outFormat,
337384
) (*restore.RestoreMeta, error) {
338-
bcp, bcpType, err := checkBackup(ctx, conn, o, nss)
385+
bcp, bcpType, err := checkBackup(ctx, conn, o, nss, nsFrom, nsTo)
339386
if err != nil {
340387
return nil, err
341388
}
342389

390+
// check if namespace exists when cloning collection
391+
if nsFrom != "" && nsTo != "" {
392+
if err := nsIsTaken(ctx, conn, nsTo); err != nil {
393+
return nil, err
394+
}
395+
}
396+
343397
name := time.Now().UTC().Format(time.RFC3339Nano)
344398

345399
cmd := ctrl.Cmd{
@@ -349,6 +403,8 @@ func doRestore(
349403
BackupName: bcp,
350404
NumParallelColls: numParallelColls,
351405
Namespaces: nss,
406+
NamespaceFrom: nsFrom,
407+
NamespaceTo: nsTo,
352408
UsersAndRoles: o.usersAndRoles,
353409
RSMap: rsMapping,
354410
External: o.extern,
@@ -715,3 +771,36 @@ func validateRestoreUsersAndRoles(usersAndRoles bool, nss []string) error {
715771

716772
return nil
717773
}
774+
775+
func validateNSFromNSTo(o *restoreOpts) error {
776+
if o.nsFrom == "" && o.nsTo == "" {
777+
return nil
778+
}
779+
if o.nsFrom == "" && o.nsTo != "" {
780+
return ErrNSFromMissing
781+
}
782+
if o.nsFrom != "" && o.nsTo == "" {
783+
return ErrNSToMissing
784+
}
785+
if _, _, ok := strings.Cut(o.nsFrom, "."); !ok {
786+
return errors.Wrap(ErrInvalidNamespace, o.nsFrom)
787+
}
788+
if _, _, ok := strings.Cut(o.nsTo, "."); !ok {
789+
return errors.Wrap(ErrInvalidNamespace, o.nsTo)
790+
}
791+
if o.nsFrom != "" && o.nsTo != "" && o.ns != "" {
792+
return ErrSelAndCloning
793+
}
794+
if o.nsFrom != "" && o.nsTo != "" && o.usersAndRoles {
795+
return ErrCloningWithUAndR
796+
}
797+
if o.nsFrom != "" && o.nsTo != "" && o.pitr != "" {
798+
// this check will be removed with: PBM-1422
799+
return ErrCloningWithPITR
800+
}
801+
if strings.Contains(o.nsTo, "*") || strings.Contains(o.nsFrom, "*") {
802+
return ErrCloningWithWildCards
803+
}
804+
805+
return nil
806+
}

cmd/pbm/restore_test.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package main
2+
3+
import (
4+
"errors"
5+
"testing"
6+
)
7+
8+
func TestCloningValidation(t *testing.T) {
9+
testCases := []struct {
10+
desc string
11+
opts restoreOpts
12+
wantErr error
13+
}{
14+
{
15+
desc: "ns-to options is missing when cloning",
16+
opts: restoreOpts{
17+
nsFrom: "d.c",
18+
},
19+
wantErr: ErrNSToMissing,
20+
},
21+
{
22+
desc: "ns-from options is missing when cloning",
23+
opts: restoreOpts{
24+
nsTo: "d.c",
25+
},
26+
wantErr: ErrNSFromMissing,
27+
},
28+
{
29+
desc: "cloning with selective restore is not allowed",
30+
opts: restoreOpts{
31+
nsFrom: "d.c1",
32+
nsTo: "d.c2",
33+
ns: "d.c",
34+
},
35+
wantErr: ErrSelAndCloning,
36+
},
37+
{
38+
desc: "cloning with restoring users and roles are not allowed",
39+
opts: restoreOpts{
40+
nsFrom: "d.c1",
41+
nsTo: "d.c2",
42+
usersAndRoles: true,
43+
},
44+
wantErr: ErrCloningWithUAndR,
45+
},
46+
{
47+
desc: "cloning with PITR is not allowed",
48+
opts: restoreOpts{
49+
nsFrom: "d.c1",
50+
nsTo: "d.c2",
51+
pitr: "2024-10-27T11:23:30",
52+
},
53+
wantErr: ErrCloningWithPITR,
54+
},
55+
{
56+
desc: "cloning with wild cards within nsFrom",
57+
opts: restoreOpts{
58+
nsFrom: "d.*",
59+
nsTo: "d.c2",
60+
},
61+
wantErr: ErrCloningWithWildCards,
62+
},
63+
{
64+
desc: "cloning with wild cards within nsTo",
65+
opts: restoreOpts{
66+
nsFrom: "d.c1",
67+
nsTo: "d.*",
68+
},
69+
wantErr: ErrCloningWithWildCards,
70+
},
71+
{
72+
desc: "cloning with ns without dot within nsFrom",
73+
opts: restoreOpts{
74+
nsFrom: "c",
75+
nsTo: "c.d",
76+
},
77+
wantErr: ErrInvalidNamespace,
78+
},
79+
{
80+
desc: "cloning with ns without dot within nsTo",
81+
opts: restoreOpts{
82+
nsFrom: "d.c",
83+
nsTo: "d",
84+
},
85+
wantErr: ErrInvalidNamespace,
86+
},
87+
{
88+
desc: "no error without cloning options",
89+
opts: restoreOpts{
90+
nsFrom: "",
91+
nsTo: "",
92+
},
93+
wantErr: nil,
94+
},
95+
{
96+
desc: "no error when cloning options are correct",
97+
opts: restoreOpts{
98+
nsFrom: "b.a",
99+
nsTo: "d.c",
100+
},
101+
wantErr: nil,
102+
},
103+
}
104+
for _, tC := range testCases {
105+
t.Run(tC.desc, func(t *testing.T) {
106+
err := validateNSFromNSTo(&tC.opts)
107+
if !errors.Is(err, tC.wantErr) {
108+
t.Errorf("Invalid validation error: want=%v, got=%v", tC.wantErr, err)
109+
}
110+
})
111+
}
112+
}

pbm/ctrl/cmd.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,8 @@ type RestoreCmd struct {
152152
Name string `bson:"name"`
153153
BackupName string `bson:"backupName"`
154154
Namespaces []string `bson:"nss,omitempty"`
155+
NamespaceFrom string `bson:"nsFrom,omitempty"`
156+
NamespaceTo string `bson:"nsTo,omitempty"`
155157
UsersAndRoles bool `bson:"usersAndRoles,omitempty"`
156158
RSMap map[string]string `bson:"rsMap,omitempty"`
157159

0 commit comments

Comments
 (0)