linkerd2/controller/api/destination/profile_translator.go

366 lines
8.7 KiB
Go

package destination
import (
"errors"
"fmt"
"strings"
"time"
"github.com/golang/protobuf/ptypes/duration"
pb "github.com/linkerd/linkerd2-proxy-api/go/destination"
sp "github.com/linkerd/linkerd2/controller/gen/apis/serviceprofile/v1alpha2"
"github.com/linkerd/linkerd2/pkg/profiles"
"github.com/linkerd/linkerd2/pkg/util"
logging "github.com/sirupsen/logrus"
)
const millisPerDecimilli = 10
// implements the ProfileUpdateListener interface
type profileTranslator struct {
stream pb.Destination_GetProfileServer
log *logging.Entry
fullyQualifiedName string
port uint32
endpoint *pb.WeightedAddr
}
func newProfileTranslator(stream pb.Destination_GetProfileServer, log *logging.Entry, fqn string, port uint32, endpoint *pb.WeightedAddr) *profileTranslator {
return &profileTranslator{
stream: stream,
log: log.WithField("component", "profile-translator"),
fullyQualifiedName: fqn,
port: port,
endpoint: endpoint,
}
}
func (pt *profileTranslator) Update(profile *sp.ServiceProfile) {
if profile == nil {
pt.stream.Send(pt.defaultServiceProfile())
return
}
destinationProfile, err := pt.toServiceProfile(profile)
if err != nil {
pt.log.Error(err)
return
}
pt.log.Debugf("Sending profile update: %+v", destinationProfile)
pt.stream.Send(destinationProfile)
}
func (pt *profileTranslator) defaultServiceProfile() *pb.DestinationProfile {
return &pb.DestinationProfile{
Routes: []*pb.Route{},
RetryBudget: defaultRetryBudget(),
FullyQualifiedName: pt.fullyQualifiedName,
Endpoint: pt.endpoint,
}
}
func defaultRetryBudget() *pb.RetryBudget {
return &pb.RetryBudget{
MinRetriesPerSecond: 10,
RetryRatio: 0.2,
Ttl: &duration.Duration{
Seconds: 10,
},
}
}
func toDuration(d time.Duration) *duration.Duration {
if d == 0 {
return nil
}
return &duration.Duration{
Seconds: int64(d / time.Second),
Nanos: int32(d % time.Second),
}
}
// toServiceProfile returns a Proxy API DestinationProfile, given a
// ServiceProfile.
func (pt *profileTranslator) toServiceProfile(profile *sp.ServiceProfile) (*pb.DestinationProfile, error) {
routes := make([]*pb.Route, 0)
for _, route := range profile.Spec.Routes {
pbRoute, err := toRoute(profile, route)
if err != nil {
return nil, err
}
routes = append(routes, pbRoute)
}
budget := defaultRetryBudget()
if profile.Spec.RetryBudget != nil {
budget.MinRetriesPerSecond = profile.Spec.RetryBudget.MinRetriesPerSecond
budget.RetryRatio = profile.Spec.RetryBudget.RetryRatio
ttl, err := time.ParseDuration(profile.Spec.RetryBudget.TTL)
if err != nil {
return nil, err
}
budget.Ttl = toDuration(ttl)
}
var opaqueProtocol bool
if profile.Spec.OpaquePorts != nil {
_, opaqueProtocol = profile.Spec.OpaquePorts[pt.port]
}
return &pb.DestinationProfile{
Routes: routes,
RetryBudget: budget,
DstOverrides: toDstOverrides(profile.Spec.DstOverrides, pt.port),
FullyQualifiedName: pt.fullyQualifiedName,
Endpoint: pt.endpoint,
OpaqueProtocol: opaqueProtocol,
}, nil
}
func toDstOverrides(dsts []*sp.WeightedDst, port uint32) []*pb.WeightedDst {
pbDsts := []*pb.WeightedDst{}
for _, dst := range dsts {
authority := dst.Authority
// Authority should be a FQDN with port
// Use the port from GetProfile is absent in authority
hostPort := strings.Split(authority, ":")
if len(hostPort) == 1 {
authority = fmt.Sprintf("%s:%d", authority, port)
}
pbDst := &pb.WeightedDst{
Authority: authority,
// Weights are expressed in decimillis: 10_000 represents 100%
Weight: uint32(dst.Weight.MilliValue() * millisPerDecimilli),
}
pbDsts = append(pbDsts, pbDst)
}
return pbDsts
}
// toRoute returns a Proxy API Route, given a ServiceProfile Route.
func toRoute(profile *sp.ServiceProfile, route *sp.RouteSpec) (*pb.Route, error) {
cond, err := toRequestMatch(route.Condition)
if err != nil {
return nil, err
}
rcs := make([]*pb.ResponseClass, 0)
for _, rc := range route.ResponseClasses {
pbRc, err := toResponseClass(rc)
if err != nil {
return nil, err
}
rcs = append(rcs, pbRc)
}
var timeout time.Duration // No default timeout
if route.Timeout != "" {
timeout, err = time.ParseDuration(route.Timeout)
if err != nil {
logging.Errorf(
"failed to parse duration for route '%s' in service profile '%s' in namespace '%s': %s",
route.Name,
profile.Name,
profile.Namespace,
err,
)
}
}
return &pb.Route{
Condition: cond,
ResponseClasses: rcs,
MetricsLabels: map[string]string{"route": route.Name},
IsRetryable: route.IsRetryable,
Timeout: toDuration(timeout),
}, nil
}
// toResponseClass returns a Proxy API ResponseClass, given a ServiceProfile
// ResponseClass.
func toResponseClass(rc *sp.ResponseClass) (*pb.ResponseClass, error) {
cond, err := toResponseMatch(rc.Condition)
if err != nil {
return nil, err
}
return &pb.ResponseClass{
Condition: cond,
IsFailure: rc.IsFailure,
}, nil
}
// toResponseMatch returns a Proxy API ResponseMatch, given a ServiceProfile
// ResponseMatch.
func toResponseMatch(rspMatch *sp.ResponseMatch) (*pb.ResponseMatch, error) {
if rspMatch == nil {
return nil, errors.New("missing response match")
}
err := profiles.ValidateResponseMatch(rspMatch)
if err != nil {
return nil, err
}
matches := make([]*pb.ResponseMatch, 0)
if rspMatch.All != nil {
all := make([]*pb.ResponseMatch, 0)
for _, m := range rspMatch.All {
pbM, err := toResponseMatch(m)
if err != nil {
return nil, err
}
all = append(all, pbM)
}
matches = append(matches, &pb.ResponseMatch{
Match: &pb.ResponseMatch_All{
All: &pb.ResponseMatch_Seq{
Matches: all,
},
},
})
}
if rspMatch.Any != nil {
any := make([]*pb.ResponseMatch, 0)
for _, m := range rspMatch.Any {
pbM, err := toResponseMatch(m)
if err != nil {
return nil, err
}
any = append(any, pbM)
}
matches = append(matches, &pb.ResponseMatch{
Match: &pb.ResponseMatch_Any{
Any: &pb.ResponseMatch_Seq{
Matches: any,
},
},
})
}
if rspMatch.Status != nil {
matches = append(matches, &pb.ResponseMatch{
Match: &pb.ResponseMatch_Status{
Status: &pb.HttpStatusRange{
Max: rspMatch.Status.Max,
Min: rspMatch.Status.Min,
},
},
})
}
if rspMatch.Not != nil {
not, err := toResponseMatch(rspMatch.Not)
if err != nil {
return nil, err
}
matches = append(matches, &pb.ResponseMatch{
Match: &pb.ResponseMatch_Not{
Not: not,
},
})
}
if len(matches) == 0 {
return nil, errors.New("A response match must have a field set")
}
if len(matches) == 1 {
return matches[0], nil
}
return &pb.ResponseMatch{
Match: &pb.ResponseMatch_All{
All: &pb.ResponseMatch_Seq{
Matches: matches,
},
},
}, nil
}
// toRequestMatch returns a Proxy API RequestMatch, given a ServiceProfile
// RequestMatch.
func toRequestMatch(reqMatch *sp.RequestMatch) (*pb.RequestMatch, error) {
if reqMatch == nil {
return nil, errors.New("missing request match")
}
err := profiles.ValidateRequestMatch(reqMatch)
if err != nil {
return nil, err
}
matches := make([]*pb.RequestMatch, 0)
if reqMatch.All != nil {
all := make([]*pb.RequestMatch, 0)
for _, m := range reqMatch.All {
pbM, err := toRequestMatch(m)
if err != nil {
return nil, err
}
all = append(all, pbM)
}
matches = append(matches, &pb.RequestMatch{
Match: &pb.RequestMatch_All{
All: &pb.RequestMatch_Seq{
Matches: all,
},
},
})
}
if reqMatch.Any != nil {
any := make([]*pb.RequestMatch, 0)
for _, m := range reqMatch.Any {
pbM, err := toRequestMatch(m)
if err != nil {
return nil, err
}
any = append(any, pbM)
}
matches = append(matches, &pb.RequestMatch{
Match: &pb.RequestMatch_Any{
Any: &pb.RequestMatch_Seq{
Matches: any,
},
},
})
}
if reqMatch.Method != "" {
matches = append(matches, &pb.RequestMatch{
Match: &pb.RequestMatch_Method{
Method: util.ParseMethod(reqMatch.Method),
},
})
}
if reqMatch.Not != nil {
not, err := toRequestMatch(reqMatch.Not)
if err != nil {
return nil, err
}
matches = append(matches, &pb.RequestMatch{
Match: &pb.RequestMatch_Not{
Not: not,
},
})
}
if reqMatch.PathRegex != "" {
matches = append(matches, &pb.RequestMatch{
Match: &pb.RequestMatch_Path{
Path: &pb.PathMatch{
Regex: reqMatch.PathRegex,
},
},
})
}
if len(matches) == 0 {
return nil, errors.New("A request match must have a field set")
}
if len(matches) == 1 {
return matches[0], nil
}
return &pb.RequestMatch{
Match: &pb.RequestMatch_All{
All: &pb.RequestMatch_Seq{
Matches: matches,
},
},
}, nil
}