sigs.k8s.io/external-dns@v0.14.1/source/gateway.go (about)

     1  /*
     2  Copyright 2021 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package source
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"net/netip"
    23  	"sort"
    24  	"strings"
    25  	"text/template"
    26  
    27  	log "github.com/sirupsen/logrus"
    28  	corev1 "k8s.io/api/core/v1"
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  	"k8s.io/apimachinery/pkg/labels"
    31  	"k8s.io/apimachinery/pkg/types"
    32  	"k8s.io/apimachinery/pkg/util/wait"
    33  	kubeinformers "k8s.io/client-go/informers"
    34  	coreinformers "k8s.io/client-go/informers/core/v1"
    35  	cache "k8s.io/client-go/tools/cache"
    36  	v1 "sigs.k8s.io/gateway-api/apis/v1"
    37  	gateway "sigs.k8s.io/gateway-api/pkg/client/clientset/versioned"
    38  	informers "sigs.k8s.io/gateway-api/pkg/client/informers/externalversions"
    39  	informers_v1 "sigs.k8s.io/gateway-api/pkg/client/informers/externalversions/apis/v1"
    40  
    41  	"sigs.k8s.io/external-dns/endpoint"
    42  )
    43  
    44  const (
    45  	gatewayGroup = "gateway.networking.k8s.io"
    46  	gatewayKind  = "Gateway"
    47  )
    48  
    49  type gatewayRoute interface {
    50  	// Object returns the underlying route object to be used by templates.
    51  	Object() kubeObject
    52  	// Metadata returns the route's metadata.
    53  	Metadata() *metav1.ObjectMeta
    54  	// Hostnames returns the route's specified hostnames.
    55  	Hostnames() []v1.Hostname
    56  	// Protocol returns the route's protocol type.
    57  	Protocol() v1.ProtocolType
    58  	// RouteStatus returns the route's common status.
    59  	RouteStatus() v1.RouteStatus
    60  }
    61  
    62  type newGatewayRouteInformerFunc func(informers.SharedInformerFactory) gatewayRouteInformer
    63  
    64  type gatewayRouteInformer interface {
    65  	List(namespace string, selector labels.Selector) ([]gatewayRoute, error)
    66  	Informer() cache.SharedIndexInformer
    67  }
    68  
    69  func newGatewayInformerFactory(client gateway.Interface, namespace string, labelSelector labels.Selector) informers.SharedInformerFactory {
    70  	var opts []informers.SharedInformerOption
    71  	if namespace != "" {
    72  		opts = append(opts, informers.WithNamespace(namespace))
    73  	}
    74  	if labelSelector != nil && !labelSelector.Empty() {
    75  		lbls := labelSelector.String()
    76  		opts = append(opts, informers.WithTweakListOptions(func(o *metav1.ListOptions) {
    77  			o.LabelSelector = lbls
    78  		}))
    79  	}
    80  	return informers.NewSharedInformerFactoryWithOptions(client, 0, opts...)
    81  }
    82  
    83  type gatewayRouteSource struct {
    84  	gwNamespace string
    85  	gwLabels    labels.Selector
    86  	gwInformer  informers_v1.GatewayInformer
    87  
    88  	rtKind        string
    89  	rtNamespace   string
    90  	rtLabels      labels.Selector
    91  	rtAnnotations labels.Selector
    92  	rtInformer    gatewayRouteInformer
    93  
    94  	nsInformer coreinformers.NamespaceInformer
    95  
    96  	fqdnTemplate             *template.Template
    97  	combineFQDNAnnotation    bool
    98  	ignoreHostnameAnnotation bool
    99  }
   100  
   101  func newGatewayRouteSource(clients ClientGenerator, config *Config, kind string, newInformerFn newGatewayRouteInformerFunc) (Source, error) {
   102  	ctx := context.TODO()
   103  
   104  	gwLabels, err := getLabelSelector(config.GatewayLabelFilter)
   105  	if err != nil {
   106  		return nil, err
   107  	}
   108  	rtLabels := config.LabelFilter
   109  	if rtLabels == nil {
   110  		rtLabels = labels.Everything()
   111  	}
   112  	rtAnnotations, err := getLabelSelector(config.AnnotationFilter)
   113  	if err != nil {
   114  		return nil, err
   115  	}
   116  	tmpl, err := parseTemplate(config.FQDNTemplate)
   117  	if err != nil {
   118  		return nil, err
   119  	}
   120  
   121  	client, err := clients.GatewayClient()
   122  	if err != nil {
   123  		return nil, err
   124  	}
   125  
   126  	informerFactory := newGatewayInformerFactory(client, config.GatewayNamespace, gwLabels)
   127  	gwInformer := informerFactory.Gateway().V1().Gateways() // TODO: Gateway informer should be shared across gateway sources.
   128  	gwInformer.Informer()                                   // Register with factory before starting.
   129  
   130  	rtInformerFactory := informerFactory
   131  	if config.Namespace != config.GatewayNamespace || !selectorsEqual(rtLabels, gwLabels) {
   132  		rtInformerFactory = newGatewayInformerFactory(client, config.Namespace, rtLabels)
   133  	}
   134  	rtInformer := newInformerFn(rtInformerFactory)
   135  	rtInformer.Informer() // Register with factory before starting.
   136  
   137  	kubeClient, err := clients.KubeClient()
   138  	if err != nil {
   139  		return nil, err
   140  	}
   141  
   142  	kubeInformerFactory := kubeinformers.NewSharedInformerFactory(kubeClient, 0)
   143  	nsInformer := kubeInformerFactory.Core().V1().Namespaces() // TODO: Namespace informer should be shared across gateway sources.
   144  	nsInformer.Informer()                                      // Register with factory before starting.
   145  
   146  	informerFactory.Start(wait.NeverStop)
   147  	kubeInformerFactory.Start(wait.NeverStop)
   148  	if rtInformerFactory != informerFactory {
   149  		rtInformerFactory.Start(wait.NeverStop)
   150  
   151  		if err := waitForCacheSync(ctx, rtInformerFactory); err != nil {
   152  			return nil, err
   153  		}
   154  	}
   155  	if err := waitForCacheSync(ctx, informerFactory); err != nil {
   156  		return nil, err
   157  	}
   158  	if err := waitForCacheSync(ctx, kubeInformerFactory); err != nil {
   159  		return nil, err
   160  	}
   161  
   162  	src := &gatewayRouteSource{
   163  		gwNamespace: config.GatewayNamespace,
   164  		gwLabels:    gwLabels,
   165  		gwInformer:  gwInformer,
   166  
   167  		rtKind:        kind,
   168  		rtNamespace:   config.Namespace,
   169  		rtLabels:      rtLabels,
   170  		rtAnnotations: rtAnnotations,
   171  		rtInformer:    rtInformer,
   172  
   173  		nsInformer: nsInformer,
   174  
   175  		fqdnTemplate:             tmpl,
   176  		combineFQDNAnnotation:    config.CombineFQDNAndAnnotation,
   177  		ignoreHostnameAnnotation: config.IgnoreHostnameAnnotation,
   178  	}
   179  	return src, nil
   180  }
   181  
   182  func (src *gatewayRouteSource) AddEventHandler(ctx context.Context, handler func()) {
   183  	log.Debugf("Adding event handlers for %s", src.rtKind)
   184  	eventHandler := eventHandlerFunc(handler)
   185  	src.gwInformer.Informer().AddEventHandler(eventHandler)
   186  	src.rtInformer.Informer().AddEventHandler(eventHandler)
   187  	src.nsInformer.Informer().AddEventHandler(eventHandler)
   188  }
   189  
   190  func (src *gatewayRouteSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) {
   191  	var endpoints []*endpoint.Endpoint
   192  	routes, err := src.rtInformer.List(src.rtNamespace, src.rtLabels)
   193  	if err != nil {
   194  		return nil, err
   195  	}
   196  	gateways, err := src.gwInformer.Lister().Gateways(src.gwNamespace).List(src.gwLabels)
   197  	if err != nil {
   198  		return nil, err
   199  	}
   200  	namespaces, err := src.nsInformer.Lister().List(labels.Everything())
   201  	if err != nil {
   202  		return nil, err
   203  	}
   204  	kind := strings.ToLower(src.rtKind)
   205  	resolver := newGatewayRouteResolver(src, gateways, namespaces)
   206  	for _, rt := range routes {
   207  		// Filter by annotations.
   208  		meta := rt.Metadata()
   209  		annots := meta.Annotations
   210  		if !src.rtAnnotations.Matches(labels.Set(annots)) {
   211  			continue
   212  		}
   213  
   214  		// Check controller annotation to see if we are responsible.
   215  		if v, ok := annots[controllerAnnotationKey]; ok && v != controllerAnnotationValue {
   216  			log.Debugf("Skipping %s %s/%s because controller value does not match, found: %s, required: %s",
   217  				src.rtKind, meta.Namespace, meta.Name, v, controllerAnnotationValue)
   218  			continue
   219  		}
   220  
   221  		// Get Route hostnames and their targets.
   222  		hostTargets, err := resolver.resolve(rt)
   223  		if err != nil {
   224  			return nil, err
   225  		}
   226  		if len(hostTargets) == 0 {
   227  			log.Debugf("No endpoints could be generated from %s %s/%s", src.rtKind, meta.Namespace, meta.Name)
   228  			continue
   229  		}
   230  
   231  		// Create endpoints from hostnames and targets.
   232  		resource := fmt.Sprintf("%s/%s/%s", kind, meta.Namespace, meta.Name)
   233  		providerSpecific, setIdentifier := getProviderSpecificAnnotations(annots)
   234  		ttl := getTTLFromAnnotations(annots, resource)
   235  		for host, targets := range hostTargets {
   236  			endpoints = append(endpoints, endpointsForHostname(host, targets, ttl, providerSpecific, setIdentifier, resource)...)
   237  		}
   238  		log.Debugf("Endpoints generated from %s %s/%s: %v", src.rtKind, meta.Namespace, meta.Name, endpoints)
   239  	}
   240  	return endpoints, nil
   241  }
   242  
   243  func namespacedName(namespace, name string) types.NamespacedName {
   244  	return types.NamespacedName{Namespace: namespace, Name: name}
   245  }
   246  
   247  type gatewayRouteResolver struct {
   248  	src *gatewayRouteSource
   249  	gws map[types.NamespacedName]gatewayListeners
   250  	nss map[string]*corev1.Namespace
   251  }
   252  
   253  type gatewayListeners struct {
   254  	gateway   *v1.Gateway
   255  	listeners map[v1.SectionName][]v1.Listener
   256  }
   257  
   258  func newGatewayRouteResolver(src *gatewayRouteSource, gateways []*v1.Gateway, namespaces []*corev1.Namespace) *gatewayRouteResolver {
   259  	// Create Gateway Listener lookup table.
   260  	gws := make(map[types.NamespacedName]gatewayListeners, len(gateways))
   261  	for _, gw := range gateways {
   262  		lss := make(map[v1.SectionName][]v1.Listener, len(gw.Spec.Listeners)+1)
   263  		for i, lis := range gw.Spec.Listeners {
   264  			lss[lis.Name] = gw.Spec.Listeners[i : i+1]
   265  		}
   266  		lss[""] = gw.Spec.Listeners
   267  		gws[namespacedName(gw.Namespace, gw.Name)] = gatewayListeners{
   268  			gateway:   gw,
   269  			listeners: lss,
   270  		}
   271  	}
   272  	// Create Namespace lookup table.
   273  	nss := make(map[string]*corev1.Namespace, len(namespaces))
   274  	for _, ns := range namespaces {
   275  		nss[ns.Name] = ns
   276  	}
   277  	return &gatewayRouteResolver{
   278  		src: src,
   279  		gws: gws,
   280  		nss: nss,
   281  	}
   282  }
   283  
   284  func (c *gatewayRouteResolver) resolve(rt gatewayRoute) (map[string]endpoint.Targets, error) {
   285  	rtHosts, err := c.hosts(rt)
   286  	if err != nil {
   287  		return nil, err
   288  	}
   289  	hostTargets := make(map[string]endpoint.Targets)
   290  
   291  	meta := rt.Metadata()
   292  	for _, rps := range rt.RouteStatus().Parents {
   293  		// Confirm the Parent is the standard Gateway kind.
   294  		ref := rps.ParentRef
   295  		group := strVal((*string)(ref.Group), gatewayGroup)
   296  		kind := strVal((*string)(ref.Kind), gatewayKind)
   297  		if group != gatewayGroup || kind != gatewayKind {
   298  			log.Debugf("Unsupported parent %s/%s for %s %s/%s", group, kind, c.src.rtKind, meta.Namespace, meta.Name)
   299  			continue
   300  		}
   301  		// Lookup the Gateway and its Listeners.
   302  		namespace := strVal((*string)(ref.Namespace), meta.Namespace)
   303  		gw, ok := c.gws[namespacedName(namespace, string(ref.Name))]
   304  		if !ok {
   305  			log.Debugf("Gateway %s/%s not found for %s %s/%s", namespace, ref.Name, c.src.rtKind, meta.Namespace, meta.Name)
   306  			continue
   307  		}
   308  		// Confirm the Gateway has accepted the Route.
   309  		if !gwRouteIsAccepted(rps.Conditions) {
   310  			log.Debugf("Gateway %s/%s has not accepted %s %s/%s", namespace, ref.Name, c.src.rtKind, meta.Namespace, meta.Name)
   311  			continue
   312  		}
   313  		// Match the Route to all possible Listeners.
   314  		match := false
   315  		section := sectionVal(ref.SectionName, "")
   316  		listeners := gw.listeners[section]
   317  		for i := range listeners {
   318  			lis := &listeners[i]
   319  			// Confirm that the Listener and Route protocols match.
   320  			if !gwProtocolMatches(rt.Protocol(), lis.Protocol) {
   321  				continue
   322  			}
   323  			// Confirm that the Listener and Route ports match, if specified.
   324  			// EXPERIMENTAL: https://gateway-api.sigs.k8s.io/geps/gep-957/
   325  			if ref.Port != nil && *ref.Port != lis.Port {
   326  				continue
   327  			}
   328  			// Confirm that the Listener allows the Route (based on namespace and kind).
   329  			if !c.routeIsAllowed(gw.gateway, lis, rt) {
   330  				continue
   331  			}
   332  			// Find all overlapping hostnames between the Route and Listener.
   333  			// For {TCP,UDP}Routes, all annotation-generated hostnames should match since the Listener doesn't specify a hostname.
   334  			// For {HTTP,TLS}Routes, hostnames (including any annotation-generated) will be required to match any Listeners specified hostname.
   335  			gwHost := ""
   336  			if lis.Hostname != nil {
   337  				gwHost = string(*lis.Hostname)
   338  			}
   339  			for _, rtHost := range rtHosts {
   340  				if gwHost == "" && rtHost == "" {
   341  					// For {HTTP,TLS}Routes, this means the Route and the Listener both allow _any_ hostnames.
   342  					// For {TCP,UDP}Routes, this should always happen since neither specifies hostnames.
   343  					continue
   344  				}
   345  				host, ok := gwMatchingHost(gwHost, rtHost)
   346  				if !ok {
   347  					continue
   348  				}
   349  				override := getTargetsFromTargetAnnotation(gw.gateway.Annotations)
   350  				hostTargets[host] = append(hostTargets[host], override...)
   351  				if len(override) == 0 {
   352  					for _, addr := range gw.gateway.Status.Addresses {
   353  						hostTargets[host] = append(hostTargets[host], addr.Value)
   354  					}
   355  				}
   356  				match = true
   357  			}
   358  		}
   359  		if !match {
   360  			log.Debugf("Gateway %s/%s section %q does not match %s %s/%s hostnames %q", namespace, ref.Name, section, c.src.rtKind, meta.Namespace, meta.Name, rtHosts)
   361  		}
   362  	}
   363  	// If a Gateway has multiple matching Listeners for the same host, then we'll
   364  	// add its IPs to the target list multiple times and should dedupe them.
   365  	for host, targets := range hostTargets {
   366  		hostTargets[host] = uniqueTargets(targets)
   367  	}
   368  	return hostTargets, nil
   369  }
   370  
   371  func (c *gatewayRouteResolver) hosts(rt gatewayRoute) ([]string, error) {
   372  	var hostnames []string
   373  	for _, name := range rt.Hostnames() {
   374  		hostnames = append(hostnames, string(name))
   375  	}
   376  	// TODO: The ignore-hostname-annotation flag help says "valid only when using fqdn-template"
   377  	// but other sources don't check if fqdn-template is set. Which should it be?
   378  	if !c.src.ignoreHostnameAnnotation {
   379  		hostnames = append(hostnames, getHostnamesFromAnnotations(rt.Metadata().Annotations)...)
   380  	}
   381  	// TODO: The combine-fqdn-annotation flag is similarly vague.
   382  	if c.src.fqdnTemplate != nil && (len(hostnames) == 0 || c.src.combineFQDNAnnotation) {
   383  		hosts, err := execTemplate(c.src.fqdnTemplate, rt.Object())
   384  		if err != nil {
   385  			return nil, err
   386  		}
   387  		hostnames = append(hostnames, hosts...)
   388  	}
   389  	// This means that the route doesn't specify a hostname and should use any provided by
   390  	// attached Gateway Listeners. This is only useful for {HTTP,TLS}Routes, but it doesn't
   391  	// break {TCP,UDP}Routes.
   392  	if len(rt.Hostnames()) == 0 {
   393  		hostnames = append(hostnames, "")
   394  	}
   395  	return hostnames, nil
   396  }
   397  
   398  func (c *gatewayRouteResolver) routeIsAllowed(gw *v1.Gateway, lis *v1.Listener, rt gatewayRoute) bool {
   399  	meta := rt.Metadata()
   400  	allow := lis.AllowedRoutes
   401  
   402  	// Check the route's namespace.
   403  	from := v1.NamespacesFromSame
   404  	if allow != nil && allow.Namespaces != nil && allow.Namespaces.From != nil {
   405  		from = *allow.Namespaces.From
   406  	}
   407  	switch from {
   408  	case v1.NamespacesFromAll:
   409  		// OK
   410  	case v1.NamespacesFromSame:
   411  		if gw.Namespace != meta.Namespace {
   412  			return false
   413  		}
   414  	case v1.NamespacesFromSelector:
   415  		selector, err := metav1.LabelSelectorAsSelector(allow.Namespaces.Selector)
   416  		if err != nil {
   417  			log.Debugf("Gateway %s/%s section %q has invalid namespace selector: %v", gw.Namespace, gw.Name, lis.Name, err)
   418  			return false
   419  		}
   420  		// Get namespace.
   421  		ns, ok := c.nss[meta.Namespace]
   422  		if !ok {
   423  			log.Errorf("Namespace not found for %s %s/%s", c.src.rtKind, meta.Namespace, meta.Name)
   424  			return false
   425  		}
   426  		if !selector.Matches(labels.Set(ns.Labels)) {
   427  			return false
   428  		}
   429  	default:
   430  		log.Debugf("Gateway %s/%s section %q has unknown namespace from %q", gw.Namespace, gw.Name, lis.Name, from)
   431  		return false
   432  	}
   433  
   434  	// Check the route's kind, if any are specified by the listener.
   435  	// TODO: Do we need to consider SupportedKinds in the ListenerStatus instead of the Spec?
   436  	// We only support core kinds and already check the protocol... Does this matter at all?
   437  	if allow == nil || len(allow.Kinds) == 0 {
   438  		return true
   439  	}
   440  	gvk := rt.Object().GetObjectKind().GroupVersionKind()
   441  	for _, gk := range allow.Kinds {
   442  		group := strVal((*string)(gk.Group), gatewayGroup)
   443  		if gvk.Group == group && gvk.Kind == string(gk.Kind) {
   444  			return true
   445  		}
   446  	}
   447  	return false
   448  }
   449  
   450  func gwRouteIsAccepted(conds []metav1.Condition) bool {
   451  	for _, c := range conds {
   452  		if v1.RouteConditionType(c.Type) == v1.RouteConditionAccepted {
   453  			return c.Status == metav1.ConditionTrue
   454  		}
   455  	}
   456  	return false
   457  }
   458  
   459  func uniqueTargets(targets endpoint.Targets) endpoint.Targets {
   460  	if len(targets) < 2 {
   461  		return targets
   462  	}
   463  	sort.Strings([]string(targets))
   464  	prev := targets[0]
   465  	n := 1
   466  	for _, v := range targets[1:] {
   467  		if v == prev {
   468  			continue
   469  		}
   470  		prev = v
   471  		targets[n] = v
   472  		n++
   473  	}
   474  	return targets[:n]
   475  }
   476  
   477  // gwProtocolMatches returns whether a and b are the same protocol,
   478  // where HTTP and HTTPS are considered the same.
   479  // and TLS and TCP are considered the same.
   480  func gwProtocolMatches(a, b v1.ProtocolType) bool {
   481  	if a == v1.HTTPSProtocolType {
   482  		a = v1.HTTPProtocolType
   483  	}
   484  	if b == v1.HTTPSProtocolType {
   485  		b = v1.HTTPProtocolType
   486  	}
   487  	// if Listener is TLS and Route is TCP set Listener type to TCP as to pass true and return valid match
   488  	if a == v1.TCPProtocolType && b == v1.TLSProtocolType {
   489  		b = v1.TCPProtocolType
   490  	}
   491  	return a == b
   492  }
   493  
   494  // gwMatchingHost returns the most-specific overlapping host and a bool indicating if one was found.
   495  // Hostnames that are prefixed with a wildcard label (`*.`) are interpreted as a suffix match.
   496  // That means that "*.example.com" would match both "test.example.com" and "foo.test.example.com",
   497  // but not "example.com". An empty string matches anything.
   498  func gwMatchingHost(a, b string) (string, bool) {
   499  	var ok bool
   500  	if a, ok = gwHost(a); !ok {
   501  		return "", false
   502  	}
   503  	if b, ok = gwHost(b); !ok {
   504  		return "", false
   505  	}
   506  
   507  	if a == "" {
   508  		return b, true
   509  	}
   510  	if b == "" || a == b {
   511  		return a, true
   512  	}
   513  	if na, nb := len(a), len(b); nb < na || (na == nb && strings.HasPrefix(b, "*.")) {
   514  		a, b = b, a
   515  	}
   516  	if strings.HasPrefix(a, "*.") && strings.HasSuffix(b, a[1:]) {
   517  		return b, true
   518  	}
   519  	return "", false
   520  }
   521  
   522  // gwHost returns the canonical host and a value indicating if it's valid.
   523  func gwHost(host string) (string, bool) {
   524  	if host == "" {
   525  		return "", true
   526  	}
   527  	if isIPAddr(host) || !isDNS1123Domain(strings.TrimPrefix(host, "*.")) {
   528  		return "", false
   529  	}
   530  	return toLowerCaseASCII(host), true
   531  }
   532  
   533  // isIPAddr returns whether s in an IP address.
   534  func isIPAddr(s string) bool {
   535  	_, err := netip.ParseAddr(s)
   536  	return err == nil
   537  }
   538  
   539  // isDNS1123Domain returns whether s is a valid domain name according to RFC 1123.
   540  func isDNS1123Domain(s string) bool {
   541  	if n := len(s); n == 0 || n > 255 {
   542  		return false
   543  	}
   544  	for lbl, rest := "", s; rest != ""; {
   545  		if lbl, rest, _ = strings.Cut(rest, "."); !isDNS1123Label(lbl) {
   546  			return false
   547  		}
   548  	}
   549  	return true
   550  }
   551  
   552  // isDNS1123Label returns whether s is a valid domain label according to RFC 1123.
   553  func isDNS1123Label(s string) bool {
   554  	n := len(s)
   555  	if n == 0 || n > 63 {
   556  		return false
   557  	}
   558  	if !isAlphaNum(s[0]) || !isAlphaNum(s[n-1]) {
   559  		return false
   560  	}
   561  	for i, k := 1, n-1; i < k; i++ {
   562  		if b := s[i]; b != '-' && !isAlphaNum(b) {
   563  			return false
   564  		}
   565  	}
   566  	return true
   567  }
   568  
   569  func isAlphaNum(b byte) bool {
   570  	switch {
   571  	case 'a' <= b && b <= 'z',
   572  		'A' <= b && b <= 'Z',
   573  		'0' <= b && b <= '9':
   574  		return true
   575  	default:
   576  		return false
   577  	}
   578  }
   579  
   580  func strVal(ptr *string, def string) string {
   581  	if ptr == nil || *ptr == "" {
   582  		return def
   583  	}
   584  	return *ptr
   585  }
   586  
   587  func sectionVal(ptr *v1.SectionName, def v1.SectionName) v1.SectionName {
   588  	if ptr == nil || *ptr == "" {
   589  		return def
   590  	}
   591  	return *ptr
   592  }
   593  
   594  func selectorsEqual(a, b labels.Selector) bool {
   595  	if a == nil || b == nil {
   596  		return a == b
   597  	}
   598  	aReq, aOK := a.DeepCopySelector().Requirements()
   599  	bReq, bOK := b.DeepCopySelector().Requirements()
   600  	if aOK != bOK || len(aReq) != len(bReq) {
   601  		return false
   602  	}
   603  	sort.Stable(labels.ByKey(aReq))
   604  	sort.Stable(labels.ByKey(bReq))
   605  	for i, r := range aReq {
   606  		if !r.Equal(bReq[i]) {
   607  			return false
   608  		}
   609  	}
   610  	return true
   611  }