Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Allow merging nested values in plugin config secretRef #2096

Merged
merged 14 commits into from
Dec 15, 2023
40 changes: 40 additions & 0 deletions docs/en/latest/concepts/apisix_route.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Copy link
Member

@kayx23 kayx23 Dec 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Does this have to be base64 encoded? If so, perhaps should be mentioned?
  2. Instead of putting a comment #content is "v2", you could consider mentioning djI= is base64 encoded v2 in the paragraph above?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes

---
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:
Expand Down
12 changes: 3 additions & 9 deletions pkg/providers/apisix/translation/apisix_pluginconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -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 {
Expand All @@ -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
Expand Down
6 changes: 4 additions & 2 deletions pkg/providers/apisix/translation/apisix_route.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
44 changes: 44 additions & 0 deletions pkg/providers/utils/insert_map.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// 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{})
AlinsRan marked this conversation as resolved.
Show resolved Hide resolved
}
InsertKeyInMap(restKey, value, newDest)
}
86 changes: 86 additions & 0 deletions pkg/providers/utils/insert_map_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// 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"
"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":"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)
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))
}
}
}
55 changes: 55 additions & 0 deletions test/e2e/suite-plugins/suite-plugins-general/secret_ref.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
Expand Down
Loading