diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a695f8be8..148baee389 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Adding a new version? You'll need three changes: This is all the way at the bottom. It's the thing we always forget. ---> +- [3.5.2](#352) - [3.5.1](#351) - [3.5.0](#350) - [3.4.8](#348) @@ -116,6 +117,17 @@ Adding a new version? You'll need three changes: ### Fixed +- Do not cleanup `null`s in the configuration of plugins with Kong running in + DBLess mode in the translator of ingress-controller. This enables user to use + explicit `null`s in plugins. + [#7751](https://github.com/Kong/kubernetes-ingress-controller/pull/7751) + +## [3.5.2] + +> Release date: 2025-09-23 + +### Fixed + - Add `request-termination` plugin to return `500` if there are no available `backendRef` only when the service is translated from `HTTPRoute` or `GRPCRoute`. @@ -4195,6 +4207,7 @@ Please read the changelog and test in your environment. - The initial versions were rapildy iterated to deliver a working ingress controller. +[3.5.2]: https://github.com/kong/kubernetes-ingress-controller/compare/v3.5.1...v3.5.2 [3.5.1]: https://github.com/kong/kubernetes-ingress-controller/compare/v3.5.0...v3.5.1 [3.5.0]: https://github.com/kong/kubernetes-ingress-controller/compare/v3.4.7...v3.5.0 [3.4.8]: https://github.com/kong/kubernetes-ingress-controller/compare/v3.4.7...v3.4.8 diff --git a/internal/dataplane/sendconfig/inmemory.go b/internal/dataplane/sendconfig/inmemory.go index 282671e51f..07e6d505f6 100644 --- a/internal/dataplane/sendconfig/inmemory.go +++ b/internal/dataplane/sendconfig/inmemory.go @@ -75,7 +75,7 @@ func (s UpdateStrategyInMemory) Update(ctx context.Context, targetState ContentW } } - configSize := mo.Some[int](len(config)) + configSize := mo.Some(len(config)) if reloadConfigErr := s.configService.ReloadDeclarativeRawConfig( ctx, bytes.NewReader(config), diff --git a/internal/dataplane/sendconfig/inmemory_schema.go b/internal/dataplane/sendconfig/inmemory_schema.go index f6e75b7fd4..ffd537ee2e 100644 --- a/internal/dataplane/sendconfig/inmemory_schema.go +++ b/internal/dataplane/sendconfig/inmemory_schema.go @@ -28,55 +28,12 @@ func (DefaultContentToDBLessConfigConverter) Convert(content *file.Content) DBLe // DBLess schema does not support decK's Info section. dblessConfig.Info = nil - // DBLess schema does not support nulls in plugin configs. - cleanUpNullsInPluginConfigs(&dblessConfig.Content) - // DBLess schema does not 1-1 match decK's schema for ConsumerGroups. convertConsumerGroups(&dblessConfig) return dblessConfig } -// cleanUpNullsInPluginConfigs removes null values from plugins' configs. -func cleanUpNullsInPluginConfigs(state *file.Content) { - for _, s := range state.Services { - for _, p := range s.Plugins { - for k, v := range p.Config { - if v == nil { - delete(p.Config, k) - } - } - } - for _, r := range state.Routes { - for _, p := range r.Plugins { - for k, v := range p.Config { - if v == nil { - delete(p.Config, k) - } - } - } - } - } - - for _, c := range state.Consumers { - for _, p := range c.Plugins { - for k, v := range p.Config { - if v == nil { - delete(p.Config, k) - } - } - } - } - - for _, p := range state.Plugins { - for k, v := range p.Config { - if v == nil { - delete(p.Config, k) - } - } - } -} - // convertConsumerGroups drops consumer groups related fields that are not supported in DBLess schema: // - Content.Consumers[].Groups, // - Content.ConsumerGroups[].Plugins diff --git a/internal/dataplane/sendconfig/inmemory_schema_test.go b/internal/dataplane/sendconfig/inmemory_schema_test.go index 2e7f63d27d..caf0fc58bb 100644 --- a/internal/dataplane/sendconfig/inmemory_schema_test.go +++ b/internal/dataplane/sendconfig/inmemory_schema_test.go @@ -279,6 +279,7 @@ func TestDefaultContentToDBLessConfigConverter(t *testing.T) { Plugin: kong.Plugin{ Name: kong.String("p1"), Config: kong.Configuration{ + "config1": nil, "config2": "value2", }, }, @@ -294,6 +295,7 @@ func TestDefaultContentToDBLessConfigConverter(t *testing.T) { Plugin: kong.Plugin{ Name: kong.String("p1"), Config: kong.Configuration{ + "config1": nil, "config2": "value2", }, }, @@ -311,6 +313,7 @@ func TestDefaultContentToDBLessConfigConverter(t *testing.T) { Plugin: kong.Plugin{ Name: kong.String("p1"), Config: kong.Configuration{ + "config1": nil, "config2": "value2", }, }, @@ -328,6 +331,7 @@ func TestDefaultContentToDBLessConfigConverter(t *testing.T) { Plugin: kong.Plugin{ Name: kong.String("p1"), Config: kong.Configuration{ + "config1": nil, "config2": "value2", }, }, diff --git a/test/integration/plugin_test.go b/test/integration/plugin_test.go index 9ff3297b5d..46b2cd9431 100644 --- a/test/integration/plugin_test.go +++ b/test/integration/plugin_test.go @@ -25,9 +25,11 @@ import ( configurationv1 "github.com/kong/kubernetes-configuration/v2/api/configuration/v1" "github.com/kong/kubernetes-configuration/v2/pkg/clientset" + "github.com/kong/kubernetes-ingress-controller/v3/internal/adminapi" "github.com/kong/kubernetes-ingress-controller/v3/internal/annotations" "github.com/kong/kubernetes-ingress-controller/v3/internal/gatewayapi" "github.com/kong/kubernetes-ingress-controller/v3/internal/labels" + managercfg "github.com/kong/kubernetes-ingress-controller/v3/pkg/manager/config" "github.com/kong/kubernetes-ingress-controller/v3/test" "github.com/kong/kubernetes-ingress-controller/v3/test/consts" "github.com/kong/kubernetes-ingress-controller/v3/test/internal/helpers" @@ -712,3 +714,99 @@ func TestPluginCrossNamespaceReference(t *testing.T) { assert.True(c, resp.StatusCode == http.StatusTeapot) }, ingressWait, waitTick) } + +func TestPluginNullInConfig(t *testing.T) { + ctx := t.Context() + + t.Parallel() + ns, cleaner := helpers.Setup(ctx, t, env) + + t.Log("deploying a minimal HTTP container deployment to test Ingress routes") + container := generators.NewContainer("httpbin", test.HTTPBinImage, test.HTTPBinPort) + deployment := generators.NewDeploymentForContainer(container) + deployment, err := env.Cluster().Client().AppsV1().Deployments(ns.Name).Create(ctx, deployment, metav1.CreateOptions{}) + require.NoError(t, err) + cleaner.Add(deployment) + + t.Logf("exposing deployment %s via service", deployment.Name) + service := generators.NewServiceForDeployment(deployment, corev1.ServiceTypeLoadBalancer) + service, err = env.Cluster().Client().CoreV1().Services(ns.Name).Create(ctx, service, metav1.CreateOptions{}) + require.NoError(t, err) + cleaner.Add(service) + + t.Logf("creating an ingress for service %s with ingress.class %s", service.Name, consts.IngressClass) + ingress := generators.NewIngressForService("/test_plugin_essentials", map[string]string{ + "konghq.com/strip-path": "true", + }, service) + ingress.Spec.IngressClassName = kong.String(consts.IngressClass) + ingress, err = env.Cluster().Client().NetworkingV1().Ingresses(ns.Name).Create(ctx, ingress, metav1.CreateOptions{}) + require.NoError(t, err) + cleaner.Add(ingress) + + t.Log("waiting for routes from Ingress to be operational") + assert.Eventually(t, func() bool { + resp, err := helpers.DefaultHTTPClient().Get(fmt.Sprintf("%s/test_plugin_essentials", proxyHTTPURL)) + if err != nil { + t.Logf("WARNING: error while waiting for %s: %v", proxyHTTPURL, err) + return false + } + defer resp.Body.Close() + if resp.StatusCode == http.StatusOK { + // now that the ingress backend is routable, make sure the contents we're getting back are what we expect + // Expected: "httpbin.org" + b := new(bytes.Buffer) + n, err := b.ReadFrom(resp.Body) + require.NoError(t, err) + require.True(t, n > 0) + return strings.Contains(b.String(), "httpbin.org") + } + return false + }, ingressWait, waitTick) + + t.Log("Creating a plugin with `null` in its configuration") + + kongplugin := &configurationv1.KongPlugin{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: ns.Name, + Name: "plugin-datadog", + }, + InstanceName: "plugin-with-null", + PluginName: "datadog", + Config: apiextensionsv1.JSON{ + Raw: []byte(`{"host":"localhost","port":8125,"prefix":null}`), + }, + } + c, err := clientset.NewForConfig(env.Cluster().Config()) + require.NoError(t, err) + kongplugin, err = c.ConfigurationV1().KongPlugins(ns.Name).Create(ctx, kongplugin, metav1.CreateOptions{}) + require.NoError(t, err) + cleaner.Add(kongplugin) + + t.Logf("Updating Ingress to use plugin %s", kongplugin.Name) + require.Eventually(t, func() bool { + ingress, err := env.Cluster().Client().NetworkingV1().Ingresses(ns.Name).Get(ctx, ingress.Name, metav1.GetOptions{}) + if err != nil { + return false + } + ingress.Annotations[annotations.AnnotationPrefix+annotations.PluginsKey] = kongplugin.Name + _, err = env.Cluster().Client().NetworkingV1().Ingresses(ns.Name).Update(ctx, ingress, metav1.UpdateOptions{}) + return err == nil + }, ingressWait, waitTick) + + t.Logf("Checking the configuration of the plugin %s in Kong", kongplugin.Name) + require.Eventually(t, func() bool { + kc, err := adminapi.NewKongAPIClient(proxyAdminURL.String(), managercfg.AdminAPIClientConfig{}, consts.KongTestPassword) + require.NoError(t, err, "failed to create Kong client") + plugins, err := kc.Plugins.ListAll(ctx) + require.NoError(t, err, "failed to list plugins") + if len(plugins) != 1 { + return false + } + plugin := plugins[0] + if plugin.Name == nil || *plugin.Name != "datadog" { + return false + } + configPrefix, ok := plugin.Config["prefix"] + return ok && configPrefix == nil + }, ingressWait, waitTick, "failed to find 'datadog' plugin with null in config.prefix in Kong") +}