github.com/telepresenceio/telepresence/v2@v2.20.0-pro.6.0.20240517030216-236ea954e789/pkg/agentmap/discorvery.go (about) 1 package agentmap 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "regexp" 8 9 appsv1 "k8s.io/api/apps/v1" 10 core "k8s.io/api/core/v1" 11 k8sErrors "k8s.io/apimachinery/pkg/api/errors" 12 meta "k8s.io/apimachinery/pkg/apis/meta/v1" 13 "k8s.io/apimachinery/pkg/labels" 14 "k8s.io/apimachinery/pkg/util/intstr" 15 16 "github.com/datawire/dlib/dlog" 17 "github.com/datawire/k8sapi/pkg/k8sapi" 18 "github.com/telepresenceio/telepresence/v2/pkg/agentconfig" 19 "github.com/telepresenceio/telepresence/v2/pkg/informer" 20 ) 21 22 var ReplicaSetNameRx = regexp.MustCompile(`\A(.+)-[a-f0-9]+\z`) 23 24 func FindOwnerWorkload(ctx context.Context, obj k8sapi.Object) (k8sapi.Workload, error) { 25 dlog.Debugf(ctx, "FindOwnerWorkload(%s,%s,%s)", obj.GetName(), obj.GetNamespace(), obj.GetKind()) 26 lbs := obj.GetLabels() 27 if wlName, ok := lbs[agentconfig.WorkloadNameLabel]; ok { 28 return GetWorkload(ctx, wlName, obj.GetNamespace(), lbs[agentconfig.WorkloadKindLabel]) 29 } 30 refs := obj.GetOwnerReferences() 31 for i := range refs { 32 if or := &refs[i]; or.Controller != nil && *or.Controller { 33 if or.Kind == "ReplicaSet" { 34 // Try the common case first. Strip replicaset's generated hash and try to 35 // get the deployment. If this succeeds, we have saved us a replicaset 36 // lookup. 37 if m := ReplicaSetNameRx.FindStringSubmatch(or.Name); m != nil { 38 if wl, err := GetWorkload(ctx, m[1], obj.GetNamespace(), "Deployment"); err == nil { 39 return wl, nil 40 } 41 } 42 } 43 wl, err := GetWorkload(ctx, or.Name, obj.GetNamespace(), or.Kind) 44 if err != nil { 45 return nil, err 46 } 47 return FindOwnerWorkload(ctx, wl) 48 } 49 } 50 if wl, ok := obj.(k8sapi.Workload); ok { 51 return wl, nil 52 } 53 return nil, fmt.Errorf("unable to find workload owner for %s.%s", obj.GetName(), obj.GetNamespace()) 54 } 55 56 func GetWorkload(ctx context.Context, name, namespace, workloadKind string) (obj k8sapi.Workload, err error) { 57 dlog.Debugf(ctx, "GetWorkload(%s,%s,%s)", name, namespace, workloadKind) 58 switch workloadKind { 59 case "Deployment": 60 obj, err = getDeployment(ctx, name, namespace) 61 case "ReplicaSet": 62 obj, err = k8sapi.GetReplicaSet(ctx, name, namespace) 63 case "StatefulSet": 64 obj, err = k8sapi.GetStatefulSet(ctx, name, namespace) 65 case "": 66 for _, wk := range []string{"Deployment", "ReplicaSet", "StatefulSet"} { 67 if obj, err = GetWorkload(ctx, name, namespace, wk); err == nil { 68 return obj, nil 69 } 70 if !k8sErrors.IsNotFound(err) { 71 return nil, err 72 } 73 } 74 err = k8sErrors.NewNotFound(core.Resource("workload"), name+"."+namespace) 75 default: 76 return nil, k8sapi.UnsupportedWorkloadKindError(workloadKind) 77 } 78 return obj, err 79 } 80 81 func getDeployment(ctx context.Context, name, namespace string) (obj k8sapi.Workload, err error) { 82 if f := informer.GetFactory(ctx, namespace); f != nil { 83 var dep *appsv1.Deployment 84 dep, err = f.Apps().V1().Deployments().Lister().Deployments(namespace).Get(name) 85 if err == nil { 86 obj = k8sapi.Deployment(dep) 87 } 88 return obj, err 89 } 90 91 // This shouldn't happen really. 92 dlog.Debugf(ctx, "fetching deployment %s.%s using direct API call", name, namespace) 93 return k8sapi.GetDeployment(ctx, name, namespace) 94 } 95 96 func findServicesForPod(ctx context.Context, pod *core.PodTemplateSpec, svcName string) ([]k8sapi.Object, error) { 97 switch { 98 case svcName != "": 99 var svc *core.Service 100 var err error 101 if f := informer.GetFactory(ctx, pod.Namespace); f != nil { 102 svc, err = f.Core().V1().Services().Lister().Services(pod.Namespace).Get(svcName) 103 } else { 104 // This shouldn't happen really. 105 dlog.Debugf(ctx, "fetching service %s.%s using direct API call", svcName, pod.Namespace) 106 svc, err = k8sapi.GetK8sInterface(ctx).CoreV1().Services(pod.Namespace).Get(ctx, svcName, meta.GetOptions{}) 107 } 108 if err != nil { 109 if k8sErrors.IsNotFound(err) { 110 return nil, fmt.Errorf( 111 "unable to find service %s specified by annotation %s declared in pod %s.%s", 112 svcName, ServiceNameAnnotation, pod.Name, pod.Namespace) 113 } 114 return nil, err 115 } 116 return []k8sapi.Object{k8sapi.Service(svc)}, nil 117 case len(pod.Labels) > 0: 118 lbs := labels.Set(pod.Labels) 119 svcs, err := findServicesSelecting(ctx, pod.Namespace, lbs) 120 if err != nil { 121 return nil, err 122 } 123 if len(svcs) > 0 { 124 return svcs, nil 125 } 126 return nil, fmt.Errorf("unable to find services that selects pod %s.%s using labels %s", pod.Name, pod.Namespace, lbs) 127 default: 128 return nil, fmt.Errorf("unable to find a service using pod %s.%s because it has no labels", pod.Name, pod.Namespace) 129 } 130 } 131 132 type objectsStringer []k8sapi.Object 133 134 func (os objectsStringer) String() string { 135 b := bytes.Buffer{} 136 l := len(os) 137 if l == 0 { 138 return "no services" 139 } 140 for i, o := range os { 141 if i > 0 { 142 if l != 2 { 143 b.WriteString(", ") 144 } 145 if i == l-1 { 146 b.WriteString(" and ") 147 } 148 } 149 b.WriteString(o.GetName()) 150 } 151 return b.String() 152 } 153 154 // findServicesSelecting finds all services that has a selector that matches the given labels. 155 func findServicesSelecting(ctx context.Context, namespace string, lbs labels.Labels) ([]k8sapi.Object, error) { 156 var ms []k8sapi.Object 157 var scanned int 158 if f := informer.GetFactory(ctx, namespace); f != nil { 159 ss, err := f.Core().V1().Services().Lister().Services(namespace).List(labels.Everything()) 160 if err != nil { 161 return nil, err 162 } 163 scanned = len(ss) 164 for _, s := range ss { 165 sel := s.Spec.Selector 166 if len(sel) > 0 && labels.SelectorFromValidatedSet(sel).Matches(lbs) { 167 ms = append(ms, k8sapi.Service(s)) 168 } 169 } 170 } else { 171 // This shouldn't happen really. 172 dlog.Debugf(ctx, "Fetching services in %s using direct API call", namespace) 173 l, err := k8sapi.GetK8sInterface(ctx).CoreV1().Services(namespace).List(ctx, meta.ListOptions{}) 174 if err != nil { 175 return nil, err 176 } 177 items := l.Items 178 scanned = len(items) 179 for i := range items { 180 s := &items[i] 181 sel := s.Spec.Selector 182 if len(sel) > 0 && labels.SelectorFromValidatedSet(sel).Matches(lbs) { 183 ms = append(ms, k8sapi.Service(s)) 184 } 185 } 186 } 187 dlog.Debugf(ctx, "Scanned %d services in namespace %s and found that %s selects labels %v", scanned, namespace, objectsStringer(ms), lbs) 188 return ms, nil 189 } 190 191 // findContainerMatchingPort finds the container that matches the given ServicePort. The match is 192 // made using Protocol, and the Name or the ContainerPort field of each port in each container 193 // depending on if the service port is symbolic or numeric. The first container with a matching 194 // port is returned along with the index of the container port that matched. 195 // 196 // The first container with no ports at all is returned together with a port index of -1, in case 197 // no port match could be made and the service port is numeric. This enables intercepts of containers 198 // that indeed do listen a port but lack a matching port description in the manifest, which is what 199 // you get if you do: 200 // 201 // kubectl create deploy my-deploy --image my-image 202 // kubectl expose deploy my-deploy --port 80 --target-port 8080 203 func findContainerMatchingPort(port *core.ServicePort, cns []core.Container) (*core.Container, int) { 204 // The protocol of the targetPort must match the protocol of the containerPort because it is 205 // not illegal to listen with both TCP and UDP on the same port. 206 proto := core.ProtocolTCP 207 if port.Protocol != "" { 208 proto = port.Protocol 209 } 210 protoEqual := func(p core.Protocol) bool { 211 return p == proto || p == "" && proto == core.ProtocolTCP 212 } 213 214 if port.TargetPort.Type == intstr.String { 215 portName := port.TargetPort.StrVal 216 for ci := range cns { 217 cn := &cns[ci] 218 for pi := range cn.Ports { 219 p := &cn.Ports[pi] 220 if p.Name == portName && protoEqual(p.Protocol) { 221 return cn, pi 222 } 223 } 224 } 225 } else { 226 portNum := port.TargetPort.IntVal 227 if portNum == 0 { 228 // The targetPort default is the value of the port field. 229 portNum = port.Port 230 } 231 for ci := range cns { 232 cn := &cns[ci] 233 for pi := range cn.Ports { 234 p := &cn.Ports[pi] 235 if p.ContainerPort == portNum && protoEqual(p.Protocol) { 236 return cn, pi 237 } 238 } 239 } 240 // As a last resort, also consider containers that don't expose their ports at all. Those 241 // containers match all ports because it's unknown what they might be listening to. 242 for ci := range cns { 243 cn := &cns[ci] 244 if len(cn.Ports) == 0 { 245 return cn, -1 246 } 247 } 248 } 249 return nil, 0 250 } 251 252 // IsPodRunning returns true if at least one container has state Running and a non-zero StartedAt. 253 func IsPodRunning(pod *core.Pod) bool { 254 for _, cn := range pod.Status.ContainerStatuses { 255 if r := cn.State.Running; r != nil && !r.StartedAt.IsZero() { 256 // At least one container is running. 257 return true 258 } 259 } 260 return false 261 } 262 263 // AgentContainer returns the pod's traffic-agent container, or nil if the pod doesn't have a traffic-agent. 264 func AgentContainer(pod *core.Pod) *core.Container { 265 return containerByName(agentconfig.ContainerName, pod.Spec.Containers) 266 } 267 268 // InitContainer returns the pod's tel-agent-init init-container, or nil if the pod doesn't have a tel-agent-init. 269 func InitContainer(pod *core.Pod) *core.Container { 270 return containerByName(agentconfig.InitContainerName, pod.Spec.InitContainers) 271 } 272 273 func containerByName(name string, cns []core.Container) *core.Container { 274 for i := range cns { 275 cn := &cns[i] 276 if cn.Name == name { 277 return cn 278 } 279 } 280 return nil 281 }