package router import ( "fmt" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "go.uber.org/zap" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/kubernetes" flaggerv1 "github.com/weaveworks/flagger/pkg/apis/flagger/v1beta1" contourv1 "github.com/weaveworks/flagger/pkg/apis/projectcontour/v1" clientset "github.com/weaveworks/flagger/pkg/client/clientset/versioned" ) // ContourRouter is managing HTTPProxy objects type ContourRouter struct { kubeClient kubernetes.Interface contourClient clientset.Interface flaggerClient clientset.Interface logger *zap.SugaredLogger } // Reconcile creates or updates the HTTP proxy func (cr *ContourRouter) Reconcile(canary *flaggerv1.Canary) error { apexName, primaryName, canaryName := canary.GetServiceNames() newSpec := contourv1.HTTPProxySpec{ Routes: []contourv1.Route{ { Conditions: []contourv1.Condition{ { Prefix: cr.makePrefix(canary), }, }, TimeoutPolicy: cr.makeTimeoutPolicy(canary), RetryPolicy: cr.makeRetryPolicy(canary), Services: []contourv1.Service{ { Name: primaryName, Port: int(canary.Spec.Service.Port), Weight: uint32(100), RequestHeadersPolicy: &contourv1.HeadersPolicy{ Set: []contourv1.HeaderValue{ cr.makeLinkerdHeaderValue(canary, primaryName), }, }, }, { Name: canaryName, Port: int(canary.Spec.Service.Port), Weight: uint32(0), RequestHeadersPolicy: &contourv1.HeadersPolicy{ Set: []contourv1.HeaderValue{ cr.makeLinkerdHeaderValue(canary, canaryName), }, }, }, }, }, }, } if len(canary.GetAnalysis().Match) > 0 { newSpec = contourv1.HTTPProxySpec{ Routes: []contourv1.Route{ { Conditions: cr.makeConditions(canary), TimeoutPolicy: cr.makeTimeoutPolicy(canary), RetryPolicy: cr.makeRetryPolicy(canary), Services: []contourv1.Service{ { Name: primaryName, Port: int(canary.Spec.Service.Port), Weight: uint32(100), RequestHeadersPolicy: &contourv1.HeadersPolicy{ Set: []contourv1.HeaderValue{ cr.makeLinkerdHeaderValue(canary, primaryName), }, }, }, { Name: canaryName, Port: int(canary.Spec.Service.Port), Weight: uint32(0), RequestHeadersPolicy: &contourv1.HeadersPolicy{ Set: []contourv1.HeaderValue{ cr.makeLinkerdHeaderValue(canary, canaryName), }, }, }, }, }, { Conditions: []contourv1.Condition{ { Prefix: cr.makePrefix(canary), }, }, TimeoutPolicy: cr.makeTimeoutPolicy(canary), RetryPolicy: cr.makeRetryPolicy(canary), Services: []contourv1.Service{ { Name: primaryName, Port: int(canary.Spec.Service.Port), Weight: uint32(100), RequestHeadersPolicy: &contourv1.HeadersPolicy{ Set: []contourv1.HeaderValue{ cr.makeLinkerdHeaderValue(canary, primaryName), }, }, }, { Name: canaryName, Port: int(canary.Spec.Service.Port), Weight: uint32(0), RequestHeadersPolicy: &contourv1.HeadersPolicy{ Set: []contourv1.HeaderValue{ cr.makeLinkerdHeaderValue(canary, canaryName), }, }, }, }, }, }, } } proxy, err := cr.contourClient.ProjectcontourV1().HTTPProxies(canary.Namespace).Get(apexName, metav1.GetOptions{}) if errors.IsNotFound(err) { proxy = &contourv1.HTTPProxy{ ObjectMeta: metav1.ObjectMeta{ Name: apexName, Namespace: canary.Namespace, OwnerReferences: []metav1.OwnerReference{ *metav1.NewControllerRef(canary, schema.GroupVersionKind{ Group: flaggerv1.SchemeGroupVersion.Group, Version: flaggerv1.SchemeGroupVersion.Version, Kind: flaggerv1.CanaryKind, }), }, }, Spec: newSpec, Status: contourv1.Status{ CurrentStatus: "valid", Description: "valid HTTPProxy", }, } _, err = cr.contourClient.ProjectcontourV1().HTTPProxies(canary.Namespace).Create(proxy) if err != nil { return fmt.Errorf("HTTPProxy %s.%s create error: %w", apexName, canary.Namespace, err) } cr.logger.With("canary", fmt.Sprintf("%s.%s", canary.Name, canary.Namespace)). Infof("HTTPProxy %s.%s created", proxy.GetName(), canary.Namespace) return nil } else if err != nil { return fmt.Errorf("HTTPProxy %s.%s get query error: %w", apexName, canary.Namespace, err) } // update HTTPProxy but keep the original destination weights if proxy != nil { if diff := cmp.Diff( newSpec, proxy.Spec, cmpopts.IgnoreFields(contourv1.Service{}, "Weight"), ); diff != "" { clone := proxy.DeepCopy() clone.Spec = newSpec _, err = cr.contourClient.ProjectcontourV1().HTTPProxies(canary.Namespace).Update(clone) if err != nil { return fmt.Errorf("HTTPProxy %s.%s update error: %w", apexName, canary.Namespace, err) } cr.logger.With("canary", fmt.Sprintf("%s.%s", canary.Name, canary.Namespace)). Infof("HTTPProxy %s.%s updated", proxy.GetName(), canary.Namespace) } } return nil } // GetRoutes returns the service weight for primary and canary func (cr *ContourRouter) GetRoutes(canary *flaggerv1.Canary) ( primaryWeight int, canaryWeight int, mirrored bool, err error, ) { apexName, primaryName, _ := canary.GetServiceNames() proxy, err := cr.contourClient.ProjectcontourV1().HTTPProxies(canary.Namespace).Get(apexName, metav1.GetOptions{}) if err != nil { err = fmt.Errorf("HTTPProxy %s.%s get query error %w", apexName, canary.Namespace, err) return } if len(proxy.Spec.Routes) < 1 || len(proxy.Spec.Routes[0].Services) < 2 { err = fmt.Errorf("HTTPProxy %s.%s services not found", apexName, canary.Namespace) return } for _, dst := range proxy.Spec.Routes[0].Services { if dst.Name == primaryName { primaryWeight = int(dst.Weight) canaryWeight = 100 - primaryWeight return } } return } // SetRoutes updates the service weight for primary and canary func (cr *ContourRouter) SetRoutes( canary *flaggerv1.Canary, primaryWeight int, canaryWeight int, _ bool, ) error { apexName, primaryName, canaryName := canary.GetServiceNames() if primaryWeight == 0 && canaryWeight == 0 { return fmt.Errorf("HTTPProxy %s.%s update failed: no valid weights", apexName, canary.Namespace) } proxy, err := cr.contourClient.ProjectcontourV1().HTTPProxies(canary.Namespace).Get(apexName, metav1.GetOptions{}) if err != nil { return fmt.Errorf("HTTPProxy %s.%s query error: %w", apexName, canary.Namespace, err) } proxy.Spec = contourv1.HTTPProxySpec{ Routes: []contourv1.Route{ { Conditions: []contourv1.Condition{ { Prefix: cr.makePrefix(canary), }, }, TimeoutPolicy: cr.makeTimeoutPolicy(canary), RetryPolicy: cr.makeRetryPolicy(canary), Services: []contourv1.Service{ { Name: primaryName, Port: int(canary.Spec.Service.Port), Weight: uint32(primaryWeight), RequestHeadersPolicy: &contourv1.HeadersPolicy{ Set: []contourv1.HeaderValue{ cr.makeLinkerdHeaderValue(canary, primaryName), }, }, }, { Name: canaryName, Port: int(canary.Spec.Service.Port), Weight: uint32(canaryWeight), RequestHeadersPolicy: &contourv1.HeadersPolicy{ Set: []contourv1.HeaderValue{ cr.makeLinkerdHeaderValue(canary, canaryName), }, }, }, }}, }, } if len(canary.GetAnalysis().Match) > 0 { proxy.Spec = contourv1.HTTPProxySpec{ Routes: []contourv1.Route{ { Conditions: cr.makeConditions(canary), TimeoutPolicy: cr.makeTimeoutPolicy(canary), RetryPolicy: cr.makeRetryPolicy(canary), Services: []contourv1.Service{ { Name: primaryName, Port: int(canary.Spec.Service.Port), Weight: uint32(primaryWeight), RequestHeadersPolicy: &contourv1.HeadersPolicy{ Set: []contourv1.HeaderValue{ cr.makeLinkerdHeaderValue(canary, primaryName), }, }, }, { Name: canaryName, Port: int(canary.Spec.Service.Port), Weight: uint32(canaryWeight), RequestHeadersPolicy: &contourv1.HeadersPolicy{ Set: []contourv1.HeaderValue{ cr.makeLinkerdHeaderValue(canary, canaryName), }, }, }, }, }, { Conditions: []contourv1.Condition{ { Prefix: cr.makePrefix(canary), }, }, TimeoutPolicy: cr.makeTimeoutPolicy(canary), RetryPolicy: cr.makeRetryPolicy(canary), Services: []contourv1.Service{ { Name: primaryName, Port: int(canary.Spec.Service.Port), Weight: uint32(100), RequestHeadersPolicy: &contourv1.HeadersPolicy{ Set: []contourv1.HeaderValue{ cr.makeLinkerdHeaderValue(canary, primaryName), }, }, }, { Name: canaryName, Port: int(canary.Spec.Service.Port), Weight: uint32(0), RequestHeadersPolicy: &contourv1.HeadersPolicy{ Set: []contourv1.HeaderValue{ cr.makeLinkerdHeaderValue(canary, canaryName), }, }, }, }, }, }, } } _, err = cr.contourClient.ProjectcontourV1().HTTPProxies(canary.Namespace).Update(proxy) if err != nil { return fmt.Errorf("HTTPProxy %s.%s update error: %w", apexName, canary.Namespace, err) } return nil } func (cr *ContourRouter) makePrefix(canary *flaggerv1.Canary) string { prefix := "/" if len(canary.Spec.Service.Match) > 0 && canary.Spec.Service.Match[0].Uri != nil && canary.Spec.Service.Match[0].Uri.Prefix != "" { prefix = canary.Spec.Service.Match[0].Uri.Prefix } return prefix } func (cr *ContourRouter) makeConditions(canary *flaggerv1.Canary) []contourv1.Condition { list := []contourv1.Condition{} if len(canary.GetAnalysis().Match) > 0 { for _, match := range canary.GetAnalysis().Match { for s, stringMatch := range match.Headers { h := &contourv1.HeaderCondition{ Name: s, Exact: stringMatch.Exact, } if stringMatch.Suffix != "" { h = &contourv1.HeaderCondition{ Name: s, Contains: stringMatch.Suffix, } } if stringMatch.Prefix != "" { h = &contourv1.HeaderCondition{ Name: s, Contains: stringMatch.Prefix, } } list = append(list, contourv1.Condition{ Prefix: cr.makePrefix(canary), Header: h, }) } } } else { list = []contourv1.Condition{ { Prefix: cr.makePrefix(canary), }, } } return list } func (cr *ContourRouter) makeTimeoutPolicy(canary *flaggerv1.Canary) *contourv1.TimeoutPolicy { if canary.Spec.Service.Timeout != "" { return &contourv1.TimeoutPolicy{ Response: canary.Spec.Service.Timeout, Idle: "5m", } } return nil } func (cr *ContourRouter) makeRetryPolicy(canary *flaggerv1.Canary) *contourv1.RetryPolicy { if canary.Spec.Service.Retries != nil { return &contourv1.RetryPolicy{ NumRetries: uint32(canary.Spec.Service.Retries.Attempts), PerTryTimeout: canary.Spec.Service.Retries.PerTryTimeout, } } return nil } func (cr *ContourRouter) makeLinkerdHeaderValue(canary *flaggerv1.Canary, serviceName string) contourv1.HeaderValue { return contourv1.HeaderValue{ Name: "l5d-dst-override", Value: fmt.Sprintf("%s.%s.svc.cluster.local:%v", serviceName, canary.Namespace, canary.Spec.Service.Port), } }