github.com/kiali/kiali@v1.84.0/graph/options.go (about)

     1  package graph
     2  
     3  // Options.go holds the option settings for a single graph request.
     4  
     5  import (
     6  	"context"
     7  	"fmt"
     8  	net_http "net/http"
     9  	"net/url"
    10  	"strconv"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/gorilla/mux"
    15  	"github.com/prometheus/common/model"
    16  	"k8s.io/client-go/tools/clientcmd/api"
    17  
    18  	"github.com/kiali/kiali/business"
    19  	"github.com/kiali/kiali/business/authentication"
    20  	"github.com/kiali/kiali/config"
    21  	"github.com/kiali/kiali/log"
    22  )
    23  
    24  // The supported vendors
    25  const (
    26  	VendorCytoscape        string = "cytoscape"
    27  	VendorIstio            string = "istio"
    28  	defaultConfigVendor    string = VendorCytoscape
    29  	defaultTelemetryVendor string = VendorIstio
    30  )
    31  
    32  const (
    33  	BoxByApp                  string = "app"
    34  	BoxByCluster              string = "cluster"
    35  	BoxByNamespace            string = "namespace"
    36  	BoxByNone                 string = "none"
    37  	RateNone                  string = "none"
    38  	RateReceived              string = "received" // tcp bytes received, grpc response messages, etc
    39  	RateRequests              string = "requests" // request count
    40  	RateSent                  string = "sent"     // tcp bytes sent, grpc request messages, etc
    41  	RateTotal                 string = "total"    // Sent+Received
    42  	defaultBoxBy              string = BoxByNone
    43  	defaultDuration           string = "10m"
    44  	defaultGraphType          string = GraphTypeWorkload
    45  	defaultIncludeIdleEdges   bool   = false
    46  	defaultInjectServiceNodes bool   = false
    47  	defaultRateGrpc           string = RateRequests
    48  	defaultRateHttp           string = RateRequests
    49  	defaultRateTcp            string = RateSent
    50  )
    51  
    52  const (
    53  	graphKindNamespace string = "namespace"
    54  	graphKindNode      string = "node"
    55  )
    56  
    57  // NodeOptions are those that apply only to node-detail graphs
    58  type NodeOptions struct {
    59  	Aggregate      string
    60  	AggregateValue string
    61  	App            string
    62  	Cluster        string
    63  	Namespace      string
    64  	Service        string
    65  	Version        string
    66  	Workload       string
    67  }
    68  
    69  // CommonOptions are those supplied to Telemetry and Config Vendors
    70  type CommonOptions struct {
    71  	Duration  time.Duration
    72  	GraphType string
    73  	Params    url.Values // make available the raw query params for vendor-specific handling
    74  	QueryTime int64      // unix time in seconds
    75  }
    76  
    77  // ConfigOptions are those supplied to Config Vendors
    78  type ConfigOptions struct {
    79  	BoxBy string
    80  	CommonOptions
    81  }
    82  
    83  type RequestedAppenders struct {
    84  	All           bool
    85  	AppenderNames []string
    86  }
    87  
    88  type RequestedRates struct {
    89  	Grpc string
    90  	Http string
    91  	Tcp  string
    92  }
    93  
    94  // ClusterSensitiveKey is the recommended [string] type for maps keying on a cluster-sensitive name
    95  type ClusterSensitiveKey = string
    96  
    97  // GetClusterSensitiveKey returns a valid key for maps using a ClusterSensitiveKey
    98  func GetClusterSensitiveKey(cluster, name string) ClusterSensitiveKey {
    99  	return fmt.Sprintf("%s:%s", cluster, name)
   100  }
   101  
   102  type AccessibleNamespace struct {
   103  	Cluster           string
   104  	CreationTimestamp time.Time
   105  	Name              string
   106  }
   107  
   108  // AccessibleNamepaces is a map with Key: ClusterSensitive namespace Key, Value: *AccessibleNamespace
   109  type AccessibleNamespaces map[ClusterSensitiveKey]*AccessibleNamespace
   110  
   111  // TelemetryOptions are those supplied to Telemetry Vendors
   112  type TelemetryOptions struct {
   113  	AccessibleNamespaces AccessibleNamespaces
   114  	Appenders            RequestedAppenders // requested appenders, nil if param not supplied
   115  	IncludeIdleEdges     bool               // include edges with request rates of 0
   116  	InjectServiceNodes   bool               // inject destination service nodes between source and destination nodes.
   117  	Namespaces           NamespaceInfoMap
   118  	Rates                RequestedRates
   119  	CommonOptions
   120  	NodeOptions
   121  }
   122  
   123  // Options comprises all available options
   124  type Options struct {
   125  	ConfigVendor    string
   126  	TelemetryVendor string
   127  	ConfigOptions
   128  	TelemetryOptions
   129  }
   130  
   131  func NewOptions(r *net_http.Request) Options {
   132  	// path variables (0 or more will be set)
   133  	vars := mux.Vars(r)
   134  	aggregate := vars["aggregate"]
   135  	aggregateValue := vars["aggregateValue"]
   136  	app := vars["app"]
   137  	namespace := vars["namespace"]
   138  	service := vars["service"]
   139  	version := vars["version"]
   140  	workload := vars["workload"]
   141  
   142  	// query params
   143  	params := r.URL.Query()
   144  	var duration model.Duration
   145  	var includeIdleEdges bool
   146  	var injectServiceNodes bool
   147  	var queryTime int64
   148  	appenders := RequestedAppenders{All: true}
   149  	boxBy := params.Get("boxBy")
   150  	// @TODO requires refactoring to use clusterNameFromQuery
   151  	cluster := params.Get("clusterName")
   152  	configVendor := params.Get("configVendor")
   153  	durationString := params.Get("duration")
   154  	graphType := params.Get("graphType")
   155  	includeIdleEdgesString := params.Get("includeIdleEdges")
   156  	injectServiceNodesString := params.Get("injectServiceNodes")
   157  	namespaces := params.Get("namespaces") // csl of namespaces
   158  	queryTimeString := params.Get("queryTime")
   159  	rateGrpc := params.Get("rateGrpc")
   160  	rateHttp := params.Get("rateHttp")
   161  	rateTcp := params.Get("rateTcp")
   162  	telemetryVendor := params.Get("telemetryVendor")
   163  
   164  	if _, ok := params["appenders"]; ok {
   165  		appenderNames := strings.Split(params.Get("appenders"), ",")
   166  		for i, appenderName := range appenderNames {
   167  			appenderNames[i] = strings.TrimSpace(appenderName)
   168  		}
   169  		appenders = RequestedAppenders{All: false, AppenderNames: appenderNames}
   170  	}
   171  	if cluster == "" {
   172  		cluster = Unknown
   173  	}
   174  	if configVendor == "" {
   175  		configVendor = defaultConfigVendor
   176  	} else if configVendor != VendorCytoscape {
   177  		BadRequest(fmt.Sprintf("Invalid configVendor [%s]", configVendor))
   178  	}
   179  	if durationString == "" {
   180  		duration, _ = model.ParseDuration(defaultDuration)
   181  	} else {
   182  		var durationErr error
   183  		duration, durationErr = model.ParseDuration(durationString)
   184  		if durationErr != nil {
   185  			BadRequest(fmt.Sprintf("Invalid duration [%s]", durationString))
   186  		}
   187  	}
   188  
   189  	if graphType == "" {
   190  		graphType = defaultGraphType
   191  	} else if graphType != GraphTypeApp && graphType != GraphTypeService && graphType != GraphTypeVersionedApp && graphType != GraphTypeWorkload {
   192  		BadRequest(fmt.Sprintf("Invalid graphType [%s]", graphType))
   193  	}
   194  	// service graphs do not inject service nodes
   195  	if graphType == GraphTypeService {
   196  		injectServiceNodesString = "false"
   197  	}
   198  	// app node graphs require an app graph type
   199  	if app != "" && graphType != GraphTypeApp && graphType != GraphTypeVersionedApp {
   200  		BadRequest(fmt.Sprintf("Invalid graphType [%s]. This node detail graph supports only graphType app or versionedApp.", graphType))
   201  	}
   202  	if boxBy == "" {
   203  		boxBy = defaultBoxBy
   204  	} else {
   205  		for _, box := range strings.Split(boxBy, ",") {
   206  			switch strings.TrimSpace(box) {
   207  			case BoxByApp:
   208  				continue
   209  			case BoxByCluster:
   210  				continue
   211  			case BoxByNamespace:
   212  				continue
   213  			default:
   214  				BadRequest(fmt.Sprintf("Invalid boxBy [%s]", boxBy))
   215  			}
   216  		}
   217  	}
   218  	if includeIdleEdgesString == "" {
   219  		includeIdleEdges = defaultIncludeIdleEdges
   220  	} else {
   221  		var includeIdleEdgesErr error
   222  		includeIdleEdges, includeIdleEdgesErr = strconv.ParseBool(includeIdleEdgesString)
   223  		if includeIdleEdgesErr != nil {
   224  			BadRequest(fmt.Sprintf("Invalid includeIdleEdges [%s]", includeIdleEdgesString))
   225  		}
   226  	}
   227  	if injectServiceNodesString == "" {
   228  		injectServiceNodes = defaultInjectServiceNodes
   229  	} else {
   230  		var injectServiceNodesErr error
   231  		injectServiceNodes, injectServiceNodesErr = strconv.ParseBool(injectServiceNodesString)
   232  		if injectServiceNodesErr != nil {
   233  			BadRequest(fmt.Sprintf("Invalid injectServiceNodes [%s]", injectServiceNodesString))
   234  		}
   235  	}
   236  	if queryTimeString == "" {
   237  		queryTime = time.Now().Unix()
   238  	} else {
   239  		var queryTimeErr error
   240  		queryTime, queryTimeErr = strconv.ParseInt(queryTimeString, 10, 64)
   241  		if queryTimeErr != nil {
   242  			BadRequest(fmt.Sprintf("Invalid queryTime [%s]", queryTimeString))
   243  		}
   244  	}
   245  	if telemetryVendor == "" {
   246  		telemetryVendor = defaultTelemetryVendor
   247  	} else if telemetryVendor != VendorIstio {
   248  		BadRequest(fmt.Sprintf("Invalid telemetryVendor [%s]", telemetryVendor))
   249  	}
   250  
   251  	// Process namespaces options:
   252  	namespaceMap := NewNamespaceInfoMap()
   253  
   254  	authInfoContext := authentication.GetAuthInfoContext(r.Context())
   255  
   256  	var authInfo *api.AuthInfo
   257  	if authInfoContext != nil {
   258  		if authInfoCheck, ok := authInfoContext.(*api.AuthInfo); !ok {
   259  			Error("authInfo is not of type *api.AuthInfo")
   260  		} else {
   261  			authInfo = authInfoCheck
   262  		}
   263  	} else {
   264  		Error("token missing in request context")
   265  	}
   266  
   267  	accessibleNamespaces := getAccessibleNamespaces(authInfo)
   268  
   269  	// If path variable is set then it is the only relevant namespace (it's a node graph)
   270  	// Else if namespaces query param is set it specifies the relevant namespaces
   271  	// Else error, at least one namespace is required.
   272  	if namespace != "" {
   273  		namespaces = namespace
   274  	}
   275  
   276  	if namespaces == "" {
   277  		BadRequest("At least one namespace must be specified via the namespaces query parameter.")
   278  	}
   279  
   280  	for _, namespaceName := range strings.Split(namespaces, ",") {
   281  		namespaceName = strings.TrimSpace(namespaceName)
   282  		var earliestCreationTimestamp *time.Time
   283  		for _, an := range accessibleNamespaces {
   284  			if namespaceName == an.Name {
   285  				if nil == earliestCreationTimestamp || earliestCreationTimestamp.After(an.CreationTimestamp) {
   286  					earliestCreationTimestamp = &an.CreationTimestamp
   287  				}
   288  			}
   289  		}
   290  		if nil == earliestCreationTimestamp {
   291  			Forbidden(fmt.Sprintf("Requested namespace [%s] is not accessible.", namespaceName))
   292  		} else {
   293  			namespaceMap[namespaceName] = NamespaceInfo{
   294  				Name:     namespaceName,
   295  				Duration: getSafeNamespaceDuration(namespaceName, *earliestCreationTimestamp, time.Duration(duration), queryTime),
   296  				IsIstio:  config.IsIstioNamespace(namespaceName),
   297  			}
   298  		}
   299  	}
   300  
   301  	// Process Rate Options
   302  
   303  	rates := RequestedRates{
   304  		Grpc: defaultRateGrpc,
   305  		Http: defaultRateHttp,
   306  		Tcp:  defaultRateTcp,
   307  	}
   308  
   309  	if rateGrpc != "" {
   310  		switch rateGrpc {
   311  		case RateNone:
   312  			rates.Grpc = RateNone
   313  		case RateReceived:
   314  			rates.Grpc = RateReceived
   315  		case RateRequests:
   316  			rates.Grpc = RateRequests
   317  		case RateSent:
   318  			rates.Grpc = RateSent
   319  		case RateTotal:
   320  			rates.Grpc = RateTotal
   321  		default:
   322  			BadRequest(fmt.Sprintf("Invalid gRPC Rate [%s]", rateGrpc))
   323  		}
   324  	}
   325  
   326  	if rateHttp != "" {
   327  		switch rateHttp {
   328  		case RateNone:
   329  			rates.Http = RateNone
   330  		case RateRequests:
   331  			rates.Http = RateRequests
   332  		default:
   333  			BadRequest(fmt.Sprintf("Invalid HTTP Rate [%s]", rateHttp))
   334  		}
   335  	}
   336  
   337  	if rateTcp != "" {
   338  		switch rateTcp {
   339  		case RateNone:
   340  			rates.Tcp = RateNone
   341  		case RateReceived:
   342  			rates.Tcp = RateReceived
   343  		case RateSent:
   344  			rates.Tcp = RateSent
   345  		case RateTotal:
   346  			rates.Tcp = RateTotal
   347  		default:
   348  			BadRequest(fmt.Sprintf("Invalid TCP Rate [%s]", rateTcp))
   349  		}
   350  	}
   351  
   352  	// Service graphs require service injection
   353  	if graphType == GraphTypeService {
   354  		injectServiceNodes = true
   355  	}
   356  
   357  	options := Options{
   358  		ConfigVendor:    configVendor,
   359  		TelemetryVendor: telemetryVendor,
   360  		ConfigOptions: ConfigOptions{
   361  			BoxBy: boxBy,
   362  			CommonOptions: CommonOptions{
   363  				Duration:  time.Duration(duration),
   364  				GraphType: graphType,
   365  				Params:    params,
   366  				QueryTime: queryTime,
   367  			},
   368  		},
   369  		TelemetryOptions: TelemetryOptions{
   370  			AccessibleNamespaces: accessibleNamespaces,
   371  			Appenders:            appenders,
   372  			IncludeIdleEdges:     includeIdleEdges,
   373  			InjectServiceNodes:   injectServiceNodes,
   374  			Namespaces:           namespaceMap,
   375  			Rates:                rates,
   376  			CommonOptions: CommonOptions{
   377  				Duration:  time.Duration(duration),
   378  				GraphType: graphType,
   379  				Params:    params,
   380  				QueryTime: queryTime,
   381  			},
   382  			NodeOptions: NodeOptions{
   383  				Aggregate:      aggregate,
   384  				AggregateValue: aggregateValue,
   385  				App:            app,
   386  				Cluster:        cluster,
   387  				Namespace:      namespace,
   388  				Service:        service,
   389  				Version:        version,
   390  				Workload:       workload,
   391  			},
   392  		},
   393  	}
   394  
   395  	return options
   396  }
   397  
   398  // GetGraphKind will return the kind of graph represented by the options.
   399  func (o *TelemetryOptions) GetGraphKind() string {
   400  	if o.NodeOptions.App != "" ||
   401  		o.NodeOptions.Version != "" ||
   402  		o.NodeOptions.Workload != "" ||
   403  		o.NodeOptions.Service != "" {
   404  		return graphKindNode
   405  	}
   406  	return graphKindNamespace
   407  }
   408  
   409  // getAccessibleNamespaces returns a Set of all namespaces accessible to the user.
   410  // The Set is implemented using the map convention. Each map entry is set to the
   411  // creation timestamp of the namespace, to be used to ensure valid time ranges for
   412  // queries against the namespace.
   413  func getAccessibleNamespaces(authInfo *api.AuthInfo) AccessibleNamespaces {
   414  	// Get the namespaces
   415  	business, err := business.Get(authInfo)
   416  	CheckError(err)
   417  
   418  	namespaces, err := business.Namespace.GetNamespaces(context.TODO())
   419  	CheckError(err)
   420  
   421  	// Create a map to store the namespaces
   422  	accessibleNamespaces := make(AccessibleNamespaces)
   423  	for _, namespace := range namespaces {
   424  		accessibleNamespaces[GetClusterSensitiveKey(namespace.Cluster, namespace.Name)] = &AccessibleNamespace{
   425  			Cluster:           namespace.Cluster,
   426  			CreationTimestamp: namespace.CreationTimestamp,
   427  			Name:              namespace.Name,
   428  		}
   429  	}
   430  
   431  	return accessibleNamespaces
   432  }
   433  
   434  // getSafeNamespaceDuration returns a safe duration for the query. If queryTime-requestedDuration > namespace
   435  // creation time just return the requestedDuration.  Otherwise reduce the duration as needed to ensure the
   436  // namespace existed for the entire time range.  An error is generated if no safe duration exists (i.e. the
   437  // queryTime precedes the namespace).
   438  func getSafeNamespaceDuration(ns string, nsCreationTime time.Time, requestedDuration time.Duration, queryTime int64) time.Duration {
   439  	var endTime time.Time
   440  	safeDuration := requestedDuration
   441  
   442  	if !nsCreationTime.IsZero() {
   443  		if queryTime != 0 {
   444  			endTime = time.Unix(queryTime, 0)
   445  		} else {
   446  			endTime = time.Now()
   447  		}
   448  
   449  		nsLifetime := endTime.Sub(nsCreationTime)
   450  		if nsLifetime <= 0 {
   451  			BadRequest(fmt.Sprintf("Namespace [%s] did not exist at requested queryTime [%v]", ns, endTime))
   452  		}
   453  
   454  		if nsLifetime < safeDuration {
   455  			safeDuration = nsLifetime
   456  			log.Debugf("Reducing requestedDuration [%v] to safeDuration [%v]", requestedDuration, safeDuration)
   457  		}
   458  	}
   459  
   460  	return safeDuration
   461  }