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  }