diff --git a/changelog/v1.18.0-beta34/issue_10304.yaml b/changelog/v1.18.0-beta34/issue_10304.yaml new file mode 100644 index 00000000000..8e9c77676fe --- /dev/null +++ b/changelog/v1.18.0-beta34/issue_10304.yaml @@ -0,0 +1,6 @@ +changelog: + - type: FIX + issueLink: https://github.com/solo-io/gloo/issues/10304 + resolvesIssue: false + description: >- + Makes the Gateway API TCPRoute controller optional. diff --git a/pkg/schemes/extended_scheme.go b/pkg/schemes/extended_scheme.go new file mode 100644 index 00000000000..0e4cc557d98 --- /dev/null +++ b/pkg/schemes/extended_scheme.go @@ -0,0 +1,54 @@ +package schemes + +import ( + "fmt" + + "github.com/solo-io/gloo/projects/gateway2/wellknown" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/discovery" + "k8s.io/client-go/rest" + + gwv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" +) + +// AddGatewayV1A2Scheme adds the gwv1a2 scheme to the provided scheme if the TCPRoute CRD exists. +func AddGatewayV1A2Scheme(restConfig *rest.Config, scheme *runtime.Scheme) error { + exists, err := CRDExists(restConfig, gwv1a2.GroupVersion.Group, gwv1a2.GroupVersion.Version, wellknown.TCPRouteKind) + if err != nil { + return fmt.Errorf("error checking if %s CRD exists: %w", wellknown.TCPRouteKind, err) + } + + if exists { + if err := gwv1a2.Install(scheme); err != nil { + return fmt.Errorf("error adding Gateway API v1alpha2 to scheme: %w", err) + } + } + + return nil +} + +// Helper function to check if a CRD exists +func CRDExists(restConfig *rest.Config, group, version, kind string) (bool, error) { + discoveryClient, err := discovery.NewDiscoveryClientForConfig(restConfig) + if err != nil { + return false, err + } + + groupVersion := fmt.Sprintf("%s/%s", group, version) + apiResourceList, err := discoveryClient.ServerResourcesForGroupVersion(groupVersion) + if err != nil { + if discovery.IsGroupDiscoveryFailedError(err) || meta.IsNoMatchError(err) { + return false, nil + } + return false, err + } + + for _, apiResource := range apiResourceList.APIResources { + if apiResource.Kind == kind { + return true, nil + } + } + + return false, nil +} diff --git a/pkg/schemes/scheme.go b/pkg/schemes/scheme.go index 7103b45196c..83dc364f559 100644 --- a/pkg/schemes/scheme.go +++ b/pkg/schemes/scheme.go @@ -1,6 +1,8 @@ package schemes import ( + "fmt" + appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" discoveryv1 "k8s.io/api/discovery/v1" @@ -23,7 +25,6 @@ import ( var SchemeBuilder = runtime.SchemeBuilder{ // K8s Gateway API resources gwv1.Install, - gwv1a2.Install, gwv1b1.Install, // Kubernetes Core resources @@ -62,3 +63,12 @@ func DefaultScheme() *runtime.Scheme { _ = AddToScheme(s) return s } + +// TestingScheme unconditionally includes the default scheme and the gwv1a2 scheme (which includes TCPRoute). +func TestingScheme() *runtime.Scheme { + s := DefaultScheme() + if err := gwv1a2.Install(s); err != nil { + panic(fmt.Sprintf("Failed to install gwv1a2 scheme: %v", err)) + } + return s +} diff --git a/projects/gateway2/controller/controller.go b/projects/gateway2/controller/controller.go index d11a2429e8e..d9d55a03373 100644 --- a/projects/gateway2/controller/controller.go +++ b/projects/gateway2/controller/controller.go @@ -2,6 +2,7 @@ package controller import ( "context" + "errors" "fmt" corev1 "k8s.io/api/core/v1" @@ -56,6 +57,8 @@ type GatewayConfig struct { Aws *deployer.AwsInfo Extensions extensions.K8sGatewayExtensions + // CRDs defines the set of discovered Gateway API CRDs + CRDs sets.Set[string] } func NewBaseGatewayController(ctx context.Context, cfg GatewayConfig) error { @@ -114,9 +117,26 @@ type controllerBuilder struct { } func (c *controllerBuilder) addIndexes(ctx context.Context) error { - return query.IterateIndices(func(obj client.Object, field string, indexer client.IndexerFunc) error { - return c.cfg.Mgr.GetFieldIndexer().IndexField(ctx, obj, field, indexer) - }) + var errs []error + + // Index for HTTPRoute + if err := c.cfg.Mgr.GetFieldIndexer().IndexField(ctx, &apiv1.HTTPRoute{}, query.HttpRouteTargetField, query.IndexerByObjType); err != nil { + errs = append(errs, err) + } + + // Index for ReferenceGrant + if err := c.cfg.Mgr.GetFieldIndexer().IndexField(ctx, &apiv1beta1.ReferenceGrant{}, query.ReferenceGrantFromField, query.IndexerByObjType); err != nil { + errs = append(errs, err) + } + + // Conditionally index for TCPRoute + if c.cfg.CRDs.Has(wellknown.TCPRouteCRD) { + if err := c.cfg.Mgr.GetFieldIndexer().IndexField(ctx, &apiv1a2.TCPRoute{}, query.TcpRouteTargetField, query.IndexerByObjType); err != nil { + errs = append(errs, err) + } + } + + return errors.Join(errs...) } func (c *controllerBuilder) addGwParamsIndexes(ctx context.Context) error { @@ -231,7 +251,7 @@ func (c *controllerBuilder) watchGw(ctx context.Context) error { return fmt.Errorf("object %T is not a client.Object", obj) } log.Info("watching gvk as gateway child", "gvk", gvk) - // unless its a service, we don't care about the status + // unless it's a service, we don't care about the status var opts []builder.OwnsOption if shouldIgnoreStatusChild(gvk) { opts = append(opts, builder.WithPredicates(predicate.GenerationChangedPredicate{})) @@ -282,7 +302,7 @@ func (c *controllerBuilder) watchCustomResourceDefinitions(_ context.Context) er return false } // Check if the CRD is one we care about - return wellknown.GatewayCRDs.Has(crd.Name) + return c.cfg.CRDs.Has(crd.Name) }), )). For(&apiextensionsv1.CustomResourceDefinition{}). @@ -296,7 +316,13 @@ func (c *controllerBuilder) watchHttpRoute(_ context.Context) error { Complete(reconcile.Func(c.reconciler.ReconcileHttpRoutes)) } -func (c *controllerBuilder) watchTcpRoute(_ context.Context) error { +func (c *controllerBuilder) watchTcpRoute(ctx context.Context) error { + if !c.cfg.CRDs.Has(wellknown.TCPRouteCRD) { + log.FromContext(ctx).Info("TCPRoute type not registered in scheme; skipping TCPRoute controller setup") + return nil + } + + // Proceed to set up the controller for TCPRoute return ctrl.NewControllerManagedBy(c.cfg.Mgr). WithEventFilter(predicate.GenerationChangedPredicate{}). For(&apiv1a2.TCPRoute{}). diff --git a/projects/gateway2/controller/start.go b/projects/gateway2/controller/start.go index a580a4d5138..787ee182c07 100644 --- a/projects/gateway2/controller/start.go +++ b/projects/gateway2/controller/start.go @@ -16,6 +16,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/log/zap" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + gwv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" gatewaykubev1 "github.com/solo-io/gloo/projects/gateway/pkg/api/v1/kube/apis/gateway.solo.io/v1" "github.com/solo-io/gloo/projects/gateway2/deployer" @@ -98,9 +99,16 @@ func NewControllerBuilder(ctx context.Context, cfg StartConfig) (*ControllerBuil } ctrl.SetLogger(zap.New(opts...)) + scheme := glooschemes.DefaultScheme() + + // Extend the scheme if the TCPRoute CRD exists. + if err := glooschemes.AddGatewayV1A2Scheme(cfg.RestConfig, scheme); err != nil { + return nil, err + } + mgrOpts := ctrl.Options{ BaseContext: func() context.Context { return ctx }, - Scheme: glooschemes.DefaultScheme(), + Scheme: scheme, PprofBindAddress: "127.0.0.1:9099", // if you change the port here, also change the port "health" in the helmchart. HealthProbeBindAddress: ":9093", @@ -227,6 +235,12 @@ func (c *ControllerBuilder) Start(ctx context.Context) error { } } + // Initialize the set of Gateway API CRDs we care about + crds, err := getGatewayCRDs(c.cfg.RestConfig) + if err != nil { + return err + } + gwCfg := GatewayConfig{ Mgr: c.mgr, GWClasses: sets.New(append(c.cfg.SetupOpts.ExtraGatewayClasses, wellknown.GatewayClassName)...), @@ -241,6 +255,7 @@ func (c *ControllerBuilder) Start(ctx context.Context) error { Aws: awsInfo, Kick: c.inputChannels.Kick, Extensions: c.k8sGwExtensions, + CRDs: crds, } if err := NewBaseGatewayController(ctx, gwCfg); err != nil { setupLog.Error(err, "unable to create controller") @@ -249,3 +264,18 @@ func (c *ControllerBuilder) Start(ctx context.Context) error { return c.mgr.Start(ctx) } + +func getGatewayCRDs(restConfig *rest.Config) (sets.Set[string], error) { + crds := wellknown.GatewayStandardCRDs + + tcpRouteExists, err := glooschemes.CRDExists(restConfig, gwv1a2.GroupVersion.Group, gwv1a2.GroupVersion.Version, wellknown.TCPRouteKind) + if err != nil { + return nil, err + } + + if tcpRouteExists { + crds.Insert(wellknown.TCPRouteCRD) + } + + return crds, nil +} diff --git a/projects/gateway2/crds/tcproute-crd.yaml b/projects/gateway2/crds/tcproute-crd.yaml new file mode 100644 index 00000000000..141e0cbd446 --- /dev/null +++ b/projects/gateway2/crds/tcproute-crd.yaml @@ -0,0 +1,825 @@ +# Used for e2e testing. +# Bump when TCPRoute in gateway-crds.yaml is updated. +# config/crd/experimental/gateway.networking.k8s.io_tcproutes.yaml +# +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + api-approved.kubernetes.io: https://github.com/kubernetes-sigs/gateway-api/pull/2997 + gateway.networking.k8s.io/bundle-version: v1.1.0 + gateway.networking.k8s.io/channel: experimental + creationTimestamp: null + name: tcproutes.gateway.networking.k8s.io +spec: + group: gateway.networking.k8s.io + names: + categories: + - gateway-api + kind: TCPRoute + listKind: TCPRouteList + plural: tcproutes + singular: tcproute + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha2 + schema: + openAPIV3Schema: + description: |- + TCPRoute provides a way to route TCP requests. When combined with a Gateway + listener, it can be used to forward connections on the port specified by the + listener to a set of backends specified by the TCPRoute. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of TCPRoute. + properties: + parentRefs: + description: |+ + ParentRefs references the resources (usually Gateways) that a Route wants + to be attached to. Note that the referenced parent resource needs to + allow this for the attachment to be complete. For Gateways, that means + the Gateway needs to allow attachment from Routes of this kind and + namespace. For Services, that means the Service must either be in the same + namespace for a "producer" route, or the mesh implementation must support + and allow "consumer" routes for the referenced Service. ReferenceGrant is + not applicable for governing ParentRefs to Services - it is not possible to + create a "producer" route for a Service in a different namespace from the + Route. + + + There are two kinds of parent resources with "Core" support: + + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + + This API may be extended in the future to support additional kinds of parent + resources. + + + ParentRefs must be _distinct_. This means either that: + + + * They select different objects. If this is the case, then parentRef + entries are distinct. In terms of fields, this means that the + multi-part key defined by `group`, `kind`, `namespace`, and `name` must + be unique across all parentRef entries in the Route. + * They do not select different objects, but for each optional field used, + each ParentRef that selects the same object must set the same set of + optional fields to different values. If one ParentRef sets a + combination of optional fields, all must set the same combination. + + + Some examples: + + + * If one ParentRef sets `sectionName`, all ParentRefs referencing the + same object must also set `sectionName`. + * If one ParentRef sets `port`, all ParentRefs referencing the same + object must also set `port`. + * If one ParentRef sets `sectionName` and `port`, all ParentRefs + referencing the same object must also set `sectionName` and `port`. + + + It is possible to separately reference multiple distinct objects that may + be collapsed by an implementation. For example, some implementations may + choose to merge compatible Gateway Listeners together. If that is the + case, the list of routes attached to those resources should also be + merged. + + + Note that for ParentRefs that cross namespace boundaries, there are specific + rules. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example, + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable other kinds of cross-namespace reference. + + + + ParentRefs from a Route to a Service in the same namespace are "producer" + routes, which apply default routing rules to inbound connections from + any namespace to the Service. + + + ParentRefs from a Route to a Service in a different namespace are + "consumer" routes, and these routing rules are only applied to outbound + connections originating from the same namespace as the Route, for which + the intended destination of the connections are a Service targeted as a + ParentRef of the Route. + + + + + + + items: + description: |- + ParentReference identifies an API object (usually a Gateway) that can be considered + a parent of this resource (usually a route). There are two kinds of parent resources + with "Core" support: + + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + + This API may be extended in the future to support additional kinds of parent + resources. + + + The API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid. + properties: + group: + default: gateway.networking.k8s.io + description: |- + Group is the group of the referent. + When unspecified, "gateway.networking.k8s.io" is inferred. + To set the core API group (such as for a "Service" kind referent), + Group must be explicitly set to "" (empty string). + + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: |- + Kind is kind of the referent. + + + There are two kinds of parent resources with "Core" support: + + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + + Support for other resources is Implementation-Specific. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name is the name of the referent. + + + Support: Core + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. When unspecified, this refers + to the local namespace of the Route. + + + Note that there are specific rules for ParentRefs which cross namespace + boundaries. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example: + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable any other kind of cross-namespace reference. + + + + ParentRefs from a Route to a Service in the same namespace are "producer" + routes, which apply default routing rules to inbound connections from + any namespace to the Service. + + + ParentRefs from a Route to a Service in a different namespace are + "consumer" routes, and these routing rules are only applied to outbound + connections originating from the same namespace as the Route, for which + the intended destination of the connections are a Service targeted as a + ParentRef of the Route. + + + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port is the network port this Route targets. It can be interpreted + differently based on the type of parent resource. + + + When the parent resource is a Gateway, this targets all listeners + listening on the specified port that also support this kind of Route(and + select this Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to a specific port + as opposed to a listener(s) whose port(s) may be changed. When both Port + and SectionName are specified, the name and port of the selected listener + must match both specified values. + + + + When the parent resource is a Service, this targets a specific port in the + Service spec. When both Port (experimental) and SectionName are specified, + the name and port of the selected port must match both specified values. + + + + Implementations MAY choose to support other parent resources. + Implementations supporting other types of parent resources MUST clearly + document how/if Port is interpreted. + + + For the purpose of status, an attachment is considered successful as + long as the parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: |- + SectionName is the name of a section within the target resource. In the + following resources, SectionName is interpreted as the following: + + + * Gateway: Listener name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + * Service: Port name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + + + Implementations MAY choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName is + interpreted. + + + When unspecified (empty string), this will reference the entire resource. + For the purpose of status, an attachment is considered successful if at + least one section in the parent resource accepts it. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from + the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, the + Route MUST be considered detached from the Gateway. + + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + maxItems: 32 + type: array + x-kubernetes-validations: + - message: sectionName or port must be specified when parentRefs includes + 2 or more references to the same parent + rule: 'self.all(p1, self.all(p2, p1.group == p2.group && p1.kind + == p2.kind && p1.name == p2.name && (((!has(p1.__namespace__) + || p1.__namespace__ == '''') && (!has(p2.__namespace__) || p2.__namespace__ + == '''')) || (has(p1.__namespace__) && has(p2.__namespace__) && + p1.__namespace__ == p2.__namespace__)) ? ((!has(p1.sectionName) + || p1.sectionName == '''') == (!has(p2.sectionName) || p2.sectionName + == '''') && (!has(p1.port) || p1.port == 0) == (!has(p2.port) + || p2.port == 0)): true))' + - message: sectionName or port must be unique when parentRefs includes + 2 or more references to the same parent + rule: self.all(p1, self.exists_one(p2, p1.group == p2.group && p1.kind + == p2.kind && p1.name == p2.name && (((!has(p1.__namespace__) + || p1.__namespace__ == '') && (!has(p2.__namespace__) || p2.__namespace__ + == '')) || (has(p1.__namespace__) && has(p2.__namespace__) && + p1.__namespace__ == p2.__namespace__ )) && (((!has(p1.sectionName) + || p1.sectionName == '') && (!has(p2.sectionName) || p2.sectionName + == '')) || ( has(p1.sectionName) && has(p2.sectionName) && p1.sectionName + == p2.sectionName)) && (((!has(p1.port) || p1.port == 0) && (!has(p2.port) + || p2.port == 0)) || (has(p1.port) && has(p2.port) && p1.port + == p2.port)))) + rules: + description: Rules are a list of TCP matchers and actions. + items: + description: TCPRouteRule is the configuration for a given rule. + properties: + backendRefs: + description: |- + BackendRefs defines the backend(s) where matching requests should be + sent. If unspecified or invalid (refers to a non-existent resource or a + Service with no endpoints), the underlying implementation MUST actively + reject connection attempts to this backend. Connection rejections must + respect weight; if an invalid backend is requested to have 80% of + connections, then 80% of connections must be rejected instead. + + + Support: Core for Kubernetes Service + + + Support: Extended for Kubernetes ServiceImport + + + Support: Implementation-specific for any other resource + + + Support for weight: Extended + items: + description: |- + BackendRef defines how a Route should forward a request to a Kubernetes + resource. + + + Note that when a namespace different than the local namespace is specified, a + ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + + + + + When the BackendRef points to a Kubernetes Service, implementations SHOULD + honor the appProtocol field if it is set for the target Service Port. + + + Implementations supporting appProtocol SHOULD recognize the Kubernetes + Standard Application Protocols defined in KEP-3726. + + + If a Service appProtocol isn't specified, an implementation MAY infer the + backend protocol through its own means. Implementations MAY infer the + protocol from the Route type referring to the backend Service. + + + If a Route is not able to send traffic to the backend using the specified + protocol then the backend is considered invalid. Implementations MUST set the + "ResolvedRefs" condition to "False" with the "UnsupportedProtocol" reason. + + + + + + Note that when the BackendTLSPolicy object is enabled by the implementation, + there are some extra rules about validity to consider here. See the fields + where this struct is used for more information about the exact behavior. + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: |- + Kind is the Kubernetes resource kind of the referent. For example + "Service". + + + Defaults to "Service" when not specified. + + + ExternalName services can refer to CNAME DNS records that may live + outside of the cluster and as such are difficult to reason about in + terms of conformance. They also may not be safe to forward to (see + CVE-2021-25740 for more information). Implementations SHOULD NOT + support ExternalName Services. + + + Support: Core (Services with a type other than ExternalName) + + + Support: Implementation-specific (Services with type ExternalName) + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the backend. When unspecified, the local + namespace is inferred. + + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port specifies the destination port number to use for this resource. + Port is required when the referent is a Kubernetes Service. In this + case, the port number is the service port number, not the target port. + For other resources, destination port might be derived from the referent + resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + weight: + default: 1 + description: |- + Weight specifies the proportion of requests forwarded to the referenced + backend. This is computed as weight/(sum of all weights in this + BackendRefs list). For non-zero values, there may be some epsilon from + the exact proportion defined here depending on the precision an + implementation supports. Weight is not a percentage and the sum of + weights does not need to equal 100. + + + If only one backend is specified and it has a weight greater than 0, 100% + of the traffic is forwarded to that backend. If weight is set to 0, no + traffic should be forwarded for this entry. If unspecified, weight + defaults to 1. + + + Support for this field varies based on the context where used. + format: int32 + maximum: 1000000 + minimum: 0 + type: integer + required: + - name + type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '(size(self.group) == 0 && self.kind == ''Service'') + ? has(self.port) : true' + maxItems: 16 + minItems: 1 + type: array + type: object + maxItems: 16 + minItems: 1 + type: array + required: + - rules + type: object + status: + description: Status defines the current state of TCPRoute. + properties: + parents: + description: |- + Parents is a list of parent resources (usually Gateways) that are + associated with the route, and the status of the route with respect to + each parent. When this route attaches to a parent, the controller that + manages the parent must add an entry to this list when the controller + first sees the route and should update the entry as appropriate when the + route or gateway is modified. + + + Note that parent references that cannot be resolved by an implementation + of this API will not be added to this list. Implementations of this API + can only populate Route status for the Gateways/parent resources they are + responsible for. + + + A maximum of 32 Gateways will be represented in this list. An empty list + means the route has not been attached to any Gateway. + items: + description: |- + RouteParentStatus describes the status of a route with respect to an + associated Parent. + properties: + conditions: + description: |- + Conditions describes the status of the route with respect to the Gateway. + Note that the route's availability is also subject to the Gateway's own + status conditions and listener status. + + + If the Route's ParentRef specifies an existing Gateway that supports + Routes of this kind AND that Gateway's controller has sufficient access, + then that Gateway's controller MUST set the "Accepted" condition on the + Route, to indicate whether the route has been accepted or rejected by the + Gateway, and why. + + + A Route MUST be considered "Accepted" if at least one of the Route's + rules is implemented by the Gateway. + + + There are a number of cases where the "Accepted" condition may not be set + due to lack of controller visibility, that includes when: + + + * The Route refers to a non-existent parent. + * The Route is of a type that the controller does not support. + * The Route is in a namespace the controller does not have access to. + items: + description: "Condition contains details for one aspect of + the current state of this API Resource.\n---\nThis struct + is intended for direct use as an array at the field path + .status.conditions. For example,\n\n\n\ttype FooStatus + struct{\n\t // Represents the observations of a foo's + current state.\n\t // Known .status.conditions.type are: + \"Available\", \"Progressing\", and \"Degraded\"\n\t // + +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // + +listType=map\n\t // +listMapKey=type\n\t Conditions + []metav1.Condition `json:\"conditions,omitempty\" patchStrategy:\"merge\" + patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t + \ // other fields\n\t}" + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: |- + type of condition in CamelCase or in foo.example.com/CamelCase. + --- + Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be + useful (see .node.status.conditions), the ability to deconflict is important. + The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + controllerName: + description: |- + ControllerName is a domain/path string that indicates the name of the + controller that wrote this status. This corresponds with the + controllerName field on GatewayClass. + + + Example: "example.net/gateway-controller". + + + The format of this field is DOMAIN "/" PATH, where DOMAIN and PATH are + valid Kubernetes names + (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names). + + + Controllers MUST populate this field when writing status. Controllers should ensure that + entries to status populated with their ControllerName are cleaned up when they are no + longer necessary. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + parentRef: + description: |- + ParentRef corresponds with a ParentRef in the spec that this + RouteParentStatus struct describes the status of. + properties: + group: + default: gateway.networking.k8s.io + description: |- + Group is the group of the referent. + When unspecified, "gateway.networking.k8s.io" is inferred. + To set the core API group (such as for a "Service" kind referent), + Group must be explicitly set to "" (empty string). + + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: |- + Kind is kind of the referent. + + + There are two kinds of parent resources with "Core" support: + + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + + Support for other resources is Implementation-Specific. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name is the name of the referent. + + + Support: Core + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. When unspecified, this refers + to the local namespace of the Route. + + + Note that there are specific rules for ParentRefs which cross namespace + boundaries. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example: + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable any other kind of cross-namespace reference. + + + + ParentRefs from a Route to a Service in the same namespace are "producer" + routes, which apply default routing rules to inbound connections from + any namespace to the Service. + + + ParentRefs from a Route to a Service in a different namespace are + "consumer" routes, and these routing rules are only applied to outbound + connections originating from the same namespace as the Route, for which + the intended destination of the connections are a Service targeted as a + ParentRef of the Route. + + + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port is the network port this Route targets. It can be interpreted + differently based on the type of parent resource. + + + When the parent resource is a Gateway, this targets all listeners + listening on the specified port that also support this kind of Route(and + select this Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to a specific port + as opposed to a listener(s) whose port(s) may be changed. When both Port + and SectionName are specified, the name and port of the selected listener + must match both specified values. + + + + When the parent resource is a Service, this targets a specific port in the + Service spec. When both Port (experimental) and SectionName are specified, + the name and port of the selected port must match both specified values. + + + + Implementations MAY choose to support other parent resources. + Implementations supporting other types of parent resources MUST clearly + document how/if Port is interpreted. + + + For the purpose of status, an attachment is considered successful as + long as the parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: |- + SectionName is the name of a section within the target resource. In the + following resources, SectionName is interpreted as the following: + + + * Gateway: Listener name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + * Service: Port name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + + + Implementations MAY choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName is + interpreted. + + + When unspecified (empty string), this will reference the entire resource. + For the purpose of status, an attachment is considered successful if at + least one section in the parent resource accepts it. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from + the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, the + Route MUST be considered detached from the Gateway. + + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + required: + - controllerName + - parentRef + type: object + maxItems: 32 + type: array + required: + - parents + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: null + storedVersions: null diff --git a/projects/gateway2/deployer/deployer_test.go b/projects/gateway2/deployer/deployer_test.go index 229c9b95229..fd03fbdc4ab 100644 --- a/projects/gateway2/deployer/deployer_test.go +++ b/projects/gateway2/deployer/deployer_test.go @@ -1394,7 +1394,7 @@ var _ = Describe("Deployer", func() { // initialize a fake controller-runtime client with the given list of objects func newFakeClientWithObjs(objs ...client.Object) client.Client { return fake.NewClientBuilder(). - WithScheme(schemes.DefaultScheme()). + WithScheme(schemes.TestingScheme()). WithObjects(objs...). Build() } diff --git a/projects/gateway2/extensions/extensions.go b/projects/gateway2/extensions/extensions.go index b4caf4de25c..edda1f93b92 100644 --- a/projects/gateway2/extensions/extensions.go +++ b/projects/gateway2/extensions/extensions.go @@ -60,7 +60,6 @@ func NewK8sGatewayExtensions( queries := query.NewData( params.Mgr.GetClient(), params.Mgr.GetScheme(), - nil, ) return &k8sGatewayExtensions{ diff --git a/projects/gateway2/query/httproute.go b/projects/gateway2/query/httproute.go index e0b79f40c6b..9c787d1c297 100644 --- a/projects/gateway2/query/httproute.go +++ b/projects/gateway2/query/httproute.go @@ -5,10 +5,9 @@ import ( "fmt" "strings" - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/utils/ptr" @@ -341,22 +340,18 @@ func (r *gatewayQueries) GetRoutesForGateway(ctx context.Context, gw *gwv1.Gatew Name: gw.Name, } - // Check if the required Gateway API CRDs exist - if err := r.checkCRDs(ctx); err != nil { - return nil, err - } - - // If requiredCRDsExist is false, return an empty result - if r.requiredCRDsExist != nil && !*r.requiredCRDsExist { - return NewRoutesForGwResult(), nil - } - // List of route types to process based on installed CRDs - routeListTypes := []client.ObjectList{} + routeListTypes := []client.ObjectList{&gwv1.HTTPRouteList{}} - // Add route types based on available CRDs - // Since we have confirmed that both CRDs exist, we can proceed to add them - routeListTypes = append(routeListTypes, &gwv1.HTTPRouteList{}, &gwv1a2.TCPRouteList{}) + // Conditionally include TCPRouteList + tcpRouteGVK := schema.GroupVersionKind{ + Group: gwv1a2.GroupVersion.Group, + Version: gwv1a2.GroupVersion.Version, + Kind: wellknown.TCPRouteKind, + } + if r.scheme.Recognizes(tcpRouteGVK) { + routeListTypes = append(routeListTypes, &gwv1a2.TCPRouteList{}) + } var routes []client.Object for _, routeList := range routeListTypes { @@ -487,29 +482,6 @@ func (r *gatewayQueries) processRoute(ctx context.Context, gw *gwv1.Gateway, rou return nil } -func (r *gatewayQueries) checkCRDs(ctx context.Context) error { - if r.requiredCRDsExist != nil && *r.requiredCRDsExist { - // CRDs are already known to exist, return early - return nil - } - - for _, crdName := range wellknown.GatewayRouteCRDs { - crd := &apiextensionsv1.CustomResourceDefinition{} - err := r.client.Get(ctx, client.ObjectKey{Name: crdName}, crd) - if err != nil { - if errors.IsNotFound(err) { - r.requiredCRDsExist = ptr.To(false) - return nil - } - return fmt.Errorf("error checking for %s CRD: %w", crdName, err) - } - } - // All required CRDs are installed - r.requiredCRDsExist = ptr.To(true) - - return nil -} - // isKindAllowed is a helper function to check if a kind is allowed. func isKindAllowed(routeKind string, allowedKinds []metav1.GroupKind) bool { for _, kind := range allowedKinds { diff --git a/projects/gateway2/query/indexers.go b/projects/gateway2/query/indexers.go index c9791926ace..83d7cfb8fb5 100644 --- a/projects/gateway2/query/indexers.go +++ b/projects/gateway2/query/indexers.go @@ -21,18 +21,18 @@ const ( // IterateIndices calls the provided function for each indexable object with the appropriate indexer function. func IterateIndices(f func(client.Object, string, client.IndexerFunc) error) error { return errors.Join( - f(&gwv1.HTTPRoute{}, HttpRouteTargetField, indexerByObjType), - f(&gwv1a2.TCPRoute{}, TcpRouteTargetField, indexerByObjType), - f(&gwv1b1.ReferenceGrant{}, ReferenceGrantFromField, indexerByObjType), + f(&gwv1.HTTPRoute{}, HttpRouteTargetField, IndexerByObjType), + f(&gwv1a2.TCPRoute{}, TcpRouteTargetField, IndexerByObjType), + f(&gwv1b1.ReferenceGrant{}, ReferenceGrantFromField, IndexerByObjType), ) } -// indexerByObjType indexes objects based on the provided object type. The following object types are supported: +// IndexerByObjType indexes objects based on the provided object type. The following object types are supported: // // - HTTPRoute // - TCPRoute // - ReferenceGrant -func indexerByObjType(obj client.Object) []string { +func IndexerByObjType(obj client.Object) []string { var results []string switch resource := obj.(type) { diff --git a/projects/gateway2/query/query.go b/projects/gateway2/query/query.go index 0fb9ba338f4..c7a6ff53351 100644 --- a/projects/gateway2/query/query.go +++ b/projects/gateway2/query/query.go @@ -165,7 +165,6 @@ func WithBackendRefResolvers( func NewData( c client.Client, scheme *runtime.Scheme, - reqCRDsExist *bool, opts ...Option, ) GatewayQueries { builtOpts := &options{} @@ -175,7 +174,6 @@ func NewData( return &gatewayQueries{ client: c, scheme: scheme, - requiredCRDsExist: reqCRDsExist, customBackendResolvers: builtOpts.customBackendResolvers, } } @@ -192,8 +190,6 @@ type gatewayQueries struct { client client.Client scheme *runtime.Scheme customBackendResolvers []BackendRefResolver - // Cache whether the required Gateway API CRDs are installed. - requiredCRDsExist *bool } func (r *gatewayQueries) referenceAllowed(ctx context.Context, from metav1.GroupKind, fromns string, to metav1.GroupKind, tons, toname string) (bool, error) { diff --git a/projects/gateway2/query/query_test.go b/projects/gateway2/query/query_test.go index cede94a0246..7ca8635d60c 100644 --- a/projects/gateway2/query/query_test.go +++ b/projects/gateway2/query/query_test.go @@ -27,8 +27,6 @@ var _ = Describe("Query", func() { var ( scheme *runtime.Scheme builder *fake.ClientBuilder - // Avoids an API server call to check whether required Gateway API CRDs are installed. - reqCRDsExist = true ) tofrom := func(o client.Object) query.From { @@ -36,7 +34,7 @@ var _ = Describe("Query", func() { } BeforeEach(func() { - scheme = schemes.DefaultScheme() + scheme = schemes.TestingScheme() builder = fake.NewClientBuilder().WithScheme(scheme) err := query.IterateIndices(func(o client.Object, f string, fun client.IndexerFunc) error { builder.WithIndex(o, f, fun) @@ -48,7 +46,7 @@ var _ = Describe("Query", func() { It("should get service from same namespace", func() { fakeClient := fake.NewFakeClient(svc("default")) - gq := query.NewData(fakeClient, scheme, &reqCRDsExist) + gq := query.NewData(fakeClient, scheme) ref := &apiv1.BackendObjectReference{ Name: "foo", } @@ -63,7 +61,7 @@ var _ = Describe("Query", func() { It("should get service from different ns if we have a ref grant", func() { rg := refGrant() fakeClient := builder.WithObjects(svc("default2"), rg).Build() - gq := query.NewData(fakeClient, scheme, &reqCRDsExist) + gq := query.NewData(fakeClient, scheme) ref := &apiv1.BackendObjectReference{ Name: "foo", Namespace: nsptr("default2"), @@ -79,7 +77,7 @@ var _ = Describe("Query", func() { It("should fail with service not found if we have a ref grant", func() { rg := refGrant() fakeClient := builder.WithObjects(rg).Build() - gq := query.NewData(fakeClient, scheme, &reqCRDsExist) + gq := query.NewData(fakeClient, scheme) ref := &apiv1.BackendObjectReference{ Name: "foo", Namespace: nsptr("default2"), @@ -122,7 +120,7 @@ var _ = Describe("Query", func() { } fakeClient := builder.WithObjects(rg, svc("default2")).Build() - gq := query.NewData(fakeClient, scheme, &reqCRDsExist) + gq := query.NewData(fakeClient, scheme) backend, err := gq.GetBackendForRef(context.Background(), tofrom(httpRoute()), ref) Expect(err).To(MatchError(query.ErrMissingReferenceGrant)) Expect(backend).To(BeNil()) @@ -130,7 +128,7 @@ var _ = Describe("Query", func() { It("should fail getting a service with no ref grant", func() { fakeClient := builder.WithObjects(svc("default3")).Build() - gq := query.NewData(fakeClient, scheme, &reqCRDsExist) + gq := query.NewData(fakeClient, scheme) ref := &apiv1.BackendObjectReference{ Name: "foo", Namespace: nsptr("default3"), @@ -145,7 +143,7 @@ var _ = Describe("Query", func() { rg := refGrant() fakeClient := builder.WithObjects(svc("default3"), rg).Build() - gq := query.NewData(fakeClient, scheme, &reqCRDsExist) + gq := query.NewData(fakeClient, scheme) ref := &apiv1.BackendObjectReference{ Name: "foo", Namespace: nsptr("default3"), @@ -160,7 +158,7 @@ var _ = Describe("Query", func() { It("should get secret from different ns if we have a ref grant", func() { rg := refGrantSecret() fakeClient := builder.WithObjects(secret("default2"), rg).Build() - gq := query.NewData(fakeClient, scheme, &reqCRDsExist) + gq := query.NewData(fakeClient, scheme) ref := apiv1.SecretObjectReference{ Name: "foo", Namespace: nsptr("default2"), @@ -190,7 +188,7 @@ var _ = Describe("Query", func() { } fakeClient := builder.WithObjects(hr).Build() - gq := query.NewData(fakeClient, scheme, &reqCRDsExist) + gq := query.NewData(fakeClient, scheme) routes, err := gq.GetRoutesForGateway(context.Background(), gwWithListener) Expect(err).NotTo(HaveOccurred()) @@ -223,7 +221,7 @@ var _ = Describe("Query", func() { } fakeClient := builder.WithObjects(hr).Build() - gq := query.NewData(fakeClient, scheme, &reqCRDsExist) + gq := query.NewData(fakeClient, scheme) routes, err := gq.GetRoutesForGateway(context.Background(), gwWithListener) Expect(err).NotTo(HaveOccurred()) @@ -253,7 +251,7 @@ var _ = Describe("Query", func() { }) fakeClient := builder.WithObjects(hr).Build() - gq := query.NewData(fakeClient, scheme, &reqCRDsExist) + gq := query.NewData(fakeClient, scheme) routes, err := gq.GetRoutesForGateway(context.Background(), gwWithListener) Expect(err).NotTo(HaveOccurred()) @@ -284,7 +282,7 @@ var _ = Describe("Query", func() { }) fakeClient := builder.WithObjects(hr).Build() - gq := query.NewData(fakeClient, scheme, &reqCRDsExist) + gq := query.NewData(fakeClient, scheme) routes, err := gq.GetRoutesForGateway(context.Background(), gwWithListener) Expect(err).NotTo(HaveOccurred()) @@ -314,7 +312,7 @@ var _ = Describe("Query", func() { }) fakeClient := builder.WithObjects(hr).Build() - gq := query.NewData(fakeClient, scheme, &reqCRDsExist) + gq := query.NewData(fakeClient, scheme) routes, err := gq.GetRoutesForGateway(context.Background(), gwWithListener) Expect(err).NotTo(HaveOccurred()) @@ -347,7 +345,7 @@ var _ = Describe("Query", func() { }) fakeClient := builder.WithObjects(hr).Build() - gq := query.NewData(fakeClient, scheme, &reqCRDsExist) + gq := query.NewData(fakeClient, scheme) routes, err := gq.GetRoutesForGateway(context.Background(), gwWithListener) Expect(err).NotTo(HaveOccurred()) @@ -378,7 +376,7 @@ var _ = Describe("Query", func() { }) fakeClient := builder.WithObjects(hr).Build() - gq := query.NewData(fakeClient, scheme, &reqCRDsExist) + gq := query.NewData(fakeClient, scheme) routes, err := gq.GetRoutesForGateway(context.Background(), gwWithListener) Expect(err).NotTo(HaveOccurred()) @@ -412,7 +410,7 @@ var _ = Describe("Query", func() { }) fakeClient := builder.WithObjects(hr).Build() - gq := query.NewData(fakeClient, scheme, &reqCRDsExist) + gq := query.NewData(fakeClient, scheme) routes, err := gq.GetRoutesForGateway(context.Background(), gwWithListener) Expect(err).NotTo(HaveOccurred()) @@ -446,7 +444,7 @@ var _ = Describe("Query", func() { }) fakeClient := builder.WithObjects(hr).Build() - gq := query.NewData(fakeClient, scheme, &reqCRDsExist) + gq := query.NewData(fakeClient, scheme) routes, err := gq.GetRoutesForGateway(context.Background(), gwWithListener) Expect(err).NotTo(HaveOccurred()) @@ -476,7 +474,7 @@ var _ = Describe("Query", func() { }) fakeClient := builder.WithObjects(hr).Build() - gq := query.NewData(fakeClient, scheme, &reqCRDsExist) + gq := query.NewData(fakeClient, scheme) routes, err := gq.GetRoutesForGateway(context.Background(), gwWithListener) Expect(err).NotTo(HaveOccurred()) @@ -514,7 +512,7 @@ var _ = Describe("Query", func() { }) fakeClient := builder.WithObjects(hr).Build() - gq := query.NewData(fakeClient, scheme, &reqCRDsExist) + gq := query.NewData(fakeClient, scheme) routes, err := gq.GetRoutesForGateway(context.Background(), gwWithListener) Expect(err).NotTo(HaveOccurred()) @@ -568,7 +566,7 @@ var _ = Describe("Query", func() { } fakeClient := builder.WithObjects(tcpRoute).Build() - gq := query.NewData(fakeClient, scheme, &reqCRDsExist) + gq := query.NewData(fakeClient, scheme) routes, err := gq.GetRoutesForGateway(context.Background(), gw) Expect(err).NotTo(HaveOccurred()) @@ -603,7 +601,7 @@ var _ = Describe("Query", func() { } fakeClient := builder.WithObjects(tcpRoute).Build() - gq := query.NewData(fakeClient, scheme, &reqCRDsExist) + gq := query.NewData(fakeClient, scheme) routes, err := gq.GetRoutesForGateway(context.Background(), gw) Expect(err).NotTo(HaveOccurred()) @@ -640,7 +638,7 @@ var _ = Describe("Query", func() { } fakeClient := builder.WithObjects(tcpRoute).Build() - gq := query.NewData(fakeClient, scheme, &reqCRDsExist) + gq := query.NewData(fakeClient, scheme) routes, err := gq.GetRoutesForGateway(context.Background(), gw) Expect(err).NotTo(HaveOccurred()) @@ -674,7 +672,7 @@ var _ = Describe("Query", func() { } fakeClient := builder.WithObjects(tcpRoute).Build() - gq := query.NewData(fakeClient, scheme, &reqCRDsExist) + gq := query.NewData(fakeClient, scheme) routes, err := gq.GetRoutesForGateway(context.Background(), gw) Expect(err).NotTo(HaveOccurred()) @@ -713,7 +711,7 @@ var _ = Describe("Query", func() { } fakeClient := builder.WithObjects(tcpRoute).Build() - gq := query.NewData(fakeClient, scheme, &reqCRDsExist) + gq := query.NewData(fakeClient, scheme) routes, err := gq.GetRoutesForGateway(context.Background(), gw) Expect(err).NotTo(HaveOccurred()) diff --git a/projects/gateway2/translator/plugins/httplisteneroptions/query/query_test.go b/projects/gateway2/translator/plugins/httplisteneroptions/query/query_test.go index 976e65f14f8..e5945e9f788 100644 --- a/projects/gateway2/translator/plugins/httplisteneroptions/query/query_test.go +++ b/projects/gateway2/translator/plugins/httplisteneroptions/query/query_test.go @@ -45,7 +45,7 @@ var _ = Describe("Query Get HttpListenerOptions", func() { }) JustBeforeEach(func() { - builder := fake.NewClientBuilder().WithScheme(schemes.DefaultScheme()) + builder := fake.NewClientBuilder().WithScheme(schemes.TestingScheme()) query.IterateIndices(func(o client.Object, f string, fun client.IndexerFunc) error { builder.WithIndex(o, f, fun) return nil diff --git a/projects/gateway2/translator/plugins/listeneroptions/query/query_test.go b/projects/gateway2/translator/plugins/listeneroptions/query/query_test.go index 744ff5482a0..c8d1c75fa6f 100644 --- a/projects/gateway2/translator/plugins/listeneroptions/query/query_test.go +++ b/projects/gateway2/translator/plugins/listeneroptions/query/query_test.go @@ -45,7 +45,7 @@ var _ = Describe("Query Get ListenerOptions", func() { }) JustBeforeEach(func() { - builder := fake.NewClientBuilder().WithScheme(schemes.DefaultScheme()) + builder := fake.NewClientBuilder().WithScheme(schemes.TestingScheme()) query.IterateIndices(func(o client.Object, f string, fun client.IndexerFunc) error { builder.WithIndex(o, f, fun) return nil diff --git a/projects/gateway2/translator/plugins/routeoptions/query/query_test.go b/projects/gateway2/translator/plugins/routeoptions/query/query_test.go index daec3580948..a5bd6aec707 100644 --- a/projects/gateway2/translator/plugins/routeoptions/query/query_test.go +++ b/projects/gateway2/translator/plugins/routeoptions/query/query_test.go @@ -30,7 +30,7 @@ var _ = Describe("Query", func() { var builder *fake.ClientBuilder BeforeEach(func() { - builder = fake.NewClientBuilder().WithScheme(schemes.DefaultScheme()) + builder = fake.NewClientBuilder().WithScheme(schemes.TestingScheme()) query.IterateIndices(func(o client.Object, f string, fun client.IndexerFunc) error { builder.WithIndex(o, f, fun) return nil diff --git a/projects/gateway2/translator/plugins/virtualhostoptions/query/query_test.go b/projects/gateway2/translator/plugins/virtualhostoptions/query/query_test.go index 284016420ad..51ab8fe7d2b 100644 --- a/projects/gateway2/translator/plugins/virtualhostoptions/query/query_test.go +++ b/projects/gateway2/translator/plugins/virtualhostoptions/query/query_test.go @@ -45,7 +45,7 @@ var _ = Describe("Query Get VirtualHostOptions", func() { }) JustBeforeEach(func() { - builder := fake.NewClientBuilder().WithScheme(schemes.DefaultScheme()) + builder := fake.NewClientBuilder().WithScheme(schemes.TestingScheme()) query.IterateIndices(func(o client.Object, f string, fun client.IndexerFunc) error { builder.WithIndex(o, f, fun) return nil diff --git a/projects/gateway2/translator/testutils/test_file_loader.go b/projects/gateway2/translator/testutils/test_file_loader.go index 9deb479fdf5..1a1d1da0a04 100644 --- a/projects/gateway2/translator/testutils/test_file_loader.go +++ b/projects/gateway2/translator/testutils/test_file_loader.go @@ -86,7 +86,7 @@ func LoadFromFiles(ctx context.Context, filename string) ([]client.Object, error } func parseFile(ctx context.Context, filename string) ([]runtime.Object, error) { - scheme := schemes.DefaultScheme() + scheme := schemes.TestingScheme() file, err := os.ReadFile(filename) if err != nil { return nil, err diff --git a/projects/gateway2/translator/testutils/test_queries.go b/projects/gateway2/translator/testutils/test_queries.go index 6a8f8bedc15..1e495fc1989 100644 --- a/projects/gateway2/translator/testutils/test_queries.go +++ b/projects/gateway2/translator/testutils/test_queries.go @@ -10,7 +10,7 @@ import ( type IndexIteratorFunc = func(f func(client.Object, string, client.IndexerFunc) error) error func BuildIndexedFakeClient(objs []client.Object, funcs ...IndexIteratorFunc) client.Client { - builder := fake.NewClientBuilder().WithScheme(schemes.DefaultScheme()) + builder := fake.NewClientBuilder().WithScheme(schemes.TestingScheme()) for _, f := range funcs { f(func(o client.Object, s string, ifunc client.IndexerFunc) error { builder.WithIndex(o, s, ifunc) @@ -22,14 +22,13 @@ func BuildIndexedFakeClient(objs []client.Object, funcs ...IndexIteratorFunc) cl } func BuildGatewayQueriesWithClient(fakeClient client.Client) query.GatewayQueries { - reqCRDsExist := true - return query.NewData(fakeClient, schemes.DefaultScheme(), &reqCRDsExist) + return query.NewData(fakeClient, schemes.TestingScheme()) } func BuildGatewayQueries( objs []client.Object, ) query.GatewayQueries { - builder := fake.NewClientBuilder().WithScheme(schemes.DefaultScheme()) + builder := fake.NewClientBuilder().WithScheme(schemes.TestingScheme()) query.IterateIndices(func(o client.Object, f string, fun client.IndexerFunc) error { builder.WithIndex(o, f, fun) return nil @@ -37,7 +36,5 @@ func BuildGatewayQueries( fakeClient := builder.WithObjects(objs...).Build() - reqCRDsExist := true - - return query.NewData(fakeClient, schemes.DefaultScheme(), &reqCRDsExist) + return query.NewData(fakeClient, schemes.TestingScheme()) } diff --git a/projects/gateway2/wellknown/gwapi.go b/projects/gateway2/wellknown/gwapi.go index 2f3f16722bc..1411ceb4dc9 100644 --- a/projects/gateway2/wellknown/gwapi.go +++ b/projects/gateway2/wellknown/gwapi.go @@ -35,6 +35,9 @@ const ( GatewayListKind = "GatewayList" GatewayClassListKind = "GatewayClassList" ReferenceGrantListKind = "ReferenceGrantList" + + // Gateway API CRD names + TCPRouteCRD = "tcproutes.gateway.networking.k8s.io" ) var ( @@ -85,16 +88,12 @@ var ( Kind: ReferenceGrantListKind, } - GatewayCRDs = sets.New[string]( + // GatewayStandardCRDs defines the set of Gateway API CRDs from the standard release channel. + GatewayStandardCRDs = sets.New[string]( "gatewayclasses.gateway.networking.k8s.io", "gateways.gateway.networking.k8s.io", "httproutes.gateway.networking.k8s.io", - "tcproutes.gateway.networking.k8s.io", + "grpcroutes.gateway.networking.k8s.io", "referencegrants.gateway.networking.k8s.io", ) - - GatewayRouteCRDs = []string{ - "httproutes.gateway.networking.k8s.io", - "tcproutes.gateway.networking.k8s.io", - } ) diff --git a/test/kubernetes/e2e/features/services/httproute/suite.go b/test/kubernetes/e2e/features/services/httproute/suite.go index 296374c8128..3adbec4f08b 100644 --- a/test/kubernetes/e2e/features/services/httproute/suite.go +++ b/test/kubernetes/e2e/features/services/httproute/suite.go @@ -55,3 +55,36 @@ func (s *testingSuite) TestConfigureHTTPRouteBackingDestinationsWithService() { }, expectedSvcResp) } + +func (s *testingSuite) TestConfigureHTTPRouteBackingDestinationsWithServiceAndWithoutTCPRoute() { + s.T().Cleanup(func() { + err := s.testInstallation.Actions.Kubectl().DeleteFile(s.ctx, routeWithServiceManifest) + s.NoError(err, "can delete manifest") + err = s.testInstallation.Actions.Kubectl().DeleteFile(s.ctx, serviceManifest) + s.NoError(err, "can delete manifest") + s.testInstallation.Assertions.EventuallyObjectsNotExist(s.ctx, proxyService, proxyDeployment) + err = s.testInstallation.Actions.Kubectl().ApplyFile(s.ctx, tcpRouteCrdManifest) + s.NoError(err, "can apply manifest") + }) + + // Remove the TCPRoute CRD to assert HTTPRoute services still work. + err := s.testInstallation.Actions.Kubectl().DeleteFile(s.ctx, tcpRouteCrdManifest) + s.NoError(err, "can delete manifest") + + err = s.testInstallation.Actions.Kubectl().ApplyFile(s.ctx, routeWithServiceManifest) + s.Assert().NoError(err, "can apply gloo.solo.io Route manifest") + + // apply the service manifest separately, after the route table is applied, to ensure it can be applied after the route table + err = s.testInstallation.Actions.Kubectl().ApplyFile(s.ctx, serviceManifest) + s.Assert().NoError(err, "can apply gloo.solo.io Service manifest") + + s.testInstallation.Assertions.EventuallyObjectsExist(s.ctx, proxyService, proxyDeployment) + s.testInstallation.Assertions.AssertEventualCurlResponse( + s.ctx, + defaults.CurlPodExecOpt, + []curl.Option{ + curl.WithHost(kubeutils.ServiceFQDN(proxyService.ObjectMeta)), + curl.WithHostHeader("example.com"), + }, + expectedSvcResp) +} diff --git a/test/kubernetes/e2e/features/services/httproute/testdata/tcproute-crd.yaml b/test/kubernetes/e2e/features/services/httproute/testdata/tcproute-crd.yaml new file mode 120000 index 00000000000..abdc43cdcc5 --- /dev/null +++ b/test/kubernetes/e2e/features/services/httproute/testdata/tcproute-crd.yaml @@ -0,0 +1 @@ +../../../../../../../projects/gateway2/crds/tcproute-crd.yaml \ No newline at end of file diff --git a/test/kubernetes/e2e/features/services/httproute/types.go b/test/kubernetes/e2e/features/services/httproute/types.go index ce9d3a72b30..bf431a974f5 100644 --- a/test/kubernetes/e2e/features/services/httproute/types.go +++ b/test/kubernetes/e2e/features/services/httproute/types.go @@ -17,6 +17,7 @@ import ( var ( routeWithServiceManifest = filepath.Join(util.MustGetThisDir(), "testdata", "route-with-service.yaml") serviceManifest = filepath.Join(util.MustGetThisDir(), "testdata", "service-for-route.yaml") + tcpRouteCrdManifest = filepath.Join(util.MustGetThisDir(), "testdata", "tcproute-crd.yaml") // Proxy resource to be translated glooProxyObjectMeta = metav1.ObjectMeta{ diff --git a/test/kubernetes/e2e/features/services/tcproute/suite.go b/test/kubernetes/e2e/features/services/tcproute/suite.go index 4c81dac042b..4906a1249ab 100644 --- a/test/kubernetes/e2e/features/services/tcproute/suite.go +++ b/test/kubernetes/e2e/features/services/tcproute/suite.go @@ -68,7 +68,7 @@ func (s *testingSuite) TestConfigureTCPRouteBackingDestinationsWithSingleService expectedTcpBarSvcResp) } -func (s *testingSuite) TestConfigureTCPRouteBackingDestinationsWithMultiService() { +func (s *testingSuite) TestConfigureTCPRouteBackingDestinationsWithMultiServices() { s.T().Cleanup(func() { err := s.testInstallation.Actions.Kubectl().DeleteFile(s.ctx, multiTcpRouteManifest) s.NoError(err, "can delete manifest")