Files
flagger/pkg/router/istio_test.go
2022-03-16 18:57:02 +00:00

570 lines
17 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"
"testing"
"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"
istiov1alpha1 "github.com/fluxcd/flagger/pkg/apis/istio/common/v1alpha1"
istiov1alpha3 "github.com/fluxcd/flagger/pkg/apis/istio/v1alpha3"
)
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.NetworkingV1alpha3().DestinationRules("default").Get(context.TODO(), "podinfo-canary", metav1.GetOptions{})
require.NoError(t, err)
_, err = mocks.meshClient.NetworkingV1alpha3().DestinationRules("default").Get(context.TODO(), "podinfo-primary", metav1.GetOptions{})
require.NoError(t, err)
vs, err := mocks.meshClient.NetworkingV1alpha3().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.NetworkingV1alpha3().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, "test-gateway.istio-system")
vsClone.Spec.Gateways = gateways
totalGateways := len(mocks.canary.Spec.Service.Gateways)
vsGateways, err := mocks.meshClient.NetworkingV1alpha3().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.NetworkingV1alpha3().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.NetworkingV1alpha3().VirtualServices("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
require.NoError(t, err)
var pRoute, cRoute istiov1alpha3.DestinationWeight
var mirror *istiov1alpha3.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("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.NetworkingV1alpha3().VirtualServices("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
require.NoError(t, err)
var pRoute, cRoute istiov1alpha3.DestinationWeight
var mirror *istiov1alpha3.Destination
var mirrorWeight *istiov1alpha3.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.NetworkingV1alpha3().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 = &istiov1alpha3.Destination{
Host: cHost,
}
}
}
}
_, err = mocks.meshClient.NetworkingV1alpha3().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.NetworkingV1alpha3().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.NetworkingV1alpha3().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.NetworkingV1alpha3().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.NetworkingV1alpha3().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 := istiov1alpha3.DestinationWeight{}
cRoute := istiov1alpha3.DestinationWeight{}
var mirror *istiov1alpha3.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.NetworkingV1alpha3().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.NetworkingV1alpha3().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{
"public-gateway.istio",
"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 := &istiov1alpha3.VirtualServiceSpec{
Http: []istiov1alpha3.HTTPRoute{
{
Match: mocks.canary.Spec.Service.Match,
Rewrite: mocks.canary.Spec.Service.Rewrite,
Timeout: mocks.canary.Spec.Service.Timeout,
Retries: mocks.canary.Spec.Service.Retries,
CorsPolicy: mocks.canary.Spec.Service.CorsPolicy,
},
},
}
kubectlSpec := &istiov1alpha3.VirtualServiceSpec{
Hosts: []string{"podinfo"},
Gateways: []string{"ingressgateway.istio-system.svc.cluster.local"},
Http: []istiov1alpha3.HTTPRoute{
{
Match: nil,
Route: []istiov1alpha3.DestinationWeight{
{
Destination: istiov1alpha3.Destination{Host: "podinfo"},
},
},
},
},
}
tables := []struct {
router *IstioRouter
spec *istiov1alpha3.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.NetworkingV1alpha3().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/v1alpha3","kind": "VirtualService","metadata": {"annotations": {},"name": "podinfo","namespace": "test"}, "spec": {"gateways": ["ingressgateway.istio-system.svc.cluster.local"],"hosts": ["podinfo"],"http": [{"route": [{"destination": {"host": "podinfo"}}]}]}}`
}
_, err = router.istioClient.NetworkingV1alpha3().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.NetworkingV1alpha3().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.NetworkingV1alpha3().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 = []istiov1alpha3.HTTPMatchRequest{
{
Name: "podinfo",
Uri: &istiov1alpha1.StringMatch{
Prefix: "/podinfo",
},
Method: &istiov1alpha1.StringMatch{
Exact: "GET",
},
IgnoreUriCase: true,
},
}
mocks.abtest.Spec.Analysis.Match = []istiov1alpha3.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.NetworkingV1alpha3().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")
}