linkerd2/controller/api/util/api_utils.go

341 lines
9.2 KiB
Go

package util
import (
"encoding/binary"
"errors"
"fmt"
"strings"
netPb "github.com/linkerd/linkerd2/controller/gen/common/net"
"github.com/linkerd/linkerd2/pkg/k8s"
pb "github.com/linkerd/linkerd2/viz/metrics-api/gen/viz"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
k8sErrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
/*
Shared utilities for interacting with the controller public api
*/
var (
// ValidTargets specifies resource types allowed as a target:
// target resource on an inbound query
// target resource on an outbound 'to' query
// destination resource on an outbound 'from' query
ValidTargets = []string{
k8s.Authority,
k8s.CronJob,
k8s.DaemonSet,
k8s.Deployment,
k8s.Job,
k8s.Namespace,
k8s.Pod,
k8s.ReplicaSet,
k8s.ReplicationController,
k8s.StatefulSet,
}
// ValidTapDestinations specifies resource types allowed as a tap destination:
// destination resource on an outbound 'to' query
ValidTapDestinations = []string{
k8s.CronJob,
k8s.DaemonSet,
k8s.Deployment,
k8s.Job,
k8s.Namespace,
k8s.Pod,
k8s.ReplicaSet,
k8s.ReplicationController,
k8s.Service,
k8s.StatefulSet,
}
)
// TapRequestParams contains parameters that are used to build a
// TapByResourceRequest.
type TapRequestParams struct {
Resource string
Namespace string
ToResource string
ToNamespace string
MaxRps float32
Scheme string
Method string
Authority string
Path string
Extract bool
LabelSelector string
}
// GRPCError generates a gRPC error code, as defined in
// google.golang.org/grpc/status.
// If the error is nil or already a gRPC error, return the error.
// If the error is of type k8s.io/apimachinery/pkg/apis/meta/v1#StatusReason,
// attempt to map the reason to a gRPC error.
func GRPCError(err error) error {
if err != nil && status.Code(err) == codes.Unknown {
code := codes.Internal
switch k8sErrors.ReasonForError(err) {
case metav1.StatusReasonUnknown:
code = codes.Unknown
case metav1.StatusReasonUnauthorized, metav1.StatusReasonForbidden:
code = codes.PermissionDenied
case metav1.StatusReasonNotFound:
code = codes.NotFound
case metav1.StatusReasonAlreadyExists:
code = codes.AlreadyExists
case metav1.StatusReasonInvalid:
code = codes.InvalidArgument
case metav1.StatusReasonExpired:
code = codes.DeadlineExceeded
case metav1.StatusReasonServiceUnavailable:
code = codes.Unavailable
}
err = status.Error(code, err.Error())
}
return err
}
// BuildResource parses input strings, typically from CLI flags, to build a
// Resource object for use in the protobuf API.
// It's the same as BuildResources but only admits one arg and only returns one resource
func BuildResource(namespace, arg string) (*pb.Resource, error) {
res, err := BuildResources(namespace, []string{arg})
if err != nil {
return &pb.Resource{}, err
}
return res[0], err
}
// BuildResources parses input strings, typically from CLI flags, to build a
// slice of Resource objects for use in the protobuf API.
// It's the same as BuildResource but it admits any number of args and returns multiple resources
func BuildResources(namespace string, args []string) ([]*pb.Resource, error) {
switch len(args) {
case 0:
return nil, errors.New("No resource arguments provided")
case 1:
return parseResources(namespace, "", args)
default:
if res, err := k8s.CanonicalResourceNameFromFriendlyName(args[0]); err == nil && res != k8s.All {
// --namespace my-ns deploy foo1 foo2 ...
return parseResources(namespace, args[0], args[1:])
}
return parseResources(namespace, "", args)
}
}
func parseResources(namespace string, resType string, args []string) ([]*pb.Resource, error) {
if err := validateResources(args); err != nil {
return nil, err
}
resources := make([]*pb.Resource, 0)
for _, arg := range args {
res, err := parseResource(namespace, resType, arg)
if err != nil {
return nil, err
}
resources = append(resources, res)
}
return resources, nil
}
func validateResources(args []string) error {
set := make(map[string]bool)
all := false
for _, arg := range args {
set[arg] = true
if arg == k8s.All {
all = true
}
}
if len(set) < len(args) {
return errors.New("cannot supply duplicate resources")
}
if all && len(args) > 1 {
return errors.New("'all' can't be supplied alongside other resources")
}
return nil
}
func parseResource(namespace, resType string, arg string) (*pb.Resource, error) {
if resType != "" {
return buildResource(namespace, resType, arg)
}
elems := strings.Split(arg, "/")
switch len(elems) {
case 1:
// --namespace my-ns deploy
return buildResource(namespace, elems[0], "")
case 2:
// --namespace my-ns deploy/foo
return buildResource(namespace, elems[0], elems[1])
default:
return &pb.Resource{}, errors.New("Invalid resource string: " + arg)
}
}
func buildResource(namespace string, resType string, name string) (*pb.Resource, error) {
canonicalType, err := k8s.CanonicalResourceNameFromFriendlyName(resType)
if err != nil {
return &pb.Resource{}, err
}
if canonicalType == k8s.Namespace {
// ignore --namespace flags if type is namespace
namespace = ""
}
return &pb.Resource{
Namespace: namespace,
Type: canonicalType,
Name: name,
}, nil
}
// BuildTapByResourceRequest builds a Public API TapByResourceRequest from a
// TapRequestParams.
func BuildTapByResourceRequest(params TapRequestParams) (*pb.TapByResourceRequest, error) {
target, err := BuildResource(params.Namespace, params.Resource)
if err != nil {
return nil, fmt.Errorf("target resource invalid: %s", err)
}
if !contains(ValidTargets, target.Type) {
return nil, fmt.Errorf("unsupported resource type [%s]", target.Type)
}
matches := []*pb.TapByResourceRequest_Match{}
if params.ToResource != "" {
destination, err := BuildResource(params.ToNamespace, params.ToResource)
if err != nil {
return nil, fmt.Errorf("destination resource invalid: %s", err)
}
if !contains(ValidTapDestinations, destination.Type) {
return nil, fmt.Errorf("unsupported resource type [%s]", destination.Type)
}
match := pb.TapByResourceRequest_Match{
Match: &pb.TapByResourceRequest_Match_Destinations{
Destinations: &pb.ResourceSelection{
Resource: destination,
},
},
}
matches = append(matches, &match)
}
if params.Scheme != "" {
match := buildMatchHTTP(&pb.TapByResourceRequest_Match_Http{
Match: &pb.TapByResourceRequest_Match_Http_Scheme{Scheme: params.Scheme},
})
matches = append(matches, &match)
}
if params.Method != "" {
match := buildMatchHTTP(&pb.TapByResourceRequest_Match_Http{
Match: &pb.TapByResourceRequest_Match_Http_Method{Method: params.Method},
})
matches = append(matches, &match)
}
if params.Authority != "" {
match := buildMatchHTTP(&pb.TapByResourceRequest_Match_Http{
Match: &pb.TapByResourceRequest_Match_Http_Authority{Authority: params.Authority},
})
matches = append(matches, &match)
}
if params.Path != "" {
match := buildMatchHTTP(&pb.TapByResourceRequest_Match_Http{
Match: &pb.TapByResourceRequest_Match_Http_Path{Path: params.Path},
})
matches = append(matches, &match)
}
extract := &pb.TapByResourceRequest_Extract{}
if params.Extract {
extract = buildExtractHTTP(&pb.TapByResourceRequest_Extract_Http{
Extract: &pb.TapByResourceRequest_Extract_Http_Headers_{
Headers: &pb.TapByResourceRequest_Extract_Http_Headers{},
},
})
}
return &pb.TapByResourceRequest{
Target: &pb.ResourceSelection{
Resource: target,
LabelSelector: params.LabelSelector,
},
MaxRps: params.MaxRps,
Match: &pb.TapByResourceRequest_Match{
Match: &pb.TapByResourceRequest_Match_All{
All: &pb.TapByResourceRequest_Match_Seq{
Matches: matches,
},
},
},
Extract: extract,
}, nil
}
func buildMatchHTTP(match *pb.TapByResourceRequest_Match_Http) pb.TapByResourceRequest_Match {
return pb.TapByResourceRequest_Match{
Match: &pb.TapByResourceRequest_Match_Http_{
Http: match,
},
}
}
func buildExtractHTTP(extract *pb.TapByResourceRequest_Extract_Http) *pb.TapByResourceRequest_Extract {
return &pb.TapByResourceRequest_Extract{
Extract: &pb.TapByResourceRequest_Extract_Http_{
Http: extract,
},
}
}
func contains(list []string, s string) bool {
for _, elem := range list {
if s == elem {
return true
}
}
return false
}
// CreateTapEvent generates tap events for use in tests
func CreateTapEvent(eventHTTP *pb.TapEvent_Http, dstMeta map[string]string, proxyDirection pb.TapEvent_ProxyDirection) *pb.TapEvent {
event := &pb.TapEvent{
ProxyDirection: proxyDirection,
Source: &netPb.TcpAddress{
Ip: &netPb.IPAddress{
Ip: &netPb.IPAddress_Ipv4{
Ipv4: uint32(1),
},
},
},
Destination: &netPb.TcpAddress{
Ip: &netPb.IPAddress{
Ip: &netPb.IPAddress_Ipv6{
Ipv6: &netPb.IPv6{
// All nodes address: https://www.iana.org/assignments/ipv6-multicast-addresses/ipv6-multicast-addresses.xhtml
First: binary.BigEndian.Uint64([]byte{0xff, 0x01, 0, 0, 0, 0, 0, 0}),
Last: binary.BigEndian.Uint64([]byte{0, 0, 0, 0, 0, 0, 0, 0x01}),
},
},
},
},
Event: &pb.TapEvent_Http_{
Http: eventHTTP,
},
DestinationMeta: &pb.TapEvent_EndpointMeta{
Labels: dstMeta,
},
}
return event
}