github.com/cilium/cilium@v1.16.2/pkg/dial/resolver.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // Copyright Authors of Cilium
     3  
     4  package dial
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"net/netip"
    10  	"strings"
    11  	"sync"
    12  
    13  	"github.com/cilium/hive/cell"
    14  	"github.com/cilium/hive/job"
    15  	k8serr "k8s.io/apimachinery/pkg/api/errors"
    16  	"k8s.io/apimachinery/pkg/types"
    17  
    18  	"github.com/cilium/cilium/pkg/k8s/resource"
    19  	slim_corev1 "github.com/cilium/cilium/pkg/k8s/slim/k8s/api/core/v1"
    20  )
    21  
    22  // ServiceResolverCell provides a ServiceResolver instance to map DNS names
    23  // matching Kubernetes services to the corresponding ClusterIP address, backed
    24  // by a lazy resource.Store, which is only started on first access.
    25  var ServiceResolverCell = cell.Module(
    26  	"service-resolver",
    27  	"Service DNS names to ClusterIP translator",
    28  
    29  	cell.Provide(newServiceResolver),
    30  )
    31  
    32  var _ Resolver = (*ServiceResolver)(nil)
    33  
    34  // ServiceResolver maps DNS names matching Kubernetes services to the
    35  // corresponding ClusterIP address.
    36  type ServiceResolver struct {
    37  	start func()
    38  	ready <-chan struct{}
    39  
    40  	store resource.Store[*slim_corev1.Service]
    41  }
    42  
    43  func newServiceResolver(jg job.Group, services resource.Resource[*slim_corev1.Service]) *ServiceResolver {
    44  	start := make(chan struct{})
    45  	ready := make(chan struct{})
    46  
    47  	sr := ServiceResolver{
    48  		start: sync.OnceFunc(func() { close(start) }),
    49  		ready: ready,
    50  	}
    51  
    52  	jg.Add(job.OneShot("service-reloader-initializer", func(ctx context.Context, health cell.Health) error {
    53  		select {
    54  		case <-ctx.Done():
    55  			return nil // We are shutting down
    56  		case <-start:
    57  		}
    58  
    59  		store, err := services.Store(ctx)
    60  		if err != nil {
    61  			return nil // We are shutting down
    62  		}
    63  
    64  		sr.store = store
    65  		health.OK("Synchronized")
    66  		close(ready)
    67  		return nil
    68  	}))
    69  
    70  	return &sr
    71  }
    72  
    73  func (sr *ServiceResolver) Resolve(ctx context.Context, host string) (string, error) {
    74  	nsname, err := ServiceURLToNamespacedName(host)
    75  	if err != nil {
    76  		// The host does not look like a k8s service DNS name
    77  		return "", err
    78  	}
    79  
    80  	sr.start()
    81  
    82  	select {
    83  	case <-ctx.Done():
    84  		// The context expired before the underlying store was ready
    85  		return "", ctx.Err()
    86  	case <-sr.ready:
    87  	}
    88  
    89  	svc, exists, err := sr.store.GetByKey(resource.Key{Namespace: nsname.Namespace, Name: nsname.Name})
    90  	switch {
    91  	case err != nil:
    92  		return "", err
    93  	case !exists:
    94  		return "", k8serr.NewNotFound(slim_corev1.Resource("service"), nsname.String())
    95  	}
    96  
    97  	if _, err := netip.ParseAddr(svc.Spec.ClusterIP); err != nil {
    98  		return "", fmt.Errorf("cannot parse ClusterIP address: %w", err)
    99  	}
   100  
   101  	return svc.Spec.ClusterIP, nil
   102  }
   103  
   104  func ServiceURLToNamespacedName(host string) (types.NamespacedName, error) {
   105  	tokens := strings.Split(host, ".")
   106  	if len(tokens) < 2 {
   107  		return types.NamespacedName{}, fmt.Errorf("%s does not match the <name>.<namespace>(.svc) form", host)
   108  	}
   109  
   110  	if len(tokens) >= 3 && tokens[2] != "svc" {
   111  		return types.NamespacedName{}, fmt.Errorf("%s does not match the <name>.<namespace>(.svc) form", host)
   112  	}
   113  
   114  	return types.NamespacedName{Namespace: tokens[1], Name: tokens[0]}, nil
   115  }