Skip to content

Commit cb97c86

Browse files
authored
Implement round-trip KRM fuzzer for NetworkServicesHTTPRoute (#11744)
This PR implements a lossless round-trip KRM fuzz test for the NetworkServicesHTTPRoute direct controller resource. ### Changes: - Created `pkg/controller/direct/networkservices/networkserviceshttproute_fuzzer.go` implementing a protobuf-based `KRMFuzzer`. - Created custom `HttprouteQueryParameters` and `HttprouteHeaders` `FromProto`/`ToProto` mappers in `networkserviceshttproute_mapper.go` to handle `PresentMatch` oneof boolean fields correctly, preventing zero-value mapping loss. - Regenerated mappers using `apis/networkservices/v1beta1/generate.sh`. - Documented findings in `.gemini/skills/create-fuzzer/journal/NetworkServicesHTTPRoute.md`. Fixes #11743 This PR was generated by the **overseer,overseer** agent (powered by the gemini-3.5-flash model).
2 parents c61bfe5 + 6fc79f1 commit cb97c86

4 files changed

Lines changed: 240 additions & 42 deletions

File tree

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Journal: KRM Fuzzer for NetworkServicesHTTPRoute
2+
3+
## Observations
4+
5+
1. **Boolean zero-value mapping loss in `oneof`**:
6+
The generated mapper uses `direct.LazyPtr(in.GetPresentMatch())` for mapping `MatchType` inside `HttprouteQueryParameters` and `HttprouteHeaders`. Since `PresentMatch` is a boolean, and `direct.LazyPtr(false)` returns `nil`, any `PresentMatch: false` (which was explicitly randomized in the fuzz tests) became `nil` after the round-trip conversion.
7+
- **Fix**: We hand-coded custom mappers `HttprouteQueryParameters_FromProto`/`ToProto` and `HttprouteHeaders_FromProto`/`ToProto` to directly inspect the proto `oneof` variant (e.g., checking `in.MatchType.(type)`), preserving both `true` and `false` correctly without losing data.
8+
9+
2. **Unimplemented fields**:
10+
Several nested fields in the `HttpRoute` proto were identified during randomized fuzz testing as having no KRM equivalent, such as:
11+
- `rules[].action.direct_response`
12+
- `rules[].action.idle_timeout`
13+
- `rules[].action.stateful_session_affinity`
14+
- `rules[].action.destinations[].request_header_modifier`
15+
- `rules[].action.destinations[].response_header_modifier`
16+
- `rules[].action.request_mirror_policy.destination.request_header_modifier`
17+
- `rules[].action.request_mirror_policy.destination.response_header_modifier`
18+
- `rules[].action.request_mirror_policy.mirror_percent`
19+
20+
These were safely marked as `Unimplemented_NotYetTriaged` in the fuzzer to bypass round-trip comparisons on those fields.

pkg/controller/direct/networkservices/mapper.generated.go

Lines changed: 58 additions & 42 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// +tool:fuzz-gen
16+
// proto.message: google.cloud.networkservices.v1.HttpRoute
17+
// api.group: networkservices.cnrm.cloud.google.com
18+
19+
package networkservices
20+
21+
import (
22+
pb "cloud.google.com/go/networkservices/apiv1/networkservicespb"
23+
"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/fuzztesting"
24+
)
25+
26+
func init() {
27+
fuzztesting.RegisterKRMFuzzer(networkServicesHTTPRouteFuzzer())
28+
}
29+
30+
func networkServicesHTTPRouteFuzzer() fuzztesting.KRMFuzzer {
31+
f := fuzztesting.NewKRMTypedFuzzer(&pb.HttpRoute{},
32+
NetworkServicesHTTPRouteSpec_FromProto, NetworkServicesHTTPRouteSpec_ToProto,
33+
NetworkServicesHTTPRouteStatus_FromProto, NetworkServicesHTTPRouteStatus_ToProto,
34+
)
35+
36+
// Field comparison between KRM NetworkServicesHTTPRouteSpec and Proto HttpRoute:
37+
// - Description <=> .description
38+
// - Hostnames <=> .hostnames
39+
// - Gateways <=> .gateways
40+
// - Meshes <=> .meshes
41+
// - Rules <=> .rules
42+
//
43+
// Fields ignored because they are not part of KRM Spec:
44+
// - .name (Identity)
45+
// - .self_link (Status)
46+
// - .create_time (Status)
47+
// - .update_time (Status)
48+
// - .labels (handled by KCC metadata labels)
49+
50+
f.SpecField(".description")
51+
f.SpecField(".hostnames")
52+
f.SpecField(".gateways")
53+
f.SpecField(".meshes")
54+
f.SpecField(".rules")
55+
56+
f.StatusField(".self_link")
57+
f.StatusField(".create_time")
58+
f.StatusField(".update_time")
59+
60+
f.Unimplemented_Identity(".name")
61+
f.Unimplemented_LabelsAnnotations(".labels")
62+
63+
f.Unimplemented_NotYetTriaged(".rules[].action.direct_response")
64+
f.Unimplemented_NotYetTriaged(".rules[].action.idle_timeout")
65+
f.Unimplemented_NotYetTriaged(".rules[].action.stateful_session_affinity")
66+
f.Unimplemented_NotYetTriaged(".rules[].action.destinations[].request_header_modifier")
67+
f.Unimplemented_NotYetTriaged(".rules[].action.destinations[].response_header_modifier")
68+
f.Unimplemented_NotYetTriaged(".rules[].action.request_mirror_policy.destination.request_header_modifier")
69+
f.Unimplemented_NotYetTriaged(".rules[].action.request_mirror_policy.destination.response_header_modifier")
70+
f.Unimplemented_NotYetTriaged(".rules[].action.request_mirror_policy.mirror_percent")
71+
72+
return f
73+
}

pkg/controller/direct/networkservices/networkserviceshttproute_mapper.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,3 +256,92 @@ func HttprouteRetryPolicy_ToProto(mapCtx *direct.MapContext, in *krm.HttprouteRe
256256
out.PerTryTimeout = direct.StringDuration_ToProto(mapCtx, in.PerTryTimeout)
257257
return out
258258
}
259+
260+
func HttprouteQueryParameters_FromProto(mapCtx *direct.MapContext, in *pb.HttpRoute_QueryParameterMatch) *krm.HttprouteQueryParameters {
261+
if in == nil {
262+
return nil
263+
}
264+
out := &krm.HttprouteQueryParameters{}
265+
out.QueryParameter = direct.LazyPtr(in.GetQueryParameter())
266+
267+
switch m := in.MatchType.(type) {
268+
case *pb.HttpRoute_QueryParameterMatch_ExactMatch:
269+
out.ExactMatch = &m.ExactMatch
270+
case *pb.HttpRoute_QueryParameterMatch_RegexMatch:
271+
out.RegexMatch = &m.RegexMatch
272+
case *pb.HttpRoute_QueryParameterMatch_PresentMatch:
273+
out.PresentMatch = &m.PresentMatch
274+
}
275+
return out
276+
}
277+
278+
func HttprouteQueryParameters_ToProto(mapCtx *direct.MapContext, in *krm.HttprouteQueryParameters) *pb.HttpRoute_QueryParameterMatch {
279+
if in == nil {
280+
return nil
281+
}
282+
out := &pb.HttpRoute_QueryParameterMatch{}
283+
if in.ExactMatch != nil {
284+
out.MatchType = &pb.HttpRoute_QueryParameterMatch_ExactMatch{ExactMatch: *in.ExactMatch}
285+
}
286+
if in.RegexMatch != nil {
287+
out.MatchType = &pb.HttpRoute_QueryParameterMatch_RegexMatch{RegexMatch: *in.RegexMatch}
288+
}
289+
if in.PresentMatch != nil {
290+
out.MatchType = &pb.HttpRoute_QueryParameterMatch_PresentMatch{PresentMatch: *in.PresentMatch}
291+
}
292+
out.QueryParameter = direct.ValueOf(in.QueryParameter)
293+
return out
294+
}
295+
296+
func HttprouteHeaders_FromProto(mapCtx *direct.MapContext, in *pb.HttpRoute_HeaderMatch) *krm.HttprouteHeaders {
297+
if in == nil {
298+
return nil
299+
}
300+
out := &krm.HttprouteHeaders{}
301+
out.Header = direct.LazyPtr(in.GetHeader())
302+
out.InvertMatch = direct.LazyPtr(in.GetInvertMatch())
303+
304+
switch m := in.MatchType.(type) {
305+
case *pb.HttpRoute_HeaderMatch_ExactMatch:
306+
out.ExactMatch = &m.ExactMatch
307+
case *pb.HttpRoute_HeaderMatch_RegexMatch:
308+
out.RegexMatch = &m.RegexMatch
309+
case *pb.HttpRoute_HeaderMatch_PrefixMatch:
310+
out.PrefixMatch = &m.PrefixMatch
311+
case *pb.HttpRoute_HeaderMatch_PresentMatch:
312+
out.PresentMatch = &m.PresentMatch
313+
case *pb.HttpRoute_HeaderMatch_SuffixMatch:
314+
out.SuffixMatch = &m.SuffixMatch
315+
case *pb.HttpRoute_HeaderMatch_RangeMatch:
316+
out.RangeMatch = HttprouteRangeMatch_FromProto(mapCtx, m.RangeMatch)
317+
}
318+
return out
319+
}
320+
321+
func HttprouteHeaders_ToProto(mapCtx *direct.MapContext, in *krm.HttprouteHeaders) *pb.HttpRoute_HeaderMatch {
322+
if in == nil {
323+
return nil
324+
}
325+
out := &pb.HttpRoute_HeaderMatch{}
326+
if in.ExactMatch != nil {
327+
out.MatchType = &pb.HttpRoute_HeaderMatch_ExactMatch{ExactMatch: *in.ExactMatch}
328+
}
329+
if in.RegexMatch != nil {
330+
out.MatchType = &pb.HttpRoute_HeaderMatch_RegexMatch{RegexMatch: *in.RegexMatch}
331+
}
332+
if in.PrefixMatch != nil {
333+
out.MatchType = &pb.HttpRoute_HeaderMatch_PrefixMatch{PrefixMatch: *in.PrefixMatch}
334+
}
335+
if in.PresentMatch != nil {
336+
out.MatchType = &pb.HttpRoute_HeaderMatch_PresentMatch{PresentMatch: *in.PresentMatch}
337+
}
338+
if in.SuffixMatch != nil {
339+
out.MatchType = &pb.HttpRoute_HeaderMatch_SuffixMatch{SuffixMatch: *in.SuffixMatch}
340+
}
341+
if in.RangeMatch != nil {
342+
out.MatchType = &pb.HttpRoute_HeaderMatch_RangeMatch{RangeMatch: HttprouteRangeMatch_ToProto(mapCtx, in.RangeMatch)}
343+
}
344+
out.Header = direct.ValueOf(in.Header)
345+
out.InvertMatch = direct.ValueOf(in.InvertMatch)
346+
return out
347+
}

0 commit comments

Comments
 (0)