mirror of
https://github.com/fluxcd/flagger.git
synced 2026-04-15 06:57:34 +00:00
Signed-off-by: Benoit Gaillard <benoit.gaillard@continental-corporation.com>
892 lines
26 KiB
Go
892 lines
26 KiB
Go
/*
|
|
Copyright 2020 The Flux authors
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package router
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
|
|
"github.com/fluxcd/flagger/pkg/apis/flagger/v1beta1"
|
|
flaggerv1 "github.com/fluxcd/flagger/pkg/apis/flagger/v1beta1"
|
|
istiov1alpha1 "github.com/fluxcd/flagger/pkg/apis/istio/common/v1alpha1"
|
|
istiov1beta1 "github.com/fluxcd/flagger/pkg/apis/istio/v1beta1"
|
|
)
|
|
|
|
func TestUnmarshalVirtualService(t *testing.T) {
|
|
body := `
|
|
{
|
|
"apiVersion": "networking.istio.io/v1beta1",
|
|
"kind": "VirtualService",
|
|
"spec": {
|
|
"gateways": [
|
|
"default/gateway"
|
|
],
|
|
"hosts": [
|
|
"my.example.com"
|
|
],
|
|
"tcp": [
|
|
{
|
|
"match": [
|
|
{
|
|
"port": 9898
|
|
}
|
|
],
|
|
"route": [
|
|
{
|
|
"destination": {
|
|
"host": "my.example.com",
|
|
"port": {
|
|
"number": 9898
|
|
}
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
`
|
|
|
|
var vs istiov1beta1.VirtualService
|
|
err := json.Unmarshal([]byte(body), &vs)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestIstioRouter_Sync(t *testing.T) {
|
|
mocks := newFixture(nil)
|
|
router := &IstioRouter{
|
|
logger: mocks.logger,
|
|
flaggerClient: mocks.flaggerClient,
|
|
istioClient: mocks.meshClient,
|
|
kubeClient: mocks.kubeClient,
|
|
}
|
|
|
|
err := router.Reconcile(mocks.canary)
|
|
require.NoError(t, err)
|
|
|
|
// test insert
|
|
_, err = mocks.meshClient.NetworkingV1beta1().DestinationRules("default").Get(context.TODO(), "podinfo-canary", metav1.GetOptions{})
|
|
require.NoError(t, err)
|
|
|
|
_, err = mocks.meshClient.NetworkingV1beta1().DestinationRules("default").Get(context.TODO(), "podinfo-primary", metav1.GetOptions{})
|
|
require.NoError(t, err)
|
|
|
|
vs, err := mocks.meshClient.NetworkingV1beta1().VirtualServices("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
|
|
require.NoError(t, err)
|
|
require.Len(t, vs.Spec.Http, 1)
|
|
require.Len(t, vs.Spec.Http[0].Route, 2)
|
|
|
|
// test update
|
|
cd, err := mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
|
|
require.NoError(t, err)
|
|
|
|
cdClone := cd.DeepCopy()
|
|
hosts := cdClone.Spec.Service.Hosts
|
|
hosts = append(hosts, "test.example.com")
|
|
cdClone.Spec.Service.Hosts = hosts
|
|
canary, err := mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Update(context.TODO(), cdClone, metav1.UpdateOptions{})
|
|
require.NoError(t, err)
|
|
|
|
// apply change
|
|
err = router.Reconcile(canary)
|
|
require.NoError(t, err)
|
|
|
|
// verify
|
|
vs, err = mocks.meshClient.NetworkingV1beta1().VirtualServices("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
|
|
require.NoError(t, err)
|
|
assert.Len(t, vs.Spec.Hosts, 2)
|
|
|
|
// test drift
|
|
vsClone := vs.DeepCopy()
|
|
gateways := vsClone.Spec.Gateways
|
|
gateways = append(gateways, "istio-system/test-gateway")
|
|
vsClone.Spec.Gateways = gateways
|
|
totalGateways := len(mocks.canary.Spec.Service.Gateways)
|
|
|
|
vsGateways, err := mocks.meshClient.NetworkingV1beta1().VirtualServices("default").Update(context.TODO(), vsClone, metav1.UpdateOptions{})
|
|
require.NoError(t, err)
|
|
|
|
totalGateways++
|
|
assert.Len(t, vsGateways.Spec.Gateways, totalGateways)
|
|
|
|
// undo change
|
|
totalGateways--
|
|
err = router.Reconcile(mocks.canary)
|
|
require.NoError(t, err)
|
|
|
|
// verify
|
|
vs, err = mocks.meshClient.NetworkingV1beta1().VirtualServices("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
|
|
require.NoError(t, err)
|
|
assert.Len(t, vs.Spec.Gateways, totalGateways)
|
|
}
|
|
|
|
func TestIstioRouter_SetRoutes(t *testing.T) {
|
|
mocks := newFixture(nil)
|
|
router := &IstioRouter{
|
|
logger: mocks.logger,
|
|
flaggerClient: mocks.flaggerClient,
|
|
istioClient: mocks.meshClient,
|
|
kubeClient: mocks.kubeClient,
|
|
}
|
|
|
|
err := router.Reconcile(mocks.canary)
|
|
require.NoError(t, err)
|
|
|
|
pHost := fmt.Sprintf("%s-primary", mocks.canary.Spec.TargetRef.Name)
|
|
cHost := fmt.Sprintf("%s-canary", mocks.canary.Spec.TargetRef.Name)
|
|
|
|
t.Run("normal", func(t *testing.T) {
|
|
p, c := 60, 40
|
|
err := router.SetRoutes(mocks.canary, p, c, false)
|
|
require.NoError(t, err)
|
|
|
|
vs, err := mocks.meshClient.NetworkingV1beta1().VirtualServices("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
|
|
require.NoError(t, err)
|
|
|
|
var pRoute, cRoute istiov1beta1.HTTPRouteDestination
|
|
var mirror *istiov1beta1.Destination
|
|
for _, http := range vs.Spec.Http {
|
|
for _, route := range http.Route {
|
|
if route.Destination.Host == pHost {
|
|
pRoute = route
|
|
}
|
|
if route.Destination.Host == cHost {
|
|
cRoute = route
|
|
mirror = http.Mirror
|
|
}
|
|
}
|
|
}
|
|
|
|
assert.Equal(t, p, pRoute.Weight)
|
|
assert.Equal(t, c, cRoute.Weight)
|
|
assert.Nil(t, mirror)
|
|
|
|
})
|
|
|
|
t.Run("session affinity", func(t *testing.T) {
|
|
canary := mocks.canary.DeepCopy()
|
|
cookieKey := "flagger-cookie"
|
|
// enable session affinity and start canary run
|
|
canary.Spec.Analysis.SessionAffinity = &v1beta1.SessionAffinity{
|
|
CookieName: cookieKey,
|
|
MaxAge: 300,
|
|
}
|
|
err := router.SetRoutes(canary, 0, 10, false)
|
|
|
|
vs, err := mocks.meshClient.NetworkingV1beta1().VirtualServices("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
|
|
require.NoError(t, err)
|
|
|
|
assert.Len(t, vs.Spec.Http, 2)
|
|
stickyRoute := vs.Spec.Http[0]
|
|
weightedRoute := vs.Spec.Http[1]
|
|
|
|
// stickyRoute should match against a cookie and direct all traffic to the canary when a canary run is active.
|
|
var found bool
|
|
for _, match := range stickyRoute.Match {
|
|
if val, ok := match.Headers[cookieHeader]; ok {
|
|
found = true
|
|
assert.True(t, strings.Contains(val.Regex, cookieKey))
|
|
for _, routeDest := range stickyRoute.Route {
|
|
if routeDest.Destination.Host == pHost {
|
|
assert.Equal(t, 0, routeDest.Weight)
|
|
}
|
|
if routeDest.Destination.Host == cHost {
|
|
assert.Equal(t, 100, routeDest.Weight)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
assert.True(t, found)
|
|
|
|
// weightedRoute should do regular weight based routing and inject the Set-Cookie header
|
|
// for all responses returned from the canary deployment.
|
|
for _, routeDest := range weightedRoute.Route {
|
|
if routeDest.Destination.Host == pHost {
|
|
assert.Equal(t, 0, routeDest.Weight)
|
|
}
|
|
if routeDest.Destination.Host == cHost {
|
|
assert.Equal(t, 10, routeDest.Weight)
|
|
val, ok := routeDest.Headers.Response.Add[setCookieHeader]
|
|
assert.True(t, ok)
|
|
assert.True(t, strings.HasPrefix(val, cookieKey))
|
|
assert.True(t, strings.Contains(val, "Max-Age=300"))
|
|
}
|
|
}
|
|
assert.True(t, strings.HasPrefix(canary.Status.SessionAffinityCookie, cookieKey))
|
|
|
|
// reconcile canary, destination rules, virtual services
|
|
err = router.Reconcile(canary)
|
|
require.NoError(t, err)
|
|
|
|
reconciledVS, err := mocks.meshClient.NetworkingV1beta1().VirtualServices("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
|
|
require.NoError(t, err)
|
|
|
|
// routes should not be changed.
|
|
assert.Len(t, vs.Spec.Http, 2)
|
|
assert.NotNil(t, reconciledVS)
|
|
assert.Equal(t, cmp.Diff(reconciledVS.Spec.Http[0], stickyRoute), "")
|
|
assert.Equal(t, cmp.Diff(reconciledVS.Spec.Http[1], weightedRoute), "")
|
|
|
|
// further continue the canary run
|
|
err = router.SetRoutes(canary, 50, 50, false)
|
|
require.NoError(t, err)
|
|
|
|
vs, err = mocks.meshClient.NetworkingV1beta1().VirtualServices("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
|
|
require.NoError(t, err)
|
|
|
|
assert.Len(t, vs.Spec.Http, 2)
|
|
stickyRoute = vs.Spec.Http[0]
|
|
weightedRoute = vs.Spec.Http[1]
|
|
|
|
found = false
|
|
for _, match := range stickyRoute.Match {
|
|
if val, ok := match.Headers[cookieHeader]; ok {
|
|
found = true
|
|
assert.True(t, strings.Contains(val.Regex, cookieKey))
|
|
for _, routeDest := range stickyRoute.Route {
|
|
if routeDest.Destination.Host == pHost {
|
|
assert.Equal(t, 0, routeDest.Weight)
|
|
}
|
|
if routeDest.Destination.Host == cHost {
|
|
assert.Equal(t, 100, routeDest.Weight)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
assert.True(t, found)
|
|
|
|
for _, routeDest := range weightedRoute.Route {
|
|
if routeDest.Destination.Host == pHost {
|
|
assert.Equal(t, 50, routeDest.Weight)
|
|
}
|
|
if routeDest.Destination.Host == cHost {
|
|
assert.Equal(t, 50, routeDest.Weight)
|
|
val, ok := routeDest.Headers.Response.Add[setCookieHeader]
|
|
assert.True(t, ok)
|
|
assert.True(t, strings.HasPrefix(val, cookieKey))
|
|
assert.True(t, strings.Contains(val, "Max-Age=300"))
|
|
}
|
|
}
|
|
assert.True(t, strings.HasPrefix(canary.Status.SessionAffinityCookie, cookieKey))
|
|
sessionAffinityCookie := canary.Status.SessionAffinityCookie
|
|
|
|
// promotion
|
|
err = router.SetRoutes(canary, 100, 0, false)
|
|
require.NoError(t, err)
|
|
|
|
vs, err = mocks.meshClient.NetworkingV1beta1().VirtualServices("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
|
|
require.NoError(t, err)
|
|
|
|
assert.Len(t, vs.Spec.Http, 2)
|
|
stickyRoute = vs.Spec.Http[0]
|
|
weightedRoute = vs.Spec.Http[1]
|
|
|
|
found = false
|
|
for _, match := range stickyRoute.Match {
|
|
if val, ok := match.Headers[cookieHeader]; ok {
|
|
found = true
|
|
assert.True(t, strings.Contains(val.Regex, cookieKey))
|
|
for _, routeDest := range stickyRoute.Route {
|
|
if routeDest.Destination.Host == pHost {
|
|
assert.Equal(t, 100, routeDest.Weight)
|
|
}
|
|
if routeDest.Destination.Host == cHost {
|
|
assert.Equal(t, 0, routeDest.Weight)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
assert.True(t, found)
|
|
|
|
assert.Equal(t, canary.Status.SessionAffinityCookie, "")
|
|
assert.Equal(t, canary.Status.PreviousSessionAffinityCookie, sessionAffinityCookie)
|
|
|
|
val, ok := stickyRoute.Headers.Response.Add[setCookieHeader]
|
|
assert.True(t, ok)
|
|
assert.True(t, strings.HasPrefix(val, sessionAffinityCookie))
|
|
assert.True(t, strings.Contains(val, "Max-Age=-1"))
|
|
|
|
// delete the Set-Cookie header from responses returned by the weighted route
|
|
for _, routeDest := range weightedRoute.Route {
|
|
if routeDest.Destination.Host == pHost {
|
|
assert.Equal(t, 100, routeDest.Weight)
|
|
}
|
|
if routeDest.Destination.Host == cHost {
|
|
assert.Equal(t, 0, routeDest.Weight)
|
|
if routeDest.Headers != nil && routeDest.Headers.Response != nil {
|
|
_, ok := routeDest.Headers.Response.Add[setCookieHeader]
|
|
assert.False(t, ok)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("mirror", func(t *testing.T) {
|
|
for _, w := range []int{0, 10, 50} {
|
|
p, c := 100, 0
|
|
|
|
// set mirror weight
|
|
mocks.canary.Spec.Analysis.MirrorWeight = w
|
|
err := router.SetRoutes(mocks.canary, p, c, true)
|
|
require.NoError(t, err)
|
|
|
|
vs, err := mocks.meshClient.NetworkingV1beta1().VirtualServices("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
|
|
require.NoError(t, err)
|
|
|
|
var pRoute, cRoute istiov1beta1.HTTPRouteDestination
|
|
var mirror *istiov1beta1.Destination
|
|
var mirrorWeight *istiov1beta1.Percent
|
|
for _, http := range vs.Spec.Http {
|
|
for _, route := range http.Route {
|
|
if route.Destination.Host == pHost {
|
|
pRoute = route
|
|
}
|
|
if route.Destination.Host == cHost {
|
|
cRoute = route
|
|
mirror = http.Mirror
|
|
mirrorWeight = http.MirrorPercentage
|
|
}
|
|
}
|
|
}
|
|
|
|
assert.Equal(t, p, pRoute.Weight)
|
|
assert.Equal(t, c, cRoute.Weight)
|
|
if assert.NotNil(t, mirror) {
|
|
assert.Equal(t, cHost, mirror.Host)
|
|
}
|
|
|
|
if w > 0 && assert.NotNil(t, mirrorWeight) {
|
|
assert.Equal(t, w, int(mirrorWeight.Value))
|
|
} else {
|
|
assert.Nil(t, mirrorWeight)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestIstioRouter_GetRoutes(t *testing.T) {
|
|
mocks := newFixture(nil)
|
|
router := &IstioRouter{
|
|
logger: mocks.logger,
|
|
flaggerClient: mocks.flaggerClient,
|
|
istioClient: mocks.meshClient,
|
|
kubeClient: mocks.kubeClient,
|
|
}
|
|
|
|
err := router.Reconcile(mocks.canary)
|
|
require.NoError(t, err)
|
|
|
|
p, c, m, err := router.GetRoutes(mocks.canary)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 100, p)
|
|
assert.Equal(t, 0, c)
|
|
assert.False(t, m)
|
|
|
|
mocks.canary = newTestMirror()
|
|
|
|
err = router.Reconcile(mocks.canary)
|
|
require.NoError(t, err)
|
|
|
|
p, c, m, err = router.GetRoutes(mocks.canary)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 100, p)
|
|
assert.Equal(t, 0, c)
|
|
|
|
// A Canary resource with mirror on does not automatically create mirroring
|
|
// in the virtual server (mirroring is activated as a temporary stage).
|
|
assert.False(t, m)
|
|
|
|
// Adjust vs to activate mirroring.
|
|
vs, err := mocks.meshClient.NetworkingV1beta1().VirtualServices("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
|
|
require.NoError(t, err)
|
|
|
|
cHost := fmt.Sprintf("%s-canary", mocks.canary.Spec.TargetRef.Name)
|
|
for i, http := range vs.Spec.Http {
|
|
for _, route := range http.Route {
|
|
if route.Destination.Host == cHost {
|
|
vs.Spec.Http[i].Mirror = &istiov1beta1.Destination{
|
|
Host: cHost,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
_, err = mocks.meshClient.NetworkingV1beta1().VirtualServices(mocks.canary.Namespace).Update(context.TODO(), vs, metav1.UpdateOptions{})
|
|
require.NoError(t, err)
|
|
|
|
p, c, m, err = router.GetRoutes(mocks.canary)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 100, p)
|
|
assert.Equal(t, 0, c)
|
|
assert.True(t, m)
|
|
}
|
|
|
|
func TestIstioRouter_HTTPRequestHeaders(t *testing.T) {
|
|
mocks := newFixture(nil)
|
|
router := &IstioRouter{
|
|
logger: mocks.logger,
|
|
flaggerClient: mocks.flaggerClient,
|
|
istioClient: mocks.meshClient,
|
|
kubeClient: mocks.kubeClient,
|
|
}
|
|
|
|
err := router.Reconcile(mocks.canary)
|
|
require.NoError(t, err)
|
|
|
|
vs, err := mocks.meshClient.NetworkingV1beta1().VirtualServices("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
|
|
require.NoError(t, err)
|
|
require.Len(t, vs.Spec.Http, 1)
|
|
assert.Equal(t, "15000", vs.Spec.Http[0].Headers.Request.Add["x-envoy-upstream-rq-timeout-ms"])
|
|
assert.Equal(t, "test", vs.Spec.Http[0].Headers.Request.Remove[0])
|
|
assert.Equal(t, "token", vs.Spec.Http[0].Headers.Response.Remove[0])
|
|
}
|
|
|
|
func TestIstioRouter_CORS(t *testing.T) {
|
|
mocks := newFixture(nil)
|
|
router := &IstioRouter{
|
|
logger: mocks.logger,
|
|
flaggerClient: mocks.flaggerClient,
|
|
istioClient: mocks.meshClient,
|
|
kubeClient: mocks.kubeClient,
|
|
}
|
|
|
|
err := router.Reconcile(mocks.canary)
|
|
require.NoError(t, err)
|
|
|
|
vs, err := mocks.meshClient.NetworkingV1beta1().VirtualServices("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
|
|
require.NoError(t, err)
|
|
require.NoError(t, err)
|
|
require.Len(t, vs.Spec.Http, 1)
|
|
assert.NotNil(t, vs.Spec.Http[0].CorsPolicy)
|
|
assert.Len(t, vs.Spec.Http[0].CorsPolicy.AllowMethods, 2)
|
|
}
|
|
|
|
func TestIstioRouter_ABTest(t *testing.T) {
|
|
mocks := newFixture(nil)
|
|
router := &IstioRouter{
|
|
logger: mocks.logger,
|
|
flaggerClient: mocks.flaggerClient,
|
|
istioClient: mocks.meshClient,
|
|
kubeClient: mocks.kubeClient,
|
|
}
|
|
|
|
err := router.Reconcile(mocks.abtest)
|
|
require.NoError(t, err)
|
|
|
|
// test insert
|
|
vs, err := mocks.meshClient.NetworkingV1beta1().VirtualServices("default").Get(context.TODO(), "abtest", metav1.GetOptions{})
|
|
require.NoError(t, err)
|
|
assert.Len(t, vs.Spec.Http, 2)
|
|
|
|
p := 0
|
|
c := 100
|
|
m := false
|
|
|
|
err = router.SetRoutes(mocks.abtest, p, c, m)
|
|
require.NoError(t, err)
|
|
|
|
vs, err = mocks.meshClient.NetworkingV1beta1().VirtualServices("default").Get(context.TODO(), "abtest", metav1.GetOptions{})
|
|
require.NoError(t, err)
|
|
|
|
pHost := fmt.Sprintf("%s-primary", mocks.abtest.Spec.TargetRef.Name)
|
|
cHost := fmt.Sprintf("%s-canary", mocks.abtest.Spec.TargetRef.Name)
|
|
pRoute := istiov1beta1.HTTPRouteDestination{}
|
|
cRoute := istiov1beta1.HTTPRouteDestination{}
|
|
var mirror *istiov1beta1.Destination
|
|
|
|
for _, http := range vs.Spec.Http {
|
|
for _, route := range http.Route {
|
|
if route.Destination.Host == pHost {
|
|
pRoute = route
|
|
}
|
|
if route.Destination.Host == cHost {
|
|
cRoute = route
|
|
mirror = http.Mirror
|
|
}
|
|
}
|
|
}
|
|
|
|
assert.Equal(t, p, pRoute.Weight)
|
|
assert.Equal(t, c, cRoute.Weight)
|
|
assert.Nil(t, mirror)
|
|
}
|
|
|
|
func TestIstioRouter_GatewayPort(t *testing.T) {
|
|
mocks := newFixture(nil)
|
|
router := &IstioRouter{
|
|
logger: mocks.logger,
|
|
flaggerClient: mocks.flaggerClient,
|
|
istioClient: mocks.meshClient,
|
|
kubeClient: mocks.kubeClient,
|
|
}
|
|
|
|
err := router.Reconcile(mocks.canary)
|
|
require.NoError(t, err)
|
|
|
|
vs, err := mocks.meshClient.NetworkingV1beta1().VirtualServices("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
|
|
require.NoError(t, err)
|
|
|
|
port := vs.Spec.Http[0].Route[0].Destination.Port.Number
|
|
assert.Equal(t, uint32(mocks.canary.Spec.Service.Port), port)
|
|
}
|
|
|
|
func TestIstioRouter_Delegate(t *testing.T) {
|
|
t.Run("ok", func(t *testing.T) {
|
|
mocks := newFixture(nil)
|
|
mocks.canary.Spec.Service.Hosts = []string{}
|
|
mocks.canary.Spec.Service.Gateways = []string{}
|
|
mocks.canary.Spec.Service.Delegation = true
|
|
|
|
router := &IstioRouter{
|
|
logger: mocks.logger,
|
|
flaggerClient: mocks.flaggerClient,
|
|
istioClient: mocks.meshClient,
|
|
kubeClient: mocks.kubeClient,
|
|
}
|
|
|
|
err := router.Reconcile(mocks.canary)
|
|
require.NoError(t, err)
|
|
|
|
vs, err := mocks.meshClient.NetworkingV1beta1().VirtualServices("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, 0, len(vs.Spec.Hosts))
|
|
assert.Equal(t, 0, len(vs.Spec.Gateways))
|
|
|
|
port := vs.Spec.Http[0].Route[0].Destination.Port.Number
|
|
assert.Equal(t, uint32(mocks.canary.Spec.Service.Port), port)
|
|
})
|
|
|
|
t.Run("invalid", func(t *testing.T) {
|
|
mocks := newFixture(nil)
|
|
if len(mocks.canary.Spec.Service.Gateways) == 0 {
|
|
// in this case, the gateways or hosts should not be not empty because it requires to cause an error.
|
|
mocks.canary.Spec.Service.Gateways = []string{
|
|
"istio/public-gateway",
|
|
"mesh",
|
|
}
|
|
}
|
|
mocks.canary.Spec.Service.Delegation = true
|
|
|
|
router := &IstioRouter{
|
|
logger: mocks.logger,
|
|
flaggerClient: mocks.flaggerClient,
|
|
istioClient: mocks.meshClient,
|
|
kubeClient: mocks.kubeClient,
|
|
}
|
|
|
|
err := router.Reconcile(mocks.canary)
|
|
require.Error(t, err)
|
|
})
|
|
}
|
|
|
|
func TestIstioRouter_Finalize(t *testing.T) {
|
|
mocks := newFixture(nil)
|
|
router := &IstioRouter{
|
|
logger: mocks.logger,
|
|
flaggerClient: mocks.flaggerClient,
|
|
istioClient: mocks.meshClient,
|
|
kubeClient: mocks.kubeClient,
|
|
}
|
|
|
|
flaggerSpec := &istiov1beta1.VirtualServiceSpec{
|
|
Http: []istiov1beta1.HTTPRoute{
|
|
{
|
|
Match: mocks.canary.Spec.Service.Match,
|
|
Rewrite: mocks.canary.Spec.Service.GetIstioRewrite(),
|
|
Timeout: mocks.canary.Spec.Service.Timeout,
|
|
Retries: mocks.canary.Spec.Service.Retries,
|
|
CorsPolicy: mocks.canary.Spec.Service.CorsPolicy,
|
|
},
|
|
},
|
|
}
|
|
|
|
kubectlSpec := &istiov1beta1.VirtualServiceSpec{
|
|
Hosts: []string{"podinfo"},
|
|
Gateways: []string{"istio-system/ingressgateway"},
|
|
Http: []istiov1beta1.HTTPRoute{
|
|
{
|
|
Match: nil,
|
|
Route: []istiov1beta1.HTTPRouteDestination{
|
|
{
|
|
Destination: istiov1beta1.Destination{Host: "podinfo"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
tables := []struct {
|
|
router *IstioRouter
|
|
spec *istiov1beta1.VirtualServiceSpec
|
|
shouldError bool
|
|
createVS bool
|
|
canary *v1beta1.Canary
|
|
callReconcile bool
|
|
annotation string
|
|
}{
|
|
// VS not found
|
|
{router: router, spec: nil, shouldError: true, createVS: false, canary: mocks.canary, callReconcile: false, annotation: ""},
|
|
// No annotation found but still finalizes
|
|
{router: router, spec: nil, shouldError: false, createVS: false, canary: mocks.canary, callReconcile: true, annotation: ""},
|
|
// Spec should match annotation after finalize
|
|
{router: router, spec: flaggerSpec, shouldError: false, createVS: true, canary: mocks.canary, callReconcile: true, annotation: "flagger"},
|
|
// Need to test kubectl annotation
|
|
{router: router, spec: kubectlSpec, shouldError: false, createVS: true, canary: mocks.canary, callReconcile: true, annotation: "kubectl"},
|
|
}
|
|
|
|
for _, table := range tables {
|
|
var err error
|
|
if table.createVS {
|
|
vs, err := router.istioClient.NetworkingV1beta1().VirtualServices(table.canary.Namespace).Get(context.TODO(), table.canary.Name, metav1.GetOptions{})
|
|
require.NoError(t, err)
|
|
|
|
if vs.Annotations == nil {
|
|
vs.Annotations = make(map[string]string)
|
|
}
|
|
|
|
switch table.annotation {
|
|
case "flagger":
|
|
b, err := json.Marshal(table.spec)
|
|
require.NoError(t, err)
|
|
vs.Annotations[configAnnotation] = string(b)
|
|
case "kubectl":
|
|
vs.Annotations[kubectlAnnotation] = `{"apiVersion": "networking.istio.io/v1beta1","kind": "VirtualService","metadata": {"annotations": {},"name": "podinfo","namespace": "test"}, "spec": {"gateways": ["istio-system/ingressgateway"],"hosts": ["podinfo"],"http": [{"route": [{"destination": {"host": "podinfo"}}]}]}}`
|
|
}
|
|
_, err = router.istioClient.NetworkingV1beta1().VirtualServices(table.canary.Namespace).Update(context.TODO(), vs, metav1.UpdateOptions{})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
if table.callReconcile {
|
|
err = router.Reconcile(table.canary)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
err = router.Finalize(table.canary)
|
|
if table.shouldError {
|
|
require.Error(t, err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
if table.spec != nil {
|
|
vs, err := router.istioClient.NetworkingV1beta1().VirtualServices(table.canary.Namespace).Get(context.TODO(), table.canary.Name, metav1.GetOptions{})
|
|
require.NoError(t, err)
|
|
require.Equal(t, *table.spec, vs.Spec)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestIstioRouter_Match(t *testing.T) {
|
|
mocks := newFixture(nil)
|
|
router := &IstioRouter{
|
|
logger: mocks.logger,
|
|
flaggerClient: mocks.flaggerClient,
|
|
istioClient: mocks.meshClient,
|
|
kubeClient: mocks.kubeClient,
|
|
}
|
|
|
|
// service.match is not exists, analysis match is exists
|
|
err := router.Reconcile(mocks.abtest)
|
|
require.NoError(t, err)
|
|
|
|
// test insert
|
|
vs, err := mocks.meshClient.NetworkingV1beta1().VirtualServices("default").Get(context.TODO(), "abtest", metav1.GetOptions{})
|
|
require.NoError(t, err)
|
|
assert.Len(t, vs.Spec.Http, 2)
|
|
assert.Len(t, vs.Spec.Http[0].Match, 1) // check for abtest-canary
|
|
require.Equal(t, vs.Spec.Http[0].Match[0].Headers["x-user-type"].Exact, "test")
|
|
assert.Len(t, vs.Spec.Http[1].Match, 0) // check for abtest-primary
|
|
|
|
// Test Case that is service.match exists and multiple analysis.match
|
|
mocks.abtest.Spec.Service.Match = []istiov1beta1.HTTPMatchRequest{
|
|
{
|
|
Name: "podinfo",
|
|
Uri: &istiov1alpha1.StringMatch{
|
|
Prefix: "/podinfo",
|
|
},
|
|
Method: &istiov1alpha1.StringMatch{
|
|
Exact: "GET",
|
|
},
|
|
IgnoreUriCase: true,
|
|
},
|
|
}
|
|
mocks.abtest.Spec.Analysis.Match = []istiov1beta1.HTTPMatchRequest{
|
|
{
|
|
Headers: map[string]istiov1alpha1.StringMatch{
|
|
"x-user-type": {
|
|
Exact: "test",
|
|
},
|
|
"x-auth-test": {
|
|
Exact: "test",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Headers: map[string]istiov1alpha1.StringMatch{
|
|
"x-session-id": {
|
|
Exact: "test",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
// apply changes
|
|
err = router.Reconcile(mocks.abtest)
|
|
require.NoError(t, err)
|
|
|
|
vs, err = mocks.meshClient.NetworkingV1beta1().VirtualServices("default").Get(context.TODO(), "abtest", metav1.GetOptions{})
|
|
require.NoError(t, err)
|
|
assert.Len(t, vs.Spec.Http, 2)
|
|
assert.Len(t, vs.Spec.Http[0].Match, 2) // check for abtest-canary
|
|
require.Equal(t, vs.Spec.Http[0].Match[0].Uri.Prefix, "/podinfo")
|
|
require.Equal(t, vs.Spec.Http[0].Match[0].Headers["x-user-type"].Exact, "test")
|
|
require.Equal(t, vs.Spec.Http[0].Match[0].Headers["x-auth-test"].Exact, "test")
|
|
require.Equal(t, vs.Spec.Http[0].Match[1].Uri.Prefix, "/podinfo")
|
|
require.Equal(t, vs.Spec.Http[0].Match[1].Headers["x-session-id"].Exact, "test")
|
|
assert.Len(t, vs.Spec.Http[1].Match, 1) // check for abtest-primary
|
|
require.Equal(t, vs.Spec.Http[1].Match[0].Uri.Prefix, "/podinfo")
|
|
}
|
|
|
|
// TCP Canary
|
|
func newTestCanaryTCP() *flaggerv1.Canary {
|
|
cd := &flaggerv1.Canary{
|
|
TypeMeta: metav1.TypeMeta{APIVersion: flaggerv1.SchemeGroupVersion.String()},
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: "default",
|
|
Name: "podinfo",
|
|
},
|
|
Spec: flaggerv1.CanarySpec{
|
|
TargetRef: flaggerv1.LocalObjectReference{
|
|
Name: "podinfo",
|
|
APIVersion: "apps/v1",
|
|
Kind: "Deployment",
|
|
},
|
|
Service: flaggerv1.CanaryService{
|
|
Port: 9898,
|
|
PortDiscovery: true,
|
|
AppProtocol: "TCP",
|
|
Match: []istiov1beta1.HTTPMatchRequest{
|
|
{
|
|
Port: 9898,
|
|
},
|
|
},
|
|
Gateways: []string{
|
|
"istio/public-gateway",
|
|
"mesh",
|
|
},
|
|
}, Analysis: &flaggerv1.CanaryAnalysis{
|
|
Threshold: 10,
|
|
StepWeight: 10,
|
|
MaxWeight: 50,
|
|
Metrics: []flaggerv1.CanaryMetric{
|
|
{
|
|
Name: "request-success-rate",
|
|
Threshold: 99,
|
|
Interval: "1m",
|
|
},
|
|
{
|
|
Name: "request-duration",
|
|
Threshold: 500,
|
|
Interval: "1m",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
return cd
|
|
}
|
|
|
|
func TestIstioRouter_SetRoutesTCP(t *testing.T) {
|
|
mocks := newFixture(newTestCanaryTCP())
|
|
router := &IstioRouter{
|
|
logger: mocks.logger,
|
|
flaggerClient: mocks.flaggerClient,
|
|
istioClient: mocks.meshClient,
|
|
kubeClient: mocks.kubeClient,
|
|
}
|
|
|
|
err := router.Reconcile(mocks.canary)
|
|
require.NoError(t, err)
|
|
|
|
pHost := fmt.Sprintf("%s-primary", mocks.canary.Spec.TargetRef.Name)
|
|
cHost := fmt.Sprintf("%s-canary", mocks.canary.Spec.TargetRef.Name)
|
|
|
|
t.Run("normal", func(t *testing.T) {
|
|
p, c := 60, 40
|
|
err := router.SetRoutes(mocks.canary, p, c, false)
|
|
require.NoError(t, err)
|
|
|
|
vs, err := mocks.meshClient.NetworkingV1beta1().VirtualServices("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
|
|
require.NoError(t, err)
|
|
|
|
var pRoute, cRoute istiov1beta1.HTTPRouteDestination
|
|
for _, tcp := range vs.Spec.Tcp {
|
|
for _, route := range tcp.Route {
|
|
if route.Destination.Host == pHost {
|
|
pRoute = route
|
|
}
|
|
if route.Destination.Host == cHost {
|
|
cRoute = route
|
|
}
|
|
}
|
|
}
|
|
|
|
assert.Equal(t, p, pRoute.Weight)
|
|
assert.Equal(t, c, cRoute.Weight)
|
|
})
|
|
}
|
|
|
|
func TestIstioRouter_GetRoutesTCP(t *testing.T) {
|
|
mocks := newFixture(newTestCanaryTCP())
|
|
router := &IstioRouter{
|
|
logger: mocks.logger,
|
|
flaggerClient: mocks.flaggerClient,
|
|
istioClient: mocks.meshClient,
|
|
kubeClient: mocks.kubeClient,
|
|
}
|
|
|
|
err := router.Reconcile(mocks.canary)
|
|
require.NoError(t, err)
|
|
|
|
p, c, m, err := router.GetRoutes(mocks.canary)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 100, p)
|
|
assert.Equal(t, 0, c)
|
|
assert.False(t, m)
|
|
|
|
mocks.canary = newTestMirror()
|
|
|
|
err = router.Reconcile(mocks.canary)
|
|
require.NoError(t, err)
|
|
|
|
p, c, m, err = router.GetRoutes(mocks.canary)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 100, p)
|
|
assert.Equal(t, 0, c)
|
|
|
|
// A TCP Canary resource has mirroring disabled
|
|
assert.False(t, m)
|
|
}
|