diff --git a/README.md b/README.md index 4355172e..dab3b810 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,8 @@ spec: kind: HorizontalPodAutoscaler name: podinfo service: + # service name (optional) + name: podinfo # ClusterIP port number port: 9898 # container port name or number (optional) diff --git a/artifacts/flagger/crd.yaml b/artifacts/flagger/crd.yaml index c424fce2..8c802972 100644 --- a/artifacts/flagger/crd.yaml +++ b/artifacts/flagger/crd.yaml @@ -115,6 +115,9 @@ spec: type: object required: ["port"] properties: + name: + description: Kubernetes service name + type: string port: description: Container port number type: number diff --git a/charts/flagger/templates/crd.yaml b/charts/flagger/templates/crd.yaml index f276da00..ef5fed8b 100644 --- a/charts/flagger/templates/crd.yaml +++ b/charts/flagger/templates/crd.yaml @@ -116,6 +116,9 @@ spec: type: object required: ['port'] properties: + name: + description: Kubernetes service name + type: string port: description: Container port number type: number diff --git a/docs/gitbook/faq.md b/docs/gitbook/faq.md index 1cec5a75..a6236a8b 100644 --- a/docs/gitbook/faq.md +++ b/docs/gitbook/faq.md @@ -188,6 +188,8 @@ spec: kind: Deployment name: podinfo service: + # service name (optional) + name: podinfo # ClusterIP port number (required) port: 9898 # container port name or number @@ -196,19 +198,21 @@ spec: portName: http ``` +If the `service.name` is not specified, then `targetRef.name` is used for the apex domain and canary/primary services name prefix. +You should treat the service name as an immutable field, changing it could result in routing conflicts. + Based on the canary spec service, Flagger generates the following Kubernetes ClusterIP service: -* `..svc.cluster.local` +* `..svc.cluster.local` selector `app=-primary` -* `-primary..svc.cluster.local` +* `-primary..svc.cluster.local` selector `app=-primary` -* `-canary..svc.cluster.local` +* `-canary..svc.cluster.local` selector `app=` This ensures that traffic coming from a namespace outside the mesh to `podinfo.test:9898` will be routed to the latest stable release of your app. - ```yaml apiVersion: v1 kind: Service diff --git a/docs/gitbook/how-it-works.md b/docs/gitbook/how-it-works.md index 717d1b6e..7a6ad1e8 100644 --- a/docs/gitbook/how-it-works.md +++ b/docs/gitbook/how-it-works.md @@ -32,6 +32,8 @@ spec: kind: HorizontalPodAutoscaler name: podinfo service: + # service name (optional) + name: podinfo # ClusterIP port number port: 9898 # ClusterIP port name can be http or grpc (default http) diff --git a/go.mod b/go.mod index 89a09224..13059054 100644 --- a/go.mod +++ b/go.mod @@ -15,10 +15,10 @@ require ( go.uber.org/multierr v1.1.0 // indirect go.uber.org/zap v1.10.0 gopkg.in/h2non/gock.v1 v1.0.14 - k8s.io/api v0.17.0 - k8s.io/apimachinery v0.17.1-beta.0 - k8s.io/client-go v0.17.0 - k8s.io/code-generator v0.17.0 + k8s.io/api v0.17.1 + k8s.io/apimachinery v0.17.1 + k8s.io/client-go v0.17.1 + k8s.io/code-generator v0.17.1 k8s.io/utils v0.0.0-20191114184206-e782cd3c129f ) diff --git a/go.sum b/go.sum index 438f044b..7706163d 100644 --- a/go.sum +++ b/go.sum @@ -290,15 +290,14 @@ gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -k8s.io/api v0.17.0 h1:H9d/lw+VkZKEVIUc8F3wgiQ+FUXTTr21M87jXLU7yqM= -k8s.io/api v0.17.0/go.mod h1:npsyOePkeP0CPwyGfXDHxvypiYMJxBWAMpQxCaJ4ZxI= -k8s.io/apimachinery v0.17.0/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg= -k8s.io/apimachinery v0.17.1-beta.0 h1:0Wl/KpAiFOMe9to5h8x2Y6JnjV+BEWJiTcUk1Vx7zdE= -k8s.io/apimachinery v0.17.1-beta.0/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg= -k8s.io/client-go v0.17.0 h1:8QOGvUGdqDMFrm9sD6IUFl256BcffynGoe80sxgTEDg= -k8s.io/client-go v0.17.0/go.mod h1:TYgR6EUHs6k45hb6KWjVD6jFZvJV4gHDikv/It0xz+k= -k8s.io/code-generator v0.17.0 h1:y+KWtDWNqlJzJu/kUy8goJZO0X71PGIpAHLX8a0JYk0= -k8s.io/code-generator v0.17.0/go.mod h1:DVmfPQgxQENqDIzVR2ddLXMH34qeszkKSdH/N+s+38s= +k8s.io/api v0.17.1 h1:i46MidoDOE9tvQ0TTEYggf3ka/pziP1+tHI/GFVeJao= +k8s.io/api v0.17.1/go.mod h1:zxiAc5y8Ngn4fmhWUtSxuUlkfz1ixT7j9wESokELzOg= +k8s.io/apimachinery v0.17.1 h1:zUjS3szTxoUjTDYNvdFkYt2uMEXLcthcbp+7uZvWhYM= +k8s.io/apimachinery v0.17.1/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg= +k8s.io/client-go v0.17.1 h1:LbbuZ5tI7OYx4et5DfRFcJuoojvpYO0c7vps2rgJsHY= +k8s.io/client-go v0.17.1/go.mod h1:HZtHJSC/VuSHcETN9QA5QDZky1tXiYrkF/7t7vRpO1A= +k8s.io/code-generator v0.17.1 h1:e3B1UqRzRUWygp7WD+QTRT3ZUahPIaRKF0OFa7duQwI= +k8s.io/code-generator v0.17.1/go.mod h1:DVmfPQgxQENqDIzVR2ddLXMH34qeszkKSdH/N+s+38s= k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6 h1:4s3/R4+OYYYUKptXPhZKjQ04WJ6EhQQVFdjOFvCazDk= k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20190822140433-26a664648505 h1:ZY6yclUKVbZ+SdWnkfY+Je5vrMpKOxmGeKRbsXVmqYM= diff --git a/kustomize/base/flagger/crd.yaml b/kustomize/base/flagger/crd.yaml index c424fce2..8c802972 100644 --- a/kustomize/base/flagger/crd.yaml +++ b/kustomize/base/flagger/crd.yaml @@ -115,6 +115,9 @@ spec: type: object required: ["port"] properties: + name: + description: Kubernetes service name + type: string port: description: Container port number type: number diff --git a/pkg/apis/flagger/v1alpha3/types.go b/pkg/apis/flagger/v1alpha3/types.go index 0d9e3c56..74d5fabe 100644 --- a/pkg/apis/flagger/v1alpha3/types.go +++ b/pkg/apis/flagger/v1alpha3/types.go @@ -17,6 +17,7 @@ limitations under the License. package v1alpha3 import ( + "fmt" "time" hpav1 "k8s.io/api/autoscaling/v1" @@ -69,7 +70,7 @@ type CanarySpec struct { // virtual service spec Service CanaryService `json:"service"` - // metrics and thresholds + // metrics, thresholds and webhooks spec CanaryAnalysis CanaryAnalysis `json:"canaryAnalysis"` // the maximum time in seconds for a canary deployment to make progress @@ -92,8 +93,9 @@ type CanaryList struct { } // CanaryService is used to create ClusterIP services -// and Istio Virtual Service +// and service mesh or ingress routing objects type CanaryService struct { + Name string `json:"name,omitempty"` Port int32 `json:"port"` PortName string `json:"portName,omitempty"` TargetPort intstr.IntOrString `json:"targetPort,omitempty"` @@ -126,7 +128,7 @@ type CanaryAnalysis struct { Iterations int `json:"iterations,omitempty"` } -// CanaryMetric holds the reference to Istio metrics used for canary analysis +// CanaryMetric holds the reference to metrics used for canary analysis type CanaryMetric struct { Name string `json:"name"` Interval string `json:"interval,omitempty"` @@ -171,6 +173,17 @@ type CanaryWebhookPayload struct { Metadata map[string]string `json:"metadata,omitempty"` } +// GetServiceNames returns the apex, primary and canary Kubernetes service names +func (c *Canary) GetServiceNames() (apexName, primaryName, canaryName string) { + apexName = c.Spec.TargetRef.Name + if c.Spec.Service.Name != "" { + apexName = c.Spec.Service.Name + } + primaryName = fmt.Sprintf("%s-primary", apexName) + canaryName = fmt.Sprintf("%s-canary", apexName) + return +} + // GetProgressDeadlineSeconds returns the progress deadline (default 600s) func (c *Canary) GetProgressDeadlineSeconds() int { if c.Spec.ProgressDeadlineSeconds != nil { diff --git a/pkg/controller/controller.go b/pkg/controller/controller.go index 0b13b25e..23298952 100644 --- a/pkg/controller/controller.go +++ b/pkg/controller/controller.go @@ -105,17 +105,25 @@ func NewController( flaggerInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: ctrl.enqueue, UpdateFunc: func(old, new interface{}) { - oldRoll, ok := checkCustomResourceType(old, logger) + oldCanary, ok := checkCustomResourceType(old, logger) if !ok { return } - newRoll, ok := checkCustomResourceType(new, logger) + newCanary, ok := checkCustomResourceType(new, logger) if !ok { return } - if diff := cmp.Diff(newRoll.Spec, oldRoll.Spec); diff != "" { - ctrl.logger.Debugf("Diff detected %s.%s %s", oldRoll.Name, oldRoll.Namespace, diff) + if diff := cmp.Diff(newCanary.Spec, oldCanary.Spec); diff != "" { + ctrl.logger.Debugf("Diff detected %s.%s %s", oldCanary.Name, oldCanary.Namespace, diff) + + // warn about routing conflicts when service name changes + if oldCanary.Spec.Service.Name != "" && oldCanary.Spec.Service.Name != newCanary.Spec.Service.Name { + ctrl.logger.With("canary", fmt.Sprintf("%s.%s", oldCanary.Name, oldCanary.Namespace)). + Warnf("The service name changed to %s, remove %s objects to avoid routing conflicts", + newCanary.Spec.Service.Name, oldCanary.Spec.Service.Name) + } + ctrl.enqueue(new) } }, diff --git a/pkg/router/appmesh.go b/pkg/router/appmesh.go index 33136035..50876004 100644 --- a/pkg/router/appmesh.go +++ b/pkg/router/appmesh.go @@ -33,16 +33,14 @@ func (ar *AppMeshRouter) Reconcile(canary *flaggerv1.Canary) error { return fmt.Errorf("mesh name cannot be empty") } - targetName := canary.Spec.TargetRef.Name - targetHost := fmt.Sprintf("%s.%s", targetName, canary.Namespace) - primaryName := fmt.Sprintf("%s-primary", targetName) + apexName, primaryName, canaryName := canary.GetServiceNames() + targetHost := fmt.Sprintf("%s.%s", apexName, canary.Namespace) primaryHost := fmt.Sprintf("%s.%s", primaryName, canary.Namespace) - canaryName := fmt.Sprintf("%s-canary", targetName) canaryHost := fmt.Sprintf("%s.%s", canaryName, canary.Namespace) // sync virtual node e.g. app-namespace // DNS app.namespace - err := ar.reconcileVirtualNode(canary, targetName, primaryHost) + err := ar.reconcileVirtualNode(canary, apexName, primaryHost) if err != nil { return err } @@ -162,14 +160,14 @@ func (ar *AppMeshRouter) reconcileVirtualNode(canary *flaggerv1.Canary, name str // reconcileVirtualService creates or updates a virtual service func (ar *AppMeshRouter) reconcileVirtualService(canary *flaggerv1.Canary, name string, canaryWeight int64) error { - targetName := canary.Spec.TargetRef.Name - canaryVirtualNode := fmt.Sprintf("%s-canary", targetName) - primaryVirtualNode := fmt.Sprintf("%s-primary", targetName) + apexName, _, _ := canary.GetServiceNames() + canaryVirtualNode := fmt.Sprintf("%s-canary", apexName) + primaryVirtualNode := fmt.Sprintf("%s-primary", apexName) protocol := ar.getProtocol(canary) - routerName := targetName + routerName := apexName if canaryWeight > 0 { - routerName = fmt.Sprintf("%s-canary", targetName) + routerName = fmt.Sprintf("%s-canary", apexName) } // App Mesh supports only URI prefix routePrefix := "/" @@ -208,7 +206,7 @@ func (ar *AppMeshRouter) reconcileVirtualService(canary *flaggerv1.Canary, name if len(canary.Spec.CanaryAnalysis.Match) > 0 && canaryWeight == 0 { routes = []appmeshv1.Route{ { - Name: fmt.Sprintf("%s-a", targetName), + Name: fmt.Sprintf("%s-a", apexName), Priority: int64p(10), Http: &appmeshv1.HttpRoute{ Match: appmeshv1.HttpRouteMatch{ @@ -231,7 +229,7 @@ func (ar *AppMeshRouter) reconcileVirtualService(canary *flaggerv1.Canary, name }, }, { - Name: fmt.Sprintf("%s-b", targetName), + Name: fmt.Sprintf("%s-b", apexName), Priority: int64p(20), Http: &appmeshv1.HttpRoute{ Match: appmeshv1.HttpRouteMatch{ @@ -341,8 +339,8 @@ func (ar *AppMeshRouter) GetRoutes(canary *flaggerv1.Canary) ( mirrored bool, err error, ) { - targetName := canary.Spec.TargetRef.Name - vsName := fmt.Sprintf("%s.%s", targetName, canary.Namespace) + apexName, _, _ := canary.GetServiceNames() + vsName := fmt.Sprintf("%s.%s", apexName, canary.Namespace) vs, err := ar.appmeshClient.AppmeshV1beta1().VirtualServices(canary.Namespace).Get(vsName, metav1.GetOptions{}) if err != nil { if errors.IsNotFound(err) { @@ -360,17 +358,17 @@ func (ar *AppMeshRouter) GetRoutes(canary *flaggerv1.Canary) ( targets := vs.Spec.Routes[0].Http.Action.WeightedTargets for _, t := range targets { - if t.VirtualNodeName == fmt.Sprintf("%s-canary", targetName) { + if t.VirtualNodeName == fmt.Sprintf("%s-canary", apexName) { canaryWeight = int(t.Weight) } - if t.VirtualNodeName == fmt.Sprintf("%s-primary", targetName) { + if t.VirtualNodeName == fmt.Sprintf("%s-primary", apexName) { primaryWeight = int(t.Weight) } } if primaryWeight == 0 && canaryWeight == 0 { err = fmt.Errorf("VirtualService %s does not contain routes for %s-primary and %s-canary", - vsName, targetName, targetName) + vsName, apexName, apexName) } mirrored = false @@ -385,8 +383,8 @@ func (ar *AppMeshRouter) SetRoutes( canaryWeight int, mirrored bool, ) error { - targetName := canary.Spec.TargetRef.Name - vsName := fmt.Sprintf("%s.%s", targetName, canary.Namespace) + apexName, _, _ := canary.GetServiceNames() + vsName := fmt.Sprintf("%s.%s", apexName, canary.Namespace) vs, err := ar.appmeshClient.AppmeshV1beta1().VirtualServices(canary.Namespace).Get(vsName, metav1.GetOptions{}) if err != nil { if errors.IsNotFound(err) { @@ -399,11 +397,11 @@ func (ar *AppMeshRouter) SetRoutes( vsClone.Spec.Routes[0].Http.Action = appmeshv1.HttpRouteAction{ WeightedTargets: []appmeshv1.WeightedTarget{ { - VirtualNodeName: fmt.Sprintf("%s-canary", targetName), + VirtualNodeName: fmt.Sprintf("%s-canary", apexName), Weight: int64(canaryWeight), }, { - VirtualNodeName: fmt.Sprintf("%s-primary", targetName), + VirtualNodeName: fmt.Sprintf("%s-primary", apexName), Weight: int64(primaryWeight), }, }, diff --git a/pkg/router/contour.go b/pkg/router/contour.go index 1a041210..9e994409 100644 --- a/pkg/router/contour.go +++ b/pkg/router/contour.go @@ -26,9 +26,7 @@ type ContourRouter struct { // 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) + apexName, primaryName, canaryName := canary.GetServiceNames() newSpec := contourv1.HTTPProxySpec{ Routes: []contourv1.Route{ @@ -131,11 +129,11 @@ func (cr *ContourRouter) Reconcile(canary *flaggerv1.Canary) error { } } - proxy, err := cr.contourClient.ProjectcontourV1().HTTPProxies(canary.Namespace).Get(targetName, metav1.GetOptions{}) + proxy, err := cr.contourClient.ProjectcontourV1().HTTPProxies(canary.Namespace).Get(apexName, metav1.GetOptions{}) if errors.IsNotFound(err) { proxy = &contourv1.HTTPProxy{ ObjectMeta: metav1.ObjectMeta{ - Name: targetName, + Name: apexName, Namespace: canary.Namespace, OwnerReferences: []metav1.OwnerReference{ *metav1.NewControllerRef(canary, schema.GroupVersionKind{ @@ -154,7 +152,7 @@ func (cr *ContourRouter) Reconcile(canary *flaggerv1.Canary) error { _, 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) + return fmt.Errorf("HTTPProxy %s.%s create error %v", 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) @@ -162,7 +160,7 @@ func (cr *ContourRouter) Reconcile(canary *flaggerv1.Canary) error { } if err != nil { - return fmt.Errorf("HTTPProxy %s.%s query error %v", targetName, canary.Namespace, err) + return fmt.Errorf("HTTPProxy %s.%s query error %v", apexName, canary.Namespace, err) } // update HTTPProxy but keep the original destination weights @@ -177,7 +175,7 @@ func (cr *ContourRouter) Reconcile(canary *flaggerv1.Canary) error { _, 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) + return fmt.Errorf("HTTPProxy %s.%s update error %v", 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) @@ -194,21 +192,20 @@ func (cr *ContourRouter) GetRoutes(canary *flaggerv1.Canary) ( mirrored bool, err error, ) { - targetName := canary.Spec.TargetRef.Name - primaryName := fmt.Sprintf("%s-primary", targetName) + apexName, primaryName, _ := canary.GetServiceNames() - proxy, err := cr.contourClient.ProjectcontourV1().HTTPProxies(canary.Namespace).Get(targetName, metav1.GetOptions{}) + proxy, err := cr.contourClient.ProjectcontourV1().HTTPProxies(canary.Namespace).Get(apexName, metav1.GetOptions{}) if err != nil { if errors.IsNotFound(err) { - err = fmt.Errorf("HTTPProxy %s.%s not found", targetName, canary.Namespace) + err = fmt.Errorf("HTTPProxy %s.%s not found", apexName, canary.Namespace) return } - err = fmt.Errorf("HTTPProxy %s.%s query error %v", targetName, canary.Namespace, err) + err = fmt.Errorf("HTTPProxy %s.%s query error %v", 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", targetName, canary.Namespace) + err = fmt.Errorf("HTTPProxy %s.%s services not found", apexName, canary.Namespace) return } @@ -230,21 +227,19 @@ func (cr *ContourRouter) SetRoutes( canaryWeight int, mirrored bool, ) error { - targetName := canary.Spec.TargetRef.Name - primaryName := fmt.Sprintf("%s-primary", targetName) - canaryName := fmt.Sprintf("%s-canary", targetName) + apexName, primaryName, canaryName := canary.GetServiceNames() if primaryWeight == 0 && canaryWeight == 0 { - return fmt.Errorf("HTTPProxy %s.%s update failed: no valid weights", targetName, canary.Namespace) + return fmt.Errorf("HTTPProxy %s.%s update failed: no valid weights", apexName, canary.Namespace) } - proxy, err := cr.contourClient.ProjectcontourV1().HTTPProxies(canary.Namespace).Get(targetName, metav1.GetOptions{}) + proxy, err := cr.contourClient.ProjectcontourV1().HTTPProxies(canary.Namespace).Get(apexName, 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 not found", apexName, canary.Namespace) } - return fmt.Errorf("HTTPProxy %s.%s query error %v", targetName, canary.Namespace, err) + return fmt.Errorf("HTTPProxy %s.%s query error %v", apexName, canary.Namespace, err) } proxy.Spec = contourv1.HTTPProxySpec{ @@ -349,7 +344,7 @@ func (cr *ContourRouter) SetRoutes( _, 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 fmt.Errorf("HTTPProxy %s.%s update error %v", apexName, canary.Namespace, err) } return nil } diff --git a/pkg/router/gloo.go b/pkg/router/gloo.go index ceae8c0f..516c7043 100644 --- a/pkg/router/gloo.go +++ b/pkg/router/gloo.go @@ -27,9 +27,9 @@ type GlooRouter struct { // Reconcile creates or updates the Istio virtual service func (gr *GlooRouter) Reconcile(canary *flaggerv1.Canary) error { - targetName := canary.Spec.TargetRef.Name - canaryName := fmt.Sprintf("%s-%s-canary-%v", canary.Namespace, canary.Spec.TargetRef.Name, canary.Spec.Service.Port) - primaryName := fmt.Sprintf("%s-%s-primary-%v", canary.Namespace, canary.Spec.TargetRef.Name, canary.Spec.Service.Port) + apexName, _, _ := canary.GetServiceNames() + canaryName := fmt.Sprintf("%s-%s-canary-%v", canary.Namespace, apexName, canary.Spec.Service.Port) + primaryName := fmt.Sprintf("%s-%s-primary-%v", canary.Namespace, apexName, canary.Spec.Service.Port) newSpec := gloov1.UpstreamGroupSpec{ Destinations: []gloov1.WeightedDestination{ @@ -54,11 +54,11 @@ func (gr *GlooRouter) Reconcile(canary *flaggerv1.Canary) error { }, } - upstreamGroup, err := gr.glooClient.GlooV1().UpstreamGroups(canary.Namespace).Get(targetName, metav1.GetOptions{}) + upstreamGroup, err := gr.glooClient.GlooV1().UpstreamGroups(canary.Namespace).Get(apexName, metav1.GetOptions{}) if errors.IsNotFound(err) { upstreamGroup = &gloov1.UpstreamGroup{ ObjectMeta: metav1.ObjectMeta{ - Name: targetName, + Name: apexName, Namespace: canary.Namespace, OwnerReferences: []metav1.OwnerReference{ *metav1.NewControllerRef(canary, schema.GroupVersionKind{ @@ -73,7 +73,7 @@ func (gr *GlooRouter) Reconcile(canary *flaggerv1.Canary) error { _, err = gr.glooClient.GlooV1().UpstreamGroups(canary.Namespace).Create(upstreamGroup) if err != nil { - return fmt.Errorf("UpstreamGroup %s.%s create error %v", targetName, canary.Namespace, err) + return fmt.Errorf("UpstreamGroup %s.%s create error %v", apexName, canary.Namespace, err) } gr.logger.With("canary", fmt.Sprintf("%s.%s", canary.Name, canary.Namespace)). Infof("UpstreamGroup %s.%s created", upstreamGroup.GetName(), canary.Namespace) @@ -81,7 +81,7 @@ func (gr *GlooRouter) Reconcile(canary *flaggerv1.Canary) error { } if err != nil { - return fmt.Errorf("UpstreamGroup %s.%s query error %v", targetName, canary.Namespace, err) + return fmt.Errorf("UpstreamGroup %s.%s query error %v", apexName, canary.Namespace, err) } // update upstreamGroup but keep the original destination weights @@ -96,7 +96,7 @@ func (gr *GlooRouter) Reconcile(canary *flaggerv1.Canary) error { _, err = gr.glooClient.GlooV1().UpstreamGroups(canary.Namespace).Update(clone) if err != nil { - return fmt.Errorf("UpstreamGroup %s.%s update error %v", targetName, canary.Namespace, err) + return fmt.Errorf("UpstreamGroup %s.%s update error %v", apexName, canary.Namespace, err) } gr.logger.With("canary", fmt.Sprintf("%s.%s", canary.Name, canary.Namespace)). Infof("UpstreamGroup %s.%s updated", upstreamGroup.GetName(), canary.Namespace) @@ -113,21 +113,21 @@ func (gr *GlooRouter) GetRoutes(canary *flaggerv1.Canary) ( mirrored bool, err error, ) { - targetName := canary.Spec.TargetRef.Name + apexName := canary.Spec.TargetRef.Name primaryName := fmt.Sprintf("%s-%s-primary-%v", canary.Namespace, canary.Spec.TargetRef.Name, canary.Spec.Service.Port) - upstreamGroup, err := gr.glooClient.GlooV1().UpstreamGroups(canary.Namespace).Get(targetName, metav1.GetOptions{}) + upstreamGroup, err := gr.glooClient.GlooV1().UpstreamGroups(canary.Namespace).Get(apexName, metav1.GetOptions{}) if err != nil { if errors.IsNotFound(err) { - err = fmt.Errorf("UpstreamGroup %s.%s not found", targetName, canary.Namespace) + err = fmt.Errorf("UpstreamGroup %s.%s not found", apexName, canary.Namespace) return } - err = fmt.Errorf("UpstreamGroup %s.%s query error %v", targetName, canary.Namespace, err) + err = fmt.Errorf("UpstreamGroup %s.%s query error %v", apexName, canary.Namespace, err) return } if len(upstreamGroup.Spec.Destinations) < 2 { - err = fmt.Errorf("UpstreamGroup %s.%s destinations not found", targetName, canary.Namespace) + err = fmt.Errorf("UpstreamGroup %s.%s destinations not found", apexName, canary.Namespace) return } @@ -149,21 +149,21 @@ func (gr *GlooRouter) SetRoutes( canaryWeight int, mirrored bool, ) error { - targetName := canary.Spec.TargetRef.Name - canaryName := fmt.Sprintf("%s-%s-canary-%v", canary.Namespace, canary.Spec.TargetRef.Name, canary.Spec.Service.Port) - primaryName := fmt.Sprintf("%s-%s-primary-%v", canary.Namespace, canary.Spec.TargetRef.Name, canary.Spec.Service.Port) + apexName, _, _ := canary.GetServiceNames() + canaryName := fmt.Sprintf("%s-%s-canary-%v", canary.Namespace, apexName, canary.Spec.Service.Port) + primaryName := fmt.Sprintf("%s-%s-primary-%v", canary.Namespace, apexName, canary.Spec.Service.Port) if primaryWeight == 0 && canaryWeight == 0 { - return fmt.Errorf("RoutingRule %s.%s update failed: no valid weights", targetName, canary.Namespace) + return fmt.Errorf("RoutingRule %s.%s update failed: no valid weights", apexName, canary.Namespace) } - upstreamGroup, err := gr.glooClient.GlooV1().UpstreamGroups(canary.Namespace).Get(targetName, metav1.GetOptions{}) + upstreamGroup, err := gr.glooClient.GlooV1().UpstreamGroups(canary.Namespace).Get(apexName, metav1.GetOptions{}) if err != nil { if errors.IsNotFound(err) { - return fmt.Errorf("UpstreamGroup %s.%s not found", targetName, canary.Namespace) + return fmt.Errorf("UpstreamGroup %s.%s not found", apexName, canary.Namespace) } - return fmt.Errorf("UpstreamGroup %s.%s query error %v", targetName, canary.Namespace, err) + return fmt.Errorf("UpstreamGroup %s.%s query error %v", apexName, canary.Namespace, err) } upstreamGroup.Spec = gloov1.UpstreamGroupSpec{ @@ -191,7 +191,7 @@ func (gr *GlooRouter) SetRoutes( _, err = gr.glooClient.GlooV1().UpstreamGroups(canary.Namespace).Update(upstreamGroup) if err != nil { - return fmt.Errorf("UpstreamGroup %s.%s update error %v", targetName, canary.Namespace, err) + return fmt.Errorf("UpstreamGroup %s.%s update error %v", apexName, canary.Namespace, err) } return nil } diff --git a/pkg/router/ingress.go b/pkg/router/ingress.go index 01da959b..3eebf620 100644 --- a/pkg/router/ingress.go +++ b/pkg/router/ingress.go @@ -27,8 +27,8 @@ func (i *IngressRouter) Reconcile(canary *flaggerv1.Canary) error { return fmt.Errorf("ingress selector is empty") } - targetName := canary.Spec.TargetRef.Name - canaryName := fmt.Sprintf("%s-canary", targetName) + apexName, _, _ := canary.GetServiceNames() + canaryName := fmt.Sprintf("%s-canary", apexName) canaryIngressName := fmt.Sprintf("%s-canary", canary.Spec.IngressRef.Name) ingress, err := i.kubeClient.ExtensionsV1beta1().Ingresses(canary.Namespace).Get(canary.Spec.IngressRef.Name, metav1.GetOptions{}) @@ -42,7 +42,7 @@ func (i *IngressRouter) Reconcile(canary *flaggerv1.Canary) error { backendExists := false for k, v := range ingressClone.Spec.Rules { for x, y := range v.HTTP.Paths { - if y.Backend.ServiceName == targetName { + if y.Backend.ServiceName == apexName { ingressClone.Spec.Rules[k].HTTP.Paths[x].Backend.ServiceName = canaryName backendExists = true break @@ -51,7 +51,7 @@ func (i *IngressRouter) Reconcile(canary *flaggerv1.Canary) error { } if !backendExists { - return fmt.Errorf("backend %s not found in ingress %s", targetName, canary.Spec.IngressRef.Name) + return fmt.Errorf("backend %s not found in ingress %s", apexName, canary.Spec.IngressRef.Name) } canaryIngress, err := i.kubeClient.ExtensionsV1beta1().Ingresses(canary.Namespace).Get(canaryIngressName, metav1.GetOptions{}) diff --git a/pkg/router/istio.go b/pkg/router/istio.go index 8037c595..2f774698 100644 --- a/pkg/router/istio.go +++ b/pkg/router/istio.go @@ -26,8 +26,7 @@ type IstioRouter struct { // Reconcile creates or updates the Istio virtual service and destination rules func (ir *IstioRouter) Reconcile(canary *flaggerv1.Canary) error { - canaryName := fmt.Sprintf("%s-canary", canary.Spec.TargetRef.Name) - primaryName := fmt.Sprintf("%s-primary", canary.Spec.TargetRef.Name) + _, primaryName, canaryName := canary.GetServiceNames() err := ir.reconcileDestinationRule(canary, canaryName) if err != nil { @@ -101,19 +100,19 @@ func (ir *IstioRouter) reconcileDestinationRule(canary *flaggerv1.Canary, name s } func (ir *IstioRouter) reconcileVirtualService(canary *flaggerv1.Canary) error { - targetName := canary.Spec.TargetRef.Name + apexName, primaryName, canaryName := canary.GetServiceNames() // set hosts and add the ClusterIP service host if it doesn't exists hosts := canary.Spec.Service.Hosts var hasServiceHost bool for _, h := range hosts { - if h == targetName || h == "*" { + if h == apexName || h == "*" { hasServiceHost = true break } } if !hasServiceHost { - hosts = append(hosts, targetName) + hosts = append(hosts, apexName) } // set gateways and add the mesh gateway if it doesn't exists @@ -132,8 +131,6 @@ func (ir *IstioRouter) reconcileVirtualService(canary *flaggerv1.Canary) error { } // create destinations with primary weight 100% and canary weight 0% - primaryName := fmt.Sprintf("%s-primary", targetName) - canaryName := fmt.Sprintf("%s-canary", targetName) canaryRoute := []istiov1alpha3.DestinationWeight{ makeDestination(canary, primaryName, 100), makeDestination(canary, canaryName, 0), @@ -181,12 +178,12 @@ func (ir *IstioRouter) reconcileVirtualService(canary *flaggerv1.Canary) error { } } - virtualService, err := ir.istioClient.NetworkingV1alpha3().VirtualServices(canary.Namespace).Get(targetName, metav1.GetOptions{}) + virtualService, err := ir.istioClient.NetworkingV1alpha3().VirtualServices(canary.Namespace).Get(apexName, metav1.GetOptions{}) // insert if errors.IsNotFound(err) { virtualService = &istiov1alpha3.VirtualService{ ObjectMeta: metav1.ObjectMeta{ - Name: targetName, + Name: apexName, Namespace: canary.Namespace, OwnerReferences: []metav1.OwnerReference{ *metav1.NewControllerRef(canary, schema.GroupVersionKind{ @@ -200,7 +197,7 @@ func (ir *IstioRouter) reconcileVirtualService(canary *flaggerv1.Canary) error { } _, err = ir.istioClient.NetworkingV1alpha3().VirtualServices(canary.Namespace).Create(virtualService) if err != nil { - return fmt.Errorf("VirtualService %s.%s create error %v", targetName, canary.Namespace, err) + return fmt.Errorf("VirtualService %s.%s create error %v", apexName, canary.Namespace, err) } ir.logger.With("canary", fmt.Sprintf("%s.%s", canary.Name, canary.Namespace)). Infof("VirtualService %s.%s created", virtualService.GetName(), canary.Namespace) @@ -208,7 +205,7 @@ func (ir *IstioRouter) reconcileVirtualService(canary *flaggerv1.Canary) error { } if err != nil { - return fmt.Errorf("VirtualService %s.%s query error %v", targetName, canary.Namespace, err) + return fmt.Errorf("VirtualService %s.%s query error %v", apexName, canary.Namespace, err) } // update service but keep the original destination weights and mirror @@ -224,7 +221,7 @@ func (ir *IstioRouter) reconcileVirtualService(canary *flaggerv1.Canary) error { _, err = ir.istioClient.NetworkingV1alpha3().VirtualServices(canary.Namespace).Update(vtClone) if err != nil { - return fmt.Errorf("VirtualService %s.%s update error %v", targetName, canary.Namespace, err) + return fmt.Errorf("VirtualService %s.%s update error %v", apexName, canary.Namespace, err) } ir.logger.With("canary", fmt.Sprintf("%s.%s", canary.Name, canary.Namespace)). Infof("VirtualService %s.%s updated", virtualService.GetName(), canary.Namespace) @@ -241,22 +238,22 @@ func (ir *IstioRouter) GetRoutes(canary *flaggerv1.Canary) ( mirrored bool, err error, ) { - targetName := canary.Spec.TargetRef.Name + apexName, primaryName, canaryName := canary.GetServiceNames() vs := &istiov1alpha3.VirtualService{} - vs, err = ir.istioClient.NetworkingV1alpha3().VirtualServices(canary.Namespace).Get(targetName, metav1.GetOptions{}) + vs, err = ir.istioClient.NetworkingV1alpha3().VirtualServices(canary.Namespace).Get(apexName, metav1.GetOptions{}) if err != nil { if errors.IsNotFound(err) { - err = fmt.Errorf("VirtualService %s.%s not found", targetName, canary.Namespace) + err = fmt.Errorf("VirtualService %s.%s not found", apexName, canary.Namespace) return } - err = fmt.Errorf("VirtualService %s.%s query error %v", targetName, canary.Namespace, err) + err = fmt.Errorf("VirtualService %s.%s query error %v", apexName, canary.Namespace, err) return } var httpRoute istiov1alpha3.HTTPRoute for _, http := range vs.Spec.Http { for _, r := range http.Route { - if r.Destination.Host == fmt.Sprintf("%s-canary", targetName) { + if r.Destination.Host == canaryName { httpRoute = http break } @@ -264,10 +261,10 @@ func (ir *IstioRouter) GetRoutes(canary *flaggerv1.Canary) ( } for _, route := range httpRoute.Route { - if route.Destination.Host == fmt.Sprintf("%s-primary", targetName) { + if route.Destination.Host == primaryName { primaryWeight = route.Weight } - if route.Destination.Host == fmt.Sprintf("%s-canary", targetName) { + if route.Destination.Host == canaryName { canaryWeight = route.Weight } } @@ -277,7 +274,7 @@ func (ir *IstioRouter) GetRoutes(canary *flaggerv1.Canary) ( if primaryWeight == 0 && canaryWeight == 0 { err = fmt.Errorf("VirtualService %s.%s does not contain routes for %s-primary and %s-canary", - targetName, canary.Namespace, targetName, targetName) + apexName, canary.Namespace, apexName, apexName) } return @@ -290,17 +287,15 @@ func (ir *IstioRouter) SetRoutes( canaryWeight int, mirrored bool, ) error { - targetName := canary.Spec.TargetRef.Name - primaryName := fmt.Sprintf("%s-primary", targetName) - canaryName := fmt.Sprintf("%s-canary", targetName) + apexName, primaryName, canaryName := canary.GetServiceNames() - vs, err := ir.istioClient.NetworkingV1alpha3().VirtualServices(canary.Namespace).Get(targetName, metav1.GetOptions{}) + vs, err := ir.istioClient.NetworkingV1alpha3().VirtualServices(canary.Namespace).Get(apexName, metav1.GetOptions{}) if err != nil { if errors.IsNotFound(err) { - return fmt.Errorf("VirtualService %s.%s not found", targetName, canary.Namespace) + return fmt.Errorf("VirtualService %s.%s not found", apexName, canary.Namespace) } - return fmt.Errorf("VirtualService %s.%s query error %v", targetName, canary.Namespace, err) + return fmt.Errorf("VirtualService %s.%s query error %v", apexName, canary.Namespace, err) } vsCopy := vs.DeepCopy() @@ -360,7 +355,7 @@ func (ir *IstioRouter) SetRoutes( vs, err = ir.istioClient.NetworkingV1alpha3().VirtualServices(canary.Namespace).Update(vsCopy) if err != nil { - return fmt.Errorf("VirtualService %s.%s update failed: %v", targetName, canary.Namespace, err) + return fmt.Errorf("VirtualService %s.%s update failed: %v", apexName, canary.Namespace, err) } return nil diff --git a/pkg/router/kubernetes_deployment.go b/pkg/router/kubernetes_deployment.go index e028f4d6..9ea6756b 100644 --- a/pkg/router/kubernetes_deployment.go +++ b/pkg/router/kubernetes_deployment.go @@ -29,18 +29,16 @@ type KubernetesDeploymentRouter struct { // Initialize creates the primary and canary services func (c *KubernetesDeploymentRouter) Initialize(canary *flaggerv1.Canary) error { - targetName := canary.Spec.TargetRef.Name - primaryName := fmt.Sprintf("%s-primary", targetName) - canaryName := fmt.Sprintf("%s-canary", targetName) + _, primaryName, canaryName := canary.GetServiceNames() // canary svc - err := c.reconcileService(canary, canaryName, targetName) + err := c.reconcileService(canary, canaryName, canary.Spec.TargetRef.Name) if err != nil { return err } // primary svc - err = c.reconcileService(canary, primaryName, primaryName) + err = c.reconcileService(canary, primaryName, fmt.Sprintf("%s-primary", canary.Spec.TargetRef.Name)) if err != nil { return err } @@ -50,11 +48,10 @@ func (c *KubernetesDeploymentRouter) Initialize(canary *flaggerv1.Canary) error // Reconcile creates or updates the main service func (c *KubernetesDeploymentRouter) Reconcile(canary *flaggerv1.Canary) error { - targetName := canary.Spec.TargetRef.Name - primaryName := fmt.Sprintf("%s-primary", targetName) + apexName, _, _ := canary.GetServiceNames() // main svc - err := c.reconcileService(canary, targetName, primaryName) + err := c.reconcileService(canary, apexName, fmt.Sprintf("%s-primary", canary.Spec.TargetRef.Name)) if err != nil { return err } @@ -70,7 +67,7 @@ func (c *KubernetesDeploymentRouter) GetRoutes(canary *flaggerv1.Canary) (primar return 0, 0, nil } -func (c *KubernetesDeploymentRouter) reconcileService(canary *flaggerv1.Canary, name string, target string) error { +func (c *KubernetesDeploymentRouter) reconcileService(canary *flaggerv1.Canary, name string, podSelector string) error { portName := canary.Spec.Service.PortName if portName == "" { portName = "http" @@ -87,7 +84,7 @@ func (c *KubernetesDeploymentRouter) reconcileService(canary *flaggerv1.Canary, svcSpec := corev1.ServiceSpec{ Type: corev1.ServiceTypeClusterIP, - Selector: map[string]string{c.labelSelector: target}, + Selector: map[string]string{c.labelSelector: podSelector}, Ports: []corev1.ServicePort{ { Name: portName, diff --git a/pkg/router/smi.go b/pkg/router/smi.go index 6a587179..2da06cc6 100644 --- a/pkg/router/smi.go +++ b/pkg/router/smi.go @@ -28,15 +28,13 @@ type SmiRouter struct { // Reconcile creates or updates the SMI traffic split func (sr *SmiRouter) Reconcile(canary *flaggerv1.Canary) error { - targetName := canary.Spec.TargetRef.Name - canaryName := fmt.Sprintf("%s-canary", targetName) - primaryName := fmt.Sprintf("%s-primary", targetName) + apexName, primaryName, canaryName := canary.GetServiceNames() var host string if len(canary.Spec.Service.Hosts) > 0 { host = canary.Spec.Service.Hosts[0] } else { - host = targetName + host = apexName } tsSpec := smiv1.TrafficSplitSpec{ @@ -53,12 +51,12 @@ func (sr *SmiRouter) Reconcile(canary *flaggerv1.Canary) error { }, } - ts, err := sr.smiClient.SplitV1alpha1().TrafficSplits(canary.Namespace).Get(targetName, metav1.GetOptions{}) + ts, err := sr.smiClient.SplitV1alpha1().TrafficSplits(canary.Namespace).Get(apexName, metav1.GetOptions{}) // create traffic split if errors.IsNotFound(err) { t := &smiv1.TrafficSplit{ ObjectMeta: metav1.ObjectMeta{ - Name: targetName, + Name: apexName, Namespace: canary.Namespace, OwnerReferences: []metav1.OwnerReference{ *metav1.NewControllerRef(canary, schema.GroupVersionKind{ @@ -83,7 +81,7 @@ func (sr *SmiRouter) Reconcile(canary *flaggerv1.Canary) error { } if err != nil { - return fmt.Errorf("traffic split %s query error %v", targetName, err) + return fmt.Errorf("traffic split %s query error %v", apexName, err) } // update traffic split @@ -93,11 +91,11 @@ func (sr *SmiRouter) Reconcile(canary *flaggerv1.Canary) error { _, err := sr.smiClient.SplitV1alpha1().TrafficSplits(canary.Namespace).Update(tsClone) if err != nil { - return fmt.Errorf("TrafficSplit %s update error %v", targetName, err) + return fmt.Errorf("TrafficSplit %s update error %v", apexName, err) } sr.logger.With("canary", fmt.Sprintf("%s.%s", canary.Name, canary.Namespace)). - Infof("TrafficSplit %s.%s updated", targetName, canary.Namespace) + Infof("TrafficSplit %s.%s updated", apexName, canary.Namespace) return nil } @@ -111,16 +109,14 @@ func (sr *SmiRouter) GetRoutes(canary *flaggerv1.Canary) ( mirrored bool, err error, ) { - targetName := canary.Spec.TargetRef.Name - canaryName := fmt.Sprintf("%s-canary", targetName) - primaryName := fmt.Sprintf("%s-primary", targetName) - ts, err := sr.smiClient.SplitV1alpha1().TrafficSplits(canary.Namespace).Get(targetName, metav1.GetOptions{}) + apexName, primaryName, canaryName := canary.GetServiceNames() + ts, err := sr.smiClient.SplitV1alpha1().TrafficSplits(canary.Namespace).Get(apexName, metav1.GetOptions{}) if err != nil { if errors.IsNotFound(err) { - err = fmt.Errorf("TrafficSplit %s.%s not found", targetName, canary.Namespace) + err = fmt.Errorf("TrafficSplit %s.%s not found", apexName, canary.Namespace) return } - err = fmt.Errorf("TrafficSplit %s.%s query error %v", targetName, canary.Namespace, err) + err = fmt.Errorf("TrafficSplit %s.%s query error %v", apexName, canary.Namespace, err) return } @@ -136,7 +132,7 @@ func (sr *SmiRouter) GetRoutes(canary *flaggerv1.Canary) ( if primaryWeight == 0 && canaryWeight == 0 { err = fmt.Errorf("TrafficSplit %s.%s does not contain routes for %s and %s", - targetName, canary.Namespace, primaryName, canaryName) + apexName, canary.Namespace, primaryName, canaryName) } mirrored = false @@ -151,16 +147,14 @@ func (sr *SmiRouter) SetRoutes( canaryWeight int, mirrored bool, ) error { - targetName := canary.Spec.TargetRef.Name - canaryName := fmt.Sprintf("%s-canary", targetName) - primaryName := fmt.Sprintf("%s-primary", targetName) - ts, err := sr.smiClient.SplitV1alpha1().TrafficSplits(canary.Namespace).Get(targetName, metav1.GetOptions{}) + apexName, primaryName, canaryName := canary.GetServiceNames() + ts, err := sr.smiClient.SplitV1alpha1().TrafficSplits(canary.Namespace).Get(apexName, metav1.GetOptions{}) if err != nil { if errors.IsNotFound(err) { - return fmt.Errorf("TrafficSplit %s.%s not found", targetName, canary.Namespace) + return fmt.Errorf("TrafficSplit %s.%s not found", apexName, canary.Namespace) } - return fmt.Errorf("TrafficSplit %s.%s query error %v", targetName, canary.Namespace, err) + return fmt.Errorf("TrafficSplit %s.%s query error %v", apexName, canary.Namespace, err) } backends := []smiv1.TrafficSplitBackend{ @@ -179,7 +173,7 @@ func (sr *SmiRouter) SetRoutes( _, err = sr.smiClient.SplitV1alpha1().TrafficSplits(canary.Namespace).Update(tsClone) if err != nil { - return fmt.Errorf("TrafficSplit %s update error %v", targetName, err) + return fmt.Errorf("TrafficSplit %s update error %v", apexName, err) } return nil diff --git a/test/e2e-kubernetes-tests.sh b/test/e2e-kubernetes-tests.sh index d86cdc53..6e50b07c 100755 --- a/test/e2e-kubernetes-tests.sh +++ b/test/e2e-kubernetes-tests.sh @@ -33,6 +33,7 @@ spec: service: port: 80 targetPort: 9898 + name: podinfo-svc portDiscovery: true canaryAnalysis: interval: 15s @@ -55,13 +56,13 @@ spec: timeout: 10s metadata: type: bash - cmd: "curl -sd 'test' http://podinfo-canary/token | grep token" + cmd: "curl -sd 'test' http://podinfo-svc-canary/token | grep token" - name: load-test url: http://flagger-loadtester.test/ timeout: 5s metadata: type: cmd - cmd: "hey -z 10m -q 10 -c 2 http://podinfo-canary.test/" + cmd: "hey -z 10m -q 10 -c 2 http://podinfo-svc-canary.test/" logCmdOutput: "true" EOF