linkerd2/viz/metrics-api/util/api_utils.go

317 lines
7.8 KiB
Go

package util
import (
"errors"
"time"
"github.com/linkerd/linkerd2/pkg/k8s"
pb "github.com/linkerd/linkerd2/viz/metrics-api/gen/viz"
corev1 "k8s.io/api/core/v1"
)
var (
defaultMetricTimeWindow = "1m"
metricTimeWindowLowerBound = time.Second * 15 //the window value needs to equal or larger than that
)
// StatsBaseRequestParams contains parameters that are used to build requests
// for metrics data. This includes requests to StatSummary and TopRoutes.
type StatsBaseRequestParams struct {
TimeWindow string
Namespace string
ResourceType string
ResourceName string
AllNamespaces bool
}
// StatsSummaryRequestParams contains parameters that are used to build
// StatSummary requests.
type StatsSummaryRequestParams struct {
StatsBaseRequestParams
ToNamespace string
ToType string
ToName string
FromNamespace string
FromType string
FromName string
SkipStats bool
TCPStats bool
LabelSelector string
}
// EdgesRequestParams contains parameters that are used to build
// Edges requests.
type EdgesRequestParams struct {
Namespace string
ResourceType string
AllNamespaces bool
}
// TopRoutesRequestParams contains parameters that are used to build TopRoutes
// requests.
type TopRoutesRequestParams struct {
StatsBaseRequestParams
ToNamespace string
ToType string
ToName string
LabelSelector string
}
// GatewayRequestParams contains parameters that are used to build a
// GatewayRequest
type GatewayRequestParams struct {
RemoteClusterName string
GatewayNamespace string
TimeWindow string
}
// BuildStatSummaryRequest builds a Public API StatSummaryRequest from a
// StatsSummaryRequestParams.
func BuildStatSummaryRequest(p StatsSummaryRequestParams) (*pb.StatSummaryRequest, error) {
window := defaultMetricTimeWindow
if p.TimeWindow != "" {
w, err := time.ParseDuration(p.TimeWindow)
if err != nil {
return nil, err
}
if w < metricTimeWindowLowerBound {
return nil, errors.New("metrics time window needs to be at least 15s")
}
window = p.TimeWindow
}
if p.AllNamespaces && p.ResourceName != "" {
return nil, errors.New("stats for a resource cannot be retrieved by name across all namespaces")
}
targetNamespace := p.Namespace
if p.AllNamespaces {
targetNamespace = ""
} else if p.Namespace == "" {
targetNamespace = corev1.NamespaceDefault
}
resourceType, err := k8s.CanonicalResourceNameFromFriendlyName(p.ResourceType)
if err != nil {
return nil, err
}
statRequest := &pb.StatSummaryRequest{
Selector: &pb.ResourceSelection{
Resource: &pb.Resource{
Namespace: targetNamespace,
Name: p.ResourceName,
Type: resourceType,
},
LabelSelector: p.LabelSelector,
},
TimeWindow: window,
SkipStats: p.SkipStats,
TcpStats: p.TCPStats,
}
if p.ToName != "" || p.ToType != "" || p.ToNamespace != "" {
if p.ToNamespace == "" {
p.ToNamespace = targetNamespace
}
if p.ToType == "" {
p.ToType = resourceType
}
toType, err := k8s.CanonicalResourceNameFromFriendlyName(p.ToType)
if err != nil {
return nil, err
}
toResource := pb.StatSummaryRequest_ToResource{
ToResource: &pb.Resource{
Namespace: p.ToNamespace,
Type: toType,
Name: p.ToName,
},
}
statRequest.Outbound = &toResource
}
if p.FromName != "" || p.FromType != "" || p.FromNamespace != "" {
if p.FromNamespace == "" {
p.FromNamespace = targetNamespace
}
if p.FromType == "" {
p.FromType = resourceType
}
fromType, err := validateFromResourceType(p.FromType)
if err != nil {
return nil, err
}
fromResource := pb.StatSummaryRequest_FromResource{
FromResource: &pb.Resource{
Namespace: p.FromNamespace,
Type: fromType,
Name: p.FromName,
},
}
statRequest.Outbound = &fromResource
}
return statRequest, nil
}
// BuildEdgesRequest builds a Public API EdgesRequest from a
// EdgesRequestParams.
func BuildEdgesRequest(p EdgesRequestParams) (*pb.EdgesRequest, error) {
namespace := p.Namespace
// If all namespaces was specified, ignore namespace value.
if p.AllNamespaces {
namespace = ""
}
resourceType, err := k8s.CanonicalResourceNameFromFriendlyName(p.ResourceType)
if err != nil {
return nil, err
}
edgesRequest := &pb.EdgesRequest{
Selector: &pb.ResourceSelection{
Resource: &pb.Resource{
Namespace: namespace,
Type: resourceType,
},
},
}
return edgesRequest, nil
}
// BuildTopRoutesRequest builds a Public API TopRoutesRequest from a
// TopRoutesRequestParams.
func BuildTopRoutesRequest(p TopRoutesRequestParams) (*pb.TopRoutesRequest, error) {
window := defaultMetricTimeWindow
if p.TimeWindow != "" {
_, err := time.ParseDuration(p.TimeWindow)
if err != nil {
return nil, err
}
window = p.TimeWindow
}
if p.AllNamespaces && p.ResourceName != "" {
return nil, errors.New("routes for a resource cannot be retrieved by name across all namespaces")
}
targetNamespace := p.Namespace
if p.AllNamespaces {
targetNamespace = ""
} else if p.Namespace == "" {
targetNamespace = corev1.NamespaceDefault
}
resourceType, err := k8s.CanonicalResourceNameFromFriendlyName(p.ResourceType)
if err != nil {
return nil, err
}
topRoutesRequest := &pb.TopRoutesRequest{
Selector: &pb.ResourceSelection{
Resource: &pb.Resource{
Namespace: targetNamespace,
Name: p.ResourceName,
Type: resourceType,
},
LabelSelector: p.LabelSelector,
},
TimeWindow: window,
}
if p.ToName != "" || p.ToType != "" || p.ToNamespace != "" {
if p.ToNamespace == "" {
p.ToNamespace = targetNamespace
}
if p.ToType == "" {
p.ToType = resourceType
}
toType, err := k8s.CanonicalResourceNameFromFriendlyName(p.ToType)
if err != nil {
return nil, err
}
toResource := pb.TopRoutesRequest_ToResource{
ToResource: &pb.Resource{
Namespace: p.ToNamespace,
Type: toType,
Name: p.ToName,
},
}
topRoutesRequest.Outbound = &toResource
} else {
topRoutesRequest.Outbound = &pb.TopRoutesRequest_None{
None: &pb.Empty{},
}
}
return topRoutesRequest, nil
}
// An authority can only receive traffic, not send it, so it can't be a --from
func validateFromResourceType(resourceType string) (string, error) {
name, err := k8s.CanonicalResourceNameFromFriendlyName(resourceType)
if err != nil {
return "", err
}
if name == k8s.Authority {
return "", errors.New("cannot query traffic --from an authority")
}
return name, nil
}
// K8sPodToPublicPod converts a Kubernetes Pod to a Public API Pod
func K8sPodToPublicPod(pod corev1.Pod, ownerKind string, ownerName string) *pb.Pod {
status := string(pod.Status.Phase)
if pod.DeletionTimestamp != nil {
status = "Terminating"
}
if pod.Status.Reason == "Evicted" {
status = "Evicted"
}
controllerComponent := pod.Labels[k8s.ControllerComponentLabel]
controllerNS := pod.Labels[k8s.ControllerNSLabel]
item := &pb.Pod{
Name: pod.Namespace + "/" + pod.Name,
Status: status,
PodIP: pod.Status.PodIP,
ControllerNamespace: controllerNS,
ControlPlane: controllerComponent != "",
ProxyReady: k8s.GetProxyReady(pod),
ProxyVersion: k8s.GetProxyVersion(pod),
ResourceVersion: pod.ResourceVersion,
}
namespacedOwnerName := pod.Namespace + "/" + ownerName
switch ownerKind {
case k8s.Deployment:
item.Owner = &pb.Pod_Deployment{Deployment: namespacedOwnerName}
case k8s.DaemonSet:
item.Owner = &pb.Pod_DaemonSet{DaemonSet: namespacedOwnerName}
case k8s.Job:
item.Owner = &pb.Pod_Job{Job: namespacedOwnerName}
case k8s.ReplicaSet:
item.Owner = &pb.Pod_ReplicaSet{ReplicaSet: namespacedOwnerName}
case k8s.ReplicationController:
item.Owner = &pb.Pod_ReplicationController{ReplicationController: namespacedOwnerName}
case k8s.StatefulSet:
item.Owner = &pb.Pod_StatefulSet{StatefulSet: namespacedOwnerName}
}
return item
}