github.com/cilium/cilium@v1.16.2/pkg/hubble/metrics/api/context.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // Copyright Authors of Hubble 3 4 package api 5 6 import ( 7 "fmt" 8 "sort" 9 "strings" 10 11 "github.com/prometheus/client_golang/prometheus" 12 "k8s.io/utils/strings/slices" 13 14 pb "github.com/cilium/cilium/api/v1/flow" 15 k8sConst "github.com/cilium/cilium/pkg/k8s/apis/cilium.io" 16 ciliumLabels "github.com/cilium/cilium/pkg/labels" 17 ) 18 19 // ContextIdentifier describes the identification method of a transmission or 20 // receiving context 21 type ContextIdentifier int 22 23 const ( 24 // ContextDisabled disables context identification 25 ContextDisabled ContextIdentifier = iota 26 // ContextIdentity uses the full set of identity labels for identification purposes 27 ContextIdentity 28 // ContextNamespace uses the namespace name for identification purposes 29 ContextNamespace 30 // ContextPod uses the namespace and pod name for identification purposes in the form of namespace/pod-name. 31 ContextPod 32 // ContextPodName uses the pod name for identification purposes 33 ContextPodName 34 // ContextDNS uses the DNS name for identification purposes 35 ContextDNS 36 // ContextIP uses the IP address for identification purposes 37 ContextIP 38 // ContextReservedIdentity uses reserved labels in the identity label list for identification 39 // purpose. It uses "reserved:kube-apiserver" label if it's present in the identity label list. 40 // Otherwise, it uses the first label in the identity label list with "reserved:" prefix. 41 ContextReservedIdentity 42 // ContextWorkload uses the namespace and the pod's workload name for identification. 43 ContextWorkload 44 // ContextWorkloadName uses the pod's workload name for identification. 45 ContextWorkloadName 46 // ContextApp uses the pod's app label for identification. 47 ContextApp 48 ) 49 50 // ContextOptionsHelp is the help text for context options 51 const ContextOptionsHelp = ` 52 sourceContext ::= identifier , { "|", identifier } 53 destinationContext ::= identifier , { "|", identifier } 54 sourceEgressContext ::= identifier , { "|", identifier } 55 sourceIngressContext ::= identifier , { "|", identifier } 56 destinationEgressContext ::= identifier , { "|", identifier } 57 destinationIngressContext ::= identifier , { "|", identifier } 58 labels ::= label , { ",", label } 59 identifier ::= identity | namespace | pod | pod-name | dns | ip | reserved-identity | workload | workload-name | app 60 label ::= source_ip | source_pod | source_namespace | source_workload | source_workload_kind | source_app | destination_ip | destination_pod | destination_namespace | destination_workload | destination_workload_kind | destination_app | traffic_direction 61 ` 62 63 var ( 64 kubeAPIServerLabel = ciliumLabels.LabelKubeAPIServer.String() 65 // contextLabelsList defines available labels for the ContextLabels 66 // ContextIdentifier and the order of those labels for GetLabelNames and GetLabelValues. 67 contextLabelsList = []string{ 68 "source_ip", 69 "source_pod", 70 "source_namespace", 71 "source_workload", 72 "source_workload_kind", 73 "source_app", 74 "destination_ip", 75 "destination_pod", 76 "destination_namespace", 77 "destination_workload", 78 "destination_workload_kind", 79 "destination_app", 80 "traffic_direction", 81 } 82 allowedContextLabels = newLabelsSet(contextLabelsList) 83 84 podAppLabels = []string{ 85 // k8s recommend app label 86 ciliumLabels.LabelSourceK8s + ":" + k8sConst.AppKubernetes + "/name", 87 // legacy k8s app label 88 ciliumLabels.LabelSourceK8s + ":" + "k8s-app", 89 // app label that is often used before people realize there's a recommended 90 // label 91 ciliumLabels.LabelSourceK8s + ":" + "app", 92 } 93 ) 94 95 // String return the context identifier as string 96 func (c ContextIdentifier) String() string { 97 switch c { 98 case ContextDisabled: 99 return "disabled" 100 case ContextIdentity: 101 return "identity" 102 case ContextNamespace: 103 return "namespace" 104 case ContextPod: 105 return "pod" 106 case ContextDNS: 107 return "dns" 108 case ContextIP: 109 return "ip" 110 case ContextReservedIdentity: 111 return "reserved-identity" 112 case ContextWorkload: 113 return "workload" 114 case ContextWorkloadName: 115 return "workload-name" 116 case ContextApp: 117 return "app" 118 } 119 return fmt.Sprintf("%d", c) 120 } 121 122 type ContextIdentifierList []ContextIdentifier 123 124 func (cs ContextIdentifierList) String() string { 125 s := make([]string, 0, len(cs)) 126 for _, c := range cs { 127 s = append(s, c.String()) 128 } 129 return strings.Join(s, "|") 130 } 131 132 // ContextOptions is the set of options to define whether and how to include 133 // sending and/or receiving context information 134 type ContextOptions struct { 135 // Destination is the destination context to include in metrics for both egress and ingress traffic 136 Destination ContextIdentifierList 137 // Destination is the destination context to include in metrics for egress traffic (overrides Destination) 138 DestinationEgress ContextIdentifierList 139 // Destination is the destination context to include in metrics for ingress traffic (overrides Destination) 140 DestinationIngress ContextIdentifierList 141 142 allDestinationCtx ContextIdentifierList 143 144 // Source is the source context to include in metrics for both egress and ingress traffic 145 Source ContextIdentifierList 146 // Source is the source context to include in metrics for egress traffic (overrides Source) 147 SourceEgress ContextIdentifierList 148 // Source is the source context to include in metrics for ingress traffic (overrides Source) 149 SourceIngress ContextIdentifierList 150 151 allSourceCtx ContextIdentifierList 152 153 // Labels is the full set of labels that have been allowlisted when using the 154 // ContextLabels ContextIdentifier. 155 Labels labelsSet 156 } 157 158 func parseContextIdentifier(s string) (ContextIdentifier, error) { 159 switch strings.ToLower(s) { 160 case "identity": 161 return ContextIdentity, nil 162 case "namespace": 163 return ContextNamespace, nil 164 case "pod": 165 return ContextPod, nil 166 case "pod-name": 167 return ContextPodName, nil 168 case "dns": 169 return ContextDNS, nil 170 case "ip": 171 return ContextIP, nil 172 case "reserved-identity": 173 return ContextReservedIdentity, nil 174 case "workload": 175 return ContextWorkload, nil 176 case "workload-name": 177 return ContextWorkloadName, nil 178 case "app": 179 return ContextApp, nil 180 default: 181 return ContextDisabled, fmt.Errorf("unknown context '%s'", s) 182 } 183 } 184 185 func parseContext(s string) (cs ContextIdentifierList, err error) { 186 for _, v := range strings.Split(s, "|") { 187 c, err := parseContextIdentifier(v) 188 if err != nil { 189 return nil, err 190 } 191 cs = append(cs, c) 192 } 193 194 return cs, nil 195 } 196 197 func parseLabels(s string) (labelsSet, error) { 198 labels := strings.Split(s, ",") 199 for _, label := range labels { 200 if !allowedContextLabels.HasLabel(label) { 201 return labelsSet{}, fmt.Errorf("invalid labelsContext value: %s", label) 202 } 203 } 204 ls := newLabelsSet(labels) 205 return ls, nil 206 } 207 208 // ParseContextOptions parses a set of options and extracts the context 209 // relevant options 210 func ParseContextOptions(options Options) (*ContextOptions, error) { 211 o := &ContextOptions{} 212 var err error 213 for key, value := range options { 214 switch strings.ToLower(key) { 215 case "destinationcontext": 216 o.Destination, err = parseContext(value) 217 o.allDestinationCtx = append(o.allDestinationCtx, o.Destination...) 218 if err != nil { 219 return nil, err 220 } 221 case "destinationegresscontext": 222 o.DestinationEgress, err = parseContext(value) 223 o.allDestinationCtx = append(o.allDestinationCtx, o.DestinationEgress...) 224 if err != nil { 225 return nil, err 226 } 227 case "destinationingresscontext": 228 o.DestinationIngress, err = parseContext(value) 229 o.allDestinationCtx = append(o.allDestinationCtx, o.DestinationIngress...) 230 if err != nil { 231 return nil, err 232 } 233 case "sourcecontext": 234 o.Source, err = parseContext(value) 235 o.allSourceCtx = append(o.allSourceCtx, o.Source...) 236 if err != nil { 237 return nil, err 238 } 239 case "sourceegresscontext": 240 o.SourceEgress, err = parseContext(value) 241 o.allSourceCtx = append(o.allSourceCtx, o.SourceEgress...) 242 if err != nil { 243 return nil, err 244 } 245 case "sourceingresscontext": 246 o.SourceIngress, err = parseContext(value) 247 o.allSourceCtx = append(o.allSourceCtx, o.SourceIngress...) 248 if err != nil { 249 return nil, err 250 } 251 case "labelscontext": 252 o.Labels, err = parseLabels(value) 253 if err != nil { 254 return nil, err 255 } 256 } 257 } 258 259 return o, nil 260 } 261 262 type labelsSet map[string]struct{} 263 264 func newLabelsSet(labels []string) labelsSet { 265 m := make(map[string]struct{}, len(labels)) 266 for _, label := range labels { 267 m[label] = struct{}{} 268 } 269 return labelsSet(m) 270 } 271 272 func (ls labelsSet) HasLabel(label string) bool { 273 _, exists := ls[label] 274 return exists 275 } 276 277 func (ls labelsSet) String() string { 278 var b strings.Builder 279 // output the labels in a consistent order 280 for _, label := range contextLabelsList { 281 if ls.HasLabel(label) { 282 if b.Len() > 0 { 283 b.WriteString(",") 284 } 285 b.WriteString(label) 286 } 287 } 288 return b.String() 289 } 290 291 func labelsContext(invertSourceDestination bool, wantedLabels labelsSet, flow *pb.Flow) (outputLabels []string, err error) { 292 source, destination := flow.GetSource(), flow.GetDestination() 293 sourceIp, destinationIp := flow.GetIP().GetSource(), flow.GetIP().GetDestination() 294 if invertSourceDestination { 295 source, destination = flow.GetDestination(), flow.GetSource() 296 sourceIp, destinationIp = flow.GetIP().GetDestination(), flow.GetIP().GetSource() 297 } 298 // Iterate over contextLabelsList so that the label order is stable, 299 // otherwise GetLabelNames and GetLabelValues might be mismatched 300 for _, label := range contextLabelsList { 301 if wantedLabels.HasLabel(label) { 302 var labelValue string 303 switch label { 304 case "source_ip": 305 labelValue = sourceIp 306 case "source_pod": 307 labelValue = source.GetPodName() 308 case "source_namespace": 309 labelValue = source.GetNamespace() 310 case "source_workload": 311 if workloads := source.GetWorkloads(); len(workloads) != 0 { 312 labelValue = workloads[0].Name 313 } 314 case "source_workload_kind": 315 if workloads := source.GetWorkloads(); len(workloads) != 0 { 316 labelValue = workloads[0].Kind 317 } 318 case "source_app": 319 labelValue = getK8sAppFromLabels(source.GetLabels()) 320 case "destination_ip": 321 labelValue = destinationIp 322 case "destination_pod": 323 labelValue = destination.GetPodName() 324 case "destination_namespace": 325 labelValue = destination.GetNamespace() 326 case "destination_workload": 327 if workloads := destination.GetWorkloads(); len(workloads) != 0 { 328 labelValue = workloads[0].Name 329 } 330 case "destination_workload_kind": 331 if workloads := destination.GetWorkloads(); len(workloads) != 0 { 332 labelValue = workloads[0].Kind 333 } 334 case "destination_app": 335 labelValue = getK8sAppFromLabels(destination.GetLabels()) 336 case "traffic_direction": 337 direction := flow.GetTrafficDirection() 338 if direction == pb.TrafficDirection_TRAFFIC_DIRECTION_UNKNOWN { 339 labelValue = "unknown" 340 } else { 341 labelValue = strings.ToLower(direction.String()) 342 } 343 default: 344 // Label is in contextLabelsList but isn't handled in the switch 345 // statement. Programmer error. 346 return nil, fmt.Errorf("BUG: Label %s not mapped in labelsContext. Please report this bug to Cilium developers.", label) 347 } 348 outputLabels = append(outputLabels, labelValue) 349 } 350 } 351 return outputLabels, nil 352 } 353 354 func handleReservedIdentityLabels(lbls []string) string { 355 // if reserved:kube-apiserver label is present, return it (instead of reserved:world, etc..) 356 if slices.Contains(lbls, kubeAPIServerLabel) { 357 return kubeAPIServerLabel 358 } 359 // else return the first reserved label. 360 for _, label := range lbls { 361 if strings.HasPrefix(label, ciliumLabels.LabelSourceReserved+":") { 362 return label 363 } 364 } 365 return "" 366 } 367 368 func getK8sAppFromLabels(labels []string) string { 369 for _, label := range labels { 370 for _, appLabel := range podAppLabels { 371 if strings.HasPrefix(label, appLabel+"=") { 372 l := ciliumLabels.ParseLabel(label) 373 if l.Value != "" { 374 return l.Value 375 } 376 } 377 } 378 } 379 return "" 380 } 381 382 // GetLabelValues returns the values of the context relevant labels according 383 // to the configured options. The order of the values is the same as the order 384 // of the label names returned by GetLabelNames() 385 func (o *ContextOptions) GetLabelValues(flow *pb.Flow) (labels []string, err error) { 386 return o.getLabelValues(false, flow) 387 } 388 389 // GetLabelValuesInvertSourceDestination is the same as GetLabelValues but the 390 // source and destination labels are inverted. This is primarily for metrics 391 // that leverage the response/return flows where the source and destination are 392 // swapped from the request flow. 393 func (o *ContextOptions) GetLabelValuesInvertSourceDestination(flow *pb.Flow) (labels []string, err error) { 394 return o.getLabelValues(true, flow) 395 } 396 397 // getLabelValues returns the values of the context relevant labels according 398 // to the configured options. The order of the values is the same as the order 399 // of the label names returned by GetLabelNames(). If invert is true, the 400 // source and destination related labels are inverted. 401 func (o *ContextOptions) getLabelValues(invert bool, flow *pb.Flow) (labels []string, err error) { 402 if len(o.Labels) != 0 { 403 labelsContextLabels, err := labelsContext(invert, o.Labels, flow) 404 if err != nil { 405 return nil, err 406 } 407 labels = append(labels, labelsContextLabels...) 408 } 409 410 var sourceLabel string 411 var sourceContextIdentifiers = o.Source 412 if o.SourceIngress != nil && flow.GetTrafficDirection() == pb.TrafficDirection_INGRESS { 413 sourceContextIdentifiers = o.SourceIngress 414 } else if o.SourceEgress != nil && flow.GetTrafficDirection() == pb.TrafficDirection_EGRESS { 415 sourceContextIdentifiers = o.SourceEgress 416 } 417 418 for _, contextID := range sourceContextIdentifiers { 419 sourceLabel = getContextIDLabelValue(contextID, flow, true) 420 // always use first non-empty context 421 if sourceLabel != "" { 422 break 423 } 424 } 425 426 var destinationLabel string 427 var destinationContextIdentifiers = o.Destination 428 if o.DestinationIngress != nil && flow.GetTrafficDirection() == pb.TrafficDirection_INGRESS { 429 destinationContextIdentifiers = o.DestinationIngress 430 } else if o.DestinationEgress != nil && flow.GetTrafficDirection() == pb.TrafficDirection_EGRESS { 431 destinationContextIdentifiers = o.DestinationEgress 432 } 433 for _, contextID := range destinationContextIdentifiers { 434 destinationLabel = getContextIDLabelValue(contextID, flow, false) 435 // always use first non-empty context 436 if destinationLabel != "" { 437 break 438 } 439 } 440 441 if invert { 442 sourceLabel, destinationLabel = destinationLabel, sourceLabel 443 } 444 if o.includeSourceLabel() { 445 labels = append(labels, sourceLabel) 446 } 447 if o.includeDestinationLabel() { 448 labels = append(labels, destinationLabel) 449 } 450 return 451 } 452 453 func getContextIDLabelValue(contextID ContextIdentifier, flow *pb.Flow, source bool) string { 454 var ep *pb.Endpoint 455 if source { 456 ep = flow.GetSource() 457 } else { 458 ep = flow.GetDestination() 459 } 460 var labelValue string 461 switch contextID { 462 case ContextNamespace: 463 labelValue = ep.GetNamespace() 464 case ContextIdentity: 465 labelValue = strings.Join(ep.GetLabels(), ",") 466 case ContextPod: 467 labelValue = ep.GetPodName() 468 if ep.GetNamespace() != "" { 469 labelValue = ep.GetNamespace() + "/" + labelValue 470 } 471 case ContextPodName: 472 labelValue = ep.GetPodName() 473 case ContextDNS: 474 if source { 475 labelValue = strings.Join(flow.GetSourceNames(), ",") 476 } else { 477 labelValue = strings.Join(flow.GetDestinationNames(), ",") 478 } 479 case ContextIP: 480 if source { 481 labelValue = flow.GetIP().GetSource() 482 } else { 483 labelValue = flow.GetIP().GetDestination() 484 } 485 case ContextReservedIdentity: 486 labelValue = handleReservedIdentityLabels(ep.GetLabels()) 487 case ContextWorkload: 488 if workloads := ep.GetWorkloads(); len(workloads) != 0 { 489 labelValue = workloads[0].Name 490 } 491 if labelValue != "" && ep.GetNamespace() != "" { 492 labelValue = ep.GetNamespace() + "/" + labelValue 493 } 494 case ContextWorkloadName: 495 if workloads := ep.GetWorkloads(); len(workloads) != 0 { 496 labelValue = workloads[0].Name 497 } 498 case ContextApp: 499 labelValue = getK8sAppFromLabels(ep.GetLabels()) 500 } 501 return labelValue 502 } 503 504 // GetLabelNames returns a slice of label names required to fulfil the 505 // configured context description requirements 506 func (o *ContextOptions) GetLabelNames() (labels []string) { 507 if len(o.Labels) != 0 { 508 // We must iterate over contextLabelsList to ensure the order of the label 509 // names the same order as label values in GetLabelValues. 510 for _, label := range contextLabelsList { 511 if o.Labels.HasLabel(label) { 512 labels = append(labels, label) 513 } 514 } 515 } 516 517 if o.includeSourceLabel() { 518 labels = append(labels, "source") 519 } 520 521 if o.includeDestinationLabel() { 522 labels = append(labels, "destination") 523 } 524 525 return 526 } 527 528 func (o *ContextOptions) includeSourceLabel() bool { 529 return len(o.Source) != 0 || len(o.SourceIngress) != 0 || len(o.SourceEgress) != 0 530 } 531 532 func (o *ContextOptions) includeDestinationLabel() bool { 533 return len(o.Destination) != 0 || len(o.DestinationIngress) != 0 || len(o.DestinationEgress) != 0 534 } 535 536 // Status returns the configuration status of context options suitable for use 537 // with Handler.Status 538 func (o *ContextOptions) Status() string { 539 var status []string 540 if len(o.Labels) != 0 { 541 status = append(status, "labels="+o.Labels.String()) 542 } 543 544 if len(o.Source) != 0 { 545 status = append(status, "source="+o.Source.String()) 546 } 547 548 if len(o.Destination) != 0 { 549 status = append(status, "destination="+o.Destination.String()) 550 } 551 552 sort.Strings(status) 553 554 return strings.Join(status, ",") 555 } 556 557 func (o *ContextOptions) DeleteMetricsAssociatedWithPod(name string, namespace string, vec *prometheus.MetricVec) { 558 for _, contextID := range o.allSourceCtx { 559 if contextID == ContextPod { 560 vec.DeletePartialMatch(prometheus.Labels{ 561 "source": namespace + "/" + name, 562 }) 563 } 564 } 565 for _, contextID := range o.allDestinationCtx { 566 if contextID == ContextPod { 567 vec.DeletePartialMatch(prometheus.Labels{ 568 "destination": namespace + "/" + name, 569 }) 570 } 571 } 572 573 if o.Labels.HasLabel("source_pod") && o.Labels.HasLabel("source_namespace") { 574 vec.DeletePartialMatch(prometheus.Labels{ 575 "source_namespace": namespace, 576 "source_pod": name, 577 }) 578 } 579 if o.Labels.HasLabel("destination_pod") && o.Labels.HasLabel("destination_namespace") { 580 vec.DeletePartialMatch(prometheus.Labels{ 581 "destination_namespace": namespace, 582 "destination_pod": name, 583 }) 584 } 585 }