Repository: containous/traefik
Pull request #6664 (2020-04-12 06:19:28)

Allows multi-level KV prefixes
<!--
PLEASE READ THIS MESSAGE.

Documentation fixes or enhancements:
- for Traefik v1: use branch v1.7
- for Traefik v2: use branch v2.2

Bug fixes:
- for Traefik v1: use branch v1.7
- for Traefik v2: use branch v2.2

Enhancements:
- for Traefik v1: we only accept bug fixes
- for Traefik v2: use branch master

HOW TO WRITE A GOOD PULL REQUEST? https://docs.traefik.io/contributing/submitting-pull-requests/

-->

### What does this PR do?
Allows slashes in root key of KV-storages.
 
<!-- A brief description of the change being made with this pull request. -->


### Motivation
We've got single KV storage shared across many of teams. Each team have its own directory in it. My team soon to have some separated Traefiks. So it would be cool, if we can separate configurations as:

```
team-a/traefik-1
team-a/traefik-2
```

There is no information about inadmissibility of slashes neither in documentation nor in logs, only message: "invalid root label". I see two possibilities of resolving this situation: add this constraint to documentation or add this feature.
As it's technically not too complex to implement, I chose 2nd variant.

<!-- What inspired you to submit this pull request? -->


### More

- [x] Added/updated tests
- [ ] Added/updated documentation



jstdips: It is possible to review and merge this PR? Will be helpful for us using Zookeeper as backend config store

Patch:
diff --git a/pkg/config/kv/kv_node.go b/pkg/config/kv/kv_node.go
index 3cb4f6c30..4b5a83594 100644
--- a/pkg/config/kv/kv_node.go
+++ b/pkg/config/kv/kv_node.go
@@ -20,13 +20,13 @@ func DecodeToNode(pairs []*store.KVPair, rootName string, filters ...string) (*p
 	var node *parser.Node
 
 	for i, pair := range sortedPairs {
-		split := strings.FieldsFunc(pair.Key, func(c rune) bool { return c == '/' })
-
-		if split[0] != rootName {
-			return nil, fmt.Errorf("invalid label root %s", split[0])
+		if !strings.HasPrefix(pair.Key, rootName+"/") {
+			return nil, fmt.Errorf("invalid label root %s", rootName)
 		}
 
-		var parts []string
+		split := strings.Split(pair.Key[len(rootName)+1:], "/")
+
+		parts := []string{rootName}
 		for _, fragment := range split {
 			if exp.MatchString(fragment) {
 				parts = append(parts, "["+fragment+"]")


Test patch:
diff --git a/pkg/config/kv/kv_test.go b/pkg/config/kv/kv_test.go
index 26f1a5e03..c94da1bce 100644
--- a/pkg/config/kv/kv_test.go
+++ b/pkg/config/kv/kv_test.go
@@ -8,42 +8,93 @@ import (
 )
 
 func TestDecode(t *testing.T) {
-	pairs := mapToPairs(map[string]string{
-		"traefik/fielda":        "bar",
-		"traefik/fieldb":        "1",
-		"traefik/fieldc":        "true",
-		"traefik/fieldd/0":      "one",
-		"traefik/fieldd/1":      "two",
-		"traefik/fielde":        "",
-		"traefik/fieldf/Test1":  "A",
-		"traefik/fieldf/Test2":  "B",
-		"traefik/fieldg/0/name": "A",
-		"traefik/fieldg/1/name": "B",
-	})
-
-	element := &sample{}
-
-	err := Decode(pairs, element, "traefik")
-	require.NoError(t, err)
-
-	expected := &sample{
-		FieldA: "bar",
-		FieldB: 1,
-		FieldC: true,
-		FieldD: []string{"one", "two"},
-		FieldE: &struct {
-			Name string
-		}{},
-		FieldF: map[string]string{
-			"Test1": "A",
-			"Test2": "B",
+	testCases := []struct {
+		desc     string
+		rootName string
+		pairs    map[string]string
+		expected *sample
+	}{
+		{
+			desc:     "simple case",
+			rootName: "traefik",
+			pairs: map[string]string{
+				"traefik/fielda":        "bar",
+				"traefik/fieldb":        "1",
+				"traefik/fieldc":        "true",
+				"traefik/fieldd/0":      "one",
+				"traefik/fieldd/1":      "two",
+				"traefik/fielde":        "",
+				"traefik/fieldf/Test1":  "A",
+				"traefik/fieldf/Test2":  "B",
+				"traefik/fieldg/0/name": "A",
+				"traefik/fieldg/1/name": "B",
+			},
+			expected: &sample{
+				FieldA: "bar",
+				FieldB: 1,
+				FieldC: true,
+				FieldD: []string{"one", "two"},
+				FieldE: &struct {
+					Name string
+				}{},
+				FieldF: map[string]string{
+					"Test1": "A",
+					"Test2": "B",
+				},
+				FieldG: []sub{
+					{Name: "A"},
+					{Name: "B"},
+				},
+			},
 		},
-		FieldG: []sub{
-			{Name: "A"},
-			{Name: "B"},
+		{
+			desc:     "multi-level root name",
+			rootName: "foo/bar/traefik",
+			pairs: map[string]string{
+				"foo/bar/traefik/fielda":        "bar",
+				"foo/bar/traefik/fieldb":        "2",
+				"foo/bar/traefik/fieldc":        "true",
+				"foo/bar/traefik/fieldd/0":      "one",
+				"foo/bar/traefik/fieldd/1":      "two",
+				"foo/bar/traefik/fielde":        "",
+				"foo/bar/traefik/fieldf/Test1":  "A",
+				"foo/bar/traefik/fieldf/Test2":  "B",
+				"foo/bar/traefik/fieldg/0/name": "A",
+				"foo/bar/traefik/fieldg/1/name": "B",
+			},
+			expected: &sample{
+				FieldA: "bar",
+				FieldB: 2,
+				FieldC: true,
+				FieldD: []string{"one", "two"},
+				FieldE: &struct {
+					Name string
+				}{},
+				FieldF: map[string]string{
+					"Test1": "A",
+					"Test2": "B",
+				},
+				FieldG: []sub{
+					{Name: "A"},
+					{Name: "B"},
+				},
+			},
 		},
 	}
-	assert.Equal(t, expected, element)
+
+	for _, test := range testCases {
+		test := test
+		t.Run(test.desc, func(t *testing.T) {
+			t.Parallel()
+
+			element := &sample{}
+
+			err := Decode(mapToPairs(test.pairs), element, test.rootName)
+			require.NoError(t, err)
+
+			assert.Equal(t, test.expected, element)
+		})
+	}
 }
 
 type sample struct {