diff --git a/conformance/base/manifests.yaml b/conformance/base/manifests.yaml index a3163bf837..c14a98eabc 100644 --- a/conformance/base/manifests.yaml +++ b/conformance/base/manifests.yaml @@ -138,8 +138,8 @@ spec: spec: containers: - name: infra-backend-v1 - # From https://github.com/kubernetes-sigs/ingress-controller-conformance/tree/master/images/echoserver - image: gcr.io/k8s-staging-gateway-api/echo-basic:v20240412-v1.0.0-394-g40c666fd + # Originally from https://github.com/kubernetes-sigs/ingress-controller-conformance/tree/master/images/echoserver + image: gcr.io/k8s-staging-gateway-api/echo-basic:v20241007-v1.2.0-6-g9f820af9 env: - name: POD_NAME valueFrom: @@ -185,7 +185,7 @@ spec: spec: containers: - name: infra-backend-v2 - image: gcr.io/k8s-staging-gateway-api/echo-basic:v20240412-v1.0.0-394-g40c666fd + image: gcr.io/k8s-staging-gateway-api/echo-basic:v20241007-v1.2.0-6-g9f820af9 env: - name: POD_NAME valueFrom: @@ -231,7 +231,7 @@ spec: spec: containers: - name: infra-backend-v3 - image: gcr.io/k8s-staging-gateway-api/echo-basic:v20240412-v1.0.0-394-g40c666fd + image: gcr.io/k8s-staging-gateway-api/echo-basic:v20241007-v1.2.0-6-g9f820af9 env: - name: POD_NAME valueFrom: @@ -277,7 +277,7 @@ spec: spec: containers: - name: tls-backend - image: gcr.io/k8s-staging-gateway-api/echo-basic:v20240412-v1.0.0-394-g40c666fd + image: gcr.io/k8s-staging-gateway-api/echo-basic:v20241007-v1.2.0-6-g9f820af9 volumeMounts: - name: secret-volume mountPath: /etc/secret-volume @@ -300,7 +300,7 @@ spec: volumes: - name: secret-volume secret: - secretName: tls-passthrough-checks-certificate + secretName: tls-checks-certificate items: - key: tls.crt path: crt @@ -346,7 +346,7 @@ spec: spec: containers: - name: tls-backend - image: gcr.io/k8s-staging-gateway-api/echo-basic:v20240412-v1.0.0-394-g40c666fd + image: gcr.io/k8s-staging-gateway-api/echo-basic:v20241007-v1.2.0-6-g9f820af9 volumeMounts: - name: secret-volume mountPath: /etc/secret-volume @@ -408,7 +408,7 @@ spec: spec: containers: - name: app-backend-v1 - image: gcr.io/k8s-staging-gateway-api/echo-basic:v20240412-v1.0.0-394-g40c666fd + image: gcr.io/k8s-staging-gateway-api/echo-basic:v20241007-v1.2.0-6-g9f820af9 env: - name: POD_NAME valueFrom: @@ -454,7 +454,7 @@ spec: spec: containers: - name: app-backend-v2 - image: gcr.io/k8s-staging-gateway-api/echo-basic:v20240412-v1.0.0-394-g40c666fd + image: gcr.io/k8s-staging-gateway-api/echo-basic:v20241007-v1.2.0-6-g9f820af9 env: - name: POD_NAME valueFrom: @@ -507,7 +507,7 @@ spec: spec: containers: - name: web-backend - image: gcr.io/k8s-staging-gateway-api/echo-basic:v20240412-v1.0.0-394-g40c666fd + image: gcr.io/k8s-staging-gateway-api/echo-basic:v20241007-v1.2.0-6-g9f820af9 env: - name: POD_NAME valueFrom: @@ -554,7 +554,7 @@ spec: spec: containers: - name: grpc-infra-backend-v1 - image: gcr.io/k8s-staging-gateway-api/echo-basic:v20240412-v1.0.0-394-g40c666fd + image: gcr.io/k8s-staging-gateway-api/echo-basic:v20241007-v1.2.0-6-g9f820af9 env: - name: POD_NAME valueFrom: @@ -603,7 +603,7 @@ spec: spec: containers: - name: grpc-infra-backend-v2 - image: gcr.io/k8s-staging-gateway-api/echo-basic:v20240412-v1.0.0-394-g40c666fd + image: gcr.io/k8s-staging-gateway-api/echo-basic:v20241007-v1.2.0-6-g9f820af9 env: - name: POD_NAME valueFrom: @@ -652,7 +652,7 @@ spec: spec: containers: - name: grpc-infra-backend-v3 - image: gcr.io/k8s-staging-gateway-api/echo-basic:v20240412-v1.0.0-394-g40c666fd + image: gcr.io/k8s-staging-gateway-api/echo-basic:v20241007-v1.2.0-6-g9f820af9 env: - name: POD_NAME valueFrom: diff --git a/conformance/conformance.go b/conformance/conformance.go index 12e5884968..ce264a9441 100644 --- a/conformance/conformance.go +++ b/conformance/conformance.go @@ -29,14 +29,11 @@ import ( "sigs.k8s.io/gateway-api/conformance/tests" conformanceconfig "sigs.k8s.io/gateway-api/conformance/utils/config" "sigs.k8s.io/gateway-api/conformance/utils/flags" - "sigs.k8s.io/gateway-api/conformance/utils/suite" "github.com/stretchr/testify/require" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - clientset "k8s.io/client-go/kubernetes" - "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/config" "sigs.k8s.io/yaml" @@ -66,7 +63,6 @@ func DefaultOptions(t *testing.T) suite.ConformanceOptions { supportedFeatures := suite.ParseSupportedFeatures(*flags.SupportedFeatures) exemptFeatures := suite.ParseSupportedFeatures(*flags.ExemptFeatures) - skipTests := suite.ParseSkipTests(*flags.SkipTests) namespaceLabels := suite.ParseKeyValuePairs(*flags.NamespaceLabels) namespaceAnnotations := suite.ParseKeyValuePairs(*flags.NamespaceAnnotations) @@ -148,7 +144,6 @@ func logOptions(t *testing.T, opts suite.ConformanceOptions) { t.Logf(" Enable All Features: %t", opts.EnableAllSupportedFeatures) t.Logf(" Supported Features: %v", opts.SupportedFeatures.UnsortedList()) t.Logf(" ExemptFeatures: %v", opts.ExemptFeatures.UnsortedList()) - t.Logf(" ConformanceProfiles: %v", opts.ConformanceProfiles.UnsortedList()) } func writeReport(logf func(string, ...any), report confv1.ConformanceReport, output string) error { diff --git a/conformance/tests/backendtlspolicy.go b/conformance/tests/backendtlspolicy.go new file mode 100644 index 0000000000..b1f5ce8a3a --- /dev/null +++ b/conformance/tests/backendtlspolicy.go @@ -0,0 +1,90 @@ +/* +Copyright 2024 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" + + "k8s.io/apimachinery/pkg/types" + + 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/conformance/utils/tls" + "sigs.k8s.io/gateway-api/pkg/features" +) + +func init() { + ConformanceTests = append(ConformanceTests, BackendTLSPolicy) +} + +var BackendTLSPolicy = suite.ConformanceTest{ + ShortName: "BackendTLSPolicy", + Description: "A single service that is targeted by a BackendTLSPolicy must successfully complete TLS termination", + Features: []features.FeatureName{ + features.SupportGateway, + features.SupportHTTPRoute, + features.SupportBackendTLSPolicy, + }, + Manifests: []string{"tests/backendtlspolicy.yaml"}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + ns := "gateway-conformance-infra" + routeNN := types.NamespacedName{Name: "gateway-conformance-infra-test", Namespace: ns} + gwNN := types.NamespacedName{Name: "gateway-backendtlspolicy", Namespace: ns} + + kubernetes.NamespacesMustBeReady(t, suite.Client, suite.TimeoutConfig, []string{ns}) + gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAcceptedMultipleListeners(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN) + kubernetes.HTTPRouteMustHaveResolvedRefsConditionsTrue(t, suite.Client, suite.TimeoutConfig, routeNN, gwNN) + + serverStr := "abc.example.com" + + // Verify that the response to a backend-tls-only call to /backendTLS will return the matching SNI. + t.Run("Simple HTTP request targeting BackendTLSPolicy should reach infra-backend", func(t *testing.T) { + h.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, + h.ExpectedResponse{ + Namespace: ns, + Request: h.Request{ + Host: serverStr, + Path: "/backendTLS", + SNI: serverStr, + }, + Response: h.Response{StatusCode: 200}, + }) + }) + + // For the re-encrypt case, we need to use the cert for the frontend tls listener. + certNN := types.NamespacedName{Name: "tls-checks-certificate", Namespace: ns} + cPem, keyPem, err := GetTLSSecret(suite.Client, certNN) + if err != nil { + t.Fatalf("unexpected error finding TLS secret: %v", err) + } + // Verify that the response to a re-encrypted call to /backendTLS will return the matching SNI. + t.Run("Re-encrypt HTTPS request targeting BackendTLSPolicy should reach infra-backend", func(t *testing.T) { + tls.MakeTLSRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, cPem, keyPem, serverStr, + h.ExpectedResponse{ + Namespace: ns, + Request: h.Request{ + Host: serverStr, + Path: "/backendTLS", + SNI: serverStr, + }, + Response: h.Response{StatusCode: 200}, + }) + }) + + }, +} diff --git a/conformance/tests/backendtlspolicy.yaml b/conformance/tests/backendtlspolicy.yaml new file mode 100644 index 0000000000..91f6a590f9 --- /dev/null +++ b/conformance/tests/backendtlspolicy.yaml @@ -0,0 +1,153 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: gateway-backendtlspolicy + namespace: gateway-conformance-infra +spec: + gatewayClassName: "{GATEWAY_CLASS_NAME}" + listeners: + - name: http + port: 80 + protocol: HTTP + hostname: "abc.example.com" + allowedRoutes: + namespaces: + from: Same + kinds: + - kind: HTTPRoute + - name: https + port: 443 + protocol: HTTPS + tls: + mode: Terminate + certificateRefs: + - group: "" + kind: Secret + name: tls-checks-certificate + hostname: "abc.example.com" + allowedRoutes: + namespaces: + from: Same + kinds: + - kind: HTTPRoute +--- +apiVersion: gateway.networking.k8s.io/v1alpha3 +kind: BackendTLSPolicy +metadata: + name: normative-test-backendtlspolicy + namespace: gateway-conformance-infra +spec: + targetRefs: + - group: "" + kind: Service + name: "backendtlspolicy-test" + sectionName: "btls" + validation: + caCertificateRefs: + - group: "" + kind: ConfigMap + # This secret is generated dynamically by the test suite. + name: "backend-tls-checks-certificate" + hostname: "abc.example.com" +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: gateway-conformance-infra-test + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: gateway-backendtlspolicy + namespace: gateway-conformance-infra + hostnames: + - abc.example.com + rules: + - backendRefs: + - group: "" + kind: Service + name: backendtlspolicy-test + port: 443 + matches: + - path: + type: Exact + value: /backendTLS +--- +apiVersion: v1 +kind: Service +metadata: + name: backendtlspolicy-test + namespace: gateway-conformance-infra +spec: + selector: + app: backendtlspolicy-test + ports: + - name: "btls" + protocol: TCP + port: 443 + targetPort: 8443 +--- +# Deployment must not be applied until after the secret is generated. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: backendtlspolicy-test + namespace: gateway-conformance-infra + labels: + app: backendtlspolicy-test +spec: + replicas: 1 + selector: + matchLabels: + app: backendtlspolicy-test + template: + metadata: + labels: + app: backendtlspolicy-test + spec: + containers: + - name: backendtlspolicy-test + image: gcr.io/k8s-staging-gateway-api/echo-basic:v20240412-v1.0.0-394-g40c666fd + volumeMounts: + - name: ca-volume + mountPath: /etc/ca-volume + - name: secret-volume + mountPath: /etc/secret-volume + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: CA_CERT + value: /etc/ca-volume/crt + - name: CA_CERT_KEY + value: /etc/ca-volume/key + - name: TLS_SERVER_CERT + value: /etc/secret-volume/crt + - name: TLS_SERVER_PRIVKEY + value: /etc/secret-volume/key + resources: + requests: + cpu: 10m + volumes: + - name: ca-volume + configMap: + # This configMap is generated dynamically by the test suite. + name: backend-tls-checks-certificate + items: + - key: ca.crt + path: crt + - key: key.crt + path: key + - name: secret-volume + secret: + # This secret is generated dynamically by the test suite. + secretName: tls-checks-certificate + items: + - key: tls.crt + path: crt + - key: tls.key + path: key diff --git a/conformance/tests/grpcroute-exact-method-matching.go b/conformance/tests/grpcroute-exact-method-matching.go index 384cf3ae02..c70c034b6c 100644 --- a/conformance/tests/grpcroute-exact-method-matching.go +++ b/conformance/tests/grpcroute-exact-method-matching.go @@ -46,7 +46,7 @@ var GRPCExactMethodMatching = suite.ConformanceTest{ ns := "gateway-conformance-infra" routeNN := types.NamespacedName{Name: "exact-matching", Namespace: ns} gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns} - gwAddr := kubernetes.GatewayAndRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), &v1.GRPCRoute{}, routeNN) + gwAddr := kubernetes.GatewayAndRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), &v1.GRPCRoute{}, true, routeNN) testCases := []grpc.ExpectedResponse{ { diff --git a/conformance/tests/grpcroute-header-matching.go b/conformance/tests/grpcroute-header-matching.go index 48b6e0da66..2db1819d3e 100644 --- a/conformance/tests/grpcroute-header-matching.go +++ b/conformance/tests/grpcroute-header-matching.go @@ -46,7 +46,7 @@ var GRPCRouteHeaderMatching = suite.ConformanceTest{ ns := "gateway-conformance-infra" routeNN := types.NamespacedName{Name: "grpc-header-matching", Namespace: ns} gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns} - gwAddr := kubernetes.GatewayAndRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), &v1.GRPCRoute{}, routeNN) + gwAddr := kubernetes.GatewayAndRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), &v1.GRPCRoute{}, true, routeNN) testCases := []grpc.ExpectedResponse{{ EchoRequest: &pb.EchoRequest{}, diff --git a/conformance/tests/grpcroute-listener-hostname-matching.go b/conformance/tests/grpcroute-listener-hostname-matching.go index 0e0492e764..ba8ee64226 100644 --- a/conformance/tests/grpcroute-listener-hostname-matching.go +++ b/conformance/tests/grpcroute-listener-hostname-matching.go @@ -52,15 +52,15 @@ var GRPCRouteListenerHostnameMatching = suite.ConformanceTest{ gwNN := types.NamespacedName{Name: "grpcroute-listener-hostname-matching", Namespace: ns} _ = kubernetes.GatewayAndRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, - kubernetes.NewGatewayRef(gwNN, "listener-1"), &v1.GRPCRoute{}, + kubernetes.NewGatewayRef(gwNN, "listener-1"), &v1.GRPCRoute{}, true, types.NamespacedName{Namespace: ns, Name: "backend-v1"}, ) _ = kubernetes.GatewayAndRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, - kubernetes.NewGatewayRef(gwNN, "listener-2"), &v1.GRPCRoute{}, + kubernetes.NewGatewayRef(gwNN, "listener-2"), &v1.GRPCRoute{}, true, types.NamespacedName{Namespace: ns, Name: "backend-v2"}, ) gwAddr := kubernetes.GatewayAndRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, - kubernetes.NewGatewayRef(gwNN, "listener-3", "listener-4"), &v1.GRPCRoute{}, + kubernetes.NewGatewayRef(gwNN, "listener-3", "listener-4"), &v1.GRPCRoute{}, true, types.NamespacedName{Namespace: ns, Name: "backend-v3"}, ) diff --git a/conformance/tests/httproute-weight.go b/conformance/tests/httproute-weight.go index 2a3e21afee..2c1a520dc0 100644 --- a/conformance/tests/httproute-weight.go +++ b/conformance/tests/httproute-weight.go @@ -105,7 +105,7 @@ func testDistribution(t *testing.T, suite *suite.ConformanceTestSuite, gwAddr st if err != nil { return fmt.Errorf("failed to roundtrip request: %w", err) } - if err := http.CompareRequest(t, &req, cReq, cRes, expected); err != nil { + if err := http.CompareRoundTrip(t, &req, cReq, cRes, expected); err != nil { return fmt.Errorf("response expectation failed for request: %w", err) } diff --git a/conformance/tests/tlsroute-simple-same-namespace.go b/conformance/tests/tlsroute-simple-same-namespace.go index 51db62aeff..b8704c4b8b 100644 --- a/conformance/tests/tlsroute-simple-same-namespace.go +++ b/conformance/tests/tlsroute-simple-same-namespace.go @@ -49,7 +49,7 @@ var TLSRouteSimpleSameNamespace = suite.ConformanceTest{ ns := "gateway-conformance-infra" routeNN := types.NamespacedName{Name: "gateway-conformance-infra-test", Namespace: ns} gwNN := types.NamespacedName{Name: "gateway-tlsroute", Namespace: ns} - certNN := types.NamespacedName{Name: "tls-passthrough-checks-certificate", Namespace: ns} + certNN := types.NamespacedName{Name: "tls-checks-certificate", Namespace: ns} kubernetes.NamespacesMustBeReady(t, suite.Client, suite.TimeoutConfig, []string{ns}) diff --git a/conformance/utils/http/http.go b/conformance/utils/http/http.go index 18b5e1feb0..e1aca27d54 100644 --- a/conformance/utils/http/http.go +++ b/conformance/utils/http/http.go @@ -58,6 +58,10 @@ type ExpectedResponse struct { // User Given TestCase name TestCaseName string + + // ServerName indicates the hostname to which the client attempts to connect, + // and which is seen by the backend. + ServerName string } // Request can be used as both the request to make and a means to verify @@ -71,6 +75,7 @@ type Request struct { UnfollowRedirect bool Protocol string Body string + SNI string } // ExpectedRequest defines expected properties of a request that reaches a backend. @@ -130,7 +135,7 @@ func MakeRequest(t *testing.T, expected *ExpectedResponse, gwAddr, protocol, sch path, query, _ := strings.Cut(expected.Request.Path, "?") reqURL := url.URL{Scheme: scheme, Host: CalculateHost(t, gwAddr, scheme), Path: path, RawQuery: query} - tlog.Logf(t, "Making %s request to %s", expected.Request.Method, reqURL.String()) + tlog.Logf(t, "Making %s request to host %s via %s", expected.Request.Method, expected.Request.Host, reqURL.String()) req := roundtripper.Request{ T: t, @@ -172,7 +177,10 @@ func CalculateHost(t *testing.T, gwAddr, scheme string) string { host, port, err = net.SplitHostPort(gwAddr) } if err != nil { - tlog.Logf(t, "Failed to parse host %q: %v", gwAddr, err) + // An address without a port causes an error, but it's fine for some cases. + if !strings.Contains(err.Error(), "missing port in address") { + tlog.Logf(t, "Failed to parse host %q: %v", gwAddr, err) + } return gwAddr } if strings.ToLower(scheme) == "http" && port == "80" { @@ -243,7 +251,7 @@ func WaitForConsistentResponse(t *testing.T, r roundtripper.RoundTripper, req ro return false } - if err := CompareRequest(t, &req, cReq, cRes, expected); err != nil { + if err := CompareRoundTrip(t, &req, cReq, cRes, expected); err != nil { tlog.Logf(t, "Response expectation failed for request: %+v not ready yet: %v (after %v)", req, err, elapsed) return false } @@ -253,14 +261,14 @@ func WaitForConsistentResponse(t *testing.T, r roundtripper.RoundTripper, req ro tlog.Logf(t, "Request passed") } -func CompareRequest(t *testing.T, req *roundtripper.Request, cReq *roundtripper.CapturedRequest, cRes *roundtripper.CapturedResponse, expected ExpectedResponse) error { +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 } } if expected.Response.StatusCode != cRes.StatusCode { - return fmt.Errorf("expected status code to be %d, got %d", 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 cRes.StatusCode == 200 { // The request expected to arrive at the backend is @@ -353,6 +361,10 @@ func CompareRequest(t *testing.T, req *roundtripper.Request, cReq *roundtripper. if !strings.HasPrefix(cReq.Pod, expected.Backend) { return fmt.Errorf("expected pod name to start with %s, got %s", expected.Backend, cReq.Pod) } + + if expected.ExpectedRequest.SNI != "" && expected.ExpectedRequest.SNI != cReq.TLS.ServerName { + return fmt.Errorf("expected SNI %q to be equal to %q", cReq.TLS.ServerName, expected.ExpectedRequest.SNI) + } } else if roundtripper.IsRedirect(cRes.StatusCode) { if expected.RedirectRequest == nil { return nil diff --git a/conformance/utils/kubernetes/certificate.go b/conformance/utils/kubernetes/certificate.go index a0f70ddd14..29c4c2ed57 100644 --- a/conformance/utils/kubernetes/certificate.go +++ b/conformance/utils/kubernetes/certificate.go @@ -27,12 +27,14 @@ import ( "io" "math/big" "net" + "strings" "testing" "time" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + kvalidation "k8s.io/apimachinery/pkg/util/validation" ) const ( @@ -46,8 +48,25 @@ func MustCreateSelfSignedCertSecret(t *testing.T, namespace, secretName string, var serverKey, serverCert bytes.Buffer - require.NoError(t, generateRSACert(hosts, &serverKey, &serverCert), "failed to generate RSA certificate") + require.NoError(t, generateRSACert(hosts, &serverKey, &serverCert, nil, nil), "failed to generate RSA certificate") + return formatSecret(serverCert, serverKey, namespace, secretName) +} + +// MustCreateCASignedCertSecret creates a CA-signed SSL certificate and stores it in a secret +func MustCreateCASignedCertSecret(t *testing.T, namespace, secretName string, hosts []string, ca *x509.Certificate, caPrivKey *rsa.PrivateKey) *corev1.Secret { + require.NotEmpty(t, hosts, "require a non-empty hosts for Subject Alternate Name values") + + var serverCert, serverKey bytes.Buffer + + require.NoError(t, generateRSACert(hosts, &serverKey, &serverCert, ca, caPrivKey), "failed to generate CA signed RSA certificate") + + return formatSecret(serverCert, serverKey, namespace, secretName) +} + +// formatSecret formats the server certificate, key, namespace, and secretName +// and converts it to a Kubernetes Secret object. +func formatSecret(serverCert bytes.Buffer, serverKey bytes.Buffer, namespace string, secretName string) *corev1.Secret { data := map[string][]byte{ corev1.TLSCertKey: serverCert.Bytes(), corev1.TLSPrivateKeyKey: serverKey.Bytes(), @@ -65,8 +84,9 @@ func MustCreateSelfSignedCertSecret(t *testing.T, namespace, secretName string, return newSecret } -// generateRSACert generates a basic self-signed certificate valid for a year -func generateRSACert(hosts []string, keyOut, certOut io.Writer) error { +// generateRSACert generates a basic self-signed certificate if ca and caPrivKey are nil, +// otherwise it creates CA-signed cert with ca and caPrivkey input. Certs are valid for a year. +func generateRSACert(hosts []string, keyOut, certOut io.Writer, ca *x509.Certificate, caPrivKey *rsa.PrivateKey) error { priv, err := rsa.GenerateKey(rand.Reader, rsaBits) if err != nil { return fmt.Errorf("failed to generate key: %w", err) @@ -97,12 +117,20 @@ func generateRSACert(hosts []string, keyOut, certOut io.Writer) error { for _, h := range hosts { if ip := net.ParseIP(h); ip != nil { template.IPAddresses = append(template.IPAddresses, ip) - } else { + } else if err = validateHost(h); err == nil { template.DNSNames = append(template.DNSNames, h) } } - derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) + if caPrivKey == nil { + caPrivKey = priv + } + // If ca is nil, we create a self-signed certificate, e.g. template is the parent. + if ca == nil { + ca = &template + } + + derBytes, err := x509.CreateCertificate(rand.Reader, &template, ca, &priv.PublicKey, caPrivKey) if err != nil { return fmt.Errorf("failed to create certificate: %w", err) } @@ -117,3 +145,116 @@ func generateRSACert(hosts []string, keyOut, certOut io.Writer) error { return nil } + +// MustCreateCASignedCertConfigMap will create a ConfigMap containing a CA Certificate, given a TLS Secret +// for that CA certificate. Also returns the CA certificate. +func MustCreateCASignedCertConfigMap(t *testing.T, namespace, configMapName string, hosts []string) (*corev1.ConfigMap, *x509.Certificate, *rsa.PrivateKey) { + require.NotEmpty(t, hosts, "require a non-empty hosts for Subject Alternate Name values") + + var certData, keyData bytes.Buffer + + ca, caBytes, caPrivKey, err := generateCACert(hosts) + if err != nil { + t.Errorf("failed to generate CA certificate and key: %v", err) + return nil, nil, nil + } + + if err := pem.Encode(&certData, &pem.Block{Type: "CERTIFICATE", Bytes: caBytes}); err != nil { + t.Errorf("failed creating cert: %v", err) + return nil, nil, nil + } + + if err := pem.Encode(&keyData, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(caPrivKey)}); err != nil { + t.Errorf("failed creating key: %v", err) + return nil, nil, nil + } + + // Store the certificate in a ConfigMap. + caConfigMap := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: configMapName, + }, + Data: map[string]string{ + "ca.crt": certData.String(), + "key.crt": keyData.String(), + }, + } + return caConfigMap, ca, caPrivKey +} + +// generateCACert generates a CA and a CA-signed certificate valid for a year. +func generateCACert(hosts []string) (*x509.Certificate, []byte, *rsa.PrivateKey, error) { + var caBytes []byte + + // Create the CA certificate template. + ca := &x509.Certificate{ + SerialNumber: big.NewInt(2024), + Subject: pkix.Name{ + Organization: []string{"Kubernetes Gateway API"}, + Country: []string{"US"}, + CommonName: "gatewayapi", + }, + Issuer: pkix.Name{ + Organization: []string{"Kubernetes Gateway API"}, + Country: []string{"US"}, + CommonName: "kubernetes", + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(1, 0, 0), + IsCA: true, // Indicates this is a CA Certificate. + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + BasicConstraintsValid: true, + } + + // Ensure only valid hosts make it into the CA cert. + for _, h := range hosts { + if ip := net.ParseIP(h); ip != nil { + ca.IPAddresses = append(ca.IPAddresses, ip) + } else if err := validateHost(h); err == nil { + ca.DNSNames = append(ca.DNSNames, h) + } + } + + // Generate the private key to sign certificates. + caPrivKey, err := rsa.GenerateKey(rand.Reader, rsaBits) + if err != nil { + return nil, caBytes, caPrivKey, fmt.Errorf("error generating key for CA: %v", err) + } + + // Create the self-signed certificate using the CA certificate. + caBytes, err = x509.CreateCertificate(rand.Reader, ca, ca, &caPrivKey.PublicKey, caPrivKey) + if err != nil { + return nil, caBytes, caPrivKey, fmt.Errorf("error creating CA: %v", err) + } + + return ca, caBytes, caPrivKey, nil +} + +// validateHost ensures that the host name length is no more than 253 characters. +// The only characters allowed in host name are alphanumeric characters, '-' or '.', +// and it must start and end with an alphanumeric character. A trailing dot is NOT allowed. +// The host name must in addition consist of one or more labels, with each label no more +// than 63 characters from the character set described above, and each label must start and +// end with an alphanumeric character. Wildcards are handled specially. +// DO NOT USE for general validation purposes, this is just for the hostnames set up for +// conformance testing. +func validateHost(host string) error { + // Remove wildcard if present. + host, _ = strings.CutPrefix(host, "*.") + + errs := kvalidation.IsDNS1123Subdomain(host) + if len(errs) != 0 { + return fmt.Errorf("host %s must conform to DNS naming conventions: %v", host, errs) + } + + labels := strings.Split(host, ".") + for _, l := range labels { + errs := kvalidation.IsDNS1123Label(l) + if len(errs) != 0 { + return fmt.Errorf("label %s in host %s must conform to DNS naming conventions: %v", l, host, errs) + } + } + return nil +} diff --git a/conformance/utils/kubernetes/certificate_test.go b/conformance/utils/kubernetes/certificate_test.go new file mode 100644 index 0000000000..f6c78048a9 --- /dev/null +++ b/conformance/utils/kubernetes/certificate_test.go @@ -0,0 +1,101 @@ +/* +Copyright 2024 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 kubernetes + +import ( + "bytes" + "crypto/x509" + "encoding/pem" + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_generateCACert(t *testing.T) { + tests := []struct { + name string + hosts []string + expectedErr []string + }{ + { + name: "one host generates cert with no host", + hosts: []string{}, + }, + { + name: "one host generates cert for same host", + hosts: []string{"abc.example.com"}, + }, + { + name: "wildcard generates cert for same host", + hosts: []string{"*.example.com"}, + }, + { + name: "two hosts generates cert for both hosts", + hosts: []string{"abc.example.com", "def.example.com"}, + }, + { + name: "bad host generates cert for no host", + hosts: []string{"--abc.example.com"}, + expectedErr: []string{"x509: certificate is not valid for any names, but wanted to match --abc.example.com"}, + }, + { + name: "one good host and one bad host generates cert for only good host", + hosts: []string{"---.example.com", "def.example.com"}, + expectedErr: []string{"x509: certificate is valid xxx for def.example.com, not ---.example.com", ""}, + }, + } + + var serverKey, serverCert bytes.Buffer + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + serverCert.Reset() + serverKey.Reset() + // Test the function generateCACert. We can only test normative function + // and hostnames, everything else is hardcoded. + _, caBytes, caPrivKey, err := generateCACert(tc.hosts) + require.NoError(t, err, "unexpected error generating RSA certificate") + + var certData bytes.Buffer + if err := pem.Encode(&certData, &pem.Block{Type: "CERTIFICATE", Bytes: caBytes}); err != nil { + require.NoError(t, err, "failed to create certificater") + } + + var keyData bytes.Buffer + if err := pem.Encode(&keyData, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(caPrivKey)}); err != nil { + require.NoError(t, err, "failed to create key") + } + + // Test that the CA certificate is decodable, parseable, and has the configured hostname/s. + block, _ := pem.Decode(certData.Bytes()) + if block == nil { + require.FailNow(t, "failed to decode PEM block containing cert") + } else if block.Type == "CERTIFICATE" { + cert, err := x509.ParseCertificate(block.Bytes) + require.NoError(t, err, "failed to parse certificate") + for idx, h := range tc.hosts { + err = cert.VerifyHostname(h) + if err != nil && len(tc.expectedErr) > 0 && tc.expectedErr[idx] == "" { + require.EqualValues(t, tc.expectedErr[idx], err.Error(), "certificate verification failed") + } else if err == nil && len(tc.expectedErr) > 0 && tc.expectedErr[idx] != "" { + require.EqualValues(t, tc.expectedErr[idx], err, "expected an error but certification verification succeeded") + } + } + } + }) + } +} diff --git a/conformance/utils/kubernetes/helpers.go b/conformance/utils/kubernetes/helpers.go index 2b43828c09..1392605944 100644 --- a/conformance/utils/kubernetes/helpers.go +++ b/conformance/utils/kubernetes/helpers.go @@ -331,12 +331,24 @@ func MeshNamespacesMustBeReady(t *testing.T, c client.Client, timeoutConfig conf // - ListenerConditionProgrammed // // The test will fail if these conditions are not met before the timeouts. -func GatewayAndRoutesMustBeAccepted(t *testing.T, c client.Client, timeoutConfig config.TimeoutConfig, controllerName string, gw GatewayRef, routeType any, routeNNs ...types.NamespacedName) string { +// Note that this also returns a Gateway address to use, but it takes the port +// from the first listener it finds. Therefore, if the Gateway has multiple listeners, +// don't use this function unless you can ignore the port and allow the url +// scheme to determine the default port to use in a URL. Set parameter `usePort` to +// false if there are multiple listeners, and true if there is only one listener. +func GatewayAndRoutesMustBeAccepted(t *testing.T, c client.Client, timeoutConfig config.TimeoutConfig, controllerName string, gw GatewayRef, routeType any, usePort bool, routeNNs ...types.NamespacedName) string { t.Helper() - RouteTypeMustHaveParentsField(t, routeType) - gwAddr, err := WaitForGatewayAddress(t, c, timeoutConfig, gw) + var err error + var gwAddr string + RouteTypeMustHaveParentsField(t, routeType) + // If the Gateway has multiple listeners, get a portless gwAddr. + if !usePort { + gwAddr, err = WaitForGatewayAddressMultipleListeners(t, c, timeoutConfig, gw) + } else { + gwAddr, err = WaitForGatewayAddress(t, c, timeoutConfig, gw) + } require.NoErrorf(t, err, "timed out waiting for Gateway address to be assigned") ns := gatewayv1.Namespace(gw.Namespace) @@ -401,7 +413,13 @@ func GatewayAndRoutesMustBeAccepted(t *testing.T, c client.Client, timeoutConfig // // The test will fail if these conditions are not met before the timeouts. func GatewayAndHTTPRoutesMustBeAccepted(t *testing.T, c client.Client, timeoutConfig config.TimeoutConfig, controllerName string, gw GatewayRef, routeNNs ...types.NamespacedName) string { - return GatewayAndRoutesMustBeAccepted(t, c, timeoutConfig, controllerName, gw, &gatewayv1.HTTPRoute{}, routeNNs...) + return GatewayAndRoutesMustBeAccepted(t, c, timeoutConfig, controllerName, gw, &gatewayv1.HTTPRoute{}, true, routeNNs...) +} + +// GatewayAndHTTPRoutesMustBeAcceptedMultipleListeners is the same as GatewayAndHTTPRoutesMustBeAccepted except it does not +// return the port in the gateway string. With multiple listeners, port varies and some tests can't succeed using the returned port. +func GatewayAndHTTPRoutesMustBeAcceptedMultipleListeners(t *testing.T, c client.Client, timeoutConfig config.TimeoutConfig, controllerName string, gw GatewayRef, routeNNs ...types.NamespacedName) string { + return GatewayAndRoutesMustBeAccepted(t, c, timeoutConfig, controllerName, gw, &gatewayv1.HTTPRoute{}, false, routeNNs...) } // GatewayAndUDPRoutesMustBeAccepted waits until the specified Gateway has an IP @@ -409,26 +427,21 @@ func GatewayAndHTTPRoutesMustBeAccepted(t *testing.T, c client.Client, timeoutCo // Gateway. The test will fail if these conditions are not met before the // timeouts. func GatewayAndUDPRoutesMustBeAccepted(t *testing.T, c client.Client, timeoutConfig config.TimeoutConfig, controllerName string, gw GatewayRef, routeNNs ...types.NamespacedName) string { - return GatewayAndRoutesMustBeAccepted(t, c, timeoutConfig, controllerName, gw, &v1alpha2.UDPRoute{}, routeNNs...) + return GatewayAndRoutesMustBeAccepted(t, c, timeoutConfig, controllerName, gw, &v1alpha2.UDPRoute{}, true, routeNNs...) } // WaitForGatewayAddress waits until at least one IP Address has been set in the -// status of the specified Gateway. +// status of the specified Gateway. Use when there is only one listener in the +// Gateway. func WaitForGatewayAddress(t *testing.T, client client.Client, timeoutConfig config.TimeoutConfig, gwRef GatewayRef) (string, error) { t.Helper() var ipAddr, port string waitErr := wait.PollUntilContextTimeout(context.Background(), 1*time.Second, timeoutConfig.GatewayMustHaveAddress, true, func(ctx context.Context) (bool, error) { - gw := &gatewayv1.Gateway{} - err := client.Get(ctx, gwRef.NamespacedName, gw) - if err != nil { - tlog.Logf(t, "error fetching Gateway: %v", err) - return false, fmt.Errorf("error fetching Gateway: %w", err) - } - - if err := ConditionsHaveLatestObservedGeneration(gw, gw.Status.Conditions); err != nil { - tlog.Log(t, "Gateway", err) - return false, nil + gw, err := getGatewayStatus(t, ctx, client, gwRef) + if gw == nil { + // The returned error is nil if the Gateway conditions don't have the latest observed generation. + return false, err } listener := gw.Spec.Listeners[0] @@ -442,20 +455,62 @@ func WaitForGatewayAddress(t *testing.T, client client.Client, timeoutConfig con } } port = strconv.FormatInt(int64(listener.Port), 10) - for _, address := range gw.Status.Addresses { if address.Type != nil && (*address.Type == gatewayv1.IPAddressType || *address.Type == v1alpha2.HostnameAddressType) { ipAddr = address.Value return true, nil } } - return false, nil }) require.NoErrorf(t, waitErr, "error waiting for Gateway to have at least one IP address in status") return net.JoinHostPort(ipAddr, port), waitErr } +// WaitForGatewayAddressMultipleListeners waits until at least one IP Address has been set in the +// status of the specified Gateway and returns it without a port. A port interferes when +// there are multiple listeners, e.g if the first listener is HTTP/80 but we want to be using another +// listener with HTTPS/443, we can't send a request to https://gwaddr:80. But we can send a request +// to https://gwaddr and expect it to succeed by using the default port for HTTPS. +func WaitForGatewayAddressMultipleListeners(t *testing.T, client client.Client, timeoutConfig config.TimeoutConfig, gwRef GatewayRef) (string, error) { + t.Helper() + + var ipAddr string + waitErr := wait.PollUntilContextTimeout(context.Background(), 1*time.Second, timeoutConfig.GatewayMustHaveAddress, true, func(ctx context.Context) (bool, error) { + gw, err := getGatewayStatus(t, ctx, client, gwRef) + if gw == nil { + // The returned error is nil if the Gateway conditions don't have the latest observed generation. + return false, err + } + + for _, address := range gw.Status.Addresses { + if address.Type != nil && (*address.Type == gatewayv1.IPAddressType || *address.Type == v1alpha2.HostnameAddressType) { + ipAddr = address.Value + return true, nil + } + } + return false, nil + }) + require.NoErrorf(t, waitErr, "error waiting for Gateway to have at least one IP address in status") + return ipAddr, waitErr +} + +func getGatewayStatus(t *testing.T, ctx context.Context, client client.Client, gwRef GatewayRef) (*gatewayv1.Gateway, error) { + gw := &gatewayv1.Gateway{} + err := client.Get(ctx, gwRef.NamespacedName, gw) + if err != nil { + tlog.Logf(t, "error fetching Gateway: %v", err) + return nil, fmt.Errorf("error fetching Gateway: %w", err) + } + + if err := ConditionsHaveLatestObservedGeneration(gw, gw.Status.Conditions); err != nil { + tlog.Log(t, "Gateway", err) + return nil, nil + } + + return gw, nil +} + // GatewayListenersMustHaveConditions checks if every listener of the specified gateway has all // the specified conditions. func GatewayListenersMustHaveConditions(t *testing.T, client client.Client, timeoutConfig config.TimeoutConfig, gwName types.NamespacedName, conditions []metav1.Condition) { diff --git a/conformance/utils/roundtripper/roundtripper.go b/conformance/utils/roundtripper/roundtripper.go index f79508784f..9490b648e2 100644 --- a/conformance/utils/roundtripper/roundtripper.go +++ b/conformance/utils/roundtripper/roundtripper.go @@ -88,6 +88,14 @@ type CapturedRequest struct { Namespace string `json:"namespace"` Pod string `json:"pod"` + TLS TLS `json:"tls"` +} + +type TLS struct { + Version string `json:"version"` + ServerName string `json:"serverName"` + NegotiatedProtocol string `json:"negotiatedProtocol"` + CipherSuite string `json:"cipherSuite"` } // RedirectRequest contains a follow up request metadata captured from a redirect @@ -228,6 +236,18 @@ func (d *DefaultRoundTripper) defaultRoundTrip(request Request, transport http.R resp, err := client.Do(req) if err != nil { + if d.Debug { + var dump []byte + if resp != nil { + dump, err = httputil.DumpResponse(resp, true) + if err != nil { + return nil, nil, err + } + tlog.Logf(request.T, "Error sending request:\n%s\n\n", formatDump(dump, "< ")) + } else { + tlog.Logf(request.T, "Error sending request: %v (no response)\n", err) + } + } return nil, nil, err } defer resp.Body.Close() diff --git a/conformance/utils/suite/conformance.go b/conformance/utils/suite/conformance.go index 12db14d6df..436b3ed2ce 100644 --- a/conformance/utils/suite/conformance.go +++ b/conformance/utils/suite/conformance.go @@ -68,7 +68,7 @@ func (test *ConformanceTest) Run(t *testing.T, suite *ConformanceTestSuite) { for _, manifestLocation := range test.Manifests { tlog.Logf(t, "Applying %s", manifestLocation) - suite.Applier.MustApplyWithCleanup(t, suite.Client, suite.TimeoutConfig, manifestLocation, true) + suite.Applier.MustApplyWithCleanup(t, suite.Client, suite.TimeoutConfig, manifestLocation, suite.Cleanup) } if featuresInfo != "" { diff --git a/conformance/utils/suite/suite.go b/conformance/utils/suite/suite.go index f1164fa2fe..62d493d06f 100644 --- a/conformance/utils/suite/suite.go +++ b/conformance/utils/suite/suite.go @@ -370,6 +370,10 @@ func (suite *ConformanceTestSuite) Setup(t *testing.T, tests []ConformanceTest) suite.Applier.MustApplyObjectsWithCleanup(t, suite.Client, suite.TimeoutConfig, []client.Object{secret}, suite.Cleanup) secret = kubernetes.MustCreateSelfSignedCertSecret(t, "gateway-conformance-app-backend", "tls-passthrough-checks-certificate", []string{"abc.example.com"}) suite.Applier.MustApplyObjectsWithCleanup(t, suite.Client, suite.TimeoutConfig, []client.Object{secret}, suite.Cleanup) + caConfigMap, ca, caPrivKey := kubernetes.MustCreateCASignedCertConfigMap(t, "gateway-conformance-infra", "backend-tls-checks-certificate", []string{"abc.example.com"}) + suite.Applier.MustApplyObjectsWithCleanup(t, suite.Client, suite.TimeoutConfig, []client.Object{caConfigMap}, suite.Cleanup) + secret = kubernetes.MustCreateCASignedCertSecret(t, "gateway-conformance-infra", "tls-checks-certificate", []string{"abc.example.com"}, ca, caPrivKey) + suite.Applier.MustApplyObjectsWithCleanup(t, suite.Client, suite.TimeoutConfig, []client.Object{secret}, suite.Cleanup) tlog.Logf(t, "Test Setup: Ensuring Gateways and Pods from base manifests are ready") namespaces := []string{ diff --git a/conformance/utils/tls/tls.go b/conformance/utils/tls/tls.go index e27fab839b..809a4938a8 100644 --- a/conformance/utils/tls/tls.go +++ b/conformance/utils/tls/tls.go @@ -54,8 +54,9 @@ func WaitForConsistentTLSResponse(t *testing.T, r roundtripper.RoundTripper, req return false } - if err := http.CompareRequest(t, &req, cReq, cRes, expected); err != nil { + if err := http.CompareRoundTrip(t, &req, cReq, cRes, expected); err != nil { tlog.Logf(t, "Response expectation failed for request: %+v not ready yet: %v (after %v)", req, err, elapsed) + tlog.Logf(t, "Full response: %+v", cReq) return false } diff --git a/docker/Dockerfile.echo-basic b/docker/Dockerfile.echo-basic index ed6081fb33..55d9b47055 100644 --- a/docker/Dockerfile.echo-basic +++ b/docker/Dockerfile.echo-basic @@ -13,7 +13,7 @@ # limitations under the License. # Build -FROM golang:1.22.2 as builder +FROM golang:1.22.2 AS builder ENV CGO_ENABLED=0 @@ -23,9 +23,10 @@ COPY ./conformance/echo-basic ./ # If left as go.mod and go.sum in the external repo, these files would # interfere with the ability to use reuse the protobuf/gRPC generated code -# for the test client in the conformance tests. -RUN mv .go.mod go.mod -RUN mv .go.sum go.sum +# for the test client in the conformance tests. Add -f in case previous run +# is aborted and not cleaned up. +RUN mv -f .go.mod go.mod +RUN mv -f .go.sum go.sum RUN go build -trimpath -ldflags="-buildid= -s -w" -o echo-basic . diff --git a/pkg/features/backendtlspolicy.go b/pkg/features/backendtlspolicy.go new file mode 100644 index 0000000000..8763b32dd1 --- /dev/null +++ b/pkg/features/backendtlspolicy.go @@ -0,0 +1,40 @@ +/* +Copyright 2024 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 features + +import "k8s.io/apimachinery/pkg/util/sets" + +// ----------------------------------------------------------------------------- +// Features - BackendTLSPolicy Conformance (Core) +// ----------------------------------------------------------------------------- + +const ( + // This option indicates support for BackendTLSPolicy. + SupportBackendTLSPolicy FeatureName = "BackendTLSPolicy" +) + +// TLSRouteFeature contains metadata for the TLSRoute feature. +var BackendTLSPolicyFeature = Feature{ + Name: SupportBackendTLSPolicy, + Channel: FeatureChannelExperimental, +} + +// BackendTLSPolicyCoreFeatures includes all the supported features for the +// BackendTLSPolicy API at a Core level of support. +var BackendTLSPolicyCoreFeatures = sets.New( + BackendTLSPolicyFeature, +) diff --git a/pkg/features/features.go b/pkg/features/features.go index 8406c4b891..2a67d0065f 100644 --- a/pkg/features/features.go +++ b/pkg/features/features.go @@ -60,7 +60,8 @@ var ( Insert(TLSRouteCoreFeatures.UnsortedList()...). Insert(MeshCoreFeatures.UnsortedList()...). Insert(MeshExtendedFeatures.UnsortedList()...). - Insert(GRPCRouteCoreFeatures.UnsortedList()...) + Insert(GRPCRouteCoreFeatures.UnsortedList()...). + Insert(BackendTLSPolicyCoreFeatures.UnsortedList()...) featureMap = map[FeatureName]Feature{} )