From ff6960191fb1310683274e627f3e42c7023f18f1 Mon Sep 17 00:00:00 2001 From: candita Date: Wed, 7 Dec 2022 20:18:43 -0500 Subject: [PATCH] Issue #1579 TLSRoute Passthrough Conformance Test (normative) rebase --- conformance/base/manifests.yaml | 62 +++++++++++ conformance/conformance_test.go | 4 +- .../tests/tlsroute-simple-same-namespace.go | 89 +++++++++++++++ .../tests/tlsroute-simple-same-namespace.yaml | 35 ++++++ conformance/utils/flags/flags.go | 1 + conformance/utils/http/http.go | 10 +- conformance/utils/kubernetes/certificate.go | 2 +- conformance/utils/kubernetes/helpers.go | 91 ++++++++++++++- .../utils/roundtripper/roundtripper.go | 104 +++++++++++++++++- conformance/utils/suite/suite.go | 2 + conformance/utils/tls/tls.go | 101 +++++++++++++++++ site-src/concepts/conformance.md | 14 ++- 12 files changed, 501 insertions(+), 14 deletions(-) create mode 100644 conformance/tests/tlsroute-simple-same-namespace.go create mode 100644 conformance/tests/tlsroute-simple-same-namespace.yaml create mode 100644 conformance/utils/tls/tls.go diff --git a/conformance/base/manifests.yaml b/conformance/base/manifests.yaml index ae2a5e3f06..1167563dbe 100644 --- a/conformance/base/manifests.yaml +++ b/conformance/base/manifests.yaml @@ -201,6 +201,68 @@ spec: cpu: 10m --- apiVersion: v1 +kind: Service +metadata: + name: infra-backend-v4 + namespace: gateway-conformance-infra +spec: + selector: + app: infra-backend-v4 + ports: + - protocol: TCP + port: 443 + targetPort: 8443 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: infra-backend-v4 + namespace: gateway-conformance-infra + labels: + app: infra-backend-v4 +spec: + replicas: 2 + selector: + matchLabels: + app: infra-backend-v4 + template: + metadata: + labels: + app: infra-backend-v4 + spec: + containers: + - name: infra-backend-v4 + image: gcr.io/k8s-staging-ingressconformance/echoserver:v20221109-7ee2f3e + volumeMounts: + - name: secret-volume + mountPath: /etc/secret-volume + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: TLS_SERVER_CERT + value: /etc/secret-volume/crt + - name: TLS_SERVER_PRIVKEY + value: /etc/secret-volume/key + resources: + requests: + cpu: 10m + volumes: + - name: secret-volume + secret: + secretName: tls-passthrough-checks-certificate + items: + - key: tls.crt + path: crt + - key: tls.key + path: key +--- +apiVersion: v1 kind: Namespace metadata: name: gateway-conformance-app-backend diff --git a/conformance/conformance_test.go b/conformance/conformance_test.go index e9fc78ea27..a81adc221c 100644 --- a/conformance/conformance_test.go +++ b/conformance/conformance_test.go @@ -44,13 +44,13 @@ func TestConformance(t *testing.T) { v1alpha2.AddToScheme(client.Scheme()) v1beta1.AddToScheme(client.Scheme()) - t.Logf("Running conformance tests with %s GatewayClass", *flags.GatewayClassName) - supportedFeatures := parseSupportedFeatures(*flags.SupportedFeatures) exemptFeatures := parseSupportedFeatures(*flags.ExemptFeatures) for feature := range exemptFeatures { supportedFeatures[feature] = false } + t.Logf("Running conformance tests with %s GatewayClass\n cleanup: %t\n debug: %t\n supported features: [%v]\n exempt features: [%v]", + *flags.GatewayClassName, *flags.CleanupBaseResources, *flags.ShowDebug, *flags.SupportedFeatures, *flags.ExemptFeatures) cSuite := suite.New(suite.Options{ Client: client, diff --git a/conformance/tests/tlsroute-simple-same-namespace.go b/conformance/tests/tlsroute-simple-same-namespace.go new file mode 100644 index 0000000000..8859a3c45b --- /dev/null +++ b/conformance/tests/tlsroute-simple-same-namespace.go @@ -0,0 +1,89 @@ +/* +Copyright 2022 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 ( + "context" + "fmt" + "testing" + "time" + + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + + "sigs.k8s.io/controller-runtime/pkg/client" + + "sigs.k8s.io/gateway-api/apis/v1beta1" + "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" +) + +func init() { + ConformanceTests = append(ConformanceTests, TLSRouteSimpleSameNamespace) +} + +var TLSRouteSimpleSameNamespace = suite.ConformanceTest{ + ShortName: "TLSRouteSimpleSameNamespace", + Description: "A single TLSRoute in the gateway-conformance-infra namespace attaches to a Gateway in the same namespace", + Manifests: []string{"tests/tlsroute-simple-same-namespace.yaml"}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + ns := v1beta1.Namespace("gateway-conformance-infra") + routeNN := types.NamespacedName{Name: "gateway-conformance-infra-test", Namespace: string(ns)} + gwNN := types.NamespacedName{Name: "gateway-tlsroute", Namespace: string(ns)} + certNN := types.NamespacedName{Name: "tls-passthrough-checks-certificate", Namespace: string(ns)} + + gwAddr, server := kubernetes.GatewayAndTLSRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN) + if len(server) != 1 { + fmt.Errorf("one and only one server required for TLS") + } + serverStr := string(server[0]) + + cPem, kPem, err := GetTLSSecret(suite.Client, certNN) + if err != nil { + fmt.Errorf("unexpected error finding TLS secret: %w", err) + } + + t.Run("Simple HTTP request for TLSRoute should reach infra-backend", func(t *testing.T) { + tls.MakeTLSRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, cPem, kPem, serverStr, + http.ExpectedResponse{ + Request: http.Request{Host: serverStr, Path: "/"}, + Backend: "infra-backend-v4", + Namespace: "gateway-conformance-infra", + }) + }) + }, +} + +// GetTLSSecret fetches the named Secret and converts both cert and key to []byte +func GetTLSSecret(client client.Client, secretName types.NamespacedName) ([]byte, []byte, error) { + var cert, key []byte + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + secret := &v1.Secret{} + err := client.Get(ctx, secretName, secret) + if err != nil { + return cert, key, fmt.Errorf("error fetching TLS Secret: %w", err) + } + cert = secret.Data["tls.crt"] + key = secret.Data["tls.key"] + + return cert, key, nil +} diff --git a/conformance/tests/tlsroute-simple-same-namespace.yaml b/conformance/tests/tlsroute-simple-same-namespace.yaml new file mode 100644 index 0000000000..07c878a2d3 --- /dev/null +++ b/conformance/tests/tlsroute-simple-same-namespace.yaml @@ -0,0 +1,35 @@ +apiVersion: gateway.networking.k8s.io/v1alpha2 +kind: TLSRoute +metadata: + name: gateway-conformance-infra-test + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: gateway-tlsroute + namespace: gateway-conformance-infra + hostnames: + - abc.example.com + rules: + - backendRefs: + - name: infra-backend-v4 + port: 443 +--- +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: Gateway +metadata: + name: gateway-tlsroute + namespace: gateway-conformance-infra +spec: + gatewayClassName: "{GATEWAY_CLASS_NAME}" + listeners: + - name: https + port: 443 + protocol: TLS + hostname: "*.example.com" + allowedRoutes: + namespaces: + from: Same + kinds: + - kind: TLSRoute + tls: + mode: Passthrough \ No newline at end of file diff --git a/conformance/utils/flags/flags.go b/conformance/utils/flags/flags.go index 5159773987..15b8334f6d 100644 --- a/conformance/utils/flags/flags.go +++ b/conformance/utils/flags/flags.go @@ -29,4 +29,5 @@ var ( CleanupBaseResources = flag.Bool("cleanup-base-resources", true, "Whether to cleanup base test resources after the run") SupportedFeatures = flag.String("supported-features", "", "Supported features included in conformance tests suites") ExemptFeatures = flag.String("exempt-features", "", "Exempt Features excluded from conformance tests suites") + RunOneTest = flag.String("test", "", "A single test name when you want to run only one") ) diff --git a/conformance/utils/http/http.go b/conformance/utils/http/http.go index a1409e79f6..333bdfb014 100644 --- a/conformance/utils/http/http.go +++ b/conformance/utils/http/http.go @@ -132,9 +132,9 @@ func MakeRequestAndExpectEventuallyConsistentResponse(t *testing.T, r roundtripp WaitForConsistentResponse(t, r, req, expected, requiredConsecutiveSuccesses, timeoutConfig.MaxTimeToConsistency) } -// awaitConvergence runs the given function until it returns 'true' `threshold` times in a row. +// AwaitConvergence runs the given function until it returns 'true' `threshold` times in a row. // Each failed attempt has a 1s delay; successful attempts have no delay. -func awaitConvergence(t *testing.T, threshold int, maxTimeToConsistency time.Duration, fn func(elapsed time.Duration) bool) { +func AwaitConvergence(t *testing.T, threshold int, maxTimeToConsistency time.Duration, fn func(elapsed time.Duration) bool) { successes := 0 attempts := 0 start := time.Now() @@ -162,7 +162,7 @@ func awaitConvergence(t *testing.T, threshold int, maxTimeToConsistency time.Dur select { // Capture the overall timeout case <-to: - t.Fatalf("timeout while waiting after %d attempts, %d/%d sucessess", attempts, successes, threshold) + t.Fatalf("timeout while waiting after %d attempts, %d/%d successes", attempts, successes, threshold) // And the per-try delay case <-time.After(delay): } @@ -173,7 +173,7 @@ func awaitConvergence(t *testing.T, threshold int, maxTimeToConsistency time.Dur // the expected response consistently. The provided threshold determines how many times in // a row this must occur to be considered "consistent". func WaitForConsistentResponse(t *testing.T, r roundtripper.RoundTripper, req roundtripper.Request, expected ExpectedResponse, threshold int, maxTimeToConsistency time.Duration) { - awaitConvergence(t, threshold, maxTimeToConsistency, func(elapsed time.Duration) bool { + AwaitConvergence(t, threshold, maxTimeToConsistency, func(elapsed time.Duration) bool { cReq, cRes, err := r.CaptureRoundTrip(req) if err != nil { t.Logf("Request failed, not ready yet: %v (after %v)", err.Error(), elapsed) @@ -293,7 +293,7 @@ func CompareRequest(cReq *roundtripper.CapturedRequest, cRes *roundtripper.Captu return nil } -// Get User-defined test case name or generate from expected response to a given request. +// GetTestCaseName gets the user-defined test case name or generates one from expected response to a given request. func (er *ExpectedResponse) GetTestCaseName(i int) string { // If TestCase name is provided then use that or else generate one. diff --git a/conformance/utils/kubernetes/certificate.go b/conformance/utils/kubernetes/certificate.go index 1077ca5681..1671cd12fd 100644 --- a/conformance/utils/kubernetes/certificate.go +++ b/conformance/utils/kubernetes/certificate.go @@ -71,7 +71,7 @@ func MustCreateSelfSignedCertSecret(t *testing.T, namespace, secretName string, return newSecret } -// generateRSACert generates a basic self signed certificate valir for a year +// generateRSACert generates a basic self signed certificate valid for a year func generateRSACert(host string, keyOut, certOut io.Writer) error { priv, err := rsa.GenerateKey(rand.Reader, rsaBits) if err != nil { diff --git a/conformance/utils/kubernetes/helpers.go b/conformance/utils/kubernetes/helpers.go index df0a1e571f..77dd07bfbf 100644 --- a/conformance/utils/kubernetes/helpers.go +++ b/conformance/utils/kubernetes/helpers.go @@ -28,12 +28,14 @@ import ( "time" "github.com/stretchr/testify/require" + v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/gateway-api/apis/v1alpha2" "sigs.k8s.io/gateway-api/apis/v1beta1" "sigs.k8s.io/gateway-api/conformance/utils/config" ) @@ -408,6 +410,38 @@ func HTTPRouteMustHaveParents(t *testing.T, client client.Client, timeoutConfig require.NoErrorf(t, waitErr, "error waiting for HTTPRoute to have parents matching expectations") } +// TLSRouteInfo waits for the specified TLSRoute to have parents +// in status that match the expected parents, and also returns the assigned +// hostnames of the TLSRoute. This will cause the test to halt if the +// specified timeout is exceeded. +func TLSRouteInfo(t *testing.T, client client.Client, timeoutConfig config.TimeoutConfig, routeName types.NamespacedName, parents []v1beta1.RouteParentStatus, namespaceRequired bool) []v1beta1.Hostname { + t.Helper() + + var actual []v1beta1.RouteParentStatus + var hostnames []v1beta1.Hostname + + waitErr := wait.PollImmediate(1*time.Second, timeoutConfig.HTTPRouteMustHaveParents, func() (bool, error) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + route := &v1alpha2.TLSRoute{} + err := client.Get(ctx, routeName, route) + if err != nil { + return false, fmt.Errorf("error fetching TLSRoute: %w", err) + } + actual = route.Status.Parents + hostnames = route.Spec.Hostnames + match := parentsForRouteMatch(t, routeName, parents, actual, namespaceRequired) + + return match, nil + }) + if waitErr != nil { + fmt.Errorf("error waiting for TLSRoute to have parents matching expectations") + } + + return hostnames +} + func parentsForRouteMatch(t *testing.T, routeName types.NamespacedName, expected, actual []v1beta1.RouteParentStatus, namespaceRequired bool) bool { t.Helper() @@ -479,7 +513,7 @@ func GatewayStatusMustHaveListeners(t *testing.T, client client.Client, timeoutC require.NoErrorf(t, waitErr, "error waiting for Gateway status to have listeners matching expectations") } -// HTTPRouteMustHaveConditions checks that the supplied HTTPRoute has the supplied Condition, +// HTTPRouteMustHaveCondition checks that the supplied HTTPRoute has the supplied Condition, // halting after the specified timeout is exceeded. func HTTPRouteMustHaveCondition(t *testing.T, client client.Client, timeoutConfig config.TimeoutConfig, routeNN types.NamespacedName, gwNN types.NamespacedName, condition metav1.Condition) { t.Helper() @@ -523,6 +557,53 @@ func parentRefToString(p v1beta1.ParentReference) string { return string(p.Name) } +// GatewayAndTLSRoutesMustBeAccepted waits until the specified Gateway has an IP +// address assigned to it and the TLSRoute has a ParentRef referring to the +// Gateway. The test will fail if these conditions are not met before the +// timeouts. +func GatewayAndTLSRoutesMustBeAccepted(t *testing.T, c client.Client, timeoutConfig config.TimeoutConfig, controllerName string, gw GatewayRef, routeNNs ...types.NamespacedName) (string, []v1beta1.Hostname) { + t.Helper() + + var hostnames []v1beta1.Hostname + + gwAddr, err := WaitForGatewayAddress(t, c, timeoutConfig, gw.NamespacedName) + require.NoErrorf(t, err, "timed out waiting for Gateway address to be assigned") + + ns := v1beta1.Namespace(gw.Namespace) + kind := v1beta1.Kind("Gateway") + + for _, routeNN := range routeNNs { + namespaceRequired := true + if routeNN.Namespace == gw.Namespace { + namespaceRequired = false + } + + var parents []v1beta1.RouteParentStatus + for _, listener := range gw.listenerNames { + parents = append(parents, v1beta1.RouteParentStatus{ + ParentRef: v1beta1.ParentReference{ + Group: (*v1beta1.Group)(&v1beta1.GroupVersion.Group), + Kind: &kind, + Name: v1beta1.ObjectName(gw.Name), + Namespace: &ns, + SectionName: listener, + }, + ControllerName: v1beta1.GatewayController(controllerName), + Conditions: []metav1.Condition{ + { + Type: string(v1beta1.RouteConditionAccepted), + Status: metav1.ConditionTrue, + Reason: string(v1beta1.RouteReasonAccepted), + }, + }, + }) + } + hostnames = TLSRouteInfo(t, c, timeoutConfig, routeNN, parents, namespaceRequired) + } + + return gwAddr, hostnames +} + // TODO(mikemorris): this and parentsMatch could possibly be rewritten as a generic function? func listenersMatch(t *testing.T, expected, actual []v1beta1.ListenerStatus) bool { t.Helper() @@ -558,6 +639,8 @@ func listenersMatch(t *testing.T, expected, actual []v1beta1.ListenerStatus) boo } func conditionsMatch(t *testing.T, expected, actual []metav1.Condition) bool { + t.Helper() + if len(actual) < len(expected) { t.Logf("Expected more conditions to be present") return false @@ -575,6 +658,8 @@ func conditionsMatch(t *testing.T, expected, actual []metav1.Condition) bool { // findConditionInList finds a condition in a list of Conditions, checking // the Name, Value, and Reason. If an empty reason is passed, any Reason will match. func findConditionInList(t *testing.T, conditions []metav1.Condition, condName, expectedStatus, expectedReason string) bool { + t.Helper() + for _, cond := range conditions { if cond.Type == condName { if cond.Status == metav1.ConditionStatus(expectedStatus) { @@ -589,11 +674,13 @@ func findConditionInList(t *testing.T, conditions []metav1.Condition, condName, } } - t.Logf("%s was not in conditions list", condName) + t.Logf("%s was not in conditions list [%v]", condName, conditions) return false } func findPodConditionInList(t *testing.T, conditions []v1.PodCondition, condName, condValue string) bool { + t.Helper() + for _, cond := range conditions { if cond.Type == v1.PodConditionType(condName) { if cond.Status == v1.ConditionStatus(condValue) { diff --git a/conformance/utils/roundtripper/roundtripper.go b/conformance/utils/roundtripper/roundtripper.go index a0b37371b8..295ae8b3f1 100644 --- a/conformance/utils/roundtripper/roundtripper.go +++ b/conformance/utils/roundtripper/roundtripper.go @@ -18,6 +18,8 @@ package roundtripper import ( "context" + "crypto/tls" + "crypto/x509" "encoding/json" "fmt" "io/ioutil" @@ -25,7 +27,6 @@ import ( "net/http/httputil" "net/url" "regexp" - "sigs.k8s.io/gateway-api/conformance/utils/config" ) @@ -33,6 +34,7 @@ import ( // This can be overridden with custom implementations whenever necessary. type RoundTripper interface { CaptureRoundTrip(Request) (*CapturedRequest, *CapturedResponse, error) + CaptureTLSRoundTrip(Request, []byte, []byte, string) (*CapturedRequest, *CapturedResponse, error) } // Request is the primary input for making a request. @@ -191,6 +193,106 @@ func IsRedirect(statusCode int) bool { return false } +// CaptureTLSRoundTrip makes a request with the provided parameters and returns the +// captured request and response from echoserver. An error will be returned if +// there is an error running the function but not if an HTTP error status code +// is received. +func (d *DefaultRoundTripper) CaptureTLSRoundTrip(request Request, cPem, kPem []byte, server string) (*CapturedRequest, *CapturedResponse, error) { + cReq := &CapturedRequest{} + client := http.DefaultClient + + // Create a certificate from the provided cert and key + cert, err := tls.X509KeyPair(cPem, kPem) + if err != nil { + return nil, nil, fmt.Errorf("unexpected error creating cert: %w", err) + } + + // Add the provided cert as a trusted CA + certPool := x509.NewCertPool() + if !certPool.AppendCertsFromPEM(cPem) { + return nil, nil, fmt.Errorf("unexpected error adding trusted CA: %w", err) + } + + if server == "" { + return nil, nil, fmt.Errorf("unexpected error, server name required for TLS") + } + + // Create the Transport for this provided host, cert, and trusted CA + client.Transport = &http.Transport{ + TLSClientConfig: &tls.Config{ + Certificates: []tls.Certificate{cert}, + ServerName: server, + RootCAs: certPool, + }, + } + + method := "GET" + if request.Method != "" { + method = request.Method + } + ctx, cancel := context.WithTimeout(context.Background(), d.TimeoutConfig.RequestTimeout) + defer cancel() + req, err := http.NewRequestWithContext(ctx, method, request.URL.String(), nil) + if err != nil { + return nil, nil, err + } + + if request.Host != "" { + req.Host = request.Host + } + + if request.Headers != nil { + for name, value := range request.Headers { + req.Header.Set(name, value[0]) + } + } + + if d.Debug { + var dump []byte + dump, err = httputil.DumpRequestOut(req, true) + if err != nil { + return nil, nil, err + } + + fmt.Printf("Sending Request:\n%s\n\n", formatDump(dump, "< ")) + } + + resp, err := client.Do(req) + if err != nil { + return nil, nil, err + } + defer resp.Body.Close() + + if d.Debug { + var dump []byte + dump, err = httputil.DumpResponse(resp, true) + if err != nil { + return nil, nil, err + } + + fmt.Printf("Received Response:\n%s\n\n", formatDump(dump, "< ")) + } + + body, _ := ioutil.ReadAll(resp.Body) + + // we cannot assume the response is JSON + if resp.Header.Get("Content-type") == "application/json" { + err = json.Unmarshal(body, cReq) + if err != nil { + return nil, nil, fmt.Errorf("unexpected error reading response: %w", err) + } + } + + cRes := &CapturedResponse{ + StatusCode: resp.StatusCode, + ContentLength: resp.ContentLength, + Protocol: resp.Proto, + Headers: resp.Header, + } + + return cReq, cRes, nil +} + var startLineRegex = regexp.MustCompile(`(?m)^`) func formatDump(data []byte, prefix string) string { diff --git a/conformance/utils/suite/suite.go b/conformance/utils/suite/suite.go index 72f317a129..cbbec4e549 100644 --- a/conformance/utils/suite/suite.go +++ b/conformance/utils/suite/suite.go @@ -159,6 +159,8 @@ func (suite *ConformanceTestSuite) Setup(t *testing.T) { suite.Applier.MustApplyObjectsWithCleanup(t, suite.Client, suite.TimeoutConfig, []client.Object{secret}, suite.Cleanup) secret = kubernetes.MustCreateSelfSignedCertSecret(t, "gateway-conformance-infra", "tls-validity-checks-certificate", []string{"*"}) suite.Applier.MustApplyObjectsWithCleanup(t, suite.Client, suite.TimeoutConfig, []client.Object{secret}, suite.Cleanup) + secret = kubernetes.MustCreateSelfSignedCertSecret(t, "gateway-conformance-infra", "tls-passthrough-checks-certificate", []string{"abc.example.com"}) + suite.Applier.MustApplyObjectsWithCleanup(t, suite.Client, suite.TimeoutConfig, []client.Object{secret}, suite.Cleanup) t.Logf("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 new file mode 100644 index 0000000000..917a110e7e --- /dev/null +++ b/conformance/utils/tls/tls.go @@ -0,0 +1,101 @@ +/* +Copyright 2022 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 tls + +import ( + "net/url" + "strings" + "testing" + "time" + + "sigs.k8s.io/gateway-api/conformance/utils/config" + "sigs.k8s.io/gateway-api/conformance/utils/http" + "sigs.k8s.io/gateway-api/conformance/utils/roundtripper" +) + +// requiredConsecutiveSuccesses is the number of requests that must succeed in a row +// for MakeRequestAndExpectEventuallyConsistentResponse to consider the response "consistent" +// before making additional assertions on the response body. If this number is not reached within +// maxTimeToConsistency, the test will fail. +const requiredConsecutiveSuccesses = 3 + +// MakeTLSRequestAndExpectEventuallyConsistentResponse makes a request with the given parameters, +// understanding that the request may fail for some amount of time. +// +// Once the request succeeds consistently with the response having the expected status code, make +// additional assertions on the response body using the provided ExpectedResponse. +func MakeTLSRequestAndExpectEventuallyConsistentResponse(t *testing.T, r roundtripper.RoundTripper, timeoutConfig config.TimeoutConfig, gwAddr string, cPem, kPem []byte, server string, expected http.ExpectedResponse) { + t.Helper() + + protocol := "HTTPS" + scheme := "https" + + if expected.Request.Method == "" { + expected.Request.Method = "GET" + } + + if expected.Response.StatusCode == 0 { + expected.Response.StatusCode = 200 + } + + t.Logf("Making %s request to %s://%s%s", expected.Request.Method, scheme, gwAddr, expected.Request.Path) + + path, query, _ := strings.Cut(expected.Request.Path, "?") + + req := roundtripper.Request{ + Method: expected.Request.Method, + Host: expected.Request.Host, + URL: url.URL{Scheme: scheme, Host: gwAddr, Path: path, RawQuery: query}, + Protocol: protocol, + Headers: map[string][]string{}, + } + + if expected.Request.Headers != nil { + for name, value := range expected.Request.Headers { + req.Headers[name] = []string{value} + } + } + + backendSetHeaders := []string{} + for name, val := range expected.BackendSetResponseHeaders { + backendSetHeaders = append(backendSetHeaders, name+":"+val) + } + req.Headers["X-Echo-Set-Header"] = []string{strings.Join(backendSetHeaders, ",")} + + WaitForConsistentTLSResponse(t, r, req, expected, requiredConsecutiveSuccesses, timeoutConfig.MaxTimeToConsistency, cPem, kPem, server) +} + +// WaitForConsistentTLSResponse - repeats the provided request until it completes with a response having +// the expected response consistently. The provided threshold determines how many times in +// a row this must occur to be considered "consistent". +func WaitForConsistentTLSResponse(t *testing.T, r roundtripper.RoundTripper, req roundtripper.Request, expected http.ExpectedResponse, threshold int, maxTimeToConsistency time.Duration, cPem, kPem []byte, server string) { + http.AwaitConvergence(t, threshold, maxTimeToConsistency, func(elapsed time.Duration) bool { + cReq, cRes, err := r.CaptureTLSRoundTrip(req, cPem, kPem, server) + if err != nil { + t.Logf("Request failed, not ready yet: %v (after %v)", err.Error(), elapsed) + return false + } + + if err := http.CompareRequest(cReq, cRes, expected); err != nil { + t.Logf("Response expectation failed for request: %v not ready yet: %v (after %v)", req, err, elapsed) + return false + } + + return true + }) + t.Logf("Request passed") +} diff --git a/site-src/concepts/conformance.md b/site-src/concepts/conformance.md index 5d4ca78a39..e53fc21437 100644 --- a/site-src/concepts/conformance.md +++ b/site-src/concepts/conformance.md @@ -76,13 +76,21 @@ capabilities. By default, conformance tests will expect a `gateway-conformance` GatewayClass to be installed in the cluster and tests will be run against that. A different -class can be specified with the `--gateway-class` flag along with the -corresponding test command. For example: +class can be specified with the `-gateway-class` flag along with the +corresponding test command. For example, to run a conformance test against Istio, +run: ```shell -go test ./conformance --gateway-class my-class +go test ./conformance/... -args -gateway-class=istio ``` +Other useful flags may be found in +[conformance flags](https://github.com/kubernetes-sigs/gateway-api/blob/main/conformance/utils/flags/flags.go). +For example, if you'd like to examine the objects in Kubernetes after your test runs, you can pass a flag to +suppress cleanup: +```shell +go test ./conformance/... -args -gateway-class=istio -cleanup-base-resources=false +``` ## Contributing to Conformance Many implementations run conformance tests as part of their full e2e test suite.