Skip to content

Commit ba2f63c

Browse files
committed
Panic for broken webhook validation assumption
- Add panics for cases when an assumption about how webhook validation (run by NKG) validates a Gateway API resource is broken. - Add unit tests for those cases to ensure that invalid resources don't reach the code that makes those assumptions. Note: panics are not unit tested as we considered it'd be redundant. Fixes #515
1 parent 7289537 commit ba2f63c

File tree

6 files changed

+175
-20
lines changed

6 files changed

+175
-20
lines changed

internal/nginx/config/servers.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,10 @@ func createLocations(pathRules []dataplane.PathRule, listenerPort int) []http.Lo
120120
// For example, type is v1beta1.HTTPRouteFilterRequestRedirect, but RequestRedirect field is nil.
121121
// The imported Webhook validation webhook catches that.
122122

123+
// FIXME(pleshakov): Ensure dataplane.Configuration -related types don't include v1beta1 types, so that
124+
// we don't need to make any assumptions like above here. After fixing this, ensure that there is a test
125+
// for checking the imported Webhook validation catches the case above.
126+
123127
// RequestRedirect and proxying are mutually exclusive.
124128
if r.Filters.RequestRedirect != nil {
125129
loc.Return = createReturnValForRedirectFilter(r.Filters.RequestRedirect, listenerPort)

internal/state/change_processor_test.go

Lines changed: 134 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1995,13 +1995,15 @@ var _ = Describe("ChangeProcessor", func() {
19951995
})
19961996

19971997
assertHREvent := func() {
1998-
e := <-fakeEventRecorder.Events
1998+
var e string
1999+
EventuallyWithOffset(1, fakeEventRecorder.Events).Should(Receive(&e))
19992000
ExpectWithOffset(1, e).To(ContainSubstring("Rejected"))
20002001
ExpectWithOffset(1, e).To(ContainSubstring("spec.rules[0].matches[0].path.type"))
20012002
}
20022003

20032004
assertGwEvent := func() {
2004-
e := <-fakeEventRecorder.Events
2005+
var e string
2006+
EventuallyWithOffset(1, fakeEventRecorder.Events).Should(Receive(&e))
20052007
ExpectWithOffset(1, e).To(ContainSubstring("Rejected"))
20062008
ExpectWithOffset(1, e).To(ContainSubstring("spec.listeners[0].hostname"))
20072009
}
@@ -2185,6 +2187,136 @@ var _ = Describe("ChangeProcessor", func() {
21852187
assertGwEvent()
21862188
})
21872189
})
2190+
2191+
Describe("Webhook assumptions", func() {
2192+
var processor state.ChangeProcessor
2193+
2194+
BeforeEach(func() {
2195+
fakeSecretMemoryMgr = &secretsfakes.FakeSecretDiskMemoryManager{}
2196+
fakeEventRecorder = record.NewFakeRecorder(1 /* number of buffered events */)
2197+
2198+
processor = state.NewChangeProcessorImpl(state.ChangeProcessorConfig{
2199+
GatewayCtlrName: controllerName,
2200+
GatewayClassName: gcName,
2201+
SecretMemoryManager: fakeSecretMemoryMgr,
2202+
RelationshipCapturer: relationship.NewCapturerImpl(),
2203+
Logger: zap.New(),
2204+
Validators: createAlwaysValidValidators(),
2205+
EventRecorder: fakeEventRecorder,
2206+
Scheme: createScheme(),
2207+
})
2208+
})
2209+
2210+
createInvalidHTTPRoute := func(invalidator func(hr *v1beta1.HTTPRoute)) *v1beta1.HTTPRoute {
2211+
hr := createRoute(
2212+
"hr",
2213+
"gateway",
2214+
"foo.example.com",
2215+
createBackendRef(
2216+
helpers.GetPointer[v1beta1.Kind]("Service"),
2217+
"test",
2218+
helpers.GetPointer[v1beta1.Namespace]("namespace"),
2219+
),
2220+
)
2221+
invalidator(hr)
2222+
return hr
2223+
}
2224+
2225+
createInvalidGateway := func(invalidator func(gw *v1beta1.Gateway)) *v1beta1.Gateway {
2226+
gw := createGateway("gateway")
2227+
invalidator(gw)
2228+
return gw
2229+
}
2230+
2231+
assertRejectedEvent := func() {
2232+
EventuallyWithOffset(1, fakeEventRecorder.Events).Should(Receive(ContainSubstring("Rejected")))
2233+
}
2234+
2235+
DescribeTable("Invalid HTTPRoutes",
2236+
func(hr *v1beta1.HTTPRoute) {
2237+
processor.CaptureUpsertChange(hr)
2238+
2239+
expectedConf := dataplane.Configuration{}
2240+
expectedStatuses := state.Statuses{}
2241+
2242+
changed, conf, statuses := processor.Process(context.Background())
2243+
2244+
Expect(changed).To(BeFalse())
2245+
Expect(helpers.Diff(expectedConf, conf)).To(BeEmpty())
2246+
assertStatuses(expectedStatuses, statuses)
2247+
2248+
assertRejectedEvent()
2249+
},
2250+
Entry(
2251+
"duplicate parentRefs",
2252+
createInvalidHTTPRoute(func(hr *v1beta1.HTTPRoute) {
2253+
hr.Spec.ParentRefs = append(hr.Spec.ParentRefs, hr.Spec.ParentRefs[len(hr.Spec.ParentRefs)-1])
2254+
}),
2255+
),
2256+
Entry(
2257+
"nil path.Type",
2258+
createInvalidHTTPRoute(func(hr *v1beta1.HTTPRoute) {
2259+
hr.Spec.Rules[0].Matches[0].Path.Type = nil
2260+
}),
2261+
),
2262+
Entry("nil path.Value",
2263+
createInvalidHTTPRoute(func(hr *v1beta1.HTTPRoute) {
2264+
hr.Spec.Rules[0].Matches[0].Path.Value = nil
2265+
}),
2266+
),
2267+
Entry(
2268+
"nil request.Redirect",
2269+
createInvalidHTTPRoute(func(hr *v1beta1.HTTPRoute) {
2270+
hr.Spec.Rules[0].Filters = append(hr.Spec.Rules[0].Filters, v1beta1.HTTPRouteFilter{
2271+
Type: v1beta1.HTTPRouteFilterRequestRedirect,
2272+
RequestRedirect: nil,
2273+
})
2274+
}),
2275+
),
2276+
Entry("nil port in BackendRef",
2277+
createInvalidHTTPRoute(func(hr *v1beta1.HTTPRoute) {
2278+
hr.Spec.Rules[0].BackendRefs[0].Port = nil
2279+
}),
2280+
),
2281+
)
2282+
2283+
DescribeTable("Invalid Gateway resources",
2284+
func(gw *v1beta1.Gateway) {
2285+
processor.CaptureUpsertChange(gw)
2286+
2287+
expectedConf := dataplane.Configuration{}
2288+
expectedStatuses := state.Statuses{}
2289+
2290+
changed, conf, statuses := processor.Process(context.Background())
2291+
2292+
Expect(changed).To(BeFalse())
2293+
Expect(helpers.Diff(expectedConf, conf)).To(BeEmpty())
2294+
assertStatuses(expectedStatuses, statuses)
2295+
2296+
assertRejectedEvent()
2297+
},
2298+
Entry("tls in HTTP listener",
2299+
createInvalidGateway(func(gw *v1beta1.Gateway) {
2300+
gw.Spec.Listeners[0].TLS = &v1beta1.GatewayTLSConfig{}
2301+
}),
2302+
),
2303+
Entry("tls is nil in HTTPS listener",
2304+
createInvalidGateway(func(gw *v1beta1.Gateway) {
2305+
gw.Spec.Listeners[0].Protocol = v1beta1.HTTPSProtocolType
2306+
gw.Spec.Listeners[0].TLS = nil
2307+
}),
2308+
),
2309+
Entry("zero certificateRefs in HTTPS listener",
2310+
createInvalidGateway(func(gw *v1beta1.Gateway) {
2311+
gw.Spec.Listeners[0].Protocol = v1beta1.HTTPSProtocolType
2312+
gw.Spec.Listeners[0].TLS = &v1beta1.GatewayTLSConfig{
2313+
Mode: helpers.GetPointer(v1beta1.TLSModeTerminate),
2314+
CertificateRefs: nil,
2315+
}
2316+
}),
2317+
),
2318+
)
2319+
})
21882320
})
21892321

21902322
Describe("Edge cases with panic", func() {

internal/state/graph/backend_refs.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -167,9 +167,11 @@ func validateBackendRef(
167167
return false, conditions.NewRouteBackendRefRefNotPermitted(valErr.Error())
168168
}
169169

170-
// The imported Webhook validation ensures ref.Port is set
171-
// any value is OK
172-
// FIXME(pleshakov): Add a unit test for the imported Webhook validation code for this case.
170+
if ref.Port == nil {
171+
panicForBrokenWebhookAssumption(fmt.Errorf("port is nil for ref %q", ref.Name))
172+
}
173+
174+
// any value of port is OK
173175

174176
if ref.Weight != nil {
175177
if err := validateWeight(*ref.Weight); err != nil {

internal/state/graph/gateway.go

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -321,8 +321,9 @@ func validateHTTPListener(listener v1beta1.Listener) []conditions.Condition {
321321
conds = append(conds, conditions.NewListenerPortUnavailable(valErr.Error()))
322322
}
323323

324-
// The imported Webhook validation ensures the tls field is not set for an HTTP listener.
325-
// FIXME(pleshakov): Add a unit test for the imported Webhook validation code for this case.
324+
if listener.TLS != nil {
325+
panicForBrokenWebhookAssumption(fmt.Errorf("tls is not nil for HTTP listener %q", listener.Name))
326+
}
326327

327328
return conds
328329
}
@@ -336,8 +337,9 @@ func validateHTTPSListener(listener v1beta1.Listener, gwNsName string) []conditi
336337
conds = append(conds, conditions.NewListenerPortUnavailable(valErr.Error()))
337338
}
338339

339-
// The imported Webhook validation ensures the tls field is not nil for an HTTPS listener.
340-
// FIXME(pleshakov): Add a unit test for the imported Webhook validation code for this case.
340+
if listener.TLS == nil {
341+
panicForBrokenWebhookAssumption(fmt.Errorf("tls is nil for HTTPS listener %q", listener.Name))
342+
}
341343

342344
tlsPath := field.NewPath("tls")
343345

@@ -356,8 +358,9 @@ func validateHTTPSListener(listener v1beta1.Listener, gwNsName string) []conditi
356358
conds = append(conds, conditions.NewListenerUnsupportedValue(valErr.Error()))
357359
}
358360

359-
// The imported Webhook validation ensures len(listener.TLS.Certificates) is not 0.
360-
// FIXME(pleshakov): Add a unit test for the imported Webhook validation code for this case.
361+
if len(listener.TLS.CertificateRefs) == 0 {
362+
panicForBrokenWebhookAssumption(fmt.Errorf("zero certificateRefs for HTTPS listener %q", listener.Name))
363+
}
361364

362365
certRef := listener.TLS.CertificateRefs[0]
363366

internal/state/graph/httproute.go

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package graph
22

33
import (
4+
"errors"
45
"fmt"
56

67
"k8s.io/apimachinery/pkg/types"
@@ -148,17 +149,16 @@ func buildSectionNameRefs(
148149
continue
149150
}
150151

151-
// The imported Webhook validation ensures unique section names.
152-
// Additionally, it ensures that if multiple refs reference the same Gateway, their section names
153-
// are not empty
154-
// FIXME(pleshakov): Add a unit test for the imported Webhook validation code for this case.
155-
156152
// FIXME(pleshakov): SectionNames across multiple Gateways might collide. Fix that.
157153
var sectionName string
158154
if p.SectionName != nil {
159155
sectionName = string(*p.SectionName)
160156
}
161157

158+
if _, exist := sectionNameRefs[sectionName]; exist {
159+
panicForBrokenWebhookAssumption(fmt.Errorf("duplicate section name %q", sectionName))
160+
}
161+
162162
sectionNameRefs[sectionName] = ParentRef{
163163
Idx: i,
164164
Gateway: gw,
@@ -520,8 +520,12 @@ func validatePathMatch(
520520
return allErrs
521521
}
522522

523-
// The imported Webhook validation ensures the path type and value are not nil
524-
// FIXME(pleshakov): Add a unit test for the imported Webhook validation code for this case.
523+
if path.Type == nil {
524+
panicForBrokenWebhookAssumption(errors.New("path type cannot be nil"))
525+
}
526+
if path.Value == nil {
527+
panicForBrokenWebhookAssumption(errors.New("path value cannot be nil"))
528+
}
525529

526530
if *path.Type != v1beta1.PathMatchPathPrefix {
527531
valErr := field.NotSupported(fieldPath, *path.Type, []string{string(v1beta1.PathMatchPathPrefix)})
@@ -553,8 +557,9 @@ func validateFilter(
553557
return allErrs
554558
}
555559

556-
// The imported Webhook validation ensures filter.RequestRedirect is not nil
557-
// FIXME(pleshakov): Add a unit test for the imported Webhook validation code for this case.
560+
if filter.RequestRedirect == nil {
561+
panicForBrokenWebhookAssumption(errors.New("requestRedirect cannot be nil"))
562+
}
558563

559564
redirect := filter.RequestRedirect
560565

internal/state/graph/validation.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package graph
22

33
import (
44
"errors"
5+
"fmt"
56
"strings"
67

78
"k8s.io/apimachinery/pkg/util/validation"
@@ -24,3 +25,11 @@ func validateHostname(hostname string) error {
2425

2526
return nil
2627
}
28+
29+
// panicForBrokenWebhookAssumption panics with the error message because an assumption about the webhook validation
30+
// (run by NKG) is broken.
31+
// Use it when you expect a validated Gateway API resource, but the actual resource breaks the validation constraints.
32+
// For example, a field that must not be nil is nil.
33+
func panicForBrokenWebhookAssumption(err error) {
34+
panic(fmt.Errorf("webhook validation assumption was broken: %w", err))
35+
}

0 commit comments

Comments
 (0)