diff --git a/api/v1alpha1/staticservice_types.go b/api/v1alpha1/staticservice_types.go new file mode 100644 index 0000000..6a45b91 --- /dev/null +++ b/api/v1alpha1/staticservice_types.go @@ -0,0 +1,69 @@ +/* +Copyright 2022 The l7mp/stunner team. + +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 v1alpha1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +//+kubebuilder:object:root=true +// //+kubebuilder:subresource:status +//+kubebuilder:resource:categories=stunner,shortName=ssvc + +// StaticService is a set of static IP address prefixes STUNner allows access to via a Route. The +// purpose is to allow a Service-like CRD containing a set of static IP address prefixes to be set +// as the backend of a UDPRoute (or TCPRoute). +type StaticService struct { + metav1.TypeMeta `json:",inline"` + // +optional + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + + // Spec defines the behavior of a service. + Spec StaticServiceSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"` +} + +// StaticServiceSpec describes the prefixes reachable via a StaticService. +type StaticServiceSpec struct { + // The list of ports reachable via this service (currently omitted). + // +patchMergeKey=port + // +patchStrategy=merge + // +listType=map + // +listMapKey=port + // +listMapKey=protocol + // +optional + Ports []corev1.ServicePort `json:"ports,omitempty" patchStrategy:"merge" patchMergeKey:"port" protobuf:"bytes,1,rep,name=ports"` + + // Prefixes is a list of IP address prefixes reachable via this route. + Prefixes []string `json:"prefixes"` +} + +//+kubebuilder:object:root=true + +// StaticServiceList holds a list of static services. +type StaticServiceList struct { + metav1.TypeMeta `json:",inline"` + // +optional + metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + + // List of services. + Items []StaticService `json:"items"` +} + +func init() { + SchemeBuilder.Register(&StaticService{}, &StaticServiceList{}) +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 3d0f7b4..56c12af 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -22,6 +22,7 @@ limitations under the License. package v1alpha1 import ( + "k8s.io/api/core/v1" runtime "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/gateway-api/apis/v1beta1" ) @@ -170,3 +171,88 @@ func (in *GatewayConfigSpec) DeepCopy() *GatewayConfigSpec { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *StaticService) DeepCopyInto(out *StaticService) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StaticService. +func (in *StaticService) DeepCopy() *StaticService { + if in == nil { + return nil + } + out := new(StaticService) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *StaticService) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *StaticServiceList) DeepCopyInto(out *StaticServiceList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]StaticService, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StaticServiceList. +func (in *StaticServiceList) DeepCopy() *StaticServiceList { + if in == nil { + return nil + } + out := new(StaticServiceList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *StaticServiceList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *StaticServiceSpec) DeepCopyInto(out *StaticServiceSpec) { + *out = *in + if in.Ports != nil { + in, out := &in.Ports, &out.Ports + *out = make([]v1.ServicePort, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Prefixes != nil { + in, out := &in.Prefixes, &out.Prefixes + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StaticServiceSpec. +func (in *StaticServiceSpec) DeepCopy() *StaticServiceSpec { + if in == nil { + return nil + } + out := new(StaticServiceSpec) + in.DeepCopyInto(out) + return out +} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 4e6acce..7fd9403 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -3,6 +3,7 @@ # It should be run by config/default resources: - bases/stunner.l7mp.io_gatewayconfigs.yaml +- bases/stunner.l7mp.io_staticservices.yaml #+kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 22784e0..bc33681 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -79,6 +79,7 @@ rules: - stunner.l7mp.io resources: - gatewayconfigs + - staticservices verbs: - get - list @@ -89,5 +90,6 @@ rules: - stunner.l7mp.io resources: - gatewayconfigs/finalizers + - staticservices/finalizers verbs: - update diff --git a/internal/controllers/rbac.go b/internal/controllers/rbac.go index 0a4af44..6eb0ebe 100644 --- a/internal/controllers/rbac.go +++ b/internal/controllers/rbac.go @@ -3,8 +3,8 @@ package controllers // RBAC for directly watched resources. // +kubebuilder:rbac:groups="gateway.networking.k8s.io",resources=gatewayclasses;gateways;udproutes,verbs=get;list;watch;update;patch // +kubebuilder:rbac:groups="gateway.networking.k8s.io",resources=gatewayclasses/status;gateways/status;udproutes/status,verbs=update;patch -// +kubebuilder:rbac:groups="stunner.l7mp.io",resources=gatewayconfigs,verbs=get;list;watch;update;patch -// +kubebuilder:rbac:groups="stunner.l7mp.io",resources=gatewayconfigs/finalizers,verbs=update +// +kubebuilder:rbac:groups="stunner.l7mp.io",resources=gatewayconfigs;staticservices,verbs=get;list;watch;update;patch +// +kubebuilder:rbac:groups="stunner.l7mp.io",resources=gatewayconfigs/finalizers;staticservices/finalizers,verbs=update // RBAC for references in watched resources. // +kubebuilder:rbac:groups=core,resources=services,verbs=get;list;watch;create;update;patch;delete diff --git a/internal/controllers/udproute.go b/internal/controllers/udproute.go index 82c7b25..0886b8f 100644 --- a/internal/controllers/udproute.go +++ b/internal/controllers/udproute.go @@ -21,6 +21,7 @@ import ( gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + stnrv1a1 "github.com/l7mp/stunner-gateway-operator/api/v1alpha1" "github.com/l7mp/stunner-gateway-operator/internal/config" "github.com/l7mp/stunner-gateway-operator/internal/event" "github.com/l7mp/stunner-gateway-operator/internal/store" @@ -28,8 +29,9 @@ import ( ) const ( - serviceTCPRouteIndex = "serviceTCPRouteIndex" - serviceUDPRouteIndex = "serviceUDPRouteIndex" + serviceTCPRouteIndex = "serviceTCPRouteIndex" + serviceUDPRouteIndex = "serviceUDPRouteIndex" + staticServiceUDPRouteIndex = "staticServiceUDPRouteIndex" ) type udpRouteReconciler struct { @@ -68,6 +70,12 @@ func RegisterUDPRouteController(mgr manager.Manager, ch chan event.Event, log lo return err } + // index UDPRoute objects as per the referenced StaticServices + if err := mgr.GetFieldIndexer().IndexField(ctx, &gwapiv1a2.UDPRoute{}, + staticServiceUDPRouteIndex, staticServiceUDPRouteIndexFunc); err != nil { + return err + } + // a label-selector predicate to select the loadbalancer services we are interested in loadBalancerPredicate, err := predicate.LabelSelectorPredicate( metav1.LabelSelector{ @@ -95,7 +103,7 @@ func RegisterUDPRouteController(mgr manager.Manager, ch chan event.Event, log lo ); err != nil { return err } - r.log.Info("watching secret objects") + r.log.Info("watching service objects") // watch EndPoints object references by one of the ref'd Services if config.EnableEndpointDiscovery { @@ -109,6 +117,16 @@ func RegisterUDPRouteController(mgr manager.Manager, ch chan event.Event, log lo r.log.Info("watching endpoint objects") } + // watch StaticService objects referenced by one of our UDPRoutes + if err := c.Watch( + &source.Kind{Type: &corev1.Service{}}, + &handler.EnqueueRequestForObject{}, + predicate.NewPredicateFuncs(r.validateStaticServiceForReconcile), + ); err != nil { + return err + } + r.log.Info("watching staticservice objects") + return nil } @@ -120,6 +138,7 @@ func (r *udpRouteReconciler) Reconcile(ctx context.Context, req reconcile.Reques routeList := []client.Object{} namespaceList := []client.Object{} svcList := []client.Object{} + ssvcList := []client.Object{} endpointsList := []client.Object{} // find all related-services that we use as LoadBalancers for Gateways (i.e., have label @@ -149,19 +168,26 @@ func (r *udpRouteReconciler) Reconcile(ctx context.Context, req reconcile.Reques for _, rule := range udproute.Spec.Rules { for _, ref := range rule.BackendRefs { ref := ref - if (ref.Group != nil && *ref.Group != corev1.GroupName) || - (ref.Kind != nil && *ref.Kind != "Service") { + + // is this a static service? + if store.IsReferenceStaticService(&ref) { + if svc := r.getStaticServiceForBackend(ctx, &udproute, &ref); svc != nil { + ssvcList = append(ssvcList, svc) + } continue } - if svc := r.getServiceForBackend(ctx, &udproute, &ref); svc != nil { - svcList = append(svcList, svc) - } + if store.IsReferenceService(&ref) { + if svc := r.getServiceForBackend(ctx, &udproute, &ref); svc != nil { + svcList = append(svcList, svc) + } - if config.EnableEndpointDiscovery { - if e := r.getEndpointsForBackend(ctx, &udproute, &ref); e != nil { - endpointsList = append(endpointsList, e) + if config.EnableEndpointDiscovery { + if e := r.getEndpointsForBackend(ctx, &udproute, &ref); e != nil { + endpointsList = append(endpointsList, e) + } } + continue } } } @@ -190,6 +216,9 @@ func (r *udpRouteReconciler) Reconcile(ctx context.Context, req reconcile.Reques store.Endpoints.Reset(endpointsList) r.log.V(2).Info("reset Endpoints store", "endpoints", store.Endpoints.String()) + store.StaticServices.Reset(ssvcList) + r.log.V(2).Info("reset StaticService store", "static-services", store.StaticServices.String()) + r.eventCh <- event.NewEventRender() return reconcile.Result{}, nil @@ -225,6 +254,33 @@ func (r *udpRouteReconciler) validateBackendForReconcile(o client.Object) bool { return true } +// validateStaticServiceForReconcile checks whether a Static Service belongs to a valid UDPRoute. +func (r *udpRouteReconciler) validateStaticServiceForReconcile(o client.Object) bool { + // are we given a service or an endpoints object? + key := "" + if svc, ok := o.(*stnrv1a1.StaticService); ok { + key = store.GetObjectKey(svc) + } else { + r.log.Info("unexpected object type, bypassing reconciliation", "object", store.GetObjectKey(o)) + return false + } + + // find the routes referring to this static service + routeList := &gwapiv1a2.UDPRouteList{} + if err := r.List(context.Background(), routeList, &client.ListOptions{ + FieldSelector: fields.OneTermEqualSelector(staticServiceUDPRouteIndex, key), + }); err != nil { + r.log.Error(err, "unable to find associated udproutes", "static-service", key) + return false + } + + if len(routeList.Items) == 0 { + return false + } + + return true +} + // getServiceForBackend finds the Service associated with a backendRef func (r *udpRouteReconciler) getServiceForBackend(ctx context.Context, udproute *gwapiv1a2.UDPRoute, ref *gwapiv1a2.BackendRef) *corev1.Service { svc := corev1.Service{} @@ -287,12 +343,47 @@ func (r *udpRouteReconciler) getEndpointsForBackend(ctx context.Context, udprout return &e } +// getStaticServiceForBackend finds the StaticService associated with a backendRef +func (r *udpRouteReconciler) getStaticServiceForBackend(ctx context.Context, udproute *gwapiv1a2.UDPRoute, ref *gwapiv1a2.BackendRef) *stnrv1a1.StaticService { + svc := stnrv1a1.StaticService{} + + // if no explicit StaticService namespace is provided, use the UDPRoute namespace to lookup the + // StaticService + namespace := udproute.GetNamespace() + if ref.Namespace != nil { + namespace = string(*ref.Namespace) + } + + if err := r.Get(ctx, + types.NamespacedName{Namespace: namespace, Name: string(ref.Name)}, + &svc, + ); err != nil { + // not fatal + if !apierrors.IsNotFound(err) { + r.log.Error(err, "error getting StaticService", "namespace", namespace, + "name", string(ref.Name)) + return nil + } + + r.log.Info("no StaticService found for UDPRoute backend", "udproute", + store.GetObjectKey(udproute), "namespace", namespace, + "name", string(ref.Name)) + return nil + } + + return &svc +} + func serviceUDPRouteIndexFunc(o client.Object) []string { udproute := o.(*gwapiv1a2.UDPRoute) var services []string for _, rule := range udproute.Spec.Rules { for _, backend := range rule.BackendRefs { + if !store.IsReferenceService(&backend) { + continue + } + if backend.Kind == nil || string(*backend.Kind) == "Service" { // if no explicit Service namespace is provided, use the UDPRoute // namespace to lookup the provided Service @@ -314,9 +405,33 @@ func serviceUDPRouteIndexFunc(o client.Object) []string { return services } -// // validateLoadBalancerReconcile checks whether a Service is annotated as a related-service for a -// // gateway. -// func (r *udpRouteReconciler) validateLoadBalancerReconcile(o client.Object) bool { -// _, found := o.GetAnnotations()[opdefault.DefaultRelatedGatewayAnnotationKey] -// return found -// } +func staticServiceUDPRouteIndexFunc(o client.Object) []string { + udproute := o.(*gwapiv1a2.UDPRoute) + var staticServices []string + + for _, rule := range udproute.Spec.Rules { + for _, backend := range rule.BackendRefs { + backend := backend + + if !store.IsReferenceStaticService(&backend) { + continue + } + + // if no explicit StaticService namespace is provided, use the UDPRoute + // namespace to lookup the provided static service + namespace := udproute.GetNamespace() + if backend.Namespace != nil { + namespace = string(*backend.Namespace) + } + + staticServices = append(staticServices, + types.NamespacedName{ + Namespace: namespace, + Name: string(backend.Name), + }.String(), + ) + } + } + + return staticServices +} diff --git a/internal/renderer/cluster_render.go b/internal/renderer/cluster_render.go index 709a789..3435530 100644 --- a/internal/renderer/cluster_render.go +++ b/internal/renderer/cluster_render.go @@ -3,17 +3,17 @@ package renderer import ( "fmt" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" - // corev1 "k8s.io/api/core/v1" gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" stnrconfv1a1 "github.com/l7mp/stunner/pkg/apis/v1alpha1" + stnrv1a1 "github.com/l7mp/stunner-gateway-operator/api/v1alpha1" "github.com/l7mp/stunner-gateway-operator/internal/config" "github.com/l7mp/stunner-gateway-operator/internal/store" ) -// FIXME handle endpoint discovery for non-headless services func (r *Renderer) renderCluster(ro *gwapiv1a2.UDPRoute) (*stnrconfv1a1.ClusterConfig, error) { r.log.V(4).Info("renderCluster", "route", store.GetObjectKey(ro)) @@ -34,22 +34,23 @@ func (r *Renderer) renderCluster(ro *gwapiv1a2.UDPRoute) (*stnrconfv1a1.ClusterC // order to set the ResolvedRefs Route status: last error is reported only var backendErr error - ctype := stnrconfv1a1.ClusterTypeStatic + ctype, prevCType := stnrconfv1a1.ClusterTypeStatic, stnrconfv1a1.ClusterTypeUnknown for _, b := range rs[0].BackendRefs { + b := b - // core.v1 has empty Group - if b.Group != nil && *b.Group != gwapiv1a2.Group("") { + if b.Group != nil && string(*b.Group) != corev1.GroupName && + string(*b.Group) != stnrv1a1.GroupVersion.Group { backendErr = NewNonCriticalError(InvalidBackendGroup) - r.log.V(2).Info("renderCluster: error resolving backend", "route", - store.GetObjectKey(ro), "backend", string(b.Name), "group", + r.log.V(2).Info("renderCluster: invalid backend Group", "route", + store.GetObjectKey(ro), "backendRef", dumpBackendRef(&b), "group", *b.Group, "error", backendErr.Error()) continue } - if b.Kind != nil && *b.Kind != "Service" { + if b.Kind != nil && string(*b.Kind) != "Service" && string(*b.Kind) != "StaticService" { backendErr = NewNonCriticalError(InvalidBackendKind) - r.log.V(2).Info("renderCluster: error resolving backend", "route", - store.GetObjectKey(ro), "backend", string(b.Name), "kind", *b.Kind, + r.log.V(2).Info("renderCluster: invalid backend Kind", "route", + store.GetObjectKey(ro), "backendRef", dumpBackendRef(&b), "kind", *b.Kind, "error", backendErr) continue } @@ -61,47 +62,79 @@ func (r *Renderer) renderCluster(ro *gwapiv1a2.UDPRoute) (*stnrconfv1a1.ClusterC } ep := []string{} - if config.EnableEndpointDiscovery || config.EnableRelayToClusterIP { - ctype = stnrconfv1a1.ClusterTypeStatic - n := types.NamespacedName{ - Namespace: ns, - Name: string(b.Name), - } + switch ref := &b; { + case store.IsReferenceService(ref): + var err error + // get endpoints if EDS is enabled if config.EnableEndpointDiscovery { - ips, err := getEndpointAddrs(n, false) - if err != nil { - r.log.V(1).Info("renderCluster: could not set endpoint addresses for backend", - "route", store.GetObjectKey(ro), "backend", string(b.Name), - "error", err.Error()) - backendErr = err + epEDS, ctypeEDS, errEDS := getEndpointsForService(ref, ns) + if errEDS != nil { + backendErr = errEDS + r.log.V(2).Info("renderCluster: error rendering Endpoints for Service backend", + "route", store.GetObjectKey(ro), "backendRef", dumpBackendRef(ref), + "error", backendErr) } - // ips is empty - ep = append(ep, ips...) + eps = append(eps, epEDS...) + ctype = ctypeEDS + err = errEDS } - if config.EnableRelayToClusterIP { - ips, err := getClusterIP(n) - if err != nil { - r.log.V(1).Info("renderCluster: could not set ClusterIP for backend", - "route", store.GetObjectKey(ro), "backend", string(b.Name), - "error", err.Error()) - backendErr = err - } - // ips is empty - ep = append(ep, ips...) + // the clusterIP or STRICT_DNS cluster if EDS is disabled + epCluster, ctypeCluster, errCluster := getClusterRouteForService(ref, ns) + if errCluster != nil { + backendErr = errCluster + r.log.V(2).Info("renderCluster: error rendering route for Service backend", + "route", store.GetObjectKey(ro), "backendRef", dumpBackendRef(ref), + "error", backendErr) } - } else { - // fall back to strict DNS and hope for the best - ctype = stnrconfv1a1.ClusterTypeStrictDNS - ep = append(ep, fmt.Sprintf("%s.%s.svc.cluster.local", string(b.Name), ns)) + + if errCluster != nil && err != nil { + // both attempts failed: skip backend + continue + } + + eps = append(eps, epCluster...) + ctype = ctypeCluster + + case store.IsReferenceStaticService(ref): + var err error + ep, ctype, err = getEndpointsForStaticService(ref, ns) + if err != nil { + backendErr = err + r.log.V(2).Info("renderCluster: error rendering endpoints for StaticService backend", + "route", store.GetObjectKey(ro), "backendRef", dumpBackendRef(ref), + "error", backendErr) + continue + } + default: + // error could also be InvalidBackendGroup: both are reported with the same + // reason in the route status + backendErr = NewNonCriticalError(InvalidBackendKind) + r.log.Info("renderCluster: invalid backend Kind and/or Group", "route", store.GetObjectKey(ro), + "backendRef", dumpBackendRef(&b), "error", backendErr) + continue } - r.log.V(3).Info("renderCluster: adding Endpoints to endpoint list", "route", + if prevCType != stnrconfv1a1.ClusterTypeUnknown && prevCType != ctype { + backendErr = NewNonCriticalError(InconsitentClusterType) + r.log.Info("renderCluster: inconsistent cluster type", "route", + store.GetObjectKey(ro), "backendRef", dumpBackendRef(&b), + "prevous-ctype", fmt.Sprintf("%#v", prevCType)) + continue + + } + + r.log.V(2).Info("renderCluster: adding Endpoints for backend", "route", store.GetObjectKey(ro), "backendRef", dumpBackendRef(&b), "cluster-type", ctype.String(), "endpoints", ep) eps = append(eps, ep...) + prevCType = ctype + } + + if ctype == stnrconfv1a1.ClusterTypeUnknown { + return nil, NewNonCriticalError(BackendNotFound) } cluster := stnrconfv1a1.ClusterConfig{ @@ -124,3 +157,74 @@ func (r *Renderer) renderCluster(ro *gwapiv1a2.UDPRoute) (*stnrconfv1a1.ClusterC return &cluster, backendErr } + +func getEndpointsForService(b *gwapiv1a2.BackendRef, ns string) ([]string, stnrconfv1a1.ClusterType, error) { + ctype := stnrconfv1a1.ClusterTypeUnknown + ep := []string{} + + if !config.EnableEndpointDiscovery { + return ep, ctype, NewCriticalError(InternalError) + } + + n := types.NamespacedName{ + Namespace: ns, + Name: string(b.Name), + } + + ips, err := getEndpointAddrs(n, false) + if err != nil { + return ep, ctype, err + } + + ctype = stnrconfv1a1.ClusterTypeStatic + ep = append(ep, ips...) + + return ep, ctype, nil +} + +// either the ClusterIP if EDS is enabled, or a STRICT_DNS route if EDS is disabled +func getClusterRouteForService(b *gwapiv1a2.BackendRef, ns string) ([]string, stnrconfv1a1.ClusterType, error) { + var ctype stnrconfv1a1.ClusterType + ep := []string{} + + if config.EnableEndpointDiscovery { + ctype = stnrconfv1a1.ClusterTypeStatic + if config.EnableRelayToClusterIP { + n := types.NamespacedName{ + Namespace: ns, + Name: string(b.Name), + } + ips, err := getClusterIP(n) + if err != nil { + return ep, ctype, err + } + ep = append(ep, ips...) + } else { + //otherwise, return an empy endpoint list: make this explicit + ep = []string{} + } + } else { + // fall back to strict DNS and hope for the best + ctype = stnrconfv1a1.ClusterTypeStrictDNS + ep = append(ep, fmt.Sprintf("%s.%s.svc.cluster.local", string(b.Name), ns)) + } + + return ep, ctype, nil +} + +func getEndpointsForStaticService(b *gwapiv1a2.BackendRef, ns string) ([]string, stnrconfv1a1.ClusterType, error) { + ctype := stnrconfv1a1.ClusterTypeUnknown + ep := []string{} + + n := types.NamespacedName{Namespace: ns, Name: string(b.Name)} + ssvc := store.StaticServices.GetObject(n) + if ssvc == nil { + return ep, ctype, NewNonCriticalError(BackendNotFound) + } + + // ignore Spec.Ports + ep = make([]string, len(ssvc.Spec.Prefixes)) + copy(ep, ssvc.Spec.Prefixes) + + return ep, stnrconfv1a1.ClusterTypeStatic, nil +} diff --git a/internal/renderer/cluster_render_test.go b/internal/renderer/cluster_render_test.go index 79cbe70..98189e6 100644 --- a/internal/renderer/cluster_render_test.go +++ b/internal/renderer/cluster_render_test.go @@ -391,6 +391,7 @@ func TestRenderClusterRender(t *testing.T) { config.EnableRelayToClusterIP = true rc, err := r.renderCluster(ro) + assert.NotNil(t, err, "error") assert.True(t, IsNonCritical(err), "non-critical error") assert.True(t, IsNonCriticalError(err, ClusterIPNotFound), "invalid clusterip error") @@ -740,7 +741,7 @@ func TestRenderClusterRender(t *testing.T) { config.EnableRelayToClusterIP = true rc, err := r.renderCluster(rs[0]) - // handle non-critical error! + // no endpoint for dummy svc: handle non-critical error! assert.NotNil(t, err, "error") assert.True(t, IsNonCritical(err), "non-critical error") assert.True(t, IsNonCriticalError(err, EndpointNotFound), "endpoint not found error") @@ -758,6 +759,184 @@ func TestRenderClusterRender(t *testing.T) { assert.Contains(t, rc.Endpoints, "2.2.2.2", "endpoint cluster-ip-2") assert.Contains(t, rc.Endpoints, "3.3.3.3", "endpoint cluster-ip-3") + // restore + config.EnableEndpointDiscovery = opdefault.DefaultEnableEndpointDiscovery + config.EnableRelayToClusterIP = opdefault.DefaultEnableRelayToClusterIP + }, + }, + // StaticService + { + name: "StaticService ok", + cls: []gwapiv1a2.GatewayClass{testutils.TestGwClass}, + cfs: []stnrv1a1.GatewayConfig{testutils.TestGwConfig}, + gws: []gwapiv1a2.Gateway{testutils.TestGw}, + rs: []gwapiv1a2.UDPRoute{testutils.TestUDPRoute}, + ssvcs: []stnrv1a1.StaticService{testutils.TestStaticSvc}, + prep: func(c *renderTestConfig) { + group := gwapiv1a2.Group(stnrv1a1.GroupVersion.Group) + kind := gwapiv1a2.Kind("StaticService") + udp := testutils.TestUDPRoute.DeepCopy() + udp.Spec.Rules[0].BackendRefs = []gwapiv1a2.BackendRef{{ + BackendObjectReference: gwapiv1a2.BackendObjectReference{ + Group: &group, + Kind: &kind, + Name: "teststaticservice-ok", + }, + }} + c.rs = []gwapiv1a2.UDPRoute{*udp} + }, + tester: func(t *testing.T, r *Renderer) { + rs := store.UDPRoutes.GetAll() + assert.Len(t, rs, 1, "route len") + + rc, err := r.renderCluster(rs[0]) + assert.NoError(t, err, "render cluster") + + assert.Equal(t, "testnamespace/udproute-ok", rc.Name, "cluster name") + assert.Equal(t, "STATIC", rc.Type, "cluster type") + assert.Len(t, rc.Endpoints, 3, "endpoints len") + // static svc + assert.Contains(t, rc.Endpoints, "10.11.12.13", "StaticService endpoint ip-1") + assert.Contains(t, rc.Endpoints, "10.11.12.14", "StaticService endpoint ip-2") + assert.Contains(t, rc.Endpoints, "10.11.12.15", "StaticService endpoint ip-3") + }, + }, + { + name: "No StaticService backend errs", + cls: []gwapiv1a2.GatewayClass{testutils.TestGwClass}, + cfs: []stnrv1a1.GatewayConfig{testutils.TestGwConfig}, + gws: []gwapiv1a2.Gateway{testutils.TestGw}, + rs: []gwapiv1a2.UDPRoute{testutils.TestUDPRoute}, + prep: func(c *renderTestConfig) { + group := gwapiv1a2.Group(stnrv1a1.GroupVersion.Group) + kind := gwapiv1a2.Kind("StaticService") + udp := testutils.TestUDPRoute.DeepCopy() + udp.Spec.Rules[0].BackendRefs = []gwapiv1a2.BackendRef{{ + BackendObjectReference: gwapiv1a2.BackendObjectReference{ + Group: &group, + Kind: &kind, + Name: "teststaticservice-dummy", + }, + }} + c.rs = []gwapiv1a2.UDPRoute{*udp} + }, + tester: func(t *testing.T, r *Renderer) { + rs := store.UDPRoutes.GetAll() + assert.Len(t, rs, 1, "route len") + + _, err := r.renderCluster(rs[0]) + assert.Error(t, err, "render cluster") + + assert.True(t, IsNonCritical(err), "non-critical error") + assert.True(t, IsNonCriticalError(err, BackendNotFound), "backend not found") + }, + }, + { + name: "Mixed cluster type errs", + cls: []gwapiv1a2.GatewayClass{testutils.TestGwClass}, + cfs: []stnrv1a1.GatewayConfig{testutils.TestGwConfig}, + gws: []gwapiv1a2.Gateway{testutils.TestGw}, + rs: []gwapiv1a2.UDPRoute{testutils.TestUDPRoute}, + svcs: []corev1.Service{testutils.TestSvc}, + eps: []corev1.Endpoints{testutils.TestEndpoint}, + ssvcs: []stnrv1a1.StaticService{testutils.TestStaticSvc}, + prep: func(c *renderTestConfig) { + group := gwapiv1a2.Group(stnrv1a1.GroupVersion.Group) + kind := gwapiv1a2.Kind("StaticService") + udp := testutils.TestUDPRoute.DeepCopy() + udp.Spec.Rules[0].BackendRefs = []gwapiv1a2.BackendRef{{ + BackendObjectReference: gwapiv1a2.BackendObjectReference{ + Group: &group, + Kind: &kind, + Name: "teststaticservice-ok", + }, + }, { + BackendObjectReference: gwapiv1a2.BackendObjectReference{ + Name: "testservice-ok", + }, + }} + c.rs = []gwapiv1a2.UDPRoute{*udp} + }, + tester: func(t *testing.T, r *Renderer) { + rs := store.UDPRoutes.GetAll() + assert.Len(t, rs, 1, "route len") + + // switch EDS off: would render a DNS cluster plus a STATIC for the + // StaticService + config.EnableEndpointDiscovery = false + config.EnableRelayToClusterIP = false + + _, err := r.renderCluster(rs[0]) + assert.Error(t, err, "render cluster") + assert.True(t, IsNonCritical(err), "critical error") + assert.True(t, IsNonCriticalError(err, InconsitentClusterType), "inconsistent type") + + // restore + config.EnableEndpointDiscovery = opdefault.DefaultEnableEndpointDiscovery + config.EnableRelayToClusterIP = opdefault.DefaultEnableRelayToClusterIP + }, + }, + { + name: "Service (w/ EDS) plus StaticService ok", + cls: []gwapiv1a2.GatewayClass{testutils.TestGwClass}, + cfs: []stnrv1a1.GatewayConfig{testutils.TestGwConfig}, + gws: []gwapiv1a2.Gateway{testutils.TestGw}, + rs: []gwapiv1a2.UDPRoute{testutils.TestUDPRoute}, + svcs: []corev1.Service{testutils.TestSvc}, + eps: []corev1.Endpoints{testutils.TestEndpoint}, + prep: func(c *renderTestConfig) { + group := gwapiv1a2.Group(stnrv1a1.GroupVersion.Group) + kind := gwapiv1a2.Kind("StaticService") + udp := testutils.TestUDPRoute.DeepCopy() + udp.Spec.Rules[0].BackendRefs = []gwapiv1a2.BackendRef{{ + BackendObjectReference: gwapiv1a2.BackendObjectReference{ + Group: &group, + Kind: &kind, + Name: "teststaticservice-ok", + }, + }, { + BackendObjectReference: gwapiv1a2.BackendObjectReference{ + Group: &group, + Kind: &kind, + Name: "teststaticservice2", + }, + }, { + BackendObjectReference: gwapiv1a2.BackendObjectReference{ + Name: "testservice-ok", + }, + }} + + c.rs = []gwapiv1a2.UDPRoute{*udp} + + ssvc2 := testutils.TestStaticSvc.DeepCopy() + ssvc2.SetName("teststaticservice2") + ssvc2.Spec.Prefixes = []string{"0.0.0.0/1", "128.0.0.0/1"} + c.ssvcs = []stnrv1a1.StaticService{testutils.TestStaticSvc, *ssvc2} + }, + tester: func(t *testing.T, r *Renderer) { + rs := store.UDPRoutes.GetAll() + assert.Len(t, rs, 1, "route len") + + config.EnableEndpointDiscovery = true + config.EnableRelayToClusterIP = false + + rc, err := r.renderCluster(rs[0]) + assert.NoError(t, err, "render cluster") + + assert.Equal(t, "testnamespace/udproute-ok", rc.Name, "cluster name") + assert.Equal(t, "STATIC", rc.Type, "cluster type") + assert.Len(t, rc.Endpoints, 9, "endpoints len") + // static svc + assert.Contains(t, rc.Endpoints, "10.11.12.13", "StaticService 1 endpoint ip-1") + assert.Contains(t, rc.Endpoints, "10.11.12.14", "StaticService 1 endpoint ip-2") + assert.Contains(t, rc.Endpoints, "10.11.12.15", "StaticService 1 endpoint ip-3") + assert.Contains(t, rc.Endpoints, "0.0.0.0/1", "StaticService 2 endpoint ip-1") + assert.Contains(t, rc.Endpoints, "128.0.0.0/1", "StaticService 2 endpoint ip-2") + assert.Contains(t, rc.Endpoints, "1.2.3.4", "Service endpoint ip-1") + assert.Contains(t, rc.Endpoints, "1.2.3.5", "Service endpoint ip-2") + assert.Contains(t, rc.Endpoints, "1.2.3.6", "Service endpoint ip-3") + assert.Contains(t, rc.Endpoints, "1.2.3.7", "Service endpoint ip-4") + // restore config.EnableEndpointDiscovery = opdefault.DefaultEnableEndpointDiscovery config.EnableRelayToClusterIP = opdefault.DefaultEnableRelayToClusterIP diff --git a/internal/renderer/errors.go b/internal/renderer/errors.go index 40f6aff..b316a7e 100644 --- a/internal/renderer/errors.go +++ b/internal/renderer/errors.go @@ -13,20 +13,24 @@ const ( ExternalAuthCredentialsNotFound InvalidAuthConfig ConfigMapRenderingError + InternalError // noncritical InvalidBackendGroup InvalidBackendKind + BackendNotFound ServiceNotFound ClusterIPNotFound EndpointNotFound + InconsitentClusterType ) type TypedError struct { reason ErrorType } -// CriticalError is a fatal rendering error that prevents the rendering of a dataplane config. +// CriticalError is a fatal rendering error that prevents the rendering the dataplane config as a +// whole, or in parts. type CriticalError struct { TypedError } @@ -51,11 +55,15 @@ func (e *CriticalError) Error() string { return "missing or invalid external authentication credentials" case ConfigMapRenderingError: return "could not render dataplane config" + case InternalError: + return "internal error" } return "Unknown error" } -// NonCriticalError is a non-fatal error that affects a Gateway or a Route status. +// NonCriticalError is a non-fatal error that affects a Gateway or a Route status: this is an event +// that is worth reporting but otherwise does not prevent the rendering of a valid dataplane +// config. type NonCriticalError struct { TypedError } @@ -72,12 +80,16 @@ func (e *NonCriticalError) Error() string { return "Invalid Group in backend reference (expecing: None)" case InvalidBackendKind: return "Invalid Kind in backend reference (expecting Service)" + case BackendNotFound: + return "Backend not found" case ServiceNotFound: return "No Service found for backend" case ClusterIPNotFound: - return "No ClusterIP found (use a STRICT_DNS cluster if service is headless)" + return "No ClusterIP found for Service (this is fine for headless Services)" case EndpointNotFound: return "No Endpoint found for backend" + case InconsitentClusterType: + return "inconsitent cluster type for backends" } return "Unknown error" } diff --git a/internal/renderer/render_pipeline.go b/internal/renderer/render_pipeline.go index 76ec0d0..c990146 100644 --- a/internal/renderer/render_pipeline.go +++ b/internal/renderer/render_pipeline.go @@ -107,8 +107,6 @@ func (r *Renderer) renderGatewayClass(c *RenderContext) error { } conf.Auth = *auth - // all errors from this point are non-critical - log.V(1).Info("finding gateway objects") conf.Listeners = []stnrconfv1a1.ListenerConfig{} for _, gw := range r.getGateways4Class(c) { @@ -162,6 +160,8 @@ func (r *Renderer) renderGatewayClass(c *RenderContext) error { lc, err := r.renderListener(gw, gwConf, &l, rs, ap) if err != nil { + // all listener rendering errors are critical: prevent the + // rendering of the listener config log.Info("error rendering configuration for listener", "gateway", gw.GetName(), "listener", l.Name, "error", err.Error()) @@ -209,13 +209,15 @@ func (r *Renderer) renderGatewayClass(c *RenderContext) error { log.Info("non-critical error rendering cluster", "route", ro.GetName(), "error", err.Error()) } else { - log.Info("fatal error rendering cluster", "route", - ro.GetName(), "error", err.Error()) + log.Error(err, "fatal error rendering cluster", "route", + ro.GetName()) continue } } - conf.Clusters = append(conf.Clusters, *rc) + if rc != nil { + conf.Clusters = append(conf.Clusters, *rc) + } } // set status: we can do this only once we know whether (1) the parent accepted the diff --git a/internal/renderer/render_pipeline_test.go b/internal/renderer/render_pipeline_test.go index 9042e9f..59226ea 100644 --- a/internal/renderer/render_pipeline_test.go +++ b/internal/renderer/render_pipeline_test.go @@ -10,6 +10,7 @@ import ( "github.com/go-logr/logr" corev1 "k8s.io/api/core/v1" + meta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" // "k8s.io/apimachinery/pkg/types" // "sigs.k8s.io/controller-runtime/pkg/log/zap" @@ -1137,5 +1138,180 @@ func TestRenderPipeline(t *testing.T) { config.EnableRelayToClusterIP = opdefault.DefaultEnableRelayToClusterIP }, }, + { + name: "StaticService - E2E test", + cls: []gwapiv1a2.GatewayClass{testutils.TestGwClass}, + cfs: []stnrv1a1.GatewayConfig{testutils.TestGwConfig}, + gws: []gwapiv1a2.Gateway{testutils.TestGw}, + svcs: []corev1.Service{testutils.TestSvc}, + ssvcs: []stnrv1a1.StaticService{testutils.TestStaticSvc}, + prep: func(c *renderTestConfig) { + group := gwapiv1a2.Group(stnrv1a1.GroupVersion.Group) + kind := gwapiv1a2.Kind("StaticService") + udp := testutils.TestUDPRoute.DeepCopy() + udp.Spec.Rules[0].BackendRefs = []gwapiv1a2.BackendRef{{ + BackendObjectReference: gwapiv1a2.BackendObjectReference{ + Group: &group, + Kind: &kind, + Name: "teststaticservice-ok", + }, + }} + c.rs = []gwapiv1a2.UDPRoute{*udp} + }, + tester: func(t *testing.T, r *Renderer) { + gc, err := r.getGatewayClass() + assert.NoError(t, err, "gw-class found") + c := &RenderContext{gc: gc, log: logr.Discard()} + c.gwConf, err = r.getGatewayConfig4Class(c) + assert.NoError(t, err, "gw-conf found") + assert.Equal(t, "gatewayconfig-ok", c.gwConf.GetName(), + "gatewayconfig name") + + c.update = event.NewEventUpdate(0) + assert.NotNil(t, c.update, "update event create") + + err = r.renderGatewayClass(c) + assert.NoError(t, err, "render success") + + // configmap + cms := c.update.UpsertQueue.ConfigMaps.Objects() + assert.Len(t, cms, 1, "configmap ready") + o := cms[0] + + // objectmeta + assert.Equal(t, o.GetName(), testutils.TestStunnerConfig, + "configmap name") + assert.Equal(t, o.GetNamespace(), + "testnamespace", "configmap namespace") + + // related gw + as := o.GetAnnotations() + assert.Len(t, as, 1, "annotations len") + _, ok := as[opdefault.RelatedGatewayAnnotationKey] + assert.True(t, ok, "annotations: related gw") + + cm, ok := o.(*corev1.ConfigMap) + assert.True(t, ok, "configmap cast") + + conf, err := store.UnpackConfigMap(cm) + assert.NoError(t, err, "configmap stunner-config unmarshal") + + assert.Equal(t, opdefault.DefaultStunnerdInstanceName, + conf.Admin.Name, "name") + assert.Equal(t, testutils.TestLogLevel, conf.Admin.LogLevel, + "loglevel") + + assert.Equal(t, testutils.TestRealm, conf.Auth.Realm, "realm") + assert.Equal(t, "plaintext", conf.Auth.Type, "auth-type") + assert.Equal(t, testutils.TestUsername, conf.Auth.Credentials["username"], + "username") + assert.Equal(t, testutils.TestPassword, conf.Auth.Credentials["password"], + "password") + + assert.Len(t, conf.Listeners, 2, "listener num") + lc := conf.Listeners[0] + assert.Equal(t, "testnamespace/gateway-1/gateway-1-listener-udp", lc.Name, "name") + assert.Equal(t, "UDP", lc.Protocol, "proto") + assert.Equal(t, "1.2.3.4", lc.PublicAddr, "public-ip") + assert.Equal(t, int(testutils.TestMinPort), lc.MinRelayPort, "min-port") + assert.Equal(t, int(testutils.TestMaxPort), lc.MaxRelayPort, "max-port") + assert.Len(t, lc.Routes, 1, "route num") + assert.Equal(t, lc.Routes[0], "testnamespace/udproute-ok", "udp route") + + lc = conf.Listeners[1] + assert.Equal(t, "testnamespace/gateway-1/gateway-1-listener-tcp", lc.Name, "name") + assert.Equal(t, "TCP", lc.Protocol, "proto") + assert.Equal(t, "1.2.3.4", lc.PublicAddr, "public-ip") + assert.Equal(t, int(testutils.TestMinPort), lc.MinRelayPort, "min-port") + assert.Equal(t, int(testutils.TestMaxPort), lc.MaxRelayPort, "max-port") + assert.Len(t, lc.Routes, 0, "route num") + + assert.Len(t, conf.Clusters, 1, "cluster num") + rc := conf.Clusters[0] + assert.Equal(t, "testnamespace/udproute-ok", rc.Name, "cluster name") + assert.Equal(t, "STATIC", rc.Type, "cluster type") + assert.Len(t, rc.Endpoints, 3, "endpoints len") + assert.Contains(t, rc.Endpoints, "10.11.12.13", "staticservice endpoint ip-1") + assert.Contains(t, rc.Endpoints, "10.11.12.14", "staticservice endpoint ip-2") + assert.Contains(t, rc.Endpoints, "10.11.12.15", "staticservice endpoint ip-3") + + //statuses + setGatewayClassStatusAccepted(gc, nil) + assert.Len(t, gc.Status.Conditions, 1, "conditions num") + assert.Equal(t, string(gwapiv1b1.GatewayClassConditionStatusAccepted), + gc.Status.Conditions[0].Type, "conditions accepted") + assert.Equal(t, metav1.ConditionTrue, + gc.Status.Conditions[0].Status, "conditions status") + assert.Equal(t, string(gwapiv1b1.GatewayClassReasonAccepted), + gc.Status.Conditions[0].Type, "conditions reason") + assert.Equal(t, int64(0), + gc.Status.Conditions[0].ObservedGeneration, "conditions gen") + + gws := c.update.UpsertQueue.Gateways.Objects() + assert.Len(t, gws, 1, "gateway num") + gw, found := gws[0].(*gwapiv1a2.Gateway) + assert.True(t, found, "gateway found") + assert.Equal(t, fmt.Sprintf("%s/%s", testutils.TestNsName, "gateway-1"), + store.GetObjectKey(gw), "gw name found") + + assert.Len(t, gw.Status.Conditions, 2, "conditions num") + + assert.Equal(t, string(gwapiv1b1.GatewayConditionAccepted), + gw.Status.Conditions[0].Type, "conditions accepted") + assert.Equal(t, int64(0), gw.Status.Conditions[0].ObservedGeneration, + "conditions gen") + assert.Equal(t, metav1.ConditionTrue, gw.Status.Conditions[0].Status, + "status") + assert.Equal(t, string(gwapiv1b1.GatewayReasonAccepted), + gw.Status.Conditions[0].Reason, "reason") + + assert.Equal(t, string(gwapiv1b1.GatewayConditionProgrammed), + gw.Status.Conditions[1].Type, "programmed") + assert.Equal(t, int64(0), gw.Status.Conditions[1].ObservedGeneration, + "conditions gen") + assert.Equal(t, metav1.ConditionTrue, gw.Status.Conditions[1].Status, + "status") + assert.Equal(t, string(gwapiv1b1.GatewayReasonProgrammed), + gw.Status.Conditions[1].Reason, "reason") + + ros := c.update.UpsertQueue.UDPRoutes.Objects() + assert.Len(t, ros, 1, "routenum") + ro, found := ros[0].(*gwapiv1a2.UDPRoute) + assert.True(t, found, "route found") + assert.Equal(t, fmt.Sprintf("%s/%s", testutils.TestNsName, "udproute-ok"), + store.GetObjectKey(ro), "route name found") + + assert.Len(t, ro.Status.Parents, 1, "parent status len") + p := ro.Spec.ParentRefs[0] + parentStatus := ro.Status.Parents[0] + + assert.Equal(t, p.Group, parentStatus.ParentRef.Group, "status parent ref group") + assert.Equal(t, p.Kind, parentStatus.ParentRef.Kind, "status parent ref kind") + assert.Equal(t, p.Namespace, parentStatus.ParentRef.Namespace, "status parent ref namespace") + assert.Equal(t, p.Name, parentStatus.ParentRef.Name, "status parent ref name") + assert.Equal(t, p.SectionName, parentStatus.ParentRef.SectionName, "status parent ref section-name") + + assert.Equal(t, gwapiv1a2.GatewayController("stunner.l7mp.io/gateway-operator"), + parentStatus.ControllerName, "status parent ref") + + d := meta.FindStatusCondition(parentStatus.Conditions, + string(gwapiv1a2.RouteConditionAccepted)) + assert.NotNil(t, d, "accepted found") + assert.Equal(t, string(gwapiv1a2.RouteConditionAccepted), d.Type, + "type") + assert.Equal(t, metav1.ConditionTrue, d.Status, "status") + assert.Equal(t, int64(0), d.ObservedGeneration, "gen") + assert.Equal(t, "Accepted", d.Reason, "reason") + + d = meta.FindStatusCondition(parentStatus.Conditions, + string(gwapiv1a2.RouteConditionResolvedRefs)) + assert.NotNil(t, d, "resolved-refs found") + assert.Equal(t, string(gwapiv1a2.RouteConditionResolvedRefs), d.Type, + "type") + assert.Equal(t, metav1.ConditionTrue, d.Status, "status") + assert.Equal(t, int64(0), d.ObservedGeneration, "gen") + assert.Equal(t, "ResolvedRefs", d.Reason, "reason") + }, + }, }) } diff --git a/internal/renderer/render_test_suite.go b/internal/renderer/render_test_suite.go index 2500bb7..80d7969 100644 --- a/internal/renderer/render_test_suite.go +++ b/internal/renderer/render_test_suite.go @@ -53,6 +53,7 @@ type renderTestConfig struct { scrts []corev1.Secret ascrts []corev1.Secret nss []corev1.Namespace + ssvcs []stnrv1a1.StaticService prep func(c *renderTestConfig) tester func(t *testing.T, r *Renderer) } @@ -130,6 +131,11 @@ func renderTester(t *testing.T, testConf []renderTestConfig) { store.Namespaces.Upsert(&c.nss[i]) } + store.StaticServices.Flush() + for i := range c.ssvcs { + store.StaticServices.Upsert(&c.ssvcs[i]) + } + log.V(1).Info("starting renderer thread") ctx, cancel := context.WithCancel(context.Background()) err := r.Start(ctx) diff --git a/internal/renderer/service_util.go b/internal/renderer/service_util.go index 1a40234..4bc74bc 100644 --- a/internal/renderer/service_util.go +++ b/internal/renderer/service_util.go @@ -19,8 +19,6 @@ import ( gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" - // "github.com/l7mp/stunner-gateway-operator/internal/operator" - "github.com/l7mp/stunner-gateway-operator/internal/store" opdefault "github.com/l7mp/stunner-gateway-operator/pkg/config" ) diff --git a/internal/renderer/udproute_util.go b/internal/renderer/udproute_util.go index 50aabe6..f54be97 100644 --- a/internal/renderer/udproute_util.go +++ b/internal/renderer/udproute_util.go @@ -255,9 +255,15 @@ func setRouteConditionStatus(ro *gwapiv1a2.UDPRoute, p *gwapiv1a2.ParentReferenc var resolvedCond metav1.Condition if backendErr != nil { - reason := gwapiv1a2.RouteReasonBackendNotFound - if IsNonCriticalError(backendErr, InvalidBackendKind) { + var reason gwapiv1a2.RouteConditionReason + switch { + case IsNonCriticalError(backendErr, InvalidBackendKind), IsNonCriticalError(backendErr, InvalidBackendGroup): + // "RouteReasonInvalidKind" is used with the "ResolvedRefs" condition when + // one of the Route's rules has a reference to an unknown or unsupported + // Group and/or Kind. reason = gwapiv1a2.RouteReasonInvalidKind + default: + reason = gwapiv1a2.RouteReasonBackendNotFound } resolvedCond = metav1.Condition{ Type: string(gwapiv1a2.RouteConditionResolvedRefs), diff --git a/internal/renderer/udproute_util_test.go b/internal/renderer/udproute_util_test.go index 422f011..5d43452 100644 --- a/internal/renderer/udproute_util_test.go +++ b/internal/renderer/udproute_util_test.go @@ -869,5 +869,147 @@ func TestRenderUDPRouteUtil(t *testing.T) { assert.Equal(t, "ResolvedRefs", d.Reason, "reason") }, }, + { + name: "missing Service backend - status", + cls: []gwapiv1a2.GatewayClass{testutils.TestGwClass}, + cfs: []stnrv1a1.GatewayConfig{testutils.TestGwConfig}, + gws: []gwapiv1a2.Gateway{testutils.TestGw}, + svcs: []corev1.Service{testutils.TestSvc}, + prep: func(c *renderTestConfig) { + udp1 := testutils.TestUDPRoute.DeepCopy() + udp1.SetName("udproute-missing-service-backend") + udp1.Spec.Rules[0].BackendRefs = []gwapiv1a2.BackendRef{{ + BackendObjectReference: gwapiv1a2.BackendObjectReference{ + Name: "dummy-svc", + }, + }} + c.rs = []gwapiv1a2.UDPRoute{*udp1} + }, + tester: func(t *testing.T, r *Renderer) { + gc, err := r.getGatewayClass() + assert.NoError(t, err, "gw-class found") + + rs := store.UDPRoutes.GetAll() + assert.Len(t, rs, 1, "route found") + ro := rs[0] + + _, err = r.renderCluster(ro) + assert.Error(t, err, "render cluster") + assert.True(t, IsNonCritical(err), "non-critical error") + assert.True(t, IsNonCriticalError(err, BackendNotFound), "backend not found") + + initRouteStatus(ro) + p := ro.Spec.ParentRefs[0] + assert.True(t, r.isParentAcceptingRoute(ro, &p, gc.GetName())) + setRouteConditionStatus(ro, &p, config.ControllerName, true, err) + + assert.Len(t, ro.Status.Parents, 1, "parent status len") + parentStatus := ro.Status.Parents[0] + + assert.Equal(t, p.Group, parentStatus.ParentRef.Group, "status parent ref group") + assert.Equal(t, p.Kind, parentStatus.ParentRef.Kind, "status parent ref kind") + assert.Equal(t, p.Namespace, parentStatus.ParentRef.Namespace, "status parent ref namespace") + assert.Equal(t, p.Name, parentStatus.ParentRef.Name, "status parent ref name") + assert.Equal(t, p.SectionName, parentStatus.ParentRef.SectionName, "status parent ref section-name") + + assert.Equal(t, gwapiv1a2.GatewayController("stunner.l7mp.io/gateway-operator"), + parentStatus.ControllerName, "status parent ref") + + d := meta.FindStatusCondition(parentStatus.Conditions, + string(gwapiv1a2.RouteConditionAccepted)) + assert.NotNil(t, d, "accepted found") + assert.Equal(t, string(gwapiv1a2.RouteConditionAccepted), d.Type, + "type") + assert.Equal(t, metav1.ConditionTrue, d.Status, "status") + assert.Equal(t, int64(0), d.ObservedGeneration, "gen") + assert.Equal(t, "Accepted", d.Reason, "reason") + + d = meta.FindStatusCondition(parentStatus.Conditions, + string(gwapiv1a2.RouteConditionResolvedRefs)) + assert.NotNil(t, d, "resolved-refs found") + assert.Equal(t, string(gwapiv1a2.RouteConditionResolvedRefs), d.Type, + "type") + assert.Equal(t, metav1.ConditionFalse, d.Status, "status") + assert.Equal(t, int64(0), d.ObservedGeneration, "gen") + assert.Equal(t, "BackendNotFound", d.Reason, "reason") + }, + }, + { + name: "missing StaticService backend - status", + cls: []gwapiv1a2.GatewayClass{testutils.TestGwClass}, + cfs: []stnrv1a1.GatewayConfig{testutils.TestGwConfig}, + gws: []gwapiv1a2.Gateway{testutils.TestGw}, + svcs: []corev1.Service{testutils.TestSvc}, + ssvcs: []stnrv1a1.StaticService{testutils.TestStaticSvc}, + prep: func(c *renderTestConfig) { + group := gwapiv1a2.Group(stnrv1a1.GroupVersion.Group) + kind := gwapiv1a2.Kind("StaticService") + udp1 := testutils.TestUDPRoute.DeepCopy() + udp1.SetName("udproute-missing-service-backend") + udp1.Spec.Rules[0].BackendRefs = []gwapiv1a2.BackendRef{{ + BackendObjectReference: gwapiv1a2.BackendObjectReference{ + Group: &group, + Kind: &kind, + Name: "teststaticservice-dummy", + }, + }, { + BackendObjectReference: gwapiv1a2.BackendObjectReference{ + Group: &group, + Kind: &kind, + Name: "teststaticservice-ok", + }, + }} + + c.rs = []gwapiv1a2.UDPRoute{*udp1} + }, + tester: func(t *testing.T, r *Renderer) { + gc, err := r.getGatewayClass() + assert.NoError(t, err, "gw-class found") + + rs := store.UDPRoutes.GetAll() + assert.Len(t, rs, 1, "route found") + ro := rs[0] + + _, err = r.renderCluster(ro) + assert.Error(t, err, "render cluster") + assert.True(t, IsNonCritical(err), "non-critical error") + assert.True(t, IsNonCriticalError(err, BackendNotFound), "backend not found") + + initRouteStatus(ro) + p := ro.Spec.ParentRefs[0] + assert.True(t, r.isParentAcceptingRoute(ro, &p, gc.GetName())) + setRouteConditionStatus(ro, &p, config.ControllerName, true, err) + + assert.Len(t, ro.Status.Parents, 1, "parent status len") + parentStatus := ro.Status.Parents[0] + + assert.Equal(t, p.Group, parentStatus.ParentRef.Group, "status parent ref group") + assert.Equal(t, p.Kind, parentStatus.ParentRef.Kind, "status parent ref kind") + assert.Equal(t, p.Namespace, parentStatus.ParentRef.Namespace, "status parent ref namespace") + assert.Equal(t, p.Name, parentStatus.ParentRef.Name, "status parent ref name") + assert.Equal(t, p.SectionName, parentStatus.ParentRef.SectionName, "status parent ref section-name") + + assert.Equal(t, gwapiv1a2.GatewayController("stunner.l7mp.io/gateway-operator"), + parentStatus.ControllerName, "status parent ref") + + d := meta.FindStatusCondition(parentStatus.Conditions, + string(gwapiv1a2.RouteConditionAccepted)) + assert.NotNil(t, d, "accepted found") + assert.Equal(t, string(gwapiv1a2.RouteConditionAccepted), d.Type, + "type") + assert.Equal(t, metav1.ConditionTrue, d.Status, "status") + assert.Equal(t, int64(0), d.ObservedGeneration, "gen") + assert.Equal(t, "Accepted", d.Reason, "reason") + + d = meta.FindStatusCondition(parentStatus.Conditions, + string(gwapiv1a2.RouteConditionResolvedRefs)) + assert.NotNil(t, d, "resolved-refs found") + assert.Equal(t, string(gwapiv1a2.RouteConditionResolvedRefs), d.Type, + "type") + assert.Equal(t, metav1.ConditionFalse, d.Status, "status") + assert.Equal(t, int64(0), d.ObservedGeneration, "gen") + assert.Equal(t, "BackendNotFound", d.Reason, "reason") + }, + }, }) } diff --git a/internal/store/staticservice.go b/internal/store/staticservice.go new file mode 100644 index 0000000..f77a9b4 --- /dev/null +++ b/internal/store/staticservice.go @@ -0,0 +1,58 @@ +package store + +import ( + "k8s.io/apimachinery/pkg/types" + + stnrv1a1 "github.com/l7mp/stunner-gateway-operator/api/v1alpha1" +) + +var StaticServices = NewStaticServiceStore() + +type StaticServiceStore struct { + Store +} + +func NewStaticServiceStore() *StaticServiceStore { + return &StaticServiceStore{ + Store: NewStore(), + } +} + +// GetAll returns all StaticService objects from the global storage +func (s *StaticServiceStore) GetAll() []*stnrv1a1.StaticService { + ret := make([]*stnrv1a1.StaticService, 0) + + objects := s.Objects() + for i := range objects { + r, ok := objects[i].(*stnrv1a1.StaticService) + if !ok { + // this is critical: throw up hands and die + panic("access to an invalid object in the global StaticServiceStore") + } + + ret = append(ret, r) + } + + return ret +} + +// GetObject returns a named StaticService object from the global storage +func (s *StaticServiceStore) GetObject(nsName types.NamespacedName) *stnrv1a1.StaticService { + o := s.Get(nsName) + if o == nil { + return nil + } + + r, ok := o.(*stnrv1a1.StaticService) + if !ok { + // this is critical: throw up hands and die + panic("access to an invalid object in the global StaticServiceStore") + } + + return r +} + +// // AddStaticService adds a StaticService object to the the global storage (this is used mainly for testing) +// func (s *StaticServiceStore) AddStaticService(gc *stnrv1a1.StaticService) { +// s.Upsert(gc) +// } diff --git a/internal/store/store.go b/internal/store/store.go index 1329014..ed8b1e0 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -59,6 +59,7 @@ func (s *storeImpl) Get(nsName types.NamespacedName) client.Object { return o } +// Reset resets a store from a list of objects and removes duplicates along the way. func (s *storeImpl) Reset(objects []client.Object) { s.lock.Lock() defer s.lock.Unlock() diff --git a/internal/store/utils.go b/internal/store/utils.go index 67ae210..fbd2633 100644 --- a/internal/store/utils.go +++ b/internal/store/utils.go @@ -150,3 +150,31 @@ func stripCM(cm *corev1.ConfigMap) *corev1.ConfigMap { return cm } + +// IsReferenceService returns true of the provided BackendRef points to a Service. +func IsReferenceService(ref *gwapiv1a2.BackendRef) bool { + // Group is the group of the referent. For example, “gateway.networking.k8s.io”. When + // unspecified or empty string, core API group is inferred. + if ref.Group != nil && *ref.Group != corev1.GroupName { + return false + } + + if ref.Kind != nil && *ref.Kind != "Service" { + return false + } + + return true +} + +// IsReferenceStaticService returns true of the provided BackendRef points to a StaticService. +func IsReferenceStaticService(ref *gwapiv1a2.BackendRef) bool { + if ref.Group == nil || string(*ref.Group) != stnrv1a1.GroupVersion.Group { + return false + } + + if ref.Kind == nil || (*ref.Kind) != "StaticService" { + return false + } + + return true +} diff --git a/internal/testutils/defs.go b/internal/testutils/defs.go index eb79850..f270689 100644 --- a/internal/testutils/defs.go +++ b/internal/testutils/defs.go @@ -238,3 +238,14 @@ var TestAuthSecret = corev1.Secret{ "secret": []byte("ext-secret"), }, } + +// StaticService +var TestStaticSvc = stnrv1a1.StaticService{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "testnamespace", + Name: "teststaticservice-ok", + }, + Spec: stnrv1a1.StaticServiceSpec{ + Prefixes: []string{"10.11.12.13", "10.11.12.14", "10.11.12.15"}, + }, +} diff --git a/test/integration_test_cases.go b/test/integration_test_cases.go index 7c07508..eabcb5a 100644 --- a/test/integration_test_cases.go +++ b/test/integration_test_cases.go @@ -58,6 +58,7 @@ var ( testNode = testutils.TestNode.DeepCopy() testSecret = testutils.TestSecret.DeepCopy() testAuthSecret = testutils.TestAuthSecret.DeepCopy() + testStaticSvc = testutils.TestStaticSvc.DeepCopy() newCert64 = "bmV3Y2VydA==" // newcert newKey64 = "bmV3a2V5" // newkey _ = fmt.Sprintf("whatever: %d", 1) // make sure we use fmt @@ -1132,7 +1133,253 @@ var _ = Describe("Integration test:", func() { Expect(l.Routes).Should(BeEmpty()) }) + It("should survive converting the route to a StaticService backend", func() { + ctrl.Log.Info("adding static service") + Expect(k8sClient.Create(ctx, testStaticSvc)).Should(Succeed()) + + ctrl.Log.Info("reseting gateway") + recreateOrUpdateGateway(func(current *gwapiv1a2.Gateway) {}) + + ctrl.Log.Info("updating Route") + recreateOrUpdateUDPRoute(func(current *gwapiv1a2.UDPRoute) { + group := gwapiv1a2.Group(stnrv1a1.GroupVersion.Group) + kind := gwapiv1a2.Kind("StaticService") + current.Spec.CommonRouteSpec = gwapiv1a2.CommonRouteSpec{ + ParentRefs: []gwapiv1a2.ParentReference{{ + Name: "gateway-1", + }}, + } + current.Spec.Rules[0].BackendRefs = []gwapiv1a2.BackendRef{{ + BackendObjectReference: gwapiv1a2.BackendObjectReference{ + Group: &group, + Kind: &kind, + Name: "teststaticservice-ok", + }, + }} + }) + + // wait until configmap gets updated + lookupKey := types.NamespacedName{ + Name: "stunner-config", // test GatewayConfig rewrites DefaultConfigMapName + Namespace: string(testutils.TestNsName), + } + cm := &corev1.ConfigMap{} + + ctrl.Log.Info("trying to Get STUNner configmap", "resource", + lookupKey) + + contains := func(strs []string, val string) bool { + for _, s := range strs { + if s == val { + return true + } + } + return false + } + Eventually(func() bool { + err := k8sClient.Get(ctx, lookupKey, cm) + if err != nil { + return false + } + + c, err := store.UnpackConfigMap(cm) + if err != nil { + return false + } + + if len(c.Clusters) == 1 && contains(c.Clusters[0].Endpoints, "10.11.12.13") { + conf = &c + return true + } + return false + + }, timeout, interval).Should(BeTrue()) + }) + + It("should render a correct STUNner config", func() { + Expect(conf.Listeners).To(HaveLen(2)) + + // not sure about the order + l := conf.Listeners[0] + if l.Name != "testnamespace/gateway-1/gateway-1-listener-udp" { + l = conf.Listeners[1] + } + + Expect(l.Name).Should(Equal("testnamespace/gateway-1/gateway-1-listener-udp")) + Expect(l.Protocol).Should(Equal("UDP")) + Expect(l.Port).Should(Equal(1)) + Expect(l.MinRelayPort).Should(Equal(1)) + Expect(l.MaxRelayPort).Should(Equal(2)) + Expect(l.Routes).To(HaveLen(1)) + Expect(l.Routes[0]).Should(Equal("testnamespace/udproute-ok")) + + l = conf.Listeners[1] + if l.Name != "testnamespace/gateway-1/gateway-1-listener-tcp" { + l = conf.Listeners[0] + } + + Expect(l.Name).Should(Equal("testnamespace/gateway-1/gateway-1-listener-tcp")) + Expect(l.Protocol).Should(Equal("TCP")) + Expect(l.Port).Should(Equal(2)) + Expect(l.MinRelayPort).Should(Equal(1)) + Expect(l.MaxRelayPort).Should(Equal(2)) + Expect(l.Routes).To(HaveLen(1)) + Expect(l.Routes[0]).Should(Equal("testnamespace/udproute-ok")) + + Expect(conf.Clusters).To(HaveLen(1)) + + c := conf.Clusters[0] + + Expect(c.Name).Should(Equal("testnamespace/udproute-ok")) + Expect(c.Type).Should(Equal("STATIC")) + Expect(c.Endpoints).To(HaveLen(3)) + Expect(c.Endpoints).Should(ContainElement("10.11.12.13")) + Expect(c.Endpoints).Should(ContainElement("10.11.12.14")) + Expect(c.Endpoints).Should(ContainElement("10.11.12.15")) + }) + + It("should set the status correctly", func() { + gc := &gwapiv1a2.GatewayClass{} + Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(&testutils.TestGwClass), + gc)).Should(Succeed()) + + Expect(gc.Status.Conditions).To(HaveLen(1)) + + s := meta.FindStatusCondition(gc.Status.Conditions, + string(gwapiv1b1.GatewayClassConditionStatusAccepted)) + Expect(s).NotTo(BeNil()) + Expect(s.Type).Should( + Equal(string(gwapiv1b1.GatewayClassConditionStatusAccepted))) + Expect(s.Status).Should(Equal(metav1.ConditionTrue)) + Expect(s.Reason).Should( + Equal(string(gwapiv1b1.GatewayClassReasonAccepted))) + + gw := &gwapiv1a2.Gateway{} + Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(&testutils.TestGw), + gw)).Should(Succeed()) + + Expect(gw.Status.Conditions).To(HaveLen(2)) + + s = meta.FindStatusCondition(gw.Status.Conditions, + string(gwapiv1b1.GatewayConditionAccepted)) + Expect(s).NotTo(BeNil()) + Expect(s.Type).Should( + Equal(string(gwapiv1b1.GatewayConditionAccepted))) + Expect(s.Status).Should(Equal(metav1.ConditionTrue)) + + s = meta.FindStatusCondition(gw.Status.Conditions, + string(gwapiv1b1.GatewayConditionProgrammed)) + Expect(s).NotTo(BeNil()) + Expect(s.Type).Should( + Equal(string(gwapiv1b1.GatewayConditionProgrammed))) + Expect(s.Status).Should(Equal(metav1.ConditionTrue)) + + // stragely recreating the gateway lets api-server to find the public ip + // for the gw so Ready status becomes true (should investigate this) + Expect(gw.Status.Listeners).To(HaveLen(3)) + + // listener[0]: OK + s = meta.FindStatusCondition(gw.Status.Listeners[0].Conditions, + string(gwapiv1b1.ListenerConditionAccepted)) + Expect(s).NotTo(BeNil()) + Expect(s.Status).Should(Equal(metav1.ConditionTrue)) + Expect(s.Reason).Should(Equal(string(gwapiv1b1.ListenerReasonAccepted))) + + s = meta.FindStatusCondition(gw.Status.Listeners[0].Conditions, + string(gwapiv1b1.ListenerConditionResolvedRefs)) + Expect(s).NotTo(BeNil()) + Expect(s.Type).Should( + Equal(string(gwapiv1b1.ListenerConditionResolvedRefs))) + Expect(s.Status).Should(Equal(metav1.ConditionTrue)) + Expect(s.Reason).Should(Equal(string(gwapiv1b1.ListenerReasonResolvedRefs))) + + s = meta.FindStatusCondition(gw.Status.Listeners[0].Conditions, + string(gwapiv1b1.ListenerConditionReady)) + Expect(s).NotTo(BeNil()) + Expect(s.Type).Should( + Equal(string(gwapiv1b1.ListenerConditionReady))) + Expect(s.Status).Should(Equal(metav1.ConditionTrue)) + Expect(s.Reason).Should(Equal(string(gwapiv1b1.ListenerReasonReady))) + + // listeners[1]: detached + s = meta.FindStatusCondition(gw.Status.Listeners[1].Conditions, + string(gwapiv1b1.ListenerConditionAccepted)) + Expect(s).NotTo(BeNil()) + Expect(s.Status).Should(Equal(metav1.ConditionFalse)) + Expect(s.Reason).Should(Equal(string(gwapiv1b1.ListenerReasonUnsupportedProtocol))) + + s = meta.FindStatusCondition(gw.Status.Listeners[1].Conditions, + string(gwapiv1b1.ListenerConditionResolvedRefs)) + Expect(s).NotTo(BeNil()) + Expect(s.Type).Should( + Equal(string(gwapiv1b1.ListenerConditionResolvedRefs))) + Expect(s.Status).Should(Equal(metav1.ConditionTrue)) + Expect(s.Reason).Should(Equal(string(gwapiv1b1.ListenerReasonResolvedRefs))) + + s = meta.FindStatusCondition(gw.Status.Listeners[1].Conditions, + string(gwapiv1b1.ListenerConditionReady)) + Expect(s).NotTo(BeNil()) + Expect(s.Type).Should( + Equal(string(gwapiv1b1.ListenerConditionReady))) + Expect(s.Status).Should(Equal(metav1.ConditionTrue)) + Expect(s.Reason).Should(Equal(string(gwapiv1b1.ListenerReasonReady))) + + // listeners[2]: ok + s = meta.FindStatusCondition(gw.Status.Listeners[2].Conditions, + string(gwapiv1b1.ListenerConditionAccepted)) + Expect(s).NotTo(BeNil()) + Expect(s.Status).Should(Equal(metav1.ConditionTrue)) + Expect(s.Reason).Should(Equal(string(gwapiv1b1.ListenerReasonAccepted))) + + s = meta.FindStatusCondition(gw.Status.Listeners[2].Conditions, + string(gwapiv1b1.ListenerConditionResolvedRefs)) + Expect(s).NotTo(BeNil()) + Expect(s.Type).Should( + Equal(string(gwapiv1b1.ListenerConditionResolvedRefs))) + Expect(s.Status).Should(Equal(metav1.ConditionTrue)) + Expect(s.Reason).Should(Equal(string(gwapiv1b1.ListenerReasonResolvedRefs))) + + s = meta.FindStatusCondition(gw.Status.Listeners[2].Conditions, + string(gwapiv1b1.ListenerConditionReady)) + Expect(s).NotTo(BeNil()) + Expect(s.Type).Should( + Equal(string(gwapiv1b1.ListenerConditionReady))) + Expect(s.Status).Should(Equal(metav1.ConditionTrue)) + Expect(s.Reason).Should(Equal(string(gwapiv1b1.ListenerReasonReady))) + + ro := &gwapiv1a2.UDPRoute{} + Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(&testutils.TestUDPRoute), + ro)).Should(Succeed()) + + Expect(ro.Status.Parents).To(HaveLen(1)) + ps := ro.Status.Parents[0] + + Expect(ps.ParentRef.Group).To(HaveValue(Equal(gwapiv1a2.Group("gateway.networking.k8s.io")))) + Expect(ps.ParentRef.Kind).To(HaveValue(Equal(gwapiv1a2.Kind("Gateway")))) + Expect(ps.ParentRef.Namespace).To(BeNil()) + Expect(ps.ParentRef.Name).To(Equal(gwapiv1a2.ObjectName("gateway-1"))) + Expect(ps.ParentRef.SectionName).To(BeNil()) + Expect(ps.ControllerName).To(Equal(gwapiv1a2.GatewayController(config.ControllerName))) + + s = meta.FindStatusCondition(ps.Conditions, + string(gwapiv1a2.RouteConditionAccepted)) + Expect(s).NotTo(BeNil()) + Expect(s.Type).Should( + Equal(string(gwapiv1a2.RouteConditionAccepted))) + Expect(s.Status).Should(Equal(metav1.ConditionTrue)) + + s = meta.FindStatusCondition(ps.Conditions, + string(gwapiv1a2.RouteConditionResolvedRefs)) + Expect(s).NotTo(BeNil()) + Expect(s.Type).Should( + Equal(string(gwapiv1a2.RouteConditionResolvedRefs))) + Expect(s.Status).Should(Equal(metav1.ConditionTrue)) + }) + It("should survive deleting the route", func() { + ctrl.Log.Info("deleting StaticService") + Expect(k8sClient.Delete(ctx, testStaticSvc)).Should(Succeed()) + ctrl.Log.Info("deleting Route") Expect(k8sClient.Delete(ctx, testUDPRoute)).Should(Succeed())