diff --git a/docs/en/latest/concepts/apisix_route.md b/docs/en/latest/concepts/apisix_route.md index 5da3982972..c40937fb25 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 f8b322176b..5ee0e2a4c2 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 50d8b65236..856709e62d 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 0000000000..750564477c --- /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 0000000000..b97bc6dd00 --- /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 c8123890f8..733d323871 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) })