Skip to content

Commit 2bb2029

Browse files
authored
fix(sendconfig): Do not cleanup nulls in plugin configuration (#7751)
* remove cleanup of nulls in plugin config * update tests * update changelog * dump plugin config in tests * find plugin in multiple plugins * use different paths for plugin tests and fix workspace
1 parent 5b8404b commit 2bb2029

File tree

5 files changed

+142
-49
lines changed

5 files changed

+142
-49
lines changed

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Adding a new version? You'll need three changes:
88
This is all the way at the bottom. It's the thing we always forget.
99
--->
1010

11+
- [3.5.2](#352)
1112
- [3.5.1](#351)
1213
- [3.5.0](#350)
1314
- [3.4.8](#348)
@@ -116,6 +117,17 @@ Adding a new version? You'll need three changes:
116117

117118
### Fixed
118119

120+
- Do not cleanup `null`s in the configuration of plugins with Kong running in
121+
DBLess mode in the translator of ingress-controller. This enables user to use
122+
explicit `null`s in plugins.
123+
[#7751](https://github.com/Kong/kubernetes-ingress-controller/pull/7751)
124+
125+
## [3.5.2]
126+
127+
> Release date: 2025-09-23
128+
129+
### Fixed
130+
119131
- Add `request-termination` plugin to return `500` if there are no available
120132
`backendRef` only when the service is translated from `HTTPRoute` or
121133
`GRPCRoute`.
@@ -4197,6 +4209,7 @@ Please read the changelog and test in your environment.
41974209
- The initial versions were rapildy iterated to deliver
41984210
a working ingress controller.
41994211

4212+
[3.5.2]: https://github.com/kong/kubernetes-ingress-controller/compare/v3.5.1...v3.5.2
42004213
[3.5.1]: https://github.com/kong/kubernetes-ingress-controller/compare/v3.5.0...v3.5.1
42014214
[3.5.0]: https://github.com/kong/kubernetes-ingress-controller/compare/v3.4.7...v3.5.0
42024215
[3.4.8]: https://github.com/kong/kubernetes-ingress-controller/compare/v3.4.7...v3.4.8

internal/dataplane/sendconfig/inmemory.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ func (s UpdateStrategyInMemory) Update(ctx context.Context, targetState ContentW
7575
}
7676
}
7777

78-
configSize := mo.Some[int](len(config))
78+
configSize := mo.Some(len(config))
7979
if reloadConfigErr := s.configService.ReloadDeclarativeRawConfig(
8080
ctx,
8181
bytes.NewReader(config),

internal/dataplane/sendconfig/inmemory_schema.go

Lines changed: 0 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -28,55 +28,12 @@ func (DefaultContentToDBLessConfigConverter) Convert(content *file.Content) DBLe
2828
// DBLess schema does not support decK's Info section.
2929
dblessConfig.Info = nil
3030

31-
// DBLess schema does not support nulls in plugin configs.
32-
cleanUpNullsInPluginConfigs(&dblessConfig.Content)
33-
3431
// DBLess schema does not 1-1 match decK's schema for ConsumerGroups.
3532
convertConsumerGroups(&dblessConfig)
3633

3734
return dblessConfig
3835
}
3936

40-
// cleanUpNullsInPluginConfigs removes null values from plugins' configs.
41-
func cleanUpNullsInPluginConfigs(state *file.Content) {
42-
for _, s := range state.Services {
43-
for _, p := range s.Plugins {
44-
for k, v := range p.Config {
45-
if v == nil {
46-
delete(p.Config, k)
47-
}
48-
}
49-
}
50-
for _, r := range state.Routes {
51-
for _, p := range r.Plugins {
52-
for k, v := range p.Config {
53-
if v == nil {
54-
delete(p.Config, k)
55-
}
56-
}
57-
}
58-
}
59-
}
60-
61-
for _, c := range state.Consumers {
62-
for _, p := range c.Plugins {
63-
for k, v := range p.Config {
64-
if v == nil {
65-
delete(p.Config, k)
66-
}
67-
}
68-
}
69-
}
70-
71-
for _, p := range state.Plugins {
72-
for k, v := range p.Config {
73-
if v == nil {
74-
delete(p.Config, k)
75-
}
76-
}
77-
}
78-
}
79-
8037
// convertConsumerGroups drops consumer groups related fields that are not supported in DBLess schema:
8138
// - Content.Consumers[].Groups,
8239
// - Content.ConsumerGroups[].Plugins

internal/dataplane/sendconfig/inmemory_schema_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,7 @@ func TestDefaultContentToDBLessConfigConverter(t *testing.T) {
279279
Plugin: kong.Plugin{
280280
Name: kong.String("p1"),
281281
Config: kong.Configuration{
282+
"config1": nil,
282283
"config2": "value2",
283284
},
284285
},
@@ -294,6 +295,7 @@ func TestDefaultContentToDBLessConfigConverter(t *testing.T) {
294295
Plugin: kong.Plugin{
295296
Name: kong.String("p1"),
296297
Config: kong.Configuration{
298+
"config1": nil,
297299
"config2": "value2",
298300
},
299301
},
@@ -311,6 +313,7 @@ func TestDefaultContentToDBLessConfigConverter(t *testing.T) {
311313
Plugin: kong.Plugin{
312314
Name: kong.String("p1"),
313315
Config: kong.Configuration{
316+
"config1": nil,
314317
"config2": "value2",
315318
},
316319
},
@@ -328,6 +331,7 @@ func TestDefaultContentToDBLessConfigConverter(t *testing.T) {
328331
Plugin: kong.Plugin{
329332
Name: kong.String("p1"),
330333
Config: kong.Configuration{
334+
"config1": nil,
331335
"config2": "value2",
332336
},
333337
},

test/integration/plugin_test.go

Lines changed: 124 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package integration
44

55
import (
66
"bytes"
7+
"encoding/json"
78
"fmt"
89
"io"
910
"net/http"
@@ -15,6 +16,7 @@ import (
1516
"github.com/kong/go-kong/kong"
1617
"github.com/kong/kubernetes-testing-framework/pkg/clusters"
1718
"github.com/kong/kubernetes-testing-framework/pkg/utils/kubernetes/generators"
19+
"github.com/samber/lo"
1820
"github.com/stretchr/testify/assert"
1921
"github.com/stretchr/testify/require"
2022
corev1 "k8s.io/api/core/v1"
@@ -25,12 +27,15 @@ import (
2527
configurationv1 "github.com/kong/kubernetes-configuration/v2/api/configuration/v1"
2628
"github.com/kong/kubernetes-configuration/v2/pkg/clientset"
2729

30+
"github.com/kong/kubernetes-ingress-controller/v3/internal/adminapi"
2831
"github.com/kong/kubernetes-ingress-controller/v3/internal/annotations"
2932
"github.com/kong/kubernetes-ingress-controller/v3/internal/gatewayapi"
3033
"github.com/kong/kubernetes-ingress-controller/v3/internal/labels"
34+
managercfg "github.com/kong/kubernetes-ingress-controller/v3/pkg/manager/config"
3135
"github.com/kong/kubernetes-ingress-controller/v3/test"
3236
"github.com/kong/kubernetes-ingress-controller/v3/test/consts"
3337
"github.com/kong/kubernetes-ingress-controller/v3/test/internal/helpers"
38+
"github.com/kong/kubernetes-ingress-controller/v3/test/internal/testenv"
3439
)
3540

3641
func TestPluginEssentials(t *testing.T) {
@@ -182,7 +187,7 @@ func TestPluginConfigPatch(t *testing.T) {
182187
cleaner.Add(service)
183188

184189
t.Logf("creating an ingress for service %s with ingress.class %s", service.Name, consts.IngressClass)
185-
ingress := generators.NewIngressForService("/test_plugin_essentials", map[string]string{
190+
ingress := generators.NewIngressForService("/test_plugin_config_patch", map[string]string{
186191
"konghq.com/strip-path": "true",
187192
}, service)
188193
ingress.Spec.IngressClassName = kong.String(consts.IngressClass)
@@ -191,7 +196,7 @@ func TestPluginConfigPatch(t *testing.T) {
191196

192197
t.Log("waiting for routes from Ingress to be operational")
193198
assert.Eventually(t, func() bool {
194-
resp, err := helpers.DefaultHTTPClient().Get(fmt.Sprintf("%s/test_plugin_essentials", proxyHTTPURL))
199+
resp, err := helpers.DefaultHTTPClient().Get(fmt.Sprintf("%s/test_plugin_config_patch", proxyHTTPURL))
195200
if err != nil {
196201
t.Logf("WARNING: error while waiting for %s: %v", proxyHTTPURL, err)
197202
return false
@@ -293,7 +298,7 @@ func TestPluginConfigPatch(t *testing.T) {
293298

294299
t.Logf("validating that plugin %s was successfully configured", kongplugin.Name)
295300
assert.Eventually(t, func() bool {
296-
resp, err := helpers.DefaultHTTPClient().Get(fmt.Sprintf("%s/test_plugin_essentials", proxyHTTPURL))
301+
resp, err := helpers.DefaultHTTPClient().Get(fmt.Sprintf("%s/test_plugin_config_patch", proxyHTTPURL))
297302
if err != nil {
298303
t.Logf("WARNING: error while waiting for %s: %v", proxyHTTPURL, err)
299304
return false
@@ -316,7 +321,7 @@ func TestPluginConfigPatch(t *testing.T) {
316321

317322
t.Logf("validating that clusterplugin %s was successfully configured", kongclusterplugin.Name)
318323
assert.Eventually(t, func() bool {
319-
resp, err := helpers.DefaultHTTPClient().Get(fmt.Sprintf("%s/test_plugin_essentials", proxyHTTPURL))
324+
resp, err := helpers.DefaultHTTPClient().Get(fmt.Sprintf("%s/test_plugin_config_patch", proxyHTTPURL))
320325
if err != nil {
321326
t.Logf("WARNING: error while waiting for %s: %v", proxyHTTPURL, err)
322327
return false
@@ -328,7 +333,7 @@ func TestPluginConfigPatch(t *testing.T) {
328333

329334
t.Log("deleting Ingress and waiting for routes to be torn down")
330335
require.NoError(t, clusters.DeleteIngress(ctx, env.Cluster(), ns.Name, ingress))
331-
helpers.EventuallyExpectHTTP404WithNoRoute(t, proxyHTTPURL, proxyHTTPURL.Host, "/test_plugin_essentials", ingressWait, waitTick, nil)
336+
helpers.EventuallyExpectHTTP404WithNoRoute(t, proxyHTTPURL, proxyHTTPURL.Host, "/test_plugin_config_patch", ingressWait, waitTick, nil)
332337
}
333338

334339
func TestPluginOrdering(t *testing.T) {
@@ -712,3 +717,117 @@ func TestPluginCrossNamespaceReference(t *testing.T) {
712717
assert.True(c, resp.StatusCode == http.StatusTeapot)
713718
}, ingressWait, waitTick)
714719
}
720+
721+
func TestPluginNullInConfig(t *testing.T) {
722+
ctx := t.Context()
723+
724+
t.Parallel()
725+
ns, cleaner := helpers.Setup(ctx, t, env)
726+
727+
t.Log("deploying a minimal HTTP container deployment to test Ingress routes")
728+
container := generators.NewContainer("httpbin", test.HTTPBinImage, test.HTTPBinPort)
729+
deployment := generators.NewDeploymentForContainer(container)
730+
deployment, err := env.Cluster().Client().AppsV1().Deployments(ns.Name).Create(ctx, deployment, metav1.CreateOptions{})
731+
require.NoError(t, err)
732+
cleaner.Add(deployment)
733+
734+
t.Logf("exposing deployment %s via service", deployment.Name)
735+
service := generators.NewServiceForDeployment(deployment, corev1.ServiceTypeLoadBalancer)
736+
service, err = env.Cluster().Client().CoreV1().Services(ns.Name).Create(ctx, service, metav1.CreateOptions{})
737+
require.NoError(t, err)
738+
cleaner.Add(service)
739+
740+
t.Logf("creating an ingress for service %s with ingress.class %s", service.Name, consts.IngressClass)
741+
ingress := generators.NewIngressForService("/test_plugin_null_in_config", map[string]string{
742+
"konghq.com/strip-path": "true",
743+
}, service)
744+
ingress.Spec.IngressClassName = kong.String(consts.IngressClass)
745+
ingress, err = env.Cluster().Client().NetworkingV1().Ingresses(ns.Name).Create(ctx, ingress, metav1.CreateOptions{})
746+
require.NoError(t, err)
747+
cleaner.Add(ingress)
748+
749+
t.Log("waiting for routes from Ingress to be operational")
750+
assert.Eventually(t, func() bool {
751+
resp, err := helpers.DefaultHTTPClient().Get(fmt.Sprintf("%s/test_plugin_null_in_config", proxyHTTPURL))
752+
if err != nil {
753+
t.Logf("WARNING: error while waiting for %s: %v", proxyHTTPURL, err)
754+
return false
755+
}
756+
defer resp.Body.Close()
757+
if resp.StatusCode == http.StatusOK {
758+
// now that the ingress backend is routable, make sure the contents we're getting back are what we expect
759+
// Expected: "<title>httpbin.org</title>"
760+
b := new(bytes.Buffer)
761+
n, err := b.ReadFrom(resp.Body)
762+
require.NoError(t, err)
763+
require.True(t, n > 0)
764+
return strings.Contains(b.String(), "<title>httpbin.org</title>")
765+
}
766+
return false
767+
}, ingressWait, waitTick)
768+
769+
t.Log("Creating a plugin with `null` in its configuration")
770+
771+
kongplugin := &configurationv1.KongPlugin{
772+
ObjectMeta: metav1.ObjectMeta{
773+
Namespace: ns.Name,
774+
Name: "plugin-datadog",
775+
},
776+
InstanceName: "plugin-with-null",
777+
PluginName: "datadog",
778+
Config: apiextensionsv1.JSON{
779+
Raw: []byte(`{"host":"localhost","port":8125,"prefix":null}`),
780+
},
781+
}
782+
c, err := clientset.NewForConfig(env.Cluster().Config())
783+
require.NoError(t, err)
784+
kongplugin, err = c.ConfigurationV1().KongPlugins(ns.Name).Create(ctx, kongplugin, metav1.CreateOptions{})
785+
require.NoError(t, err)
786+
cleaner.Add(kongplugin)
787+
788+
t.Logf("Updating Ingress to use plugin %s", kongplugin.Name)
789+
require.Eventually(t, func() bool {
790+
ingress, err := env.Cluster().Client().NetworkingV1().Ingresses(ns.Name).Get(ctx, ingress.Name, metav1.GetOptions{})
791+
if err != nil {
792+
return false
793+
}
794+
ingress.Annotations[annotations.AnnotationPrefix+annotations.PluginsKey] = kongplugin.Name
795+
_, err = env.Cluster().Client().NetworkingV1().Ingresses(ns.Name).Update(ctx, ingress, metav1.UpdateOptions{})
796+
return err == nil
797+
}, ingressWait, waitTick)
798+
799+
t.Logf("Checking the configuration of the plugin %s in Kong", kongplugin.Name)
800+
kc, err := adminapi.NewKongAPIClient(proxyAdminURL.String(), managercfg.AdminAPIClientConfig{}, consts.KongTestPassword)
801+
require.NoError(t, err, "failed to create Kong client")
802+
// For integration tests in enterprise edition and postgres DB backed Kong gateway,
803+
// the tests are run in "notdefault" workspace of Kong.
804+
if testenv.DBMode() != testenv.DBModeOff && testenv.KongEnterpriseEnabled() {
805+
kc.SetWorkspace(consts.KongTestWorkspace)
806+
}
807+
require.Eventually(t, func() bool {
808+
plugins, err := kc.Plugins.ListAll(ctx)
809+
require.NoError(t, err, "failed to list plugins")
810+
811+
datadogPlugin, found := lo.Find(plugins, func(p *kong.Plugin) bool {
812+
return p.Name != nil && *p.Name == "datadog"
813+
})
814+
if !found {
815+
t.Logf("datadog plugin not found. %d plugins found: %s",
816+
len(plugins),
817+
strings.Join(
818+
lo.Map(plugins, func(p *kong.Plugin, _ int) string {
819+
return lo.FromPtrOr(p.Name, "_")
820+
}),
821+
","),
822+
)
823+
return false
824+
}
825+
826+
configJSON, err := json.Marshal(datadogPlugin.Config)
827+
require.NoError(t, err)
828+
t.Logf("Configuration of datadog plugin: %s", string(configJSON))
829+
830+
configPrefix, ok := datadogPlugin.Config["prefix"]
831+
return ok && configPrefix == nil
832+
}, ingressWait, waitTick, "failed to find 'datadog' plugin with null in config.prefix in Kong")
833+
}

0 commit comments

Comments
 (0)