diff --git a/cmd/webhook/main.go b/cmd/webhook/main.go new file mode 100644 index 0000000..ad07850 --- /dev/null +++ b/cmd/webhook/main.go @@ -0,0 +1,177 @@ +/* +Copyright 2023. + +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 main + +import ( + "flag" + "fmt" + "os" + "strings" + + // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) + // to ensure that exec-entrypoint and run can make use of them. + + "github.com/google/uuid" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" + _ "k8s.io/client-go/plugin/pkg/client/auth" + + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/healthz" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + "sigs.k8s.io/controller-runtime/pkg/manager" + + // "github.com/f5devcentral/bigip-kubernetes-gateway/internal/pkg" + "github.com/f5devcentral/bigip-kubernetes-gateway/internal/webhooks" + f5_bigip "github.com/f5devcentral/f5-bigip-rest-go/bigip" + "github.com/f5devcentral/f5-bigip-rest-go/utils" + + //+kubebuilder:scaffold:imports + + gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" +) + +var ( + scheme = runtime.NewScheme() + setupLog = ctrl.Log.WithName("setup") + stopCh = make(chan struct{}) + cmdflags = webhooks.CmdFlags{} +) + +func init() { + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + utilruntime.Must(gatewayv1beta1.AddToScheme(scheme)) +} + +// 530 kubebuilder init --domain f5.com --repo f5.com/bigip-k8s-gateway +// 531 kubebuilder create api --group gateways --version v1 --kind Adc + +func main() { + var ( + metricsAddr string + enableLeaderElection bool + probeAddr string + controllerName string + ) + + flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") + flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") + flag.BoolVar(&enableLeaderElection, "leader-elect", false, + "Enable leader election for controller manager. "+ + "Enabling this will ensure there is only one active controller manager.") + + flag.StringVar(&cmdflags.CertDir, "certificate-directory", "/certificate-directory", "Directory that contains tls.crt and tls.key for webook https server.") + flag.StringVar(&controllerName, "controller-name", "f5.io/gateway-controller-name", "This controller name.") + flag.StringVar(&cmdflags.LogLevel, "log-level", utils.LogLevel_Type_INFO, "The log level, valid values: trace, debug, info, warn, error") + flag.StringVar(&cmdflags.Validates, "validates", "", fmt.Sprintf("The items to validate synchronizingly, on operations "+ + "concating multiple values with ',', valid values: %s", strings.Join(webhooks.SupportedValidatingKeys(), ","))) + + opts := zap.Options{ + Development: true, + } + opts.BindFlags(flag.CommandLine) + flag.Parse() + + if err := webhooks.ValidateGivenKeys(strings.Split(cmdflags.Validates, ",")); err != nil { + setupLog.Error(err, "--validates fault") + os.Exit(1) + } else { + webhooks.TurnOnValidatingFor(strings.Split(cmdflags.Validates, ",")) + } + + ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) + var err error + webhooks.WebhookManager, err = ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ + Scheme: scheme, + MetricsBindAddress: metricsAddr, + Port: 9443, + CertDir: cmdflags.CertDir, + HealthProbeBindAddress: probeAddr, + LeaderElection: enableLeaderElection, + LeaderElectionID: "303cfed9.f5.com", + // LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily + // when the Manager ends. This requires the binary to immediately end when the + // Manager is stopped, otherwise, this setting is unsafe. Setting this significantly + // speeds up voluntary leader transitions as the new leader don't have to wait + // LeaseDuration time first. + // + // In the default scaffold provided, the program ends immediately after + // the manager stops, so would be fine to enable this option. However, + // if you are doing or is intended to do any operation such as perform cleanups + // after the manager stops then its usage might be unsafe. + // LeaderElectionReleaseOnCancel: true, + }) + if err != nil { + setupLog.Error(err, "unable to start manager: "+err.Error()) + os.Exit(1) + } + + prometheus.MustRegister(utils.FunctionDurationTimeCostCount) + prometheus.MustRegister(utils.FunctionDurationTimeCostTotal) + prometheus.MustRegister(f5_bigip.BIGIPiControlTimeCostCount) + prometheus.MustRegister(f5_bigip.BIGIPiControlTimeCostTotal) + webhooks.WebhookManager.AddMetricsExtraHandler("/stats/", promhttp.Handler()) + setupWebhooks(webhooks.WebhookManager) + + if err := webhooks.WebhookManager.AddHealthzCheck("healthz", healthz.Ping); err != nil { + setupLog.Error(err, "unable to set up health check") + os.Exit(1) + } + if err := webhooks.WebhookManager.AddReadyzCheck("readyz", healthz.Ping); err != nil { + setupLog.Error(err, "unable to set up ready check") + os.Exit(1) + } + + defer close(stopCh) + setupLog.Info("starting manager") + if err := webhooks.WebhookManager.Start(ctrl.SetupSignalHandler()); err != nil { + setupLog.Error(err, "problem running manager") + os.Exit(1) + } +} + +func setupWebhooks(mgr manager.Manager) { + slog := utils.NewLog().WithLevel(cmdflags.LogLevel).WithRequestID(uuid.NewString()) + + if err := (&webhooks.GatewayClassWebhook{Logger: slog}). + SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "gatewayclass") + os.Exit(1) + } + + if err := (&webhooks.GatewayWebhook{ + Logger: slog, + }).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "gateway") + os.Exit(1) + } + + if err := (&webhooks.HTTPRouteWebhook{Logger: slog}). + SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "httproute") + os.Exit(1) + } + + if err := (&webhooks.ReferenceGrantWebhook{Logger: slog}). + SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "referencegrant") + os.Exit(1) + } +} diff --git a/internal/webhooks/types.go b/internal/webhooks/types.go new file mode 100644 index 0000000..06661f4 --- /dev/null +++ b/internal/webhooks/types.go @@ -0,0 +1,8 @@ +package webhooks + +type CmdFlags struct { + CertDir string + Validates string + DeployMethod string + LogLevel string +} diff --git a/internal/webhooks/vars.go b/internal/webhooks/vars.go new file mode 100644 index 0000000..2d237d8 --- /dev/null +++ b/internal/webhooks/vars.go @@ -0,0 +1,23 @@ +package webhooks + +import "sigs.k8s.io/controller-runtime/pkg/manager" + +var ( + validateMap = map[string]bool{ + VK_gateway_gatewayClassName: false, + VK_gateway_listeners_tls_certificateRefs: false, + VK_httproute_parentRefs: false, + VK_httproute_rules_backendRefs: false, + } +) + +const ( + VK_gateway_gatewayClassName = "gateway.gatewayClassName" + VK_gateway_listeners_tls_certificateRefs = "gateway.listeners.tls.certificateRefs" + VK_httproute_parentRefs = "httproute.parentRefs" + VK_httproute_rules_backendRefs = "httproute.rules.backendRefs" +) + +var ( + WebhookManager manager.Manager +) diff --git a/internal/webhooks/webhook_utils.go b/internal/webhooks/webhook_utils.go index a0e88bd..2efcbfa 100644 --- a/internal/webhooks/webhook_utils.go +++ b/internal/webhooks/webhook_utils.go @@ -1,6 +1,7 @@ package webhooks import ( + "context" "fmt" "reflect" "strings" @@ -9,25 +10,10 @@ import ( "github.com/f5devcentral/f5-bigip-rest-go/utils" v1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" ) -var ( - validateMap = map[string]bool{ - VK_gateway_gatewayClassName: false, - VK_gateway_listeners_tls_certificateRefs: false, - VK_httproute_parentRefs: false, - VK_httproute_rules_backendRefs: false, - } -) - -const ( - VK_gateway_gatewayClassName = "gateway.gatewayClassName" - VK_gateway_listeners_tls_certificateRefs = "gateway.listeners.tls.certificateRefs" - VK_httproute_parentRefs = "httproute.parentRefs" - VK_httproute_rules_backendRefs = "httproute.rules.backendRefs" -) - func SupportedValidatingKeys() []string { keys := make([]string, 0, len(validateMap)) for k := range validateMap { @@ -67,6 +53,13 @@ func ValidateGivenKeys(keys []string) error { func validateListenersTLSCertificateRefs(gw *gatewayv1beta1.Gateway) error { invalidRefs, invalidTypes := []string{}, []string{} + + var rgs gatewayv1beta1.ReferenceGrantList + err := WebhookManager.GetCache().List(context.TODO(), &rgs, &client.ListOptions{}) + if err != nil { + return err + } + for _, ls := range gw.Spec.Listeners { if ls.Protocol != gatewayv1beta1.HTTPSProtocolType { continue @@ -91,8 +84,9 @@ func validateListenersTLSCertificateRefs(gw *gatewayv1beta1.Gateway) error { ns = string(*ref.Namespace) } kn := utils.Keyname(ns, string(ref.Name)) - scrt := pkg.ActiveSIGs.GetSecret(kn) - if scrt == nil || !pkg.ActiveSIGs.CanRefer(gw, scrt) { + var scrt v1.Secret + err := objectFromMgrCache(kn, &scrt) + if err != nil || !canRefer(&rgs, gw, &scrt) { invalidRefs = append(invalidRefs, fmt.Sprintf("secret '%s' not found", kn)) continue } @@ -122,14 +116,17 @@ func validateHTTPRouteParentRefs(hr *gatewayv1beta1.HTTPRoute) error { continue } gwkey := utils.Keyname(ns, string(pr.Name)) - if gw := pkg.ActiveSIGs.GetGateway(gwkey); gw == nil { + var gw gatewayv1beta1.Gateway + err := objectFromMgrCache(gwkey, &gw) + if err != nil { invalidRefs = append(invalidRefs, fmt.Sprintf("no gateway '%s' found", gwkey)) continue } else { for _, ls := range gw.Spec.Listeners { if ls.Name == *pr.SectionName { - namespace := pkg.ActiveSIGs.GetNamespace(hr.Namespace) - if !pkg.RouteMatches(gw.Namespace, &ls, namespace, reflect.TypeOf(*hr).Name()) { + var namespace v1.Namespace + err := objectFromMgrCache(hr.Namespace, &namespace) + if err != nil || !pkg.RouteMatches(gw.Namespace, &ls, &namespace, reflect.TypeOf(*hr).Name()) { invalidRefs = append(invalidRefs, fmt.Sprintf("invalid reference to %s", utils.Keyname(gw.Namespace, gw.Name, string(ls.Name)))) } } @@ -142,6 +139,12 @@ func validateHTTPRouteParentRefs(hr *gatewayv1beta1.HTTPRoute) error { func validateHTTPRouteBackendRefs(hr *gatewayv1beta1.HTTPRoute) error { + var rgs gatewayv1beta1.ReferenceGrantList + err := WebhookManager.GetCache().List(context.TODO(), &rgs, &client.ListOptions{}) + if err != nil { + return err + } + invalidRefs, invalidTypes := []string{}, []string{} for _, rl := range hr.Spec.Rules { for _, br := range rl.BackendRefs { @@ -155,8 +158,9 @@ func validateHTTPRouteBackendRefs(hr *gatewayv1beta1.HTTPRoute) error { ns = string(*br.Namespace) } svckey := utils.Keyname(ns, string(br.Name)) - svc := pkg.ActiveSIGs.GetService(svckey) - if svc == nil || !pkg.ActiveSIGs.CanRefer(hr, svc) { + var svc v1.Service + err := objectFromMgrCache(svckey, &svc) + if err != nil || !canRefer(&rgs, hr, &svc) { invalidRefs = append(invalidRefs, fmt.Sprintf("no backRef found: '%s'", svckey)) continue } @@ -174,7 +178,9 @@ func validateHTTPRouteBackendRefs(hr *gatewayv1beta1.HTTPRoute) error { ns := hr.Namespace svckey := utils.Keyname(ns, string(er.Name)) - if svc := pkg.ActiveSIGs.GetService(svckey); svc == nil { + var svc v1.Service + err := objectFromMgrCache(svckey, &svc) + if err != nil { invalidRefs = append(invalidRefs, fmt.Sprintf("no backRef found: '%s'", svckey)) continue } @@ -186,7 +192,23 @@ func validateHTTPRouteBackendRefs(hr *gatewayv1beta1.HTTPRoute) error { } func validateGatewayClassIsReferred(gwc *gatewayv1beta1.GatewayClass) error { - if gws := pkg.ActiveSIGs.AttachedGateways(gwc); len(gws) != 0 { + if gwc == nil { + return nil + } + + var gwList gatewayv1beta1.GatewayList + err := WebhookManager.GetCache().List(context.TODO(), &gwList, &client.ListOptions{}) + if err != nil { + return err + } + + gws := []*gatewayv1beta1.Gateway{} + for _, gw := range gwList.Items { + if gw.Spec.GatewayClassName == gatewayv1beta1.ObjectName(gwc.Name) { + gws = append(gws, &gw) + } + } + if len(gws) != 0 { names := []string{} for _, gw := range gws { names = append(names, utils.Keyname(gw.Namespace, gw.Name)) @@ -197,8 +219,70 @@ func validateGatewayClassIsReferred(gwc *gatewayv1beta1.GatewayClass) error { } } +func gwListenerName(gw *gatewayv1beta1.Gateway, ls *gatewayv1beta1.Listener) string { + return strings.Join([]string{"gw", gw.Namespace, gw.Name, string(ls.Name)}, ".") +} + +func hrParentName(hr *gatewayv1beta1.HTTPRoute, pr *gatewayv1beta1.ParentReference) string { + ns := hr.Namespace + if pr.Namespace != nil { + ns = string(*pr.Namespace) + } + sn := "" + if pr.SectionName != nil { + sn = string(*pr.SectionName) + } + return strings.Join([]string{"gw", ns, string(pr.Name), sn}, ".") +} + func validateGatewayIsReferred(gw *gatewayv1beta1.Gateway) error { - hrs := pkg.ActiveSIGs.AttachedHTTPRoutes(gw) + + if gw == nil { + return nil + } + + listeners := map[string]*gatewayv1beta1.Listener{} + for _, ls := range gw.Spec.Listeners { + lskey := gwListenerName(gw, &ls) + listeners[lskey] = ls.DeepCopy() + } + + var hrList gatewayv1beta1.HTTPRouteList + err := WebhookManager.GetCache().List(context.TODO(), &hrList, &client.ListOptions{}) + if err != nil { + return err + } + + var nsList v1.NamespaceList + err = WebhookManager.GetCache().List(context.TODO(), &nsList, &client.ListOptions{}) + if err != nil { + return nil + } + nsmap := map[string]*v1.Namespace{} + for _, ns := range nsList.Items { + nsmap[ns.Name] = &ns + } + + hrs := []*gatewayv1beta1.HTTPRoute{} + + for _, hr := range hrList.Items { + for _, pr := range hr.Spec.ParentRefs { + ns := hr.Namespace + if pr.Namespace != nil { + ns = string(*pr.Namespace) + } + if utils.Keyname(ns, string(pr.Name)) == utils.Keyname(gw.Namespace, gw.Name) { + vsname := hrParentName(&hr, &pr) + routeNamespace := nsmap[hr.Namespace] + routetype := reflect.TypeOf(hr).Name() + if pkg.RouteMatches(gw.Namespace, listeners[vsname], routeNamespace, routetype) { + hrs = append(hrs, &hr) + break + } + } + } + } + if len(hrs) != 0 { names := []string{} for _, hr := range hrs { @@ -212,7 +296,9 @@ func validateGatewayIsReferred(gw *gatewayv1beta1.Gateway) error { func validateGatewayClassExists(gw *gatewayv1beta1.Gateway) error { className := gw.Spec.GatewayClassName - if gwc := pkg.ActiveSIGs.GetGatewayClass(string(className)); gwc == nil { + var gwc gatewayv1beta1.GatewayClass + err := objectFromMgrCache(string(className), &gwc) + if err != nil { return fmt.Errorf("gatewayclass '%s' not found", className) } else { return nil @@ -275,3 +361,83 @@ func fmtInvalids(a []string, b ...[]string) error { return nil } } + +func objectKeyFromString(keyname string) client.ObjectKey { + kn := strings.Split(keyname, "/") + if len(kn) == 1 { + return client.ObjectKey{ + Namespace: "", + Name: kn[0], + } + } else { + return client.ObjectKey{ + Namespace: kn[0], + Name: kn[len(kn)-1], + } + } +} + +// objectFromMgrCache return object from cache. +func objectFromMgrCache(keyname string, obj client.Object) error { + return WebhookManager.GetCache().Get(context.TODO(), objectKeyFromString(keyname), obj, &client.GetOptions{}) +} + +// canRefer return bool if 'from' can refers to 'to'. +// for example: a gateway to a secret containing tls objects. +func canRefer(rgs *gatewayv1beta1.ReferenceGrantList, from, to client.Object) bool { + fromns := client.Object.GetNamespace(from) + tons := client.Object.GetNamespace(to) + if fromns == tons { + return true + } + + fromgvk := client.Object.GetObjectKind(from).GroupVersionKind() + if fromgvk.Group != gatewayv1beta1.GroupName { + return false + } + rgf := gatewayv1beta1.ReferenceGrantFrom{ + Group: gatewayv1beta1.Group(fromgvk.Group), + Kind: gatewayv1beta1.Kind(fromgvk.Kind), + Namespace: gatewayv1beta1.Namespace(fromns), + } + // f := stringifyRGFrom(&rgf) + + togvk := client.Object.GetObjectKind(to).GroupVersionKind() + toname := gatewayv1beta1.ObjectName(client.Object.GetName(to)) + rgt := gatewayv1beta1.ReferenceGrantTo{ + Group: gatewayv1beta1.Group(togvk.Group), + Kind: gatewayv1beta1.Kind(togvk.Kind), + Name: &toname, + } + // t := stringifyRGTo(&rgt, tons) + + rgtAll := gatewayv1beta1.ReferenceGrantTo{ + Group: gatewayv1beta1.Group(togvk.Group), + Kind: gatewayv1beta1.Kind(togvk.Kind), + } + // toAll := stringifyRGTo(&rgtAll, tons) + + return rgExists(rgs, &rgf, &rgt) || rgExists(rgs, &rgf, &rgtAll) +} + +func rgExists(rgs *gatewayv1beta1.ReferenceGrantList, rgf *gatewayv1beta1.ReferenceGrantFrom, rgt *gatewayv1beta1.ReferenceGrantTo) bool { + for _, rg := range rgs.Items { + f, t := false, false + for _, rgFrom := range rg.Spec.From { + if reflect.DeepEqual(&rgFrom, rgf) { + f = true + break + } + } + for _, rgTo := range rg.Spec.To { + if reflect.DeepEqual(&rgTo, rgt) { + t = true + break + } + } + if f && t { + return true + } + } + return false +} diff --git a/internal/webhooks/webhook_utils_test.go b/internal/webhooks/webhook_utils_test.go index edf69ce..f8885b2 100644 --- a/internal/webhooks/webhook_utils_test.go +++ b/internal/webhooks/webhook_utils_test.go @@ -5,11 +5,12 @@ import ( "testing" "github.com/f5devcentral/bigip-kubernetes-gateway/internal/pkg" + "github.com/f5devcentral/f5-bigip-rest-go/utils" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "github.com/f5devcentral/f5-bigip-rest-go/utils" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" ) @@ -594,3 +595,131 @@ var _ = Describe("validate*Types", func() { Expect(validateGatewayType(g, k)).ToNot(Succeed()) }) }) + +func Test_rgExists(t *testing.T) { + rgObj := gatewayv1beta1.ReferenceGrant{ + TypeMeta: metav1.TypeMeta{ + Kind: string(rgKind), + APIVersion: group + "/" + version, + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: nsDefault, + Name: "myreferencegrant", + }, + Spec: gatewayv1beta1.ReferenceGrantSpec{ + From: []gatewayv1beta1.ReferenceGrantFrom{ + { + Group: gatewayv1beta1.Group(group), + Kind: gatewayv1beta1.Kind(gwKind), + Namespace: gatewayv1beta1.Namespace(nsDefault), + }, + }, + To: []gatewayv1beta1.ReferenceGrantTo{ + { + Group: v1.GroupName, + Kind: gatewayv1beta1.Kind(scrtKind), + }, + }, + }, + } + rgList := gatewayv1beta1.ReferenceGrantList{Items: []gatewayv1beta1.ReferenceGrant{rgObj}} + + type args struct { + rgs *gatewayv1beta1.ReferenceGrantList + rgf *gatewayv1beta1.ReferenceGrantFrom + rgt *gatewayv1beta1.ReferenceGrantTo + } + tests := []struct { + name string + args args + want bool + }{ + // TODO: Add test cases. + { + name: "normal", + args: args{ + rgs: &rgList, + rgf: &rgObj.Spec.From[0], + rgt: &rgObj.Spec.To[0], + }, + want: true, + }, + { + name: "normal all", + args: args{ + rgs: &rgList, + rgf: &rgObj.Spec.From[0], + rgt: &gatewayv1beta1.ReferenceGrantTo{ + Group: "abc", + Kind: gatewayv1beta1.Kind(scrtKind), + }, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := rgExists(tt.args.rgs, tt.args.rgf, tt.args.rgt); got != tt.want { + t.Errorf("rgExists() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_canRefer(t *testing.T) { + rgObj := gatewayv1beta1.ReferenceGrant{ + TypeMeta: metav1.TypeMeta{ + Kind: string(rgKind), + APIVersion: group + "/" + version, + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: nsDefault, + Name: "myreferencegrant", + }, + Spec: gatewayv1beta1.ReferenceGrantSpec{ + From: []gatewayv1beta1.ReferenceGrantFrom{ + { + Group: gatewayv1beta1.Group(group), + Kind: gatewayv1beta1.Kind(gwKind), + Namespace: gatewayv1beta1.Namespace(nsDefault), + }, + }, + To: []gatewayv1beta1.ReferenceGrantTo{ + { + Group: v1.GroupName, + Kind: gatewayv1beta1.Kind(scrtKind), + }, + }, + }, + } + rgList := gatewayv1beta1.ReferenceGrantList{Items: []gatewayv1beta1.ReferenceGrant{rgObj}} + + type args struct { + rgs *gatewayv1beta1.ReferenceGrantList + from client.Object + to client.Object + } + tests := []struct { + name string + args args + want bool + }{ + // TODO: Add test cases. + { + name: "normal", + args: args{ + rgs: &rgList, + from: gwObj, + to: scrtObj, + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := canRefer(tt.args.rgs, tt.args.from, tt.args.to); got != tt.want { + t.Errorf("canRefer() = %v, want %v", got, tt.want) + } + }) + } +}