From ee0e7b091a7c0e30aa76e99930bad35631aaaa91 Mon Sep 17 00:00:00 2001 From: stefanprodan Date: Wed, 18 Dec 2019 17:36:21 +0200 Subject: [PATCH] Implement Contour router for traffic shifting --- pkg/router/contour.go | 196 ++++++++++++++++++++++++++++++++++++++++++ pkg/router/factory.go | 7 ++ 2 files changed, 203 insertions(+) create mode 100644 pkg/router/contour.go diff --git a/pkg/router/contour.go b/pkg/router/contour.go new file mode 100644 index 00000000..5ee34905 --- /dev/null +++ b/pkg/router/contour.go @@ -0,0 +1,196 @@ +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/v1alpha3" + 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 { + targetName := canary.Spec.TargetRef.Name + primaryName := fmt.Sprintf("%s-primary", targetName) + canaryName := fmt.Sprintf("%s-canary", targetName) + + newSpec := contourv1.HTTPProxySpec{ + Routes: []contourv1.Route{ + { + Conditions: []contourv1.Condition{ + { + Prefix: "/", + }, + }, + Services: []contourv1.Service{ + { + Name: primaryName, + Port: int(canary.Spec.Service.Port), + Weight: uint32(100), + }, + { + Name: canaryName, + Port: int(canary.Spec.Service.Port), + Weight: uint32(0), + }, + }}, + }, + } + + proxy, err := cr.contourClient.ProjectcontourV1().HTTPProxies(canary.Namespace).Get(targetName, metav1.GetOptions{}) + if errors.IsNotFound(err) { + proxy = &contourv1.HTTPProxy{ + ObjectMeta: metav1.ObjectMeta{ + Name: targetName, + Namespace: canary.Namespace, + OwnerReferences: []metav1.OwnerReference{ + *metav1.NewControllerRef(canary, schema.GroupVersionKind{ + Group: flaggerv1.SchemeGroupVersion.Group, + Version: flaggerv1.SchemeGroupVersion.Version, + Kind: flaggerv1.CanaryKind, + }), + }, + }, + Spec: newSpec, + } + + _, err = cr.contourClient.ProjectcontourV1().HTTPProxies(canary.Namespace).Create(proxy) + if err != nil { + return fmt.Errorf("HTTPProxy %s.%s create error %v", targetName, 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 + } + + if err != nil { + return fmt.Errorf("HTTPProxy %s.%s query error %v", targetName, 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 %v", targetName, 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, +) { + targetName := canary.Spec.TargetRef.Name + primaryName := fmt.Sprintf("%s-primary", targetName) + + proxy, err := cr.contourClient.ProjectcontourV1().HTTPProxies(canary.Namespace).Get(targetName, metav1.GetOptions{}) + if err != nil { + if errors.IsNotFound(err) { + err = fmt.Errorf("HTTPProxy %s.%s not found", targetName, canary.Namespace) + return + } + err = fmt.Errorf("HTTPProxy %s.%s query error %v", targetName, 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", targetName, 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, + mirrored bool, +) error { + targetName := canary.Spec.TargetRef.Name + primaryName := fmt.Sprintf("%s-primary", targetName) + canaryName := fmt.Sprintf("%s-canary", targetName) + + if primaryWeight == 0 && canaryWeight == 0 { + return fmt.Errorf("HTTPProxy %s.%s update failed: no valid weights", targetName, canary.Namespace) + } + + proxy, err := cr.contourClient.ProjectcontourV1().HTTPProxies(canary.Namespace).Get(targetName, metav1.GetOptions{}) + if err != nil { + if errors.IsNotFound(err) { + return fmt.Errorf("HTTPProxy %s.%s not found", targetName, canary.Namespace) + + } + return fmt.Errorf("HTTPProxy %s.%s query error %v", targetName, canary.Namespace, err) + } + + proxy.Spec = contourv1.HTTPProxySpec{ + Routes: []contourv1.Route{ + { + Conditions: []contourv1.Condition{ + { + Prefix: "/", + }, + }, + Services: []contourv1.Service{ + { + Name: primaryName, + Port: int(canary.Spec.Service.Port), + Weight: uint32(primaryWeight), + }, + { + Name: canaryName, + Port: int(canary.Spec.Service.Port), + Weight: uint32(canaryWeight), + }, + }}, + }, + } + + _, err = cr.contourClient.ProjectcontourV1().HTTPProxies(canary.Namespace).Update(proxy) + if err != nil { + return fmt.Errorf("HTTPProxy %s.%s update error %v", targetName, canary.Namespace, err) + } + return nil +} diff --git a/pkg/router/factory.go b/pkg/router/factory.go index 2e2803e5..8d2df93e 100644 --- a/pkg/router/factory.go +++ b/pkg/router/factory.go @@ -93,6 +93,13 @@ func (factory *Factory) MeshRouter(provider string) Interface { smiClient: factory.meshClient, targetMesh: "linkerd", } + case provider == "contour": + return &ContourRouter{ + logger: factory.logger, + flaggerClient: factory.flaggerClient, + kubeClient: factory.kubeClient, + contourClient: factory.meshClient, + } case strings.HasPrefix(provider, "gloo"): upstreamDiscoveryNs := "gloo-system" if strings.HasPrefix(provider, "gloo:") {