diff --git a/conformance/tests/backendtlspolicy-invalid-ca-certificate-ref.go b/conformance/tests/backendtlspolicy-invalid-ca-certificate-ref.go new file mode 100644 index 0000000000..824d8dd199 --- /dev/null +++ b/conformance/tests/backendtlspolicy-invalid-ca-certificate-ref.go @@ -0,0 +1,98 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package tests + +import ( + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + + gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + gatewayv1alpha3 "sigs.k8s.io/gateway-api/apis/v1alpha3" + h "sigs.k8s.io/gateway-api/conformance/utils/http" + "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" + "sigs.k8s.io/gateway-api/conformance/utils/suite" + "sigs.k8s.io/gateway-api/pkg/features" +) + +func init() { + ConformanceTests = append(ConformanceTests, BackendTLSPolicyInvalidCACertificateRef) +} + +var BackendTLSPolicyInvalidCACertificateRef = suite.ConformanceTest{ + ShortName: "BackendTLSPolicyInvalidCACertificateRef", + Description: "A BackendTLSPolicy that specifies a single invalid CACertificateRef should have the Accepted and ResolvedRefs status condition set False with appropriate reasons, and HTTP requests to a backend targeted by this policy should fail with a 5xx response.", + Features: []features.FeatureName{ + features.SupportGateway, + features.SupportHTTPRoute, + features.SupportBackendTLSPolicy, + }, + Manifests: []string{"tests/backendtlspolicy-invalid-ca-certificate-ref.yaml"}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + ns := "gateway-conformance-infra" + routeNN := types.NamespacedName{Name: "backendtlspolicy-invalid-ca-certificate-ref", Namespace: ns} + gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns} + + serverStr := "abc.example.com" + + kubernetes.NamespacesMustBeReady(t, suite.Client, suite.TimeoutConfig, []string{ns}) + gwAddr := kubernetes.GatewayAndRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), &gatewayv1.HTTPRoute{}, false, routeNN) + + for _, policyNN := range []types.NamespacedName{ + {Name: "nonexistent-ca-certificate-ref", Namespace: ns}, + {Name: "malformed-ca-certificate-ref", Namespace: ns}, + } { + t.Run("BackendTLSPolicy_"+policyNN.Name, func(t *testing.T) { + t.Run("BackendTLSPolicy with a single invalid CACertificateRef has a Accepted Condition with status False and Reason NoValidCACertificate", func(t *testing.T) { + acceptedCond := metav1.Condition{ + Type: string(gatewayv1alpha2.PolicyConditionAccepted), + Status: metav1.ConditionFalse, + Reason: string(gatewayv1alpha3.BackendTLSPolicyReasonNoValidCACertificate), + } + + kubernetes.BackendTLSPolicyMustHaveCondition(t, suite.Client, suite.TimeoutConfig, policyNN, gwNN, acceptedCond) + }) + + t.Run("BackendTLSPolicy with a single invalid CACertificateRef has a ResolvedRefs Condition with status False and Reason InvalidCACertificateRef", func(t *testing.T) { + resolvedRefsCond := metav1.Condition{ + Type: string(gatewayv1alpha3.BackendTLSPolicyConditionResolvedRefs), + Status: metav1.ConditionFalse, + Reason: string(gatewayv1alpha3.BackendTLSPolicyReasonInvalidCACertificateRef), + } + + kubernetes.BackendTLSPolicyMustHaveCondition(t, suite.Client, suite.TimeoutConfig, policyNN, gwNN, resolvedRefsCond) + }) + + t.Run("HTTP Request to backend targeted by an invalid BackendTLSPolicy receive a 5xx", func(t *testing.T) { + h.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, + h.ExpectedResponse{ + Namespace: ns, + Request: h.Request{ + Host: serverStr, + Path: "/backendtlspolicy-" + policyNN.Name, + }, + Response: h.Response{ + StatusCodes: []int{500, 502, 503}, + }, + }) + }) + }) + } + }, +} diff --git a/conformance/tests/backendtlspolicy-invalid-ca-certificate-ref.yaml b/conformance/tests/backendtlspolicy-invalid-ca-certificate-ref.yaml new file mode 100644 index 0000000000..138812fddb --- /dev/null +++ b/conformance/tests/backendtlspolicy-invalid-ca-certificate-ref.yaml @@ -0,0 +1,97 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: backendtlspolicy-invalid-ca-certificate-ref + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: same-namespace + namespace: gateway-conformance-infra + hostnames: + - abc.example.com + rules: + - backendRefs: + - name: backendtlspolicy-nonexistent-ca-certificate-ref-test + port: 443 + matches: + - path: + type: Exact + value: /backendtlspolicy-nonexistent-ca-certificate-ref + - backendRefs: + - name: backendtlspolicy-malformed-ca-certificate-ref-test + port: 443 + matches: + - path: + type: Exact + value: /backendtlspolicy-malformed-ca-certificate-ref +--- +apiVersion: v1 +kind: Service +metadata: + name: backendtlspolicy-nonexistent-ca-certificate-ref-test + namespace: gateway-conformance-infra +spec: + selector: + app: tls-backend + ports: + - name: "https" + protocol: TCP + appProtocol: HTTPS + port: 443 + targetPort: 8443 +--- +apiVersion: v1 +kind: Service +metadata: + name: backendtlspolicy-malformed-ca-certificate-ref-test + namespace: gateway-conformance-infra +spec: + selector: + app: tls-backend + ports: + - name: "https" + protocol: TCP + appProtocol: HTTPS + port: 443 + targetPort: 8443 +--- +apiVersion: gateway.networking.k8s.io/v1alpha3 +kind: BackendTLSPolicy +metadata: + name: nonexistent-ca-certificate-ref + namespace: gateway-conformance-infra +spec: + targetRefs: + - group: "" + kind: Service + name: "backendtlspolicy-nonexistent-ca-certificate-ref-test" + validation: + caCertificateRefs: + - group: "" + kind: ConfigMap + name: "nonexistent-ca-certificate" + hostname: "abc.example.com" +--- +apiVersion: gateway.networking.k8s.io/v1alpha3 +kind: BackendTLSPolicy +metadata: + name: malformed-ca-certificate-ref + namespace: gateway-conformance-infra +spec: + targetRefs: + - group: "" + kind: Service + name: "backendtlspolicy-malformed-ca-certificate-ref-test" + validation: + caCertificateRefs: + - group: "" + kind: ConfigMap + name: "malformed-ca-certificate" + hostname: "abc.example.com" +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: malformed-ca-certificate + namespace: gateway-conformance-infra +data: {} diff --git a/conformance/tests/backendtlspolicy-invalid-kind.go b/conformance/tests/backendtlspolicy-invalid-kind.go new file mode 100644 index 0000000000..5a3bba597e --- /dev/null +++ b/conformance/tests/backendtlspolicy-invalid-kind.go @@ -0,0 +1,93 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package tests + +import ( + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + + gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + gatewayv1alpha3 "sigs.k8s.io/gateway-api/apis/v1alpha3" + h "sigs.k8s.io/gateway-api/conformance/utils/http" + "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" + "sigs.k8s.io/gateway-api/conformance/utils/suite" + "sigs.k8s.io/gateway-api/pkg/features" +) + +func init() { + ConformanceTests = append(ConformanceTests, BackendTLSPolicyInvalidKind) +} + +var BackendTLSPolicyInvalidKind = suite.ConformanceTest{ + ShortName: "BackendTLSPolicyInvalidKind", + Description: "A BackendTLSPolicy that specifies a single CACertificateRef with an invalid kind should have the Accepted and ResolvedRefs status condition set False with appropriate reasons, and HTTP requests to a backend targeted by this policy should fail with a 5xx response.", + Features: []features.FeatureName{ + features.SupportGateway, + features.SupportHTTPRoute, + features.SupportBackendTLSPolicy, + }, + Manifests: []string{"tests/backendtlspolicy-invalid-kind.yaml"}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + ns := "gateway-conformance-infra" + routeNN := types.NamespacedName{Name: "backendtlspolicy-invalid-kind-test", Namespace: ns} + gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns} + + serverStr := "abc.example.com" + + kubernetes.NamespacesMustBeReady(t, suite.Client, suite.TimeoutConfig, []string{ns}) + gwAddr := kubernetes.GatewayAndRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), &gatewayv1.HTTPRoute{}, false, routeNN) + + policyNN := types.NamespacedName{Name: "invalid-kind", Namespace: ns} + + t.Run("BackendTLSPolicy with a single invalid CACertificateRef has a Accepted Condition with status False and Reason NoValidCACertificate", func(t *testing.T) { + acceptedCond := metav1.Condition{ + Type: string(gatewayv1alpha2.PolicyConditionAccepted), + Status: metav1.ConditionFalse, + Reason: string(gatewayv1alpha3.BackendTLSPolicyReasonNoValidCACertificate), + } + + kubernetes.BackendTLSPolicyMustHaveCondition(t, suite.Client, suite.TimeoutConfig, policyNN, gwNN, acceptedCond) + }) + + t.Run("BackendTLSPolicy with a single invalid CACertificateRef has a ResolvedRefs Condition with status False and Reason InvalidKind", func(t *testing.T) { + resolvedRefsCond := metav1.Condition{ + Type: string(gatewayv1alpha3.BackendTLSPolicyConditionResolvedRefs), + Status: metav1.ConditionFalse, + Reason: string(gatewayv1alpha3.BackendTLSPolicyReasonInvalidKind), + } + + kubernetes.BackendTLSPolicyMustHaveCondition(t, suite.Client, suite.TimeoutConfig, policyNN, gwNN, resolvedRefsCond) + }) + + t.Run("HTTP Request to backend targeted by an invalid BackendTLSPolicy receive a 5xx", func(t *testing.T) { + h.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, + h.ExpectedResponse{ + Namespace: ns, + Request: h.Request{ + Host: serverStr, + Path: "/backendtlspolicy-" + policyNN.Name, + }, + Response: h.Response{ + StatusCodes: []int{500, 502, 503}, + }, + }) + }) + }, +} diff --git a/conformance/tests/backendtlspolicy-invalid-kind.yaml b/conformance/tests/backendtlspolicy-invalid-kind.yaml new file mode 100644 index 0000000000..a7eaeb5d5c --- /dev/null +++ b/conformance/tests/backendtlspolicy-invalid-kind.yaml @@ -0,0 +1,51 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: backendtlspolicy-invalid-kind-test + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: same-namespace + namespace: gateway-conformance-infra + hostnames: + - abc.example.com + rules: + - backendRefs: + - name: backendtlspolicy-invalid-kind-test + port: 443 + matches: + - path: + type: Exact + value: /backendtlspolicy-invalid-kind +--- +apiVersion: v1 +kind: Service +metadata: + name: backendtlspolicy-invalid-kind-test + namespace: gateway-conformance-infra +spec: + selector: + app: tls-backend + ports: + - name: "https" + protocol: TCP + appProtocol: HTTPS + port: 443 + targetPort: 8443 +--- +apiVersion: gateway.networking.k8s.io/v1alpha3 +kind: BackendTLSPolicy +metadata: + name: invalid-kind + namespace: gateway-conformance-infra +spec: + targetRefs: + - group: "" + kind: Service + name: "backendtlspolicy-invalid-kind-test" + validation: + caCertificateRefs: + - group: invalid.io + kind: InvalidKind + name: "invalid-kind" + hostname: "abc.example.com" diff --git a/conformance/tests/backendtlspolicy.go b/conformance/tests/backendtlspolicy.go index fc6c6576eb..62ed54c4af 100644 --- a/conformance/tests/backendtlspolicy.go +++ b/conformance/tests/backendtlspolicy.go @@ -20,11 +20,11 @@ import ( "testing" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" "sigs.k8s.io/gateway-api/apis/v1alpha2" + "sigs.k8s.io/gateway-api/apis/v1alpha3" h "sigs.k8s.io/gateway-api/conformance/utils/http" "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" "sigs.k8s.io/gateway-api/conformance/utils/suite" @@ -54,20 +54,20 @@ var BackendTLSPolicy = suite.ConformanceTest{ gwAddr := kubernetes.GatewayAndRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), &gatewayv1.HTTPRoute{}, false, routeNN) kubernetes.HTTPRouteMustHaveResolvedRefsConditionsTrue(t, suite.Client, suite.TimeoutConfig, routeNN, gwNN) - policyCond := metav1.Condition{ + acceptedCond := metav1.Condition{ Type: string(v1alpha2.PolicyConditionAccepted), Status: metav1.ConditionTrue, Reason: string(v1alpha2.PolicyReasonAccepted), } + resolvedRefsCond := metav1.Condition{ + Type: string(v1alpha3.BackendTLSPolicyConditionResolvedRefs), + Status: metav1.ConditionTrue, + Reason: string(v1alpha3.BackendTLSPolicyReasonResolvedRefs), + } validPolicyNN := types.NamespacedName{Name: "normative-test-backendtlspolicy", Namespace: ns} - kubernetes.BackendTLSPolicyMustHaveCondition(t, suite.Client, suite.TimeoutConfig, validPolicyNN, gwNN, policyCond) - - invalidPolicyNN := types.NamespacedName{Name: "backendtlspolicy-host-mismatch", Namespace: ns} - kubernetes.BackendTLSPolicyMustHaveCondition(t, suite.Client, suite.TimeoutConfig, invalidPolicyNN, gwNN, policyCond) - - invalidCertPolicyNN := types.NamespacedName{Name: "backendtlspolicy-cert-mismatch", Namespace: ns} - kubernetes.BackendTLSPolicyMustHaveCondition(t, suite.Client, suite.TimeoutConfig, invalidCertPolicyNN, gwNN, policyCond) + kubernetes.BackendTLSPolicyMustHaveCondition(t, suite.Client, suite.TimeoutConfig, validPolicyNN, gwNN, acceptedCond) + kubernetes.BackendTLSPolicyMustHaveCondition(t, suite.Client, suite.TimeoutConfig, validPolicyNN, gwNN, resolvedRefsCond) serverStr := "abc.example.com" @@ -107,6 +107,10 @@ var BackendTLSPolicy = suite.ConformanceTest{ // Verify that the request sent to a Service targeted by a BackendTLSPolicy with mismatched host will fail. t.Run("HTTP request sent to Service targeted by BackendTLSPolicy with mismatched hostname should return an HTTP error", func(t *testing.T) { + invalidPolicyNN := types.NamespacedName{Name: "backendtlspolicy-host-mismatch", Namespace: ns} + kubernetes.BackendTLSPolicyMustHaveCondition(t, suite.Client, suite.TimeoutConfig, invalidPolicyNN, gwNN, acceptedCond) + kubernetes.BackendTLSPolicyMustHaveCondition(t, suite.Client, suite.TimeoutConfig, invalidPolicyNN, gwNN, resolvedRefsCond) + h.MakeRequestAndExpectFailure(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, h.ExpectedResponse{ Namespace: ns, @@ -120,6 +124,10 @@ var BackendTLSPolicy = suite.ConformanceTest{ // Verify that request sent to Service targeted by BackendTLSPolicy with mismatched cert should failed. t.Run("HTTP request send to Service targeted by BackendTLSPolicy with mismatched cert should return HTTP error", func(t *testing.T) { + invalidCertPolicyNN := types.NamespacedName{Name: "backendtlspolicy-cert-mismatch", Namespace: ns} + kubernetes.BackendTLSPolicyMustHaveCondition(t, suite.Client, suite.TimeoutConfig, invalidCertPolicyNN, gwNN, acceptedCond) + kubernetes.BackendTLSPolicyMustHaveCondition(t, suite.Client, suite.TimeoutConfig, invalidCertPolicyNN, gwNN, resolvedRefsCond) + h.MakeRequestAndExpectFailure(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, h.ExpectedResponse{ Namespace: ns, diff --git a/conformance/utils/echo/pod.go b/conformance/utils/echo/pod.go index 0222ff12dd..dff03ed5fa 100644 --- a/conformance/utils/echo/pod.go +++ b/conformance/utils/echo/pod.go @@ -20,6 +20,8 @@ import ( "bytes" "context" "fmt" + "slices" + "strconv" "strings" "testing" "time" @@ -84,8 +86,14 @@ func makeRequest(t *testing.T, exp *http.ExpectedResponse) []string { exp.Request.Method = "GET" } - if exp.Response.StatusCode == 0 { - exp.Response.StatusCode = 200 + // if the deprecated field StatusCode is set, append it to StatusCodes for backwards compatibility + //nolint:staticcheck + if exp.Response.StatusCode != 0 { + exp.Response.StatusCodes = append(exp.Response.StatusCodes, exp.Response.StatusCode) + } + + if len(exp.Response.StatusCodes) == 0 { + exp.Response.StatusCodes = []int{200} } r := exp.Request @@ -110,8 +118,12 @@ func compareRequest(exp http.ExpectedResponse, resp Response) error { } wantReq := exp.ExpectedRequest wantResp := exp.Response - if fmt.Sprint(wantResp.StatusCode) != resp.Code { - return fmt.Errorf("wanted status code %v, got %v", wantResp.StatusCode, resp.Code) + statusCode, err := strconv.Atoi(resp.Code) + if err != nil { + return fmt.Errorf("invalid status code '%v': %v", resp.Code, err) + } + if !slices.Contains(wantResp.StatusCodes, statusCode) { + return fmt.Errorf("wanted status code to be one of %v, got %d", wantResp.StatusCodes, statusCode) } if wantReq.Headers != nil { if resp.RequestHeaders == nil { diff --git a/conformance/utils/http/http.go b/conformance/utils/http/http.go index 50f8d1efdd..6cd68dd084 100644 --- a/conformance/utils/http/http.go +++ b/conformance/utils/http/http.go @@ -22,6 +22,7 @@ import ( "math/big" "net" "net/url" + "slices" "strings" "testing" "time" @@ -91,7 +92,9 @@ type ExpectedRequest struct { // Response defines expected properties of a response from a backend. type Response struct { + // Deprecated: Use StatusCodes instead, which supports matching against multiple status codes. StatusCode int + StatusCodes []int Headers map[string]string AbsentHeaders []string Protocol string @@ -138,8 +141,13 @@ func MakeRequest(t *testing.T, expected *ExpectedResponse, gwAddr, protocol, sch expected.Request.Method = "GET" } - if expected.Response.StatusCode == 0 { - expected.Response.StatusCode = 200 + // if the deprecated field StatusCode is set, append it to StatusCodes for backwards compatibility + if expected.Response.StatusCode != 0 { + expected.Response.StatusCodes = append(expected.Response.StatusCodes, expected.Response.StatusCode) + } + + if len(expected.Response.StatusCodes) == 0 { + expected.Response.StatusCodes = []int{200} } if expected.Request.Protocol == "" { @@ -300,12 +308,14 @@ func WaitForConsistentFailureResponse(t *testing.T, r roundtripper.RoundTripper, func CompareRoundTrip(t *testing.T, req *roundtripper.Request, cReq *roundtripper.CapturedRequest, cRes *roundtripper.CapturedResponse, expected ExpectedResponse) error { if roundtripper.IsTimeoutError(cRes.StatusCode) { - if roundtripper.IsTimeoutError(expected.Response.StatusCode) { - return nil + for _, statusCode := range expected.Response.StatusCodes { + if roundtripper.IsTimeoutError(statusCode) { + return nil + } } } - if expected.Response.StatusCode != cRes.StatusCode { - return fmt.Errorf("expected status code to be %d, got %d. CRes: %v", expected.Response.StatusCode, cRes.StatusCode, cRes) + if !slices.Contains(expected.Response.StatusCodes, cRes.StatusCode) { + return fmt.Errorf("expected status code to be one of %v, got %d. CRes: %v", expected.Response.StatusCodes, cRes.StatusCode, cRes) } if expected.Response.Protocol != "" && expected.Response.Protocol != cRes.Protocol { return fmt.Errorf("expected protocol to be %s, got %s", expected.Response.Protocol, cRes.Protocol) @@ -467,7 +477,8 @@ func (er *ExpectedResponse) GetTestCaseName(i int) string { if er.Backend != "" { return fmt.Sprintf("%s should go to %s", reqStr, er.Backend) } - return fmt.Sprintf("%s should receive a %d", reqStr, er.Response.StatusCode) + + return fmt.Sprintf("%s should receive one of %v", reqStr, er.Response.StatusCodes) } func setRedirectRequestDefaults(req *roundtripper.Request, cRes *roundtripper.CapturedResponse, expected *ExpectedResponse) {