From dd272c68701ff7598cb481ae69a2e3e8692c3fce Mon Sep 17 00:00:00 2001 From: stefanprodan Date: Mon, 4 Nov 2019 18:26:28 +0200 Subject: [PATCH] Expose canaries on public domains with App Mesh Gateway - map canary service hosts to domain gateway annotation - map canary retries and timeout to gateway annotations --- pkg/router/appmesh.go | 47 ++++++++++++++++++++++++++++++++++---- pkg/router/appmesh_test.go | 44 +++++++++++++++++++++++++++++++++++ pkg/router/router_test.go | 7 ++++++ 3 files changed, 93 insertions(+), 5 deletions(-) diff --git a/pkg/router/appmesh.go b/pkg/router/appmesh.go index a30df7ce..33136035 100644 --- a/pkg/router/appmesh.go +++ b/pkg/router/appmesh.go @@ -2,6 +2,7 @@ package router import ( "fmt" + "strconv" "strings" "time" @@ -80,7 +81,7 @@ func (ar *AppMeshRouter) Reconcile(canary *flaggerv1.Canary) error { // reconcileVirtualNode creates or updates a virtual node // the virtual node naming format is name-role-namespace func (ar *AppMeshRouter) reconcileVirtualNode(canary *flaggerv1.Canary, name string, host string) error { - protocol := getProtocol(canary) + protocol := ar.getProtocol(canary) vnSpec := appmeshv1.VirtualNodeSpec{ MeshName: canary.Spec.Service.MeshName, Listeners: []appmeshv1.Listener{ @@ -164,7 +165,7 @@ func (ar *AppMeshRouter) reconcileVirtualService(canary *flaggerv1.Canary, name targetName := canary.Spec.TargetRef.Name canaryVirtualNode := fmt.Sprintf("%s-canary", targetName) primaryVirtualNode := fmt.Sprintf("%s-primary", targetName) - protocol := getProtocol(canary) + protocol := ar.getProtocol(canary) routerName := targetName if canaryWeight > 0 { @@ -212,7 +213,7 @@ func (ar *AppMeshRouter) reconcileVirtualService(canary *flaggerv1.Canary, name Http: &appmeshv1.HttpRoute{ Match: appmeshv1.HttpRouteMatch{ Prefix: routePrefix, - Headers: makeHeaders(canary), + Headers: ar.makeHeaders(canary), }, RetryPolicy: makeRetryPolicy(canary), Action: appmeshv1.HttpRouteAction{ @@ -284,6 +285,15 @@ func (ar *AppMeshRouter) reconcileVirtualService(canary *flaggerv1.Canary, name }, Spec: vsSpec, } + + // set App Mesh Gateway annotation on primary virtual service + if canaryWeight == 0 { + a := ar.gatewayAnnotations(canary) + if len(a) > 0 { + virtualService.ObjectMeta.Annotations = a + } + } + _, err = ar.appmeshClient.AppmeshV1beta1().VirtualServices(canary.Namespace).Create(virtualService) if err != nil { return fmt.Errorf("VirtualService %s create error %v", name, err) @@ -304,6 +314,14 @@ func (ar *AppMeshRouter) reconcileVirtualService(canary *flaggerv1.Canary, name vsClone.Spec = vsSpec vsClone.Spec.Routes[0].Http.Action = virtualService.Spec.Routes[0].Http.Action + // update App Mesh Gateway annotation on primary virtual service + if canaryWeight == 0 { + a := ar.gatewayAnnotations(canary) + if len(a) > 0 { + vsClone.ObjectMeta.Annotations = a + } + } + _, err = ar.appmeshClient.AppmeshV1beta1().VirtualServices(canary.Namespace).Update(vsClone) if err != nil { return fmt.Errorf("VirtualService %s update error %v", name, err) @@ -432,7 +450,7 @@ func makeRetryPolicy(canary *flaggerv1.Canary) *appmeshv1.HttpRetryPolicy { } // makeRetryPolicy creates an App Mesh HttpRouteHeader from the Canary.CanaryAnalysis.Match -func makeHeaders(canary *flaggerv1.Canary) []appmeshv1.HttpRouteHeader { +func (ar *AppMeshRouter) makeHeaders(canary *flaggerv1.Canary) []appmeshv1.HttpRouteHeader { headers := []appmeshv1.HttpRouteHeader{} for _, m := range canary.Spec.CanaryAnalysis.Match { @@ -453,13 +471,32 @@ func makeHeaders(canary *flaggerv1.Canary) []appmeshv1.HttpRouteHeader { return headers } -func getProtocol(canary *flaggerv1.Canary) string { +func (ar *AppMeshRouter) getProtocol(canary *flaggerv1.Canary) string { if strings.Contains(canary.Spec.Service.PortName, "grpc") { return "grpc" } return "http" } +func (ar *AppMeshRouter) gatewayAnnotations(canary *flaggerv1.Canary) map[string]string { + a := make(map[string]string) + domains := "" + for _, value := range canary.Spec.Service.Hosts { + domains += value + "," + } + if domains != "" { + a["gateway.appmesh.k8s.aws/expose"] = "true" + a["gateway.appmesh.k8s.aws/domain"] = domains + if canary.Spec.Service.Timeout != "" { + a["gateway.appmesh.k8s.aws/timeout"] = canary.Spec.Service.Timeout + } + if canary.Spec.Service.Retries != nil && canary.Spec.Service.Retries.Attempts > 0 { + a["gateway.appmesh.k8s.aws/retries"] = strconv.Itoa(canary.Spec.Service.Retries.Attempts) + } + } + return a +} + func int64p(i int64) *int64 { return &i } diff --git a/pkg/router/appmesh_test.go b/pkg/router/appmesh_test.go index 003b127b..e5509033 100644 --- a/pkg/router/appmesh_test.go +++ b/pkg/router/appmesh_test.go @@ -2,6 +2,8 @@ package router import ( "fmt" + "strconv" + "strings" "testing" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -226,3 +228,45 @@ func TestAppmeshRouter_ABTest(t *testing.T) { t.Errorf("Got http match header exact %v wanted %v", exactMatch, "test") } } + +func TestAppmeshRouter_Gateway(t *testing.T) { + mocks := setupfakeClients() + router := &AppMeshRouter{ + logger: mocks.logger, + flaggerClient: mocks.flaggerClient, + appmeshClient: mocks.meshClient, + kubeClient: mocks.kubeClient, + } + + err := router.Reconcile(mocks.appmeshCanary) + if err != nil { + t.Fatal(err.Error()) + } + + // check virtual service + vsName := fmt.Sprintf("%s.%s", mocks.appmeshCanary.Spec.TargetRef.Name, mocks.appmeshCanary.Namespace) + vs, err := router.appmeshClient.AppmeshV1beta1().VirtualServices("default").Get(vsName, metav1.GetOptions{}) + if err != nil { + t.Fatal(err.Error()) + } + + expose := vs.Annotations["gateway.appmesh.k8s.aws/expose"] + if expose != "true" { + t.Errorf("Got gateway expose annotation %v wanted %v", expose, "true") + } + + domain := vs.Annotations["gateway.appmesh.k8s.aws/domain"] + if !strings.Contains(domain, mocks.appmeshCanary.Spec.Service.Hosts[0]) { + t.Errorf("Got gateway domain annotation %v wanted %v", domain, mocks.appmeshCanary.Spec.Service.Hosts[0]) + } + + timeout := vs.Annotations["gateway.appmesh.k8s.aws/timeout"] + if timeout != mocks.appmeshCanary.Spec.Service.Timeout { + t.Errorf("Got gateway timeout annotation %v wanted %v", timeout, mocks.appmeshCanary.Spec.Service.Timeout) + } + + retries := vs.Annotations["gateway.appmesh.k8s.aws/retries"] + if retries != strconv.Itoa(mocks.appmeshCanary.Spec.Service.Retries.Attempts) { + t.Errorf("Got gateway retries annotation %v wanted %v", retries, strconv.Itoa(mocks.appmeshCanary.Spec.Service.Retries.Attempts)) + } +} diff --git a/pkg/router/router_test.go b/pkg/router/router_test.go index f781f692..ca78079e 100644 --- a/pkg/router/router_test.go +++ b/pkg/router/router_test.go @@ -70,7 +70,14 @@ func newMockCanaryAppMesh() *flaggerv1.Canary { Service: flaggerv1.CanaryService{ Port: 9898, MeshName: "global", + Hosts: []string{"*"}, Backends: []string{"backend.default"}, + Timeout: "25", + Retries: &istiov1alpha3.HTTPRetry{ + Attempts: 5, + PerTryTimeout: "gateway-error", + RetryOn: "5s", + }, }, CanaryAnalysis: flaggerv1.CanaryAnalysis{ Threshold: 10, StepWeight: 10,