-
Notifications
You must be signed in to change notification settings - Fork 35
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(restore): adds --dc-mapping flag to restore command
This adds support for --dc-mapping flag to restore command. It specifies mapping between DCs from the backup and DCs in the restored(target) cluster. All DCs from the source cluster should be explicitly mapped to all DCs in the target cluster. The only exception is when source and target cluster has exact match: source dcs == target dcs. Only works with tables restoration (--restore-tables=true). Syntax: "source_dc1,source_dc2=>target_dc1,target_dc2" Multiple mappings are separated by semicolons (;). Exclamation mark (!) before a DC indicates that it should be ignored during restore. Examples: "dc1,dc2=>dc3" - data from dc1 and dc2 DCs should be restored to dc3 DC. "dc1,dc2=>dc3,!dc4" - data from dc1 and dc2 DCs should be restored to dc3 DC. Ignoring dc4 DC from target cluster. "dc1,!dc2=>dc2" - data from dc1 should be restored to dc2 DC. Ignoring dc2 from source cluster. Fixes: #3829
- Loading branch information
1 parent
42f72f8
commit db3309d
Showing
4 changed files
with
294 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
package restore | ||
|
||
import ( | ||
"slices" | ||
"strings" | ||
|
||
"github.com/pkg/errors" | ||
) | ||
|
||
type dcMappings []dcMapping | ||
|
||
type dcMapping struct { | ||
Source []string `json:"source"` | ||
IgnoreSource []string `json:"ignore_source"` | ||
Target []string `json:"target"` | ||
IgnoreTarget []string `json:"ignore_target"` | ||
} | ||
|
||
const ignoreDCPrefix = "!" | ||
|
||
// Set parses --dc-mapping flag, where the syntax is following: | ||
// ; - used to split different mappings | ||
// => - used to split source => target DCs | ||
// , - used to seprate DCs | ||
// ! - used to ignore DC. | ||
func (dcm *dcMappings) Set(v string) error { | ||
mappingParts := strings.Split(v, ";") | ||
for _, dcMapPart := range mappingParts { | ||
sourceTargetParts := strings.Split(dcMapPart, "=>") | ||
if len(sourceTargetParts) != 2 { | ||
return errors.New("invalid syntax, mapping should be in a format of sourceDcs=>targetDcs, but got: " + dcMapPart) | ||
} | ||
if sourceTargetParts[0] == "" || sourceTargetParts[1] == "" { | ||
return errors.New("invalid syntax, mapping should be in a format of sourceDcs=>targetDcs, but got: " + dcMapPart) | ||
} | ||
|
||
var mapping dcMapping | ||
mapping.Source, mapping.IgnoreSource = parseDCList(strings.Split(sourceTargetParts[0], ",")) | ||
mapping.Target, mapping.IgnoreTarget = parseDCList(strings.Split(sourceTargetParts[1], ",")) | ||
|
||
*dcm = append(*dcm, mapping) | ||
} | ||
return nil | ||
} | ||
|
||
func parseDCList(list []string) (dcs, ignore []string) { | ||
for _, dc := range list { | ||
if strings.HasPrefix(dc, ignoreDCPrefix) { | ||
ignore = append(ignore, strings.TrimPrefix(dc, ignoreDCPrefix)) | ||
continue | ||
} | ||
dcs = append(dcs, dc) | ||
} | ||
return dcs, ignore | ||
} | ||
|
||
// String builds --dc-mapping flag back from struct. | ||
func (dcm *dcMappings) String() string { | ||
if dcm == nil { | ||
return "" | ||
} | ||
var res strings.Builder | ||
for i, mapping := range *dcm { | ||
source := slices.Concat(mapping.Source, addIgnorePrefix(mapping.IgnoreSource)) | ||
target := slices.Concat(mapping.Target, addIgnorePrefix(mapping.IgnoreTarget)) | ||
res.WriteString( | ||
strings.Join(source, ",") + "=>" + strings.Join(target, ","), | ||
) | ||
if i != len(*dcm)-1 { | ||
res.WriteString(";") | ||
} | ||
} | ||
return res.String() | ||
} | ||
|
||
func addIgnorePrefix(ignore []string) []string { | ||
var result []string | ||
for _, v := range ignore { | ||
result = append(result, ignoreDCPrefix+v) | ||
} | ||
return result | ||
} | ||
|
||
// Type implements pflag.Value interface | ||
func (dcm *dcMappings) Type() string { | ||
return "dc-mapping" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,185 @@ | ||
// Copyright (C) 2025 ScyllaDB | ||
package restore | ||
|
||
import ( | ||
"fmt" | ||
"slices" | ||
"testing" | ||
) | ||
|
||
func TestSetDCMapping(t *testing.T) { | ||
testCases := []struct { | ||
input string | ||
expectedErr bool | ||
expectedMappings dcMappings | ||
}{ | ||
{ | ||
input: "dc1=>dc2", | ||
expectedMappings: dcMappings{ | ||
{Source: []string{"dc1"}, Target: []string{"dc2"}}, | ||
}, | ||
}, | ||
{ | ||
input: "dc1, dc2=>dc1, dc2", | ||
expectedMappings: dcMappings{ | ||
{Source: []string{"dc1", "dc2"}, Target: []string{"dc1", "dc2"}}, | ||
}, | ||
}, | ||
{ | ||
input: "dc1=>dc3;dc2=>dc4", | ||
expectedMappings: dcMappings{ | ||
{Source: []string{"dc1"}, Target: []string{"dc3"}}, | ||
{Source: []string{"dc2"}, Target: []string{"dc4"}}, | ||
}, | ||
}, | ||
{ | ||
input: "dc1,dc2=>dc3", | ||
expectedMappings: dcMappings{ | ||
{Source: []string{"dc1", "dc2"}, Target: []string{"dc3"}}, | ||
}, | ||
}, | ||
{ | ||
input: "dc1,!dc2=>dc3", | ||
expectedMappings: dcMappings{ | ||
{ | ||
Source: []string{"dc1"}, | ||
Target: []string{"dc3"}, | ||
IgnoreSource: []string{"dc2"}, | ||
}, | ||
}, | ||
}, | ||
{ | ||
input: "dc1,!dc2=>dc3,!dc4", | ||
expectedMappings: dcMappings{ | ||
{ | ||
Source: []string{"dc1"}, | ||
Target: []string{"dc3"}, | ||
IgnoreSource: []string{"dc2"}, | ||
IgnoreTarget: []string{"dc4"}, | ||
}, | ||
}, | ||
}, | ||
{ | ||
input: "dc1,!dc2=>dc3,!dc4", | ||
expectedMappings: dcMappings{ | ||
{ | ||
Source: []string{"dc1"}, | ||
Target: []string{"dc3"}, | ||
|
||
IgnoreSource: []string{"dc2"}, | ||
IgnoreTarget: []string{"dc4"}, | ||
}, | ||
}, | ||
}, | ||
{ | ||
input: "!dc1,dc2=>dc3,!dc4", | ||
expectedMappings: dcMappings{ | ||
{ | ||
Source: []string{"dc2"}, | ||
Target: []string{"dc3"}, | ||
|
||
IgnoreSource: []string{"dc1"}, | ||
IgnoreTarget: []string{"dc4"}, | ||
}, | ||
}, | ||
}, | ||
{ | ||
input: "dc1=>dc3=>dc2=>dc4", | ||
expectedMappings: dcMappings{}, | ||
expectedErr: true, | ||
}, | ||
{ | ||
input: ";", | ||
expectedMappings: dcMappings{}, | ||
expectedErr: true, | ||
}, | ||
{ | ||
input: "=>", | ||
expectedMappings: dcMappings{}, | ||
expectedErr: true, | ||
}, | ||
{ | ||
input: "dc1=>", | ||
expectedMappings: dcMappings{}, | ||
expectedErr: true, | ||
}, | ||
{ | ||
input: "dc1=>;", | ||
expectedMappings: dcMappings{}, | ||
expectedErr: true, | ||
}, | ||
} | ||
|
||
for _, tc := range testCases { | ||
t.Run(tc.input, func(t *testing.T) { | ||
var mappings dcMappings | ||
|
||
err := mappings.Set(tc.input) | ||
if tc.expectedErr && err == nil { | ||
t.Fatal("Expected err, but got nil") | ||
} | ||
if !tc.expectedErr && err != nil { | ||
t.Fatalf("Unexpected err: %v", err) | ||
} | ||
slices.EqualFunc(tc.expectedMappings, mappings, func(a, b dcMapping) bool { | ||
return slices.Equal(a.Source, b.Source) && | ||
slices.Equal(a.Target, b.Target) && | ||
slices.Equal(a.IgnoreSource, b.IgnoreSource) && | ||
slices.Equal(a.IgnoreTarget, b.IgnoreTarget) | ||
}) | ||
}) | ||
} | ||
|
||
} | ||
|
||
func TestDCMappingString(t *testing.T) { | ||
testCases := []struct { | ||
mappings dcMappings | ||
expected string | ||
}{ | ||
{ | ||
mappings: dcMappings{ | ||
{Source: []string{"dc1"}, Target: []string{"dc2"}}, | ||
}, | ||
expected: "dc1=>dc2", | ||
}, | ||
{ | ||
mappings: dcMappings{ | ||
{Source: []string{"dc1"}, Target: []string{"dc2"}}, | ||
{Source: []string{"dc3"}, Target: []string{"dc4"}}, | ||
}, | ||
expected: "dc1=>dc2;dc3=>dc4", | ||
}, | ||
{ | ||
mappings: dcMappings{ | ||
{ | ||
Source: []string{"dc1"}, | ||
Target: []string{"dc2"}, | ||
IgnoreSource: []string{"dc2"}, | ||
}, | ||
}, | ||
expected: "dc1,!dc2=>dc2", | ||
}, | ||
{ | ||
mappings: dcMappings{ | ||
{ | ||
Source: []string{"dc1"}, | ||
Target: []string{"dc2"}, | ||
IgnoreSource: []string{"dc2"}, | ||
IgnoreTarget: []string{"dc3"}, | ||
}, | ||
}, | ||
expected: "dc1,!dc2=>dc2,!dc3", | ||
}, | ||
{}, | ||
} | ||
|
||
for i, tc := range testCases { | ||
t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) { | ||
actual := tc.mappings.String() | ||
if actual != tc.expected { | ||
t.Fatalf("Expected %q, but got %q", tc.expected, actual) | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters