This is an automated email from the ASF dual-hosted git repository.

ashishtiwari pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix-ingress-controller.git


The following commit(s) were added to refs/heads/master by this push:
     new a759ae5c feat: Allow merging nested values in plugin config secretRef 
(#2096)
a759ae5c is described below

commit a759ae5c37404e0e43a5188281cb04bd617318f1
Author: Ashish Tiwari <[email protected]>
AuthorDate: Fri Dec 15 16:00:58 2023 +0530

    feat: Allow merging nested values in plugin config secretRef (#2096)
    
    * feat: Allow merging nested values in plugin config secretRef
    
    
    
    ---------
    
    Signed-off-by: Ashish Tiwari <[email protected]>
---
 docs/en/latest/concepts/apisix_route.md            |  40 +++++++
 .../apisix/translation/apisix_pluginconfig.go      |  12 +-
 pkg/providers/apisix/translation/apisix_route.go   |   6 +-
 pkg/providers/utils/insert_map.go                  |  45 +++++++
 pkg/providers/utils/insert_map_test.go             | 133 +++++++++++++++++++++
 .../suite-plugins-general/secret_ref.go            |  55 +++++++++
 6 files changed, 280 insertions(+), 11 deletions(-)

diff --git a/docs/en/latest/concepts/apisix_route.md 
b/docs/en/latest/concepts/apisix_route.md
index 5da39829..c40937fb 100644
--- a/docs/en/latest/concepts/apisix_route.md
+++ b/docs/en/latest/concepts/apisix_route.md
@@ -262,6 +262,46 @@ spec:
           secretRef: echo
 ```
 
+## Config with secretRef where the secret data contains path to a specific key 
that needs to be overridden in plugin config
+
+You can also configure specific fields in the plugin configuration that are 
deeply nested by passing the path to that field. The path is dot-separated keys 
that lead to that field. The below example overrides the `X-Foo` header field 
in the plugin configuration from `v1` to `v2`.
+
+```yaml
+apiVersion: v1
+kind: Secret
+metadata:
+ #content is "v2"
+ name: echo
+data:
+ headers.X-Foo: djI=
+---
+apiVersion: apisix.apache.org/v2
+kind: ApisixRoute
+metadata:
+ name: httpbin-route
+spec:
+ http:
+ - name: rule1
+   match:
+     hosts:
+     - httpbin.org
+     paths:
+       - /ip
+   backends:
+   - serviceName: %s
+     servicePort: %d
+     weight: 10
+   plugins:
+   - name: echo
+     enable: true
+     config:
+       before_body: "This is the preface"
+       after_body: "This is the epilogue"
+       headers:
+         X-Foo: v1
+     secretRef: echo
+```
+
 ## Websocket proxy
 
 You can route requests to 
[WebSocket](https://en.wikipedia.org/wiki/WebSocket#:~:text=WebSocket%20is%20a%20computer%20communications,WebSocket%20is%20distinct%20from%20HTTP.)
 services by setting the `websocket` attribute to `true` as shown below:
diff --git a/pkg/providers/apisix/translation/apisix_pluginconfig.go 
b/pkg/providers/apisix/translation/apisix_pluginconfig.go
index f8b32217..5ee0e2a4 100644
--- a/pkg/providers/apisix/translation/apisix_pluginconfig.go
+++ b/pkg/providers/apisix/translation/apisix_pluginconfig.go
@@ -21,6 +21,7 @@ import (
        configv2 
"github.com/apache/apisix-ingress-controller/pkg/kube/apisix/apis/config/v2"
        "github.com/apache/apisix-ingress-controller/pkg/log"
        "github.com/apache/apisix-ingress-controller/pkg/providers/translation"
+       "github.com/apache/apisix-ingress-controller/pkg/providers/utils"
        apisixv1 
"github.com/apache/apisix-ingress-controller/pkg/types/apisix/v1"
 )
 
@@ -33,14 +34,6 @@ func (t *translator) TranslatePluginConfigV2(config 
*configv2.ApisixPluginConfig
                                continue
                        }
                        if plugin.Config != nil {
-                               // Here, it will override same key.
-                               if t, ok := pluginMap[plugin.Name]; ok {
-                                       log.Infow("TranslatePluginConfigV2 
override same plugin key",
-                                               zap.String("key", plugin.Name),
-                                               zap.Any("old", t),
-                                               zap.Any("new", plugin.Config),
-                                       )
-                               }
                                if plugin.SecretRef != "" {
                                        sec, err := 
t.SecretLister.Secrets(config.Namespace).Get(plugin.SecretRef)
                                        if err != nil {
@@ -52,8 +45,9 @@ func (t *translator) TranslatePluginConfigV2(config 
*configv2.ApisixPluginConfig
                                        log.Debugw("Add new items, then 
override items with the same plugin key",
                                                zap.Any("plugin", plugin.Name),
                                                zap.String("secretRef", 
plugin.SecretRef))
+
                                        for key, value := range sec.Data {
-                                               plugin.Config[key] = 
string(value)
+                                               utils.InsertKeyInMap(key, 
string(value), plugin.Config)
                                        }
                                }
                                pluginMap[plugin.Name] = plugin.Config
diff --git a/pkg/providers/apisix/translation/apisix_route.go 
b/pkg/providers/apisix/translation/apisix_route.go
index 50d8b652..856709e6 100644
--- a/pkg/providers/apisix/translation/apisix_route.go
+++ b/pkg/providers/apisix/translation/apisix_route.go
@@ -31,6 +31,7 @@ import (
        _const 
"github.com/apache/apisix-ingress-controller/pkg/kube/apisix/const"
        "github.com/apache/apisix-ingress-controller/pkg/log"
        "github.com/apache/apisix-ingress-controller/pkg/providers/translation"
+       "github.com/apache/apisix-ingress-controller/pkg/providers/utils"
        apisixv1 
"github.com/apache/apisix-ingress-controller/pkg/types/apisix/v1"
 )
 
@@ -101,8 +102,9 @@ func (t *translator) translateHTTPRouteV2(ctx 
*translation.TranslateContext, ar
                                        log.Debugw("Add new items, then 
override items with the same plugin key",
                                                zap.Any("plugin", plugin.Name),
                                                zap.String("secretRef", 
plugin.SecretRef))
+
                                        for key, value := range sec.Data {
-                                               plugin.Config[key] = 
string(value)
+                                               utils.InsertKeyInMap(key, 
string(value), plugin.Config)
                                        }
                                }
                                pluginMap[plugin.Name] = plugin.Config
@@ -536,7 +538,7 @@ func (t *translator) translateStreamRouteV2(ctx 
*translation.TranslateContext, a
                                                zap.Any("plugin", plugin.Name),
                                                zap.String("secretRef", 
plugin.SecretRef))
                                        for key, value := range sec.Data {
-                                               plugin.Config[key] = 
string(value)
+                                               utils.InsertKeyInMap(key, 
string(value), plugin.Config)
                                        }
                                }
                                pluginMap[plugin.Name] = plugin.Config
diff --git a/pkg/providers/utils/insert_map.go 
b/pkg/providers/utils/insert_map.go
new file mode 100644
index 00000000..75056447
--- /dev/null
+++ b/pkg/providers/utils/insert_map.go
@@ -0,0 +1,45 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements.  See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package utils
+
+import (
+       "strings"
+)
+
+// InsertKeyInMap takes a dot separated string and recursively goes inside the 
destination
+// to fill the value
+func InsertKeyInMap(key string, value interface{}, dest 
map[string]interface{}) {
+       if key == "" {
+               return
+       }
+       keys := strings.SplitN(key, ".", 2)
+       //base condition. the length of keys will be atleast 1
+       if len(keys) < 2 {
+               dest[keys[0]] = value
+               return
+       }
+
+       ikey := keys[0]
+       restKey := keys[1]
+       if dest[ikey] == nil {
+               dest[ikey] = make(map[string]interface{})
+       }
+       newDest, ok := dest[ikey].(map[string]interface{})
+       if !ok {
+               newDest = make(map[string]interface{})
+               dest[ikey] = newDest
+       }
+       InsertKeyInMap(restKey, value, newDest)
+}
diff --git a/pkg/providers/utils/insert_map_test.go 
b/pkg/providers/utils/insert_map_test.go
new file mode 100644
index 00000000..b97bc6dd
--- /dev/null
+++ b/pkg/providers/utils/insert_map_test.go
@@ -0,0 +1,133 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements.  See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package utils
+
+import (
+       "encoding/json"
+       "fmt"
+       "testing"
+)
+
+func TestInsertKeyInMap(t *testing.T) {
+       type testCase struct {
+               key    string
+               value  interface{}
+               dest   string
+               merged string
+       }
+       testCases := []testCase{{
+               dest: `{
+                       "a":1,
+                       "b":{
+                               "c":{
+                                       "d":"e"
+                               },
+                               "f":"g"
+                       }
+               }`,
+               key:   `b.c`,
+               value: 2,
+               merged: `{
+                       "a":1,
+                       "b":{
+                               "c":2,
+                               "f":"g"
+                       }
+               }`,
+       }, {
+               dest: `{
+                       "a":1,
+                       "b":{
+                               "c": 2,
+                               "f":"g"
+                       }
+               }`,
+               key: `b.c`,
+               value: map[string]string{
+                       "d": "e",
+               },
+               merged: `{
+                       "a":1,
+                       "b":{
+                               "c":{
+                                       "d":"e"
+                               },
+                               "f":"g"
+                       }
+               }`,
+       }, {
+               dest: `{
+                       "a":1,
+                       "b":{
+                               "c": 2,
+                               "f":"g"
+                       }
+               }`,
+               key: `b.c.d`,
+               value: map[string]string{
+                       "x": "y",
+               },
+               merged: `{
+                       "a":1,
+                       "b":{
+                               "c":{
+                                       "d":{
+                                               "x":"y"
+                                       }
+                               },
+                               "f":"g"
+                       }
+               }`,
+       }, {
+               dest: `{
+                       "a":1,
+                       "b":"old"
+               }
+               `,
+               key:   "b",
+               value: "new",
+               merged: `{
+                       "a":1,
+                       "b":"new"
+               }`,
+       }}
+
+       for _, t0 := range testCases {
+               destMap := make(map[string]interface{})
+               err := json.Unmarshal([]byte(t0.dest), &destMap)
+               if err != nil {
+                       t.Fatal(err)
+               }
+               out := make(map[string]interface{})
+               err = json.Unmarshal([]byte(t0.merged), &out)
+               if err != nil {
+                       t.Fatal(err)
+               }
+               outB, err := json.MarshalIndent(out, " ", "")
+               if err != nil {
+                       t.Fatal(err)
+               }
+
+               InsertKeyInMap(t0.key, t0.value, destMap)
+               fmt.Println(destMap)
+               merged, err := json.MarshalIndent(destMap, " ", "")
+               if err != nil {
+                       t.Fatal(err)
+               }
+               if string(outB) != string(merged) {
+                       t.Errorf("Expected \n%s\n but got \n%s\n", 
string(outB), string(merged))
+               }
+       }
+}
diff --git a/test/e2e/suite-plugins/suite-plugins-general/secret_ref.go 
b/test/e2e/suite-plugins/suite-plugins-general/secret_ref.go
index c8123890..733d3238 100644
--- a/test/e2e/suite-plugins/suite-plugins-general/secret_ref.go
+++ b/test/e2e/suite-plugins/suite-plugins-general/secret_ref.go
@@ -87,7 +87,62 @@ spec:
                        resp.Body().Contains("This is the epilogue")
                        resp.Body().Contains("my custom body")
                })
+
+               ginkgo.It("suite-plugins-general: nested plugin config with 
secretRef", func() {
+                       backendSvc, backendPorts := s.DefaultHTTPBackend()
+                       secret := `
+apiVersion: v1
+kind: Secret
+metadata:
+ name: echo
+data:
+ headers.X-Foo: djI=
+ # content is "my custom body"
+ body: Im15IGN1c3RvbSBib2R5Ig==
+`
+                       assert.Nil(ginkgo.GinkgoT(), 
s.CreateResourceFromString(secret), "creating echo secret for ApisixRoute")
+                       ar := fmt.Sprintf(`
+apiVersion: apisix.apache.org/v2
+kind: ApisixRoute
+metadata:
+ name: httpbin-route
+spec:
+ http:
+ - name: rule1
+   match:
+     hosts:
+     - httpbin.org
+     paths:
+       - /ip
+   backends:
+   - serviceName: %s
+     servicePort: %d
+     weight: 10
+   plugins:
+   - name: echo
+     enable: true
+     config:
+       before_body: "This is the preface"
+       after_body: "This is the epilogue"
+       headers:
+         X-Foo: v1
+     secretRef: echo
+       
+`, backendSvc, backendPorts[0])
+
+                       assert.Nil(ginkgo.GinkgoT(), 
s.CreateVersionedApisixResource(ar))
+
+                       err := s.EnsureNumApisixUpstreamsCreated(1)
+                       assert.Nil(ginkgo.GinkgoT(), err, "Checking number of 
upstreams")
+                       err = s.EnsureNumApisixRoutesCreated(1)
+                       assert.Nil(ginkgo.GinkgoT(), err, "Checking number of 
routes")
+
+                       resp := 
s.NewAPISIXClient().GET("/ip").WithHeader("Host", "httpbin.org").Expect()
+                       resp.Status(http.StatusOK)
+                       resp.Header("X-Foo").Equal("v2")
+               })
        }
+
        ginkgo.Describe("suite-plugins-general: scaffold v2", func() {
                suites(scaffold.NewDefaultV2Scaffold)
        })

Reply via email to