diff --git a/conformance/tests/tcproute-invalid-reference-grant.go b/conformance/tests/tcproute-invalid-reference-grant.go new file mode 100644 index 0000000000..e97206bfd7 --- /dev/null +++ b/conformance/tests/tcproute-invalid-reference-grant.go @@ -0,0 +1,60 @@ +/* +Copyright 2023 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" + + "sigs.k8s.io/gateway-api/apis/v1beta1" + "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, TCPRouteInvalidReferenceGrant) +} + +var TCPRouteInvalidReferenceGrant = suite.ConformanceTest{ + ShortName: "TCPRouteInvalidReferenceGrant", + Description: "A single TCPRoute in the gateway-conformance-infra namespace with a backendRef in another namespace without valid ReferenceGrant, should have the ResolvedRefs condition set to False", + Features: []features.FeatureName{ + features.SupportGateway, + features.SupportTCPRoute, + features.SupportReferenceGrant, + }, + Manifests: []string{"tests/tcproute-invalid-reference-grant.yaml"}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + routeNN := types.NamespacedName{Name: "gateway-conformance-infra-test", Namespace: "gateway-conformance-infra"} + gwNN := types.NamespacedName{Name: "gateway-tcproute-referencegrant", Namespace: "gateway-conformance-infra"} + + kubernetes.GatewayAndTCPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN) + + t.Run("TCPRoute with BackendRef to a Service in another namespace and no ReferenceGrant has a ResolvedRefs Condition with status False and Reason RefNotPermitted", func(t *testing.T) { + resolvedRefsCond := metav1.Condition{ + Type: string(v1beta1.RouteConditionResolvedRefs), + Status: metav1.ConditionFalse, + Reason: string(v1beta1.RouteReasonRefNotPermitted), + } + + kubernetes.TCPRouteMustHaveCondition(t, suite.Client, suite.TimeoutConfig, routeNN, gwNN, resolvedRefsCond) + }) + }, +} diff --git a/conformance/tests/tcproute-invalid-reference-grant.yaml b/conformance/tests/tcproute-invalid-reference-grant.yaml new file mode 100644 index 0000000000..bc0dd039a3 --- /dev/null +++ b/conformance/tests/tcproute-invalid-reference-grant.yaml @@ -0,0 +1,136 @@ +--- +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: ReferenceGrant +metadata: + name: reference-grant-wrong-namespace + namespace: gateway-conformance-infra +spec: + from: + - group: gateway.networking.k8s.io + kind: TCPRoute + namespace: gateway-conformance-infra + to: + - group: "" + kind: Service + name: tcp-backend +--- +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: ReferenceGrant +metadata: + name: reference-grant-wrong-from-group + namespace: gateway-conformance-app-backend +spec: + from: + - group: not-the-group-youre-looking-for + kind: TCPRoute + namespace: gateway-conformance-infra + to: + - group: "" + kind: Service + name: tcp-backend +--- +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: ReferenceGrant +metadata: + name: reference-grant-wrong-from-kind + namespace: gateway-conformance-app-backend +spec: + from: + - group: gateway.networking.k8s.io + kind: Gateway + namespace: gateway-conformance-infra + to: + - group: "" + kind: Service + name: tcp-backend +--- +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: ReferenceGrant +metadata: + name: reference-grant-wrong-from-namespace + namespace: gateway-conformance-app-backend +spec: + from: + - group: gateway.networking.k8s.io + kind: TCPRoute + namespace: not-the-namespace-youre-looking-for + to: + - group: "" + kind: Service + name: tcp-backend +--- +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: ReferenceGrant +metadata: + name: reference-grant-wrong-to-group + namespace: gateway-conformance-app-backend +spec: + from: + - group: gateway.networking.k8s.io + kind: TCPRoute + namespace: gateway-conformance-infra + to: + - group: not-the-group-youre-looking-for + kind: Service + name: tcp-backend +--- +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: ReferenceGrant +metadata: + name: reference-grant-wrong-to-kind + namespace: gateway-conformance-app-backend +spec: + from: + - group: gateway.networking.k8s.io + kind: TCPRoute + namespace: gateway-conformance-infra + to: + - group: "" + kind: Secret + name: tcp-backend +--- +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: ReferenceGrant +metadata: + name: reference-grant-wrong-to-name + namespace: gateway-conformance-app-backend +spec: + from: + - group: gateway.networking.k8s.io + kind: TCPRoute + namespace: gateway-conformance-infra + to: + - group: "" + kind: Service + name: not-the-service-youre-looking-for +--- +apiVersion: gateway.networking.k8s.io/v1alpha2 +kind: TCPRoute +metadata: + name: gateway-conformance-infra-test + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: gateway-tcproute-referencegrant + rules: + - backendRefs: + - name: tcp-backend + namespace: gateway-conformance-app-backend + port: 1234 +--- +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: Gateway +metadata: + name: gateway-tcproute-referencegrant + namespace: gateway-conformance-infra +spec: + gatewayClassName: "{GATEWAY_CLASS_NAME}" + listeners: + - name: tcp + port: 1234 + protocol: TCP + allowedRoutes: + namespaces: + from: Same + kinds: + - kind: TCPRoute diff --git a/conformance/utils/config/timeout.go b/conformance/utils/config/timeout.go index 9a7c1626da..1a90b5a1fa 100644 --- a/conformance/utils/config/timeout.go +++ b/conformance/utils/config/timeout.go @@ -68,6 +68,10 @@ type TimeoutConfig struct { // Max value for conformant implementation: None TLSRouteMustHaveCondition time.Duration + // TCPRouteMustHaveCondition represents the maximum time for a TCPRoute to have the supplied Condition. + // Max value for conformant implementation: None + TCPRouteMustHaveCondition time.Duration + // RouteMustHaveParents represents the maximum time for an xRoute to have parents in status that match the expected parents. // Max value for conformant implementation: None RouteMustHaveParents time.Duration @@ -118,6 +122,7 @@ func DefaultTimeoutConfig() TimeoutConfig { HTTPRouteMustNotHaveParents: 60 * time.Second, HTTPRouteMustHaveCondition: 60 * time.Second, TLSRouteMustHaveCondition: 60 * time.Second, + TCPRouteMustHaveCondition: 60 * time.Second, RouteMustHaveParents: 60 * time.Second, ManifestFetchTimeout: 10 * time.Second, MaxTimeToConsistency: 30 * time.Second, @@ -182,6 +187,9 @@ func SetupTimeoutConfig(timeoutConfig *TimeoutConfig) { if timeoutConfig.TLSRouteMustHaveCondition == 0 { timeoutConfig.TLSRouteMustHaveCondition = defaultTimeoutConfig.TLSRouteMustHaveCondition } + if timeoutConfig.TCPRouteMustHaveCondition == 0 { + timeoutConfig.TCPRouteMustHaveCondition = defaultTimeoutConfig.TCPRouteMustHaveCondition + } if timeoutConfig.DefaultTestTimeout == 0 { timeoutConfig.DefaultTestTimeout = defaultTimeoutConfig.DefaultTestTimeout } diff --git a/conformance/utils/kubernetes/helpers.go b/conformance/utils/kubernetes/helpers.go index 2b43828c09..3434b268b5 100644 --- a/conformance/utils/kubernetes/helpers.go +++ b/conformance/utils/kubernetes/helpers.go @@ -651,6 +651,30 @@ func TLSRouteMustHaveParents(t *testing.T, client client.Client, timeoutConfig c return route } +// TCPRouteMustHaveParents waits for the specified TCPRoute to have parents +// in status that match the expected parents, and also returns the TCPRoute. +// This will cause the test to halt if the specified timeout is exceeded. +func TCPRouteMustHaveParents(t *testing.T, client client.Client, timeoutConfig config.TimeoutConfig, routeName types.NamespacedName, parents []v1alpha2.RouteParentStatus, namespaceRequired bool) v1alpha2.TCPRoute { + t.Helper() + + var actual []gatewayv1.RouteParentStatus + var route v1alpha2.TCPRoute + + waitErr := wait.PollUntilContextTimeout(context.Background(), 1*time.Second, timeoutConfig.RouteMustHaveParents, true, func(ctx context.Context) (bool, error) { + err := client.Get(ctx, routeName, &route) + if err != nil { + return false, fmt.Errorf("error fetching TCPRoute: %w", err) + } + actual = route.Status.Parents + match := parentsForRouteMatch(t, routeName, parents, actual, namespaceRequired) + + return match, nil + }) + require.NoErrorf(t, waitErr, "error waiting for TCPRoute to have parents matching expectations") + + return route +} + func parentsForRouteMatch(t *testing.T, routeName types.NamespacedName, expected, actual []gatewayv1.RouteParentStatus, namespaceRequired bool) bool { t.Helper() @@ -853,6 +877,86 @@ func TLSRouteMustHaveCondition(t *testing.T, client client.Client, timeoutConfig require.NoErrorf(t, waitErr, "error waiting for TLSRoute status to have a Condition matching expectations") } +// GatewayAndTCPRoutesMustBeAccepted waits until the specified Gateway has an IP +// address assigned to it and the TCPRoute has a ParentRef referring to the +// Gateway. The test will fail if these conditions are not met before the +// timeouts. +func GatewayAndTCPRoutesMustBeAccepted(t *testing.T, c client.Client, timeoutConfig config.TimeoutConfig, controllerName string, gw GatewayRef, routeNNs ...types.NamespacedName) string { + t.Helper() + + 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) + kind := gatewayv1.Kind("Gateway") + + for _, routeNN := range routeNNs { + namespaceRequired := true + if routeNN.Namespace == gw.Namespace { + namespaceRequired = false + } + + var parents []gatewayv1.RouteParentStatus + for _, listener := range gw.listenerNames { + parents = append(parents, gatewayv1.RouteParentStatus{ + ParentRef: gatewayv1.ParentReference{ + Group: (*gatewayv1.Group)(&gatewayv1.GroupVersion.Group), + Kind: &kind, + Name: gatewayv1.ObjectName(gw.Name), + Namespace: &ns, + SectionName: listener, + }, + ControllerName: gatewayv1.GatewayController(controllerName), + Conditions: []metav1.Condition{ + { + Type: string(gatewayv1.RouteConditionAccepted), + Status: metav1.ConditionTrue, + Reason: string(gatewayv1.RouteReasonAccepted), + }, + }, + }) + } + _ = TCPRouteMustHaveParents(t, c, timeoutConfig, routeNN, parents, namespaceRequired) + } + + return gwAddr +} + +// TCPRouteMustHaveCondition checks that the supplied TCPRoute has the supplied Condition, +// halting after the specified timeout is exceeded. +func TCPRouteMustHaveCondition(t *testing.T, client client.Client, timeoutConfig config.TimeoutConfig, routeNN types.NamespacedName, gwNN types.NamespacedName, condition metav1.Condition) { + t.Helper() + + waitErr := wait.PollUntilContextTimeout(context.Background(), 1*time.Second, timeoutConfig.TCPRouteMustHaveCondition, true, func(ctx context.Context) (bool, error) { + route := &v1alpha2.TCPRoute{} + err := client.Get(ctx, routeNN, route) + if err != nil { + return false, fmt.Errorf("error fetching TCPRoute: %w", err) + } + + parents := route.Status.Parents + var conditionFound bool + for _, parent := range parents { + if err := ConditionsHaveLatestObservedGeneration(route, parent.Conditions); err != nil { + tlog.Logf(t, "TCPRoute %s (parentRef=%v) %v", + routeNN, parentRefToString(parent.ParentRef), err, + ) + return false, nil + } + + if parent.ParentRef.Name == gatewayv1.ObjectName(gwNN.Name) && (parent.ParentRef.Namespace == nil || string(*parent.ParentRef.Namespace) == gwNN.Namespace) { + if findConditionInList(t, parent.Conditions, condition.Type, string(condition.Status), condition.Reason) { + conditionFound = true + } + } + } + + return conditionFound, nil + }) + + require.NoErrorf(t, waitErr, "error waiting for TCPRoute status to have a Condition matching expectations") +} + // TODO(mikemorris): this and parentsMatch could possibly be rewritten as a generic function? func listenersMatch(t *testing.T, expected, actual []gatewayv1.ListenerStatus) bool { t.Helper() diff --git a/conformance/utils/suite/profiles.go b/conformance/utils/suite/profiles.go index 2f589ed4eb..493f231c07 100644 --- a/conformance/utils/suite/profiles.go +++ b/conformance/utils/suite/profiles.go @@ -54,6 +54,10 @@ const ( // which covers GRPC functionality with Gateways. GatewayGRPCConformanceProfileName ConformanceProfileName = "GATEWAY-GRPC" + // GatewayTCPConformanceProfileName indicates the name of the conformance profile + // which covers TCP functionality with Gateways. + GatewayTCPConformanceProfileName ConformanceProfileName = "GATEWAY-TCP" + // MeshHTTPConformanceProfileName indicates the name of the conformance profile // which covers HTTP functionality with service mesh. MeshHTTPConformanceProfileName ConformanceProfileName = "MESH-HTTP" @@ -108,6 +112,18 @@ var ( ExtendedFeatures: features.SetsToNamesSet(features.GatewayExtendedFeatures), } + // GatewayTCPConformanceProfile is a ConformanceProfile that covers testing TCP + // related functionality with Gateways. + GatewayTCPConformanceProfile = ConformanceProfile{ + Name: GatewayTCPConformanceProfileName, + CoreFeatures: sets.New( + features.SupportGateway, + features.SupportReferenceGrant, + features.SupportTCPRoute, + ), + ExtendedFeatures: features.SetsToNamesSet(features.GatewayExtendedFeatures), + } + // MeshHTTPConformanceProfile is a ConformanceProfile that covers testing HTTP // service mesh related functionality. MeshHTTPConformanceProfile = ConformanceProfile{ @@ -155,6 +171,7 @@ var conformanceProfileMap = map[ConformanceProfileName]ConformanceProfile{ GatewayHTTPConformanceProfileName: GatewayHTTPConformanceProfile, GatewayTLSConformanceProfileName: GatewayTLSConformanceProfile, GatewayGRPCConformanceProfileName: GatewayGRPCConformanceProfile, + GatewayTCPConformanceProfileName: GatewayTCPConformanceProfile, MeshHTTPConformanceProfileName: MeshHTTPConformanceProfile, MeshGRPCConformanceProfileName: MeshGRPCConformanceProfile, } diff --git a/pkg/features/features.go b/pkg/features/features.go index 8406c4b891..63670393a2 100644 --- a/pkg/features/features.go +++ b/pkg/features/features.go @@ -58,6 +58,7 @@ var ( Insert(HTTPRouteCoreFeatures.UnsortedList()...). Insert(HTTPRouteExtendedFeatures.UnsortedList()...). Insert(TLSRouteCoreFeatures.UnsortedList()...). + Insert(TCPRouteCoreFeatures.UnsortedList()...). Insert(MeshCoreFeatures.UnsortedList()...). Insert(MeshExtendedFeatures.UnsortedList()...). Insert(GRPCRouteCoreFeatures.UnsortedList()...) diff --git a/pkg/features/tcproute.go b/pkg/features/tcproute.go new file mode 100644 index 0000000000..a96a9623e2 --- /dev/null +++ b/pkg/features/tcproute.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 - TCPRoute Conformance (Core) +// ----------------------------------------------------------------------------- + +const ( + // This option indicates support for TCPRoute + SupportTCPRoute FeatureName = "TCPRoute" +) + +// TCPRouteFeature contains metadata for the TCPRoute feature. +var TCPRouteFeature = Feature{ + Name: SupportTCPRoute, + Channel: FeatureChannelExperimental, +} + +// TCPRouteCoreFeatures includes all the supported features for the TCPRoute API at +// a Core level of support. +var TCPRouteCoreFeatures = sets.New( + TCPRouteFeature, +)