From aa5549fba4c7e10b9b9f214e13db57e24b06a355 Mon Sep 17 00:00:00 2001 From: Eguzki Astiz Lezaun Date: Thu, 25 Jun 2026 11:12:26 +0200 Subject: [PATCH 1/9] authorino operator 0.25.1 Signed-off-by: Eguzki Astiz Lezaun --- release.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release.yaml b/release.yaml index 997cd5370..513cb7508 100644 --- a/release.yaml +++ b/release.yaml @@ -5,7 +5,7 @@ olm: - "stable" default-channel: "stable" dependencies: - authorino-operator: "0.25.0" + authorino-operator: "0.25.1" console-plugin: "0.4.0" developer-portal-controller: "0.2.0" dns-operator: "0.17.0" From 82316f2a55983a5625cc9a19f24c69014fbe456b Mon Sep 17 00:00:00 2001 From: Eguzki Astiz Lezaun Date: Thu, 25 Jun 2026 11:13:44 +0200 Subject: [PATCH 2/9] Prepare release 1.5.0-rc3 Signed-off-by: Eguzki Astiz Lezaun --- .../kuadrant-operator.clusterserviceversion.yaml | 10 +++++----- bundle/metadata/dependencies.yaml | 2 +- charts/kuadrant-operator/Chart.yaml | 6 +++--- charts/kuadrant-operator/templates/manifests.yaml | 2 +- config/dependencies/authorino/kustomization.yaml | 2 +- config/deploy/olm/catalogsource.yaml | 2 +- config/manager/kustomization.yaml | 2 +- .../bases/kuadrant-operator.clusterserviceversion.yaml | 6 +++--- release.yaml | 2 +- 9 files changed, 17 insertions(+), 17 deletions(-) diff --git a/bundle/manifests/kuadrant-operator.clusterserviceversion.yaml b/bundle/manifests/kuadrant-operator.clusterserviceversion.yaml index 926f36aa3..6bca7dc9b 100644 --- a/bundle/manifests/kuadrant-operator.clusterserviceversion.yaml +++ b/bundle/manifests/kuadrant-operator.clusterserviceversion.yaml @@ -224,14 +224,14 @@ metadata: capabilities: Basic Install categories: Integration & Delivery console.openshift.io/plugins: '["kuadrant-console-plugin"]' - containerImage: quay.io/kuadrant/kuadrant-operator:v1.5.0-rc2 - createdAt: "2026-06-02T08:04:09Z" + containerImage: quay.io/kuadrant/kuadrant-operator:v1.5.0-rc3 + createdAt: "2026-06-25T09:13:03Z" description: A Kubernetes Operator to manage the lifecycle of the Kuadrant system operators.operatorframework.io/builder: operator-sdk-v1.33.0 operators.operatorframework.io/project_layout: go.kubebuilder.io/v4 repository: https://github.com/Kuadrant/kuadrant-operator support: kuadrant - name: kuadrant-operator.v1.5.0-rc2 + name: kuadrant-operator.v1.5.0-rc3 namespace: placeholder spec: apiservicedefinitions: {} @@ -763,7 +763,7 @@ spec: valueFrom: fieldRef: fieldPath: metadata.namespace - image: quay.io/kuadrant/kuadrant-operator:v1.5.0-rc2 + image: quay.io/kuadrant/kuadrant-operator:v1.5.0-rc3 livenessProbe: httpGet: path: /healthz @@ -922,4 +922,4 @@ spec: name: console-plugin-latest - image: quay.io/kuadrant/console-plugin:v0.1.5-2 name: console-plugin-pf5 - version: 1.5.0-rc2 + version: 1.5.0-rc3 diff --git a/bundle/metadata/dependencies.yaml b/bundle/metadata/dependencies.yaml index d706353cb..5f54101b4 100644 --- a/bundle/metadata/dependencies.yaml +++ b/bundle/metadata/dependencies.yaml @@ -2,7 +2,7 @@ dependencies: - type: olm.package value: packageName: authorino-operator - version: "0.25.0" + version: "0.25.1" - type: olm.package value: packageName: limitador-operator diff --git a/charts/kuadrant-operator/Chart.yaml b/charts/kuadrant-operator/Chart.yaml index df8c4ab00..900fab017 100644 --- a/charts/kuadrant-operator/Chart.yaml +++ b/charts/kuadrant-operator/Chart.yaml @@ -20,11 +20,11 @@ sources: kubeVersion: ">=1.19.0-0" type: application # The chart version and dependencies will be properly set when the chart is released matching the operator version -version: "1.5.0-rc2" -appVersion: "1.5.0-rc2" +version: "1.5.0-rc3" +appVersion: "1.5.0-rc3" dependencies: - name: authorino-operator - version: 0.25.0 + version: 0.25.1 repository: https://kuadrant.io/helm-charts/ - name: limitador-operator version: 0.18.2 diff --git a/charts/kuadrant-operator/templates/manifests.yaml b/charts/kuadrant-operator/templates/manifests.yaml index 152ef860d..67e0af286 100644 --- a/charts/kuadrant-operator/templates/manifests.yaml +++ b/charts/kuadrant-operator/templates/manifests.yaml @@ -14498,7 +14498,7 @@ spec: valueFrom: fieldRef: fieldPath: metadata.namespace - image: quay.io/kuadrant/kuadrant-operator:v1.5.0-rc2 + image: quay.io/kuadrant/kuadrant-operator:v1.5.0-rc3 livenessProbe: httpGet: path: /healthz diff --git a/config/dependencies/authorino/kustomization.yaml b/config/dependencies/authorino/kustomization.yaml index 6c2a6953f..9cbb029fe 100644 --- a/config/dependencies/authorino/kustomization.yaml +++ b/config/dependencies/authorino/kustomization.yaml @@ -1,2 +1,2 @@ resources: -- github.com/Kuadrant/authorino-operator/config/deploy?ref=v0.25.0 +- github.com/Kuadrant/authorino-operator/config/deploy?ref=v0.25.1 diff --git a/config/deploy/olm/catalogsource.yaml b/config/deploy/olm/catalogsource.yaml index e9c9dfd01..10e49d148 100644 --- a/config/deploy/olm/catalogsource.yaml +++ b/config/deploy/olm/catalogsource.yaml @@ -4,7 +4,7 @@ metadata: name: kuadrant-operator-catalog spec: sourceType: grpc - image: quay.io/kuadrant/kuadrant-operator-catalog:v1.5.0-rc2 + image: quay.io/kuadrant/kuadrant-operator-catalog:v1.5.0-rc3 displayName: Kuadrant Operators grpcPodConfig: securityContextConfig: restricted diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml index 192f0c556..82feab20c 100644 --- a/config/manager/kustomization.yaml +++ b/config/manager/kustomization.yaml @@ -12,4 +12,4 @@ kind: Kustomization images: - name: controller newName: quay.io/kuadrant/kuadrant-operator - newTag: v1.5.0-rc2 + newTag: v1.5.0-rc3 diff --git a/config/manifests/bases/kuadrant-operator.clusterserviceversion.yaml b/config/manifests/bases/kuadrant-operator.clusterserviceversion.yaml index 5b036b524..9d19627d5 100644 --- a/config/manifests/bases/kuadrant-operator.clusterserviceversion.yaml +++ b/config/manifests/bases/kuadrant-operator.clusterserviceversion.yaml @@ -6,13 +6,13 @@ metadata: capabilities: Basic Install categories: Integration & Delivery console.openshift.io/plugins: '["kuadrant-console-plugin"]' - containerImage: quay.io/kuadrant/kuadrant-operator:v1.5.0-rc2 + containerImage: quay.io/kuadrant/kuadrant-operator:v1.5.0-rc3 description: A Kubernetes Operator to manage the lifecycle of the Kuadrant system operators.operatorframework.io/builder: operator-sdk-v1.9.0 operators.operatorframework.io/project_layout: unknown repository: https://github.com/Kuadrant/kuadrant-operator support: kuadrant - name: kuadrant-operator.v1.5.0-rc2 + name: kuadrant-operator.v1.5.0-rc3 namespace: placeholder spec: apiservicedefinitions: {} @@ -88,4 +88,4 @@ spec: provider: name: Red Hat url: https://github.com/Kuadrant/kuadrant-operator - version: 1.5.0-rc2 + version: 1.5.0-rc3 diff --git a/release.yaml b/release.yaml index 513cb7508..fda0766d3 100644 --- a/release.yaml +++ b/release.yaml @@ -1,5 +1,5 @@ kuadrant-operator: - version: "1.5.0-rc2" + version: "1.5.0-rc3" olm: channels: - "stable" From 1049e5e4eb1a8e85756279bc77087c04bda5c997 Mon Sep 17 00:00:00 2001 From: Adam Cattermole Date: Thu, 25 Jun 2026 15:27:29 +0100 Subject: [PATCH 3/9] wasm-shim 0.14.1 Signed-off-by: Adam Cattermole --- .../manifests/kuadrant-operator.clusterserviceversion.yaml | 6 +++--- charts/kuadrant-operator/templates/manifests.yaml | 2 +- config/manager/manager.yaml | 2 +- release.yaml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/bundle/manifests/kuadrant-operator.clusterserviceversion.yaml b/bundle/manifests/kuadrant-operator.clusterserviceversion.yaml index 6bca7dc9b..2bb298ad7 100644 --- a/bundle/manifests/kuadrant-operator.clusterserviceversion.yaml +++ b/bundle/manifests/kuadrant-operator.clusterserviceversion.yaml @@ -225,7 +225,7 @@ metadata: categories: Integration & Delivery console.openshift.io/plugins: '["kuadrant-console-plugin"]' containerImage: quay.io/kuadrant/kuadrant-operator:v1.5.0-rc3 - createdAt: "2026-06-25T09:13:03Z" + createdAt: "2026-06-26T08:32:19Z" description: A Kubernetes Operator to manage the lifecycle of the Kuadrant system operators.operatorframework.io/builder: operator-sdk-v1.33.0 operators.operatorframework.io/project_layout: go.kubebuilder.io/v4 @@ -752,7 +752,7 @@ spec: - name: EXTENSIONS_DESCRIPTOR_SERVICE_PORT value: "50051" - name: RELATED_IMAGE_WASMSHIM - value: quay.io/kuadrant/wasm-shim:v0.13.0 + value: quay.io/kuadrant/wasm-shim:v0.14.1 - name: RELATED_IMAGE_DEVELOPERPORTAL value: quay.io/kuadrant/developer-portal-controller:v0.2.0 - name: RELATED_IMAGE_CONSOLE_PLUGIN_LATEST @@ -914,7 +914,7 @@ spec: name: Red Hat url: https://github.com/Kuadrant/kuadrant-operator relatedImages: - - image: quay.io/kuadrant/wasm-shim:v0.13.0 + - image: quay.io/kuadrant/wasm-shim:v0.14.1 name: wasmshim - image: quay.io/kuadrant/developer-portal-controller:v0.2.0 name: developerportal diff --git a/charts/kuadrant-operator/templates/manifests.yaml b/charts/kuadrant-operator/templates/manifests.yaml index 67e0af286..787ebd09c 100644 --- a/charts/kuadrant-operator/templates/manifests.yaml +++ b/charts/kuadrant-operator/templates/manifests.yaml @@ -14487,7 +14487,7 @@ spec: - name: EXTENSIONS_DESCRIPTOR_SERVICE_PORT value: "50051" - name: RELATED_IMAGE_WASMSHIM - value: quay.io/kuadrant/wasm-shim:v0.13.0 + value: quay.io/kuadrant/wasm-shim:v0.14.1 - name: RELATED_IMAGE_DEVELOPERPORTAL value: quay.io/kuadrant/developer-portal-controller:v0.2.0 - name: RELATED_IMAGE_CONSOLE_PLUGIN_LATEST diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index c26225574..0afe74889 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -36,7 +36,7 @@ spec: - name: EXTENSIONS_DESCRIPTOR_SERVICE_PORT value: "50051" - name: RELATED_IMAGE_WASMSHIM - value: "quay.io/kuadrant/wasm-shim:v0.13.0" + value: "quay.io/kuadrant/wasm-shim:v0.14.1" - name: RELATED_IMAGE_DEVELOPERPORTAL value: "quay.io/kuadrant/developer-portal-controller:v0.2.0" - name: RELATED_IMAGE_CONSOLE_PLUGIN_LATEST diff --git a/release.yaml b/release.yaml index fda0766d3..ad4a246b7 100644 --- a/release.yaml +++ b/release.yaml @@ -10,4 +10,4 @@ dependencies: developer-portal-controller: "0.2.0" dns-operator: "0.17.0" limitador-operator: "0.18.2" - wasm-shim: "0.13.0" + wasm-shim: "0.14.1" From 8861979fc71dc6b9dbf85af2782f857ba56148ae Mon Sep 17 00:00:00 2001 From: Adam Cattermole Date: Mon, 15 Jun 2026 14:21:38 +0100 Subject: [PATCH 4/9] Set failure_policy to FAIL_RELOAD Signed-off-by: Adam Cattermole --- internal/istio/utils.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/istio/utils.go b/internal/istio/utils.go index 0b6fe41fe..6f105d5b0 100644 --- a/internal/istio/utils.go +++ b/internal/istio/utils.go @@ -131,6 +131,7 @@ func buildWasmFilterConfig(wasmURL, imagePullSecret, imageSHA, clusterName strin }, "allow_precompiled": true, }, + "failure_policy": "FAIL_RELOAD", "allow_on_headers_stop_iteration": true, } From e773e0fd5a58d5b319766c5d2e1f150baa7f63a2 Mon Sep 17 00:00:00 2001 From: Adam Cattermole Date: Thu, 18 Jun 2026 12:10:06 +0100 Subject: [PATCH 5/9] Make ConditionalData ordering deterministic Signed-off-by: Adam Cattermole --- internal/controller/data_plane_policies_workflow.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/internal/controller/data_plane_policies_workflow.go b/internal/controller/data_plane_policies_workflow.go index 202084979..346bbd878 100644 --- a/internal/controller/data_plane_policies_workflow.go +++ b/internal/controller/data_plane_policies_workflow.go @@ -219,6 +219,14 @@ func mergeAndVerify(ctx context.Context, actions []wasm.Action) ([]wasm.Action, // Merge source policy locators - deduplicate them lastAction.SourcePolicyLocators = lo.Uniq(append(lastAction.SourcePolicyLocators, currentAction.SourcePolicyLocators...)) slices.Sort(lastAction.SourcePolicyLocators) + + // Sort by the first predicate (if any), then by the concatenation of all predicates + slices.SortFunc(lastAction.ConditionalData, func(a, b wasm.ConditionalData) int { + // Compare by predicates (join them into a single string for comparison) + aKey := strings.Join(a.Predicates, "|") + bKey := strings.Join(b.Predicates, "|") + return strings.Compare(aKey, bKey) + }) } else { result = append(result, currentAction) } From 0be29d10795b73937c2aa30771e18d2a4b143a3d Mon Sep 17 00:00:00 2001 From: Adam Cattermole Date: Thu, 18 Jun 2026 12:12:26 +0100 Subject: [PATCH 6/9] Make limit rules ordering deterministic Signed-off-by: Adam Cattermole --- .../controller/ratelimit_workflow_helpers.go | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/internal/controller/ratelimit_workflow_helpers.go b/internal/controller/ratelimit_workflow_helpers.go index 07bf101fa..d28921f21 100644 --- a/internal/controller/ratelimit_workflow_helpers.go +++ b/internal/controller/ratelimit_workflow_helpers.go @@ -5,6 +5,8 @@ import ( "encoding/hex" "errors" "fmt" + "slices" + "strings" "sync" "unicode" @@ -331,7 +333,12 @@ func buildWasmActionsForTokenRateLimit(effectivePolicy EffectiveTokenRateLimitPo } limitsNamespace := LimitsNamespaceFromRoute(parsed.GetRoute()) - topLevelRules, limitRules := lo.FilterReject(lo.Entries(rules), + rulesEntries := lo.Entries(rules) + slices.SortFunc(rulesEntries, func(a, b lo.Entry[string, kuadrantv1.MergeableRule]) int { + return strings.Compare(a.Key, b.Key) + }) + + topLevelRules, limitRules := lo.FilterReject(rulesEntries, func(r lo.Entry[string, kuadrantv1.MergeableRule], _ int) bool { return r.Key == kuadrantv1.RulesKeyTopLevelPredicates }, @@ -386,7 +393,12 @@ func buildWasmActionsForAnyRateLimit( } limitsNamespace := LimitsNamespaceFromRoute(parsed.GetRoute()) - topLevelRules, limitRules := lo.FilterReject(lo.Entries(rules), + rulesEntries := lo.Entries(rules) + slices.SortFunc(rulesEntries, func(a, b lo.Entry[string, kuadrantv1.MergeableRule]) int { + return strings.Compare(a.Key, b.Key) + }) + + topLevelRules, limitRules := lo.FilterReject(rulesEntries, func(r lo.Entry[string, kuadrantv1.MergeableRule], _ int) bool { return r.Key == topLevelPredicatesKey }, From a0ffb570b69b25e5930a213be1921bcbecbf9a41 Mon Sep 17 00:00:00 2001 From: Adam Cattermole Date: Thu, 18 Jun 2026 12:12:52 +0100 Subject: [PATCH 7/9] Compare envoyfilter via proto Signed-off-by: Adam Cattermole --- internal/istio/utils.go | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/internal/istio/utils.go b/internal/istio/utils.go index 6f105d5b0..53e04044b 100644 --- a/internal/istio/utils.go +++ b/internal/istio/utils.go @@ -7,6 +7,7 @@ import ( "github.com/kuadrant/policy-machinery/controller" "github.com/kuadrant/policy-machinery/machinery" "github.com/samber/lo" + "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/structpb" istioapimetav1alpha1 "istio.io/api/meta/v1alpha1" istioapinetworkingv1alpha3 "istio.io/api/networking/v1alpha3" @@ -195,12 +196,9 @@ func EqualEnvoyFilters(a, b *istioclientgonetworkingv1alpha3.EnvoyFilter) bool { if (aListener == nil) != (bListener == nil) { return false } - // For HTTP_FILTER patches, we compare the listener match structure if present - // Since the structure is complex, we'll compare the JSON representation + // For HTTP_FILTER patches, compare the match structure using protobuf equality if aListener != nil && bListener != nil { - aMatchJSON, aErr := json.Marshal(aConfigPatch.Match) - bMatchJSON, _ := json.Marshal(bConfigPatch.Match) - if string(aMatchJSON) != string(bMatchJSON) || aErr != nil { + if !proto.Equal(aConfigPatch.Match, bConfigPatch.Match) { return false } } @@ -215,10 +213,8 @@ func EqualEnvoyFilters(a, b *istioclientgonetworkingv1alpha3.EnvoyFilter) bool { return false } default: - // For other patch types, compare the match structure via JSON - aMatchJSON, aErr := json.Marshal(aConfigPatch.Match) - bMatchJSON, _ := json.Marshal(bConfigPatch.Match) - if string(aMatchJSON) != string(bMatchJSON) || aErr != nil { + // For other patch types, compare the match structure using protobuf equality + if !proto.Equal(aConfigPatch.Match, bConfigPatch.Match) { return false } } @@ -230,9 +226,9 @@ func EqualEnvoyFilters(a, b *istioclientgonetworkingv1alpha3.EnvoyFilter) bool { if aPatch.Operation != bPatch.Operation || aPatch.FilterClass != bPatch.FilterClass { return false } - aPatchJSON, _ := aPatch.Value.MarshalJSON() - bPatchJSON, _ := bPatch.Value.MarshalJSON() - return string(aPatchJSON) == string(bPatchJSON) + + // Use protobuf equality for patch values to handle non-deterministic map ordering. + return proto.Equal(aPatch.Value, bPatch.Value) }) }) } From 96bf0714849f0e01d97dc52da96027a5384af9e9 Mon Sep 17 00:00:00 2001 From: Adam Cattermole Date: Thu, 18 Jun 2026 13:05:18 +0100 Subject: [PATCH 8/9] Add unit tests for deterministic generation Signed-off-by: Adam Cattermole --- .../istio_extension_reconciler_test.go | 82 ++++++++++++++++ internal/istio/utils_test.go | 94 +++++++++++++++++++ 2 files changed, 176 insertions(+) diff --git a/internal/controller/istio_extension_reconciler_test.go b/internal/controller/istio_extension_reconciler_test.go index 005970d13..d4be344fc 100644 --- a/internal/controller/istio_extension_reconciler_test.go +++ b/internal/controller/istio_extension_reconciler_test.go @@ -636,4 +636,86 @@ func TestMergeAndVerifyEdgeCases(t *testing.T) { _, err := mergeAndVerify(context.TODO(), actions) assert.ErrorContains(t, err, "duplicate key '' with different values") }) + + t.Run("deterministic conditional data ordering after merge", func(t *testing.T) { + // Create two sets of actions in different orders + actionsOrder1 := []wasm.Action{ + { + ServiceName: wasm.RateLimitCheckServiceName, + Scope: "route-0", + ConditionalData: []wasm.ConditionalData{ + { + Predicates: []string{"auth.identity.bob"}, + Data: []wasm.DataType{ + {Value: &wasm.Static{Static: wasm.StaticSpec{Key: "limit.bob", Value: "1"}}}, + }, + }, + }, + }, + { + ServiceName: wasm.RateLimitCheckServiceName, + Scope: "route-0", + ConditionalData: []wasm.ConditionalData{ + { + Predicates: []string{"auth.identity.alice"}, + Data: []wasm.DataType{ + {Value: &wasm.Static{Static: wasm.StaticSpec{Key: "limit.alice", Value: "1"}}}, + }, + }, + }, + }, + } + + actionsOrder2 := []wasm.Action{ + { + ServiceName: wasm.RateLimitCheckServiceName, + Scope: "route-0", + ConditionalData: []wasm.ConditionalData{ + { + Predicates: []string{"auth.identity.alice"}, + Data: []wasm.DataType{ + {Value: &wasm.Static{Static: wasm.StaticSpec{Key: "limit.alice", Value: "1"}}}, + }, + }, + }, + }, + { + ServiceName: wasm.RateLimitCheckServiceName, + Scope: "route-0", + ConditionalData: []wasm.ConditionalData{ + { + Predicates: []string{"auth.identity.bob"}, + Data: []wasm.DataType{ + {Value: &wasm.Static{Static: wasm.StaticSpec{Key: "limit.bob", Value: "1"}}}, + }, + }, + }, + }, + } + + result1, err := mergeAndVerify(context.TODO(), actionsOrder1) + assert.NilError(t, err) + assert.Equal(t, 1, len(result1)) + + result2, err := mergeAndVerify(context.TODO(), actionsOrder2) + assert.NilError(t, err) + assert.Equal(t, 1, len(result2)) + + // Both results should have exactly the same ConditionalData in the same order + assert.Equal(t, len(result1[0].ConditionalData), len(result2[0].ConditionalData)) + for i := range result1[0].ConditionalData { + cd1 := result1[0].ConditionalData[i] + cd2 := result2[0].ConditionalData[i] + + // Predicates should be in the same order + assert.DeepEqual(t, cd1.Predicates, cd2.Predicates) + + // Data should be in the same order + assert.Equal(t, len(cd1.Data), len(cd2.Data)) + } + + // The ConditionalData should be sorted by predicates (alice before bob) + assert.Equal(t, "auth.identity.alice", result1[0].ConditionalData[0].Predicates[0]) + assert.Equal(t, "auth.identity.bob", result1[0].ConditionalData[1].Predicates[0]) + }) } diff --git a/internal/istio/utils_test.go b/internal/istio/utils_test.go index 387afca8d..4f5e8e77a 100644 --- a/internal/istio/utils_test.go +++ b/internal/istio/utils_test.go @@ -209,6 +209,100 @@ func TestEqualEnvoyFilters(t *testing.T) { assert.Assert(subT, !EqualEnvoyFilters(a, b)) }) + + t.Run("equal patch values with maps in different order", func(subT *testing.T) { + a := testBasicEnvoyFilter(subT) + b := testBasicEnvoyFilter(subT) + + // Create identical data but from separate map instances (which could marshal differently) + mapData := map[string]any{ + "actionSets": []any{ + map[string]any{ + "name": "route-0", + "routeRuleConditions": map[string]any{ + "hostnames": []any{"*.example.com"}, + }, + "actions": []any{ + map[string]any{ + "service": "ratelimit", + "scope": "route-0-rlp", + "conditionalData": []any{ + map[string]any{ + "predicates": []string{"auth.identity.bob"}, + "data": []any{ + map[string]any{ + "static": map[string]any{ + "key": "limit.route-0__1234", + "value": "1", + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + // Marshal to JSON and unmarshal into structpb.Struct for both a and b + aJSON, _ := json.Marshal(mapData) + aPatchValue := &_struct.Struct{} + assert.NilError(subT, aPatchValue.UnmarshalJSON(aJSON)) + a.Spec.ConfigPatches[0].Patch.Value = aPatchValue + + bJSON, _ := json.Marshal(mapData) + bPatchValue := &_struct.Struct{} + assert.NilError(subT, bPatchValue.UnmarshalJSON(bJSON)) + b.Spec.ConfigPatches[0].Patch.Value = bPatchValue + + // With proto.Equal, these should be equal even if JSON string representations differ + assert.Assert(subT, EqualEnvoyFilters(a, b), + "EnvoyFilters with semantically identical patch values should be equal") + }) + + t.Run("equal HTTP_FILTER match with complex listener structure", func(subT *testing.T) { + // This test validates proto.Equal is used for Match comparison (another fix for #2033) + a := testBasicEnvoyFilter(subT) + a.Spec.ConfigPatches[0].ApplyTo = istioapinetworkingv1alpha3.EnvoyFilter_HTTP_FILTER + a.Spec.ConfigPatches[0].Match = &istioapinetworkingv1alpha3.EnvoyFilter_EnvoyConfigObjectMatch{ + Context: istioapinetworkingv1alpha3.EnvoyFilter_GATEWAY, + ObjectTypes: &istioapinetworkingv1alpha3.EnvoyFilter_EnvoyConfigObjectMatch_Listener{ + Listener: &istioapinetworkingv1alpha3.EnvoyFilter_ListenerMatch{ + FilterChain: &istioapinetworkingv1alpha3.EnvoyFilter_ListenerMatch_FilterChainMatch{ + Filter: &istioapinetworkingv1alpha3.EnvoyFilter_ListenerMatch_FilterMatch{ + Name: "envoy.filters.network.http_connection_manager", + SubFilter: &istioapinetworkingv1alpha3.EnvoyFilter_ListenerMatch_SubFilterMatch{ + Name: "envoy.filters.http.router", + }, + }, + }, + }, + }, + } + + b := testBasicEnvoyFilter(subT) + b.Spec.ConfigPatches[0].ApplyTo = istioapinetworkingv1alpha3.EnvoyFilter_HTTP_FILTER + b.Spec.ConfigPatches[0].Match = &istioapinetworkingv1alpha3.EnvoyFilter_EnvoyConfigObjectMatch{ + Context: istioapinetworkingv1alpha3.EnvoyFilter_GATEWAY, + ObjectTypes: &istioapinetworkingv1alpha3.EnvoyFilter_EnvoyConfigObjectMatch_Listener{ + Listener: &istioapinetworkingv1alpha3.EnvoyFilter_ListenerMatch{ + FilterChain: &istioapinetworkingv1alpha3.EnvoyFilter_ListenerMatch_FilterChainMatch{ + Filter: &istioapinetworkingv1alpha3.EnvoyFilter_ListenerMatch_FilterMatch{ + Name: "envoy.filters.network.http_connection_manager", + SubFilter: &istioapinetworkingv1alpha3.EnvoyFilter_ListenerMatch_SubFilterMatch{ + Name: "envoy.filters.http.router", + }, + }, + }, + }, + }, + } + + // With proto.Equal, these identical Match structures should be equal + assert.Assert(subT, EqualEnvoyFilters(a, b), + "EnvoyFilters with identical HTTP_FILTER Match structures should be equal") + }) } func TestEqualTargetRefs(t *testing.T) { From 191643d6ea6a945902850e018562dad87f63c470 Mon Sep 17 00:00:00 2001 From: Adam Cattermole Date: Mon, 22 Jun 2026 14:15:09 +0100 Subject: [PATCH 9/9] Cleanup orphaned WasmPlugin objects Signed-off-by: Adam Cattermole --- .../kuadrant-operator.clusterserviceversion.yaml | 6 ++++++ charts/kuadrant-operator/templates/manifests.yaml | 6 ++++++ config/rbac/role.yaml | 6 ++++++ internal/controller/istio_extension_reconciler.go | 8 ++++++++ 4 files changed, 26 insertions(+) diff --git a/bundle/manifests/kuadrant-operator.clusterserviceversion.yaml b/bundle/manifests/kuadrant-operator.clusterserviceversion.yaml index 2bb298ad7..740dda6b9 100644 --- a/bundle/manifests/kuadrant-operator.clusterserviceversion.yaml +++ b/bundle/manifests/kuadrant-operator.clusterserviceversion.yaml @@ -502,6 +502,12 @@ spec: - patch - update - watch + - apiGroups: + - extensions.istio.io + resources: + - wasmplugins + verbs: + - delete - apiGroups: - extensions.kuadrant.io resources: diff --git a/charts/kuadrant-operator/templates/manifests.yaml b/charts/kuadrant-operator/templates/manifests.yaml index 787ebd09c..4b652dd27 100644 --- a/charts/kuadrant-operator/templates/manifests.yaml +++ b/charts/kuadrant-operator/templates/manifests.yaml @@ -14111,6 +14111,12 @@ rules: - patch - update - watch +- apiGroups: + - extensions.istio.io + resources: + - wasmplugins + verbs: + - delete - apiGroups: - extensions.kuadrant.io resources: diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 993979629..385e376d9 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -112,6 +112,12 @@ rules: - patch - update - watch +- apiGroups: + - extensions.istio.io + resources: + - wasmplugins + verbs: + - delete - apiGroups: - extensions.kuadrant.io resources: diff --git a/internal/controller/istio_extension_reconciler.go b/internal/controller/istio_extension_reconciler.go index bcd72b478..4dd2efd31 100644 --- a/internal/controller/istio_extension_reconciler.go +++ b/internal/controller/istio_extension_reconciler.go @@ -16,6 +16,7 @@ import ( istioapinetworkingv1alpha3 "istio.io/api/networking/v1alpha3" istiov1beta1 "istio.io/api/type/v1beta1" istioclientgonetworkingv1alpha3 "istio.io/client-go/pkg/apis/networking/v1alpha3" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" k8stypes "k8s.io/apimachinery/pkg/types" @@ -35,6 +36,7 @@ import ( "github.com/kuadrant/kuadrant-operator/internal/wasm" ) +//+kubebuilder:rbac:groups=extensions.istio.io,resources=wasmplugins,verbs=delete //+kubebuilder:rbac:groups=networking.istio.io,resources=envoyfilters,verbs=get;list;watch;create;update;patch;delete // IstioExtensionReconciler reconciles Istio EnvoyFilter custom resources for wasm plugin injection @@ -131,6 +133,12 @@ func (r *IstioExtensionReconciler) Reconcile(ctx context.Context, _ []controller continue } + // Clean up old WasmPlugin for this specific gateway - temporary to be removed + wasmPluginName := wasm.ExtensionName(gateway.GetName()) + if err := r.client.Resource(kuadrantistio.WasmPluginsResource).Namespace(gateway.GetNamespace()).Delete(ctx, wasmPluginName, metav1.DeleteOptions{}); err != nil && !apierrors.IsNotFound(err) { + logger.Error(err, "failed to delete old wasmplugin", "gateway", gatewayKey.String(), "wasmplugin", wasmPluginName) + } + existingEnvoyFilter := existingEnvoyFilterObj.(*controller.RuntimeObject).Object.(*istioclientgonetworkingv1alpha3.EnvoyFilter) // delete