Skip to content

Commit c854390

Browse files
authored
Merge pull request #8793 from ipfs/kylehuntsman/fix/repo/omitempty-error
fix(fsrepo): deep merge when setting config
2 parents beaa8fc + f62bd64 commit c854390

File tree

5 files changed

+197
-22
lines changed

5 files changed

+197
-22
lines changed

repo/common/common.go

+37-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,13 @@ func MapGetKV(v map[string]interface{}, key string) (interface{}, error) {
2121

2222
cursor, ok = mcursor[part]
2323
if !ok {
24-
return nil, fmt.Errorf("%s key has no attributes", sofar)
24+
// Construct the current path traversed to print a nice error message
25+
var path string
26+
if len(sofar) > 0 {
27+
path += sofar + "."
28+
}
29+
path += part
30+
return nil, fmt.Errorf("%s not found", path)
2531
}
2632
}
2733
return cursor, nil
@@ -54,3 +60,33 @@ func MapSetKV(v map[string]interface{}, key string, value interface{}) error {
5460
}
5561
return nil
5662
}
63+
64+
// Merges the right map into the left map, recursively traversing child maps
65+
// until a non-map value is found
66+
func MapMergeDeep(left, right map[string]interface{}) map[string]interface{} {
67+
// We want to alter a copy of the map, not the original
68+
result := make(map[string]interface{})
69+
for k, v := range left {
70+
result[k] = v
71+
}
72+
73+
for key, rightVal := range right {
74+
// If right value is a map
75+
if rightMap, ok := rightVal.(map[string]interface{}); ok {
76+
// If key is in left
77+
if leftVal, found := result[key]; found {
78+
// If left value is also a map
79+
if leftMap, ok := leftVal.(map[string]interface{}); ok {
80+
// Merge nested map
81+
result[key] = MapMergeDeep(leftMap, rightMap)
82+
continue
83+
}
84+
}
85+
}
86+
87+
// Otherwise set new value to result
88+
result[key] = rightVal
89+
}
90+
91+
return result
92+
}

repo/common/common_test.go

+132
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package common
2+
3+
import (
4+
"testing"
5+
6+
"github.com/ipfs/go-ipfs/thirdparty/assert"
7+
)
8+
9+
func TestMapMergeDeepReturnsNew(t *testing.T) {
10+
leftMap := make(map[string]interface{})
11+
leftMap["A"] = "Hello World"
12+
13+
rightMap := make(map[string]interface{})
14+
rightMap["A"] = "Foo"
15+
16+
MapMergeDeep(leftMap, rightMap)
17+
18+
assert.True(leftMap["A"] == "Hello World", t, "MapMergeDeep should return a new map instance")
19+
}
20+
21+
func TestMapMergeDeepNewKey(t *testing.T) {
22+
leftMap := make(map[string]interface{})
23+
leftMap["A"] = "Hello World"
24+
/*
25+
leftMap
26+
{
27+
A: "Hello World"
28+
}
29+
*/
30+
31+
rightMap := make(map[string]interface{})
32+
rightMap["B"] = "Bar"
33+
/*
34+
rightMap
35+
{
36+
B: "Bar"
37+
}
38+
*/
39+
40+
result := MapMergeDeep(leftMap, rightMap)
41+
/*
42+
expected
43+
{
44+
A: "Hello World"
45+
B: "Bar"
46+
}
47+
*/
48+
49+
assert.True(result["B"] == "Bar", t, "New keys in right map should exist in resulting map")
50+
}
51+
52+
func TestMapMergeDeepRecursesOnMaps(t *testing.T) {
53+
leftMapA := make(map[string]interface{})
54+
leftMapA["B"] = "A value!"
55+
leftMapA["C"] = "Another value!"
56+
57+
leftMap := make(map[string]interface{})
58+
leftMap["A"] = leftMapA
59+
/*
60+
leftMap
61+
{
62+
A: {
63+
B: "A value!"
64+
C: "Another value!"
65+
}
66+
}
67+
*/
68+
69+
rightMapA := make(map[string]interface{})
70+
rightMapA["C"] = "A different value!"
71+
72+
rightMap := make(map[string]interface{})
73+
rightMap["A"] = rightMapA
74+
/*
75+
rightMap
76+
{
77+
A: {
78+
C: "A different value!"
79+
}
80+
}
81+
*/
82+
83+
result := MapMergeDeep(leftMap, rightMap)
84+
/*
85+
expected
86+
{
87+
A: {
88+
B: "A value!"
89+
C: "A different value!"
90+
}
91+
}
92+
*/
93+
94+
resultA := result["A"].(map[string]interface{})
95+
assert.True(resultA["B"] == "A value!", t, "Unaltered values should not change")
96+
assert.True(resultA["C"] == "A different value!", t, "Nested values should be altered")
97+
}
98+
99+
func TestMapMergeDeepRightNotAMap(t *testing.T) {
100+
leftMapA := make(map[string]interface{})
101+
leftMapA["B"] = "A value!"
102+
103+
leftMap := make(map[string]interface{})
104+
leftMap["A"] = leftMapA
105+
/*
106+
origMap
107+
{
108+
A: {
109+
B: "A value!"
110+
}
111+
}
112+
*/
113+
114+
rightMap := make(map[string]interface{})
115+
rightMap["A"] = "Not a map!"
116+
/*
117+
newMap
118+
{
119+
A: "Not a map!"
120+
}
121+
*/
122+
123+
result := MapMergeDeep(leftMap, rightMap)
124+
/*
125+
expected
126+
{
127+
A: "Not a map!"
128+
}
129+
*/
130+
131+
assert.True(result["A"] == "Not a map!", t, "Right values that are not a map should be set on the result")
132+
}

repo/fsrepo/fsrepo.go

+26-18
Original file line numberDiff line numberDiff line change
@@ -542,8 +542,26 @@ func (r *FSRepo) BackupConfig(prefix string) (string, error) {
542542
return orig.Name(), nil
543543
}
544544

545-
// setConfigUnsynced is for private use.
546-
func (r *FSRepo) setConfigUnsynced(updated *config.Config) error {
545+
// SetConfig updates the FSRepo's config. The user must not modify the config
546+
// object after calling this method.
547+
// FIXME: There is an inherent contradiction with storing non-user-generated
548+
// Go config.Config structures as user-generated JSON nested maps. This is
549+
// evidenced by the issue of `omitempty` property of fields that aren't defined
550+
// by the user and Go still needs to initialize them to its default (which
551+
// is not reflected in the repo's config file, see
552+
// https://github.com/ipfs/go-ipfs/issues/8088 for more details).
553+
// In general we should call this API with a JSON nested maps as argument
554+
// (`map[string]interface{}`). Many calls to this function are forced to
555+
// synthesize the config.Config struct from their available JSON map just to
556+
// satisfy this (causing incompatibilities like the `omitempty` one above).
557+
// We need to comb SetConfig calls and replace them when possible with a
558+
// JSON map variant.
559+
func (r *FSRepo) SetConfig(updated *config.Config) error {
560+
561+
// packageLock is held to provide thread-safety.
562+
packageLock.Lock()
563+
defer packageLock.Unlock()
564+
547565
configFilename, err := config.Filename(r.path)
548566
if err != nil {
549567
return err
@@ -559,10 +577,8 @@ func (r *FSRepo) setConfigUnsynced(updated *config.Config) error {
559577
if err != nil {
560578
return err
561579
}
562-
for k, v := range m {
563-
mapconf[k] = v
564-
}
565-
if err := serialize.WriteConfigFile(configFilename, mapconf); err != nil {
580+
mergedMap := common.MapMergeDeep(mapconf, m)
581+
if err := serialize.WriteConfigFile(configFilename, mergedMap); err != nil {
566582
return err
567583
}
568584
// Do not use `*r.config = ...`. This will modify the *shared* config
@@ -571,17 +587,6 @@ func (r *FSRepo) setConfigUnsynced(updated *config.Config) error {
571587
return nil
572588
}
573589

574-
// SetConfig updates the FSRepo's config. The user must not modify the config
575-
// object after calling this method.
576-
func (r *FSRepo) SetConfig(updated *config.Config) error {
577-
578-
// packageLock is held to provide thread-safety.
579-
packageLock.Lock()
580-
defer packageLock.Unlock()
581-
582-
return r.setConfigUnsynced(updated)
583-
}
584-
585590
// GetConfigKey retrieves only the value of a particular key.
586591
func (r *FSRepo) GetConfigKey(key string) (interface{}, error) {
587592
packageLock.Lock()
@@ -645,10 +650,13 @@ func (r *FSRepo) SetConfigKey(key string, value interface{}) error {
645650
if err != nil {
646651
return err
647652
}
653+
r.config = conf
654+
648655
if err := serialize.WriteConfigFile(filename, mapconf); err != nil {
649656
return err
650657
}
651-
return r.setConfigUnsynced(conf) // TODO roll this into this method
658+
659+
return nil
652660
}
653661

654662
// Datastore returns a repo-owned datastore. If FSRepo is Closed, return value

test/sharness/t0171-peering.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ peer_addrs() {
3333
peer() {
3434
PEER1="$1" &&
3535
PEER2="$2" &&
36-
PEER_LIST="$(ipfsi "$PEER1" config Peering.Peers)" &&
36+
PEER_LIST="$(ipfsi "$PEER1" config Peering.Peers || true)" &&
3737
{ [[ "$PEER_LIST" == "null" ]] || PEER_LIST_INNER="${PEER_LIST:1:-1}"; } &&
3838
ADDR_INFO="$(printf '[%s{"ID": "%s", "Addrs": %s}]' \
3939
"${PEER_LIST_INNER:+${PEER_LIST_INNER},}" \

test/sharness/t0250-files-api.sh

+1-2
Original file line numberDiff line numberDiff line change
@@ -875,8 +875,7 @@ test_expect_success "set up automatic sharding/unsharding data" '
875875
done
876876
'
877877

878-
# TODO: This does not need to report an error https://github.com/ipfs/go-ipfs/issues/8088
879-
test_expect_failure "reset automatic sharding" '
878+
test_expect_success "reset automatic sharding" '
880879
ipfs config --json Internal.UnixFSShardingSizeThreshold null
881880
'
882881

0 commit comments

Comments
 (0)