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 }