github.com/netdata/go.d.plugin@v0.58.1/agent/discovery/sd/kubernetes/pod.go (about) 1 // SPDX-License-Identifier: GPL-3.0-or-later 2 3 package kubernetes 4 5 import ( 6 "context" 7 "fmt" 8 "net" 9 "strconv" 10 "strings" 11 12 "github.com/netdata/go.d.plugin/agent/discovery/sd/model" 13 "github.com/netdata/go.d.plugin/logger" 14 15 corev1 "k8s.io/api/core/v1" 16 "k8s.io/client-go/tools/cache" 17 "k8s.io/client-go/util/workqueue" 18 ) 19 20 type podTargetGroup struct { 21 targets []model.Target 22 source string 23 } 24 25 func (p podTargetGroup) Provider() string { return "sd:k8s:pod" } 26 func (p podTargetGroup) Source() string { return fmt.Sprintf("%s(%s)", p.Provider(), p.source) } 27 func (p podTargetGroup) Targets() []model.Target { return p.targets } 28 29 type PodTarget struct { 30 model.Base `hash:"ignore"` 31 32 hash uint64 33 tuid string 34 35 Address string 36 Namespace string 37 Name string 38 Annotations map[string]any 39 Labels map[string]any 40 NodeName string 41 PodIP string 42 ControllerName string 43 ControllerKind string 44 ContName string 45 Image string 46 Env map[string]any 47 Port string 48 PortName string 49 PortProtocol string 50 } 51 52 func (p PodTarget) Hash() uint64 { return p.hash } 53 func (p PodTarget) TUID() string { return p.tuid } 54 55 func newPodDiscoverer(pod, cmap, secret cache.SharedInformer) *podDiscoverer { 56 57 if pod == nil || cmap == nil || secret == nil { 58 panic("nil pod or cmap or secret informer") 59 } 60 61 queue := workqueue.NewWithConfig(workqueue.QueueConfig{Name: "pod"}) 62 63 _, _ = pod.AddEventHandler(cache.ResourceEventHandlerFuncs{ 64 AddFunc: func(obj any) { enqueue(queue, obj) }, 65 UpdateFunc: func(_, obj any) { enqueue(queue, obj) }, 66 DeleteFunc: func(obj any) { enqueue(queue, obj) }, 67 }) 68 69 return &podDiscoverer{ 70 Logger: log, 71 podInformer: pod, 72 cmapInformer: cmap, 73 secretInformer: secret, 74 queue: queue, 75 } 76 } 77 78 type podDiscoverer struct { 79 *logger.Logger 80 model.Base 81 82 podInformer cache.SharedInformer 83 cmapInformer cache.SharedInformer 84 secretInformer cache.SharedInformer 85 queue *workqueue.Type 86 } 87 88 func (p *podDiscoverer) String() string { 89 return "k8s pod" 90 } 91 92 func (p *podDiscoverer) Discover(ctx context.Context, in chan<- []model.TargetGroup) { 93 p.Info("instance is started") 94 defer p.Info("instance is stopped") 95 defer p.queue.ShutDown() 96 97 go p.podInformer.Run(ctx.Done()) 98 go p.cmapInformer.Run(ctx.Done()) 99 go p.secretInformer.Run(ctx.Done()) 100 101 if !cache.WaitForCacheSync(ctx.Done(), 102 p.podInformer.HasSynced, p.cmapInformer.HasSynced, p.secretInformer.HasSynced) { 103 p.Error("failed to sync caches") 104 return 105 } 106 107 go p.run(ctx, in) 108 109 <-ctx.Done() 110 } 111 112 func (p *podDiscoverer) run(ctx context.Context, in chan<- []model.TargetGroup) { 113 for { 114 item, shutdown := p.queue.Get() 115 if shutdown { 116 return 117 } 118 p.handleQueueItem(ctx, in, item) 119 } 120 } 121 122 func (p *podDiscoverer) handleQueueItem(ctx context.Context, in chan<- []model.TargetGroup, item any) { 123 defer p.queue.Done(item) 124 125 key := item.(string) 126 namespace, name, err := cache.SplitMetaNamespaceKey(key) 127 if err != nil { 128 return 129 } 130 131 obj, ok, err := p.podInformer.GetStore().GetByKey(key) 132 if err != nil { 133 return 134 } 135 136 if !ok { 137 tgg := &podTargetGroup{source: podSourceFromNsName(namespace, name)} 138 send(ctx, in, tgg) 139 return 140 } 141 142 pod, err := toPod(obj) 143 if err != nil { 144 return 145 } 146 147 tgg := p.buildTargetGroup(pod) 148 149 for _, tgt := range tgg.Targets() { 150 tgt.Tags().Merge(p.Tags()) 151 } 152 153 send(ctx, in, tgg) 154 155 } 156 157 func (p *podDiscoverer) buildTargetGroup(pod *corev1.Pod) model.TargetGroup { 158 if pod.Status.PodIP == "" || len(pod.Spec.Containers) == 0 { 159 return &podTargetGroup{ 160 source: podSource(pod), 161 } 162 } 163 return &podTargetGroup{ 164 source: podSource(pod), 165 targets: p.buildTargets(pod), 166 } 167 } 168 169 func (p *podDiscoverer) buildTargets(pod *corev1.Pod) (targets []model.Target) { 170 var name, kind string 171 for _, ref := range pod.OwnerReferences { 172 if ref.Controller != nil && *ref.Controller { 173 name = ref.Name 174 kind = ref.Kind 175 break 176 } 177 } 178 179 for _, container := range pod.Spec.Containers { 180 env := p.collectEnv(pod.Namespace, container) 181 182 if len(container.Ports) == 0 { 183 tgt := &PodTarget{ 184 tuid: podTUID(pod, container), 185 Address: pod.Status.PodIP, 186 Namespace: pod.Namespace, 187 Name: pod.Name, 188 Annotations: mapAny(pod.Annotations), 189 Labels: mapAny(pod.Labels), 190 NodeName: pod.Spec.NodeName, 191 PodIP: pod.Status.PodIP, 192 ControllerName: name, 193 ControllerKind: kind, 194 ContName: container.Name, 195 Image: container.Image, 196 Env: mapAny(env), 197 } 198 hash, err := calcHash(tgt) 199 if err != nil { 200 continue 201 } 202 tgt.hash = hash 203 204 targets = append(targets, tgt) 205 } else { 206 for _, port := range container.Ports { 207 portNum := strconv.FormatUint(uint64(port.ContainerPort), 10) 208 tgt := &PodTarget{ 209 tuid: podTUIDWithPort(pod, container, port), 210 Address: net.JoinHostPort(pod.Status.PodIP, portNum), 211 Namespace: pod.Namespace, 212 Name: pod.Name, 213 Annotations: mapAny(pod.Annotations), 214 Labels: mapAny(pod.Labels), 215 NodeName: pod.Spec.NodeName, 216 PodIP: pod.Status.PodIP, 217 ControllerName: name, 218 ControllerKind: kind, 219 ContName: container.Name, 220 Image: container.Image, 221 Env: mapAny(env), 222 Port: portNum, 223 PortName: port.Name, 224 PortProtocol: string(port.Protocol), 225 } 226 hash, err := calcHash(tgt) 227 if err != nil { 228 continue 229 } 230 tgt.hash = hash 231 232 targets = append(targets, tgt) 233 } 234 } 235 } 236 237 return targets 238 } 239 240 func (p *podDiscoverer) collectEnv(ns string, container corev1.Container) map[string]string { 241 vars := make(map[string]string) 242 243 // When a key exists in multiple sources, 244 // the value associated with the last source will take precedence. 245 // Values defined by an Env with a duplicate key will take precedence. 246 // 247 // Order (https://github.com/kubernetes/kubectl/blob/master/pkg/describe/describe.go) 248 // - envFrom: configMapRef, secretRef 249 // - env: value || valueFrom: fieldRef, resourceFieldRef, secretRef, configMap 250 251 for _, src := range container.EnvFrom { 252 switch { 253 case src.ConfigMapRef != nil: 254 p.envFromConfigMap(vars, ns, src) 255 case src.SecretRef != nil: 256 p.envFromSecret(vars, ns, src) 257 } 258 } 259 260 for _, env := range container.Env { 261 if env.Name == "" || isVar(env.Name) { 262 continue 263 } 264 switch { 265 case env.Value != "": 266 vars[env.Name] = env.Value 267 case env.ValueFrom != nil && env.ValueFrom.SecretKeyRef != nil: 268 p.valueFromSecret(vars, ns, env) 269 case env.ValueFrom != nil && env.ValueFrom.ConfigMapKeyRef != nil: 270 p.valueFromConfigMap(vars, ns, env) 271 } 272 } 273 274 if len(vars) == 0 { 275 return nil 276 } 277 return vars 278 } 279 280 func (p *podDiscoverer) valueFromConfigMap(vars map[string]string, ns string, env corev1.EnvVar) { 281 if env.ValueFrom.ConfigMapKeyRef.Name == "" || env.ValueFrom.ConfigMapKeyRef.Key == "" { 282 return 283 } 284 285 sr := env.ValueFrom.ConfigMapKeyRef 286 key := ns + "/" + sr.Name 287 288 item, exist, err := p.cmapInformer.GetStore().GetByKey(key) 289 if err != nil || !exist { 290 return 291 } 292 293 cmap, err := toConfigMap(item) 294 if err != nil { 295 return 296 } 297 298 if v, ok := cmap.Data[sr.Key]; ok { 299 vars[env.Name] = v 300 } 301 } 302 303 func (p *podDiscoverer) valueFromSecret(vars map[string]string, ns string, env corev1.EnvVar) { 304 if env.ValueFrom.SecretKeyRef.Name == "" || env.ValueFrom.SecretKeyRef.Key == "" { 305 return 306 } 307 308 secretKey := env.ValueFrom.SecretKeyRef 309 key := ns + "/" + secretKey.Name 310 311 item, exist, err := p.secretInformer.GetStore().GetByKey(key) 312 if err != nil || !exist { 313 return 314 } 315 316 secret, err := toSecret(item) 317 if err != nil { 318 return 319 } 320 321 if v, ok := secret.Data[secretKey.Key]; ok { 322 vars[env.Name] = string(v) 323 } 324 } 325 326 func (p *podDiscoverer) envFromConfigMap(vars map[string]string, ns string, src corev1.EnvFromSource) { 327 if src.ConfigMapRef.Name == "" { 328 return 329 } 330 331 key := ns + "/" + src.ConfigMapRef.Name 332 item, exist, err := p.cmapInformer.GetStore().GetByKey(key) 333 if err != nil || !exist { 334 return 335 } 336 337 cmap, err := toConfigMap(item) 338 if err != nil { 339 return 340 } 341 342 for k, v := range cmap.Data { 343 vars[src.Prefix+k] = v 344 } 345 } 346 347 func (p *podDiscoverer) envFromSecret(vars map[string]string, ns string, src corev1.EnvFromSource) { 348 if src.SecretRef.Name == "" { 349 return 350 } 351 352 key := ns + "/" + src.SecretRef.Name 353 item, exist, err := p.secretInformer.GetStore().GetByKey(key) 354 if err != nil || !exist { 355 return 356 } 357 358 secret, err := toSecret(item) 359 if err != nil { 360 return 361 } 362 363 for k, v := range secret.Data { 364 vars[src.Prefix+k] = string(v) 365 } 366 } 367 368 func podTUID(pod *corev1.Pod, container corev1.Container) string { 369 return fmt.Sprintf("%s_%s_%s", 370 pod.Namespace, 371 pod.Name, 372 container.Name, 373 ) 374 } 375 376 func podTUIDWithPort(pod *corev1.Pod, container corev1.Container, port corev1.ContainerPort) string { 377 return fmt.Sprintf("%s_%s_%s_%s_%s", 378 pod.Namespace, 379 pod.Name, 380 container.Name, 381 strings.ToLower(string(port.Protocol)), 382 strconv.FormatUint(uint64(port.ContainerPort), 10), 383 ) 384 } 385 386 func podSourceFromNsName(namespace, name string) string { 387 return namespace + "/" + name 388 } 389 390 func podSource(pod *corev1.Pod) string { 391 return podSourceFromNsName(pod.Namespace, pod.Name) 392 } 393 394 func toPod(obj any) (*corev1.Pod, error) { 395 pod, ok := obj.(*corev1.Pod) 396 if !ok { 397 return nil, fmt.Errorf("received unexpected object type: %T", obj) 398 } 399 return pod, nil 400 } 401 402 func toConfigMap(obj any) (*corev1.ConfigMap, error) { 403 cmap, ok := obj.(*corev1.ConfigMap) 404 if !ok { 405 return nil, fmt.Errorf("received unexpected object type: %T", obj) 406 } 407 return cmap, nil 408 } 409 410 func toSecret(obj any) (*corev1.Secret, error) { 411 secret, ok := obj.(*corev1.Secret) 412 if !ok { 413 return nil, fmt.Errorf("received unexpected object type: %T", obj) 414 } 415 return secret, nil 416 } 417 418 func isVar(name string) bool { 419 // Variable references $(VAR_NAME) are expanded using the previous defined 420 // environment variables in the container and any service environment 421 // variables. 422 return strings.IndexByte(name, '$') != -1 423 } 424 425 func mapAny(src map[string]string) map[string]any { 426 if src == nil { 427 return nil 428 } 429 m := make(map[string]any, len(src)) 430 for k, v := range src { 431 m[k] = v 432 } 433 return m 434 }