rollouts/pkg/trafficrouting/manager_test.go

1423 lines
53 KiB
Go

/*
Copyright 2022 The Kruise 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 trafficrouting
import (
"context"
"encoding/json"
"fmt"
"reflect"
"testing"
"time"
rolloutapi "github.com/openkruise/rollouts/api"
"github.com/openkruise/rollouts/api/v1alpha1"
"github.com/openkruise/rollouts/api/v1beta1"
"github.com/openkruise/rollouts/pkg/util"
"github.com/openkruise/rollouts/pkg/util/configuration"
apps "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
netv1 "k8s.io/api/networking/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/intstr"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
utilpointer "k8s.io/utils/pointer"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)
var (
scheme *runtime.Scheme
nginxIngressAnnotationDefaultPrefix = "nginx.ingress.kubernetes.io"
demoService = corev1.Service{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "Service",
},
ObjectMeta: metav1.ObjectMeta{
Name: "echoserver",
},
Spec: corev1.ServiceSpec{
Ports: []corev1.ServicePort{
{
Name: "http",
Port: 80,
TargetPort: intstr.FromInt(8080),
},
},
Selector: map[string]string{
"app": "echoserver",
},
},
}
demoIngress = netv1.Ingress{
TypeMeta: metav1.TypeMeta{
APIVersion: "networking.k8s.io/v1",
Kind: "Ingress",
},
ObjectMeta: metav1.ObjectMeta{
Name: "echoserver",
Annotations: map[string]string{
"kubernetes.io/ingress.class": "nginx",
},
},
Spec: netv1.IngressSpec{
Rules: []netv1.IngressRule{
{
Host: "echoserver.example.com",
IngressRuleValue: netv1.IngressRuleValue{
HTTP: &netv1.HTTPIngressRuleValue{
Paths: []netv1.HTTPIngressPath{
{
Path: "/apis/echo",
Backend: netv1.IngressBackend{
Service: &netv1.IngressServiceBackend{
Name: "echoserver",
Port: netv1.ServiceBackendPort{
Name: "http",
},
},
},
},
},
},
},
},
},
},
}
demoRollout = &v1beta1.Rollout{
ObjectMeta: metav1.ObjectMeta{
Name: "rollout-demo",
Labels: map[string]string{},
Annotations: map[string]string{
util.RolloutHashAnnotation: "rollout-hash-v1",
},
},
Spec: v1beta1.RolloutSpec{
WorkloadRef: v1beta1.ObjectRef{
APIVersion: "apps/v1",
Kind: "Deployment",
Name: "echoserver",
},
Strategy: v1beta1.RolloutStrategy{
Canary: &v1beta1.CanaryStrategy{
Steps: []v1beta1.CanaryStep{
{
TrafficRoutingStrategy: v1beta1.TrafficRoutingStrategy{
Traffic: utilpointer.String("5%"),
},
Replicas: &intstr.IntOrString{IntVal: 1},
},
{
TrafficRoutingStrategy: v1beta1.TrafficRoutingStrategy{
Traffic: utilpointer.String("20%"),
},
Replicas: &intstr.IntOrString{IntVal: 2},
},
{
TrafficRoutingStrategy: v1beta1.TrafficRoutingStrategy{
Traffic: utilpointer.String("60%"),
},
Replicas: &intstr.IntOrString{IntVal: 6},
},
{
TrafficRoutingStrategy: v1beta1.TrafficRoutingStrategy{
Traffic: utilpointer.String("100%"),
},
Replicas: &intstr.IntOrString{IntVal: 10},
},
},
TrafficRoutings: []v1beta1.TrafficRoutingRef{
{
Service: "echoserver",
Ingress: &v1beta1.IngressTrafficRouting{
Name: "echoserver",
},
},
},
},
},
},
Status: v1beta1.RolloutStatus{
Phase: v1beta1.RolloutPhaseProgressing,
CanaryStatus: &v1beta1.CanaryStatus{
CommonStatus: v1beta1.CommonStatus{
ObservedWorkloadGeneration: 1,
RolloutHash: "rollout-hash-v1",
ObservedRolloutID: "rollout-id-1",
StableRevision: "podtemplatehash-v1",
CurrentStepIndex: 1,
CurrentStepState: v1beta1.CanaryStepStateTrafficRouting,
PodTemplateHash: "podtemplatehash-v2",
LastUpdateTime: &metav1.Time{Time: time.Now()},
},
CanaryRevision: "revision-v2",
},
Conditions: []v1beta1.RolloutCondition{
{
Type: v1beta1.RolloutConditionProgressing,
Reason: v1alpha1.ProgressingReasonInRolling,
Status: corev1.ConditionFalse,
},
},
},
}
demoIstioRollout = &v1beta1.Rollout{
ObjectMeta: metav1.ObjectMeta{
Name: "rollout-demo",
Labels: map[string]string{},
Annotations: map[string]string{
util.RolloutHashAnnotation: "rollout-hash-v1",
},
},
Spec: v1beta1.RolloutSpec{
WorkloadRef: v1beta1.ObjectRef{
APIVersion: "apps/v1",
Kind: "Deployment",
Name: "echoserver",
},
Strategy: v1beta1.RolloutStrategy{
Canary: &v1beta1.CanaryStrategy{
DisableGenerateCanaryService: false,
Steps: []v1beta1.CanaryStep{
{
TrafficRoutingStrategy: v1beta1.TrafficRoutingStrategy{
Traffic: utilpointer.String("5%"),
},
Replicas: &intstr.IntOrString{IntVal: 1},
},
{
TrafficRoutingStrategy: v1beta1.TrafficRoutingStrategy{
Traffic: utilpointer.String("20%"),
},
Replicas: &intstr.IntOrString{IntVal: 2},
},
{
TrafficRoutingStrategy: v1beta1.TrafficRoutingStrategy{
Traffic: utilpointer.String("60%"),
},
Replicas: &intstr.IntOrString{IntVal: 6},
},
{
TrafficRoutingStrategy: v1beta1.TrafficRoutingStrategy{
Traffic: utilpointer.String("100%"),
},
Replicas: &intstr.IntOrString{IntVal: 10},
},
},
TrafficRoutings: []v1beta1.TrafficRoutingRef{
{
Service: "echoserver",
CustomNetworkRefs: []v1beta1.ObjectRef{
{
APIVersion: "networking.istio.io/v1alpha3",
Kind: "VirtualService",
Name: "echoserver",
},
{
APIVersion: "networking.istio.io/v1alpha3",
Kind: "DestinationRule",
Name: "dr-demo",
},
},
},
},
},
},
},
Status: v1beta1.RolloutStatus{
Phase: v1beta1.RolloutPhaseProgressing,
CanaryStatus: &v1beta1.CanaryStatus{
CommonStatus: v1beta1.CommonStatus{
ObservedWorkloadGeneration: 1,
RolloutHash: "rollout-hash-v1",
ObservedRolloutID: "rollout-id-1",
StableRevision: "podtemplatehash-v1",
CurrentStepIndex: 1,
CurrentStepState: v1beta1.CanaryStepStateTrafficRouting,
PodTemplateHash: "podtemplatehash-v2",
LastUpdateTime: &metav1.Time{Time: time.Now()},
},
CanaryRevision: "revision-v2",
},
Conditions: []v1beta1.RolloutCondition{
{
Type: v1beta1.RolloutConditionProgressing,
Reason: v1alpha1.ProgressingReasonInRolling,
Status: corev1.ConditionFalse,
},
},
},
}
demoConf = corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: configuration.RolloutConfigurationName,
Namespace: util.GetRolloutNamespace(),
},
Data: map[string]string{
fmt.Sprintf("%s.nginx", configuration.LuaTrafficRoutingIngressTypePrefix): `
annotations = obj.annotations
annotations["nginx.ingress.kubernetes.io/canary"] = "true"
annotations["nginx.ingress.kubernetes.io/canary-by-cookie"] = nil
annotations["nginx.ingress.kubernetes.io/canary-by-header"] = nil
annotations["nginx.ingress.kubernetes.io/canary-by-header-pattern"] = nil
annotations["nginx.ingress.kubernetes.io/canary-by-header-value"] = nil
annotations["nginx.ingress.kubernetes.io/canary-weight"] = nil
if ( obj.weight ~= "-1" )
then
annotations["nginx.ingress.kubernetes.io/canary-weight"] = obj.weight
end
if ( not obj.matches )
then
return annotations
end
for _,match in ipairs(obj.matches) do
header = match.headers[1]
if ( header.name == "canary-by-cookie" )
then
annotations["nginx.ingress.kubernetes.io/canary-by-cookie"] = header.value
else
annotations["nginx.ingress.kubernetes.io/canary-by-header"] = header.name
if ( header.type == "RegularExpression" )
then
annotations["nginx.ingress.kubernetes.io/canary-by-header-pattern"] = header.value
else
annotations["nginx.ingress.kubernetes.io/canary-by-header-value"] = header.value
end
end
end
return annotations
`,
},
}
istioLuaConfig = corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: configuration.RolloutConfigurationName,
Namespace: util.GetRolloutNamespace(),
},
Data: map[string]string{
"lua.traffic.routing.VirtualService.networking.istio.io": `
spec = obj.data.spec
if obj.canaryWeight == -1 then
obj.canaryWeight = 100
obj.stableWeight = 0
end
function GetHost(destination)
local host = destination.destination.host
dot_position = string.find(host, ".", 1, true)
if (dot_position) then
host = string.sub(host, 1, dot_position - 1)
end
return host
end
-- find routes of VirtualService with stableService
function GetRulesToPatch(spec, stableService, protocol)
local matchedRoutes = {}
if (spec[protocol] ~= nil) then
for _, rule in ipairs(spec[protocol]) do
-- skip routes contain matches
if (rule.match == nil) then
for _, route in ipairs(rule.route) do
if GetHost(route) == stableService then
table.insert(matchedRoutes, rule)
end
end
end
end
end
return matchedRoutes
end
function CalculateWeight(route, stableWeight, n)
local weight
if (route.weight) then
weight = math.floor(route.weight * stableWeight / 100)
else
weight = math.floor(stableWeight / n)
end
return weight
end
-- generate routes with matches, insert a rule before other rules, only support http headers, cookies etc.
function GenerateRoutesWithMatches(spec, matches, stableService, canaryService)
for _, match in ipairs(matches) do
local route = {}
route["match"] = {}
for key, value in pairs(match) do
local vsMatch = {}
vsMatch[key] = {}
for _, rule in ipairs(value) do
if rule["type"] == "RegularExpression" then
matchType = "regex"
elseif rule["type"] == "Exact" then
matchType = "exact"
elseif rule["type"] == "Prefix" then
matchType = "prefix"
end
if key == "headers" then
vsMatch[key][rule["name"]] = {}
vsMatch[key][rule["name"]][matchType] = rule.value
else
vsMatch[key][matchType] = rule.value
end
end
table.insert(route["match"], vsMatch)
end
route.route = {
{
destination = {}
}
}
-- stableService == canaryService indicates DestinationRule exists and subset is set to be canary by default
if stableService == canaryService then
route.route[1].destination.host = stableService
route.route[1].destination.subset = "canary"
else
route.route[1].destination.host = canaryService
end
table.insert(spec.http, 1, route)
end
end
-- generate routes without matches, change every rule whose host is stableService
function GenerateRoutes(spec, stableService, canaryService, stableWeight, canaryWeight, protocol)
local matchedRules = GetRulesToPatch(spec, stableService, protocol)
for _, rule in ipairs(matchedRules) do
local canary
if stableService ~= canaryService then
canary = {
destination = {
host = canaryService,
},
weight = canaryWeight,
}
else
canary = {
destination = {
host = stableService,
subset = "canary",
},
weight = canaryWeight,
}
end
-- incase there are multiple versions traffic already, do a for-loop
for _, route in ipairs(rule.route) do
-- update stable service weight
route.weight = CalculateWeight(route, stableWeight, #rule.route)
end
table.insert(rule.route, canary)
end
end
if (obj.matches and next(obj.matches) ~= nil)
then
GenerateRoutesWithMatches(spec, obj.matches, obj.stableService, obj.canaryService)
else
GenerateRoutes(spec, obj.stableService, obj.canaryService, obj.stableWeight, obj.canaryWeight, "http")
GenerateRoutes(spec, obj.stableService, obj.canaryService, obj.stableWeight, obj.canaryWeight, "tcp")
GenerateRoutes(spec, obj.stableService, obj.canaryService, obj.stableWeight, obj.canaryWeight, "tls")
end
return obj.data
`,
"lua.traffic.routing.DestinationRule.networking.istio.io": `
local spec = obj.data.spec
local canary = {}
canary.labels = {}
canary.name = "canary"
local podLabelKey = "istio.service.tag"
canary.labels[podLabelKey] = "gray"
table.insert(spec.subsets, canary)
return obj.data
`,
},
}
virtualServiceDemo = `
{
"apiVersion": "networking.istio.io/v1alpha3",
"kind": "VirtualService",
"metadata": {
"name": "echoserver",
"annotations": {
"virtual": "test"
}
},
"spec": {
"hosts": [
"echoserver.example.com"
],
"http": [
{
"route": [
{
"destination": {
"host": "echoserver"
}
}
]
}
]
}
}
`
destinationRuleDemo = `
{
"apiVersion": "networking.istio.io/v1alpha3",
"kind": "DestinationRule",
"metadata": {
"name": "dr-demo"
},
"spec": {
"host": "echoserver",
"subsets": [
{
"labels": {
"version": "base"
},
"name": "version-base"
}
],
"trafficPolicy": {
"loadBalancer": {
"simple": "ROUND_ROBIN"
}
}
}
}
`
)
func init() {
scheme = runtime.NewScheme()
_ = clientgoscheme.AddToScheme(scheme)
_ = rolloutapi.AddToScheme(scheme)
}
func TestDoTrafficRouting(t *testing.T) {
cases := []struct {
name string
getObj func() ([]*corev1.Service, []*netv1.Ingress)
getRollout func() (*v1beta1.Rollout, *util.Workload)
expectObj func() ([]*corev1.Service, []*netv1.Ingress)
expectDone bool
}{
{
name: "DoTrafficRouting test1",
getObj: func() ([]*corev1.Service, []*netv1.Ingress) {
return []*corev1.Service{demoService.DeepCopy()}, []*netv1.Ingress{demoIngress.DeepCopy()}
},
getRollout: func() (*v1beta1.Rollout, *util.Workload) {
rollout := demoRollout.DeepCopy()
workload := &util.Workload{RevisionLabelKey: apps.DefaultDeploymentUniqueLabelKey}
rollout.Status.CanaryStatus.LastUpdateTime = &metav1.Time{Time: time.Now().Add(-time.Hour)}
return rollout, workload
},
expectObj: func() ([]*corev1.Service, []*netv1.Ingress) {
s1 := demoService.DeepCopy()
s1.Spec.Selector[apps.DefaultDeploymentUniqueLabelKey] = "podtemplatehash-v1"
s2 := demoService.DeepCopy()
s2.Name = "echoserver-canary"
s2.Spec.Selector[apps.DefaultDeploymentUniqueLabelKey] = "podtemplatehash-v2"
return []*corev1.Service{s1, s2}, []*netv1.Ingress{demoIngress.DeepCopy()}
},
expectDone: false,
},
{
name: "DoTrafficRouting test2",
getObj: func() ([]*corev1.Service, []*netv1.Ingress) {
s1 := demoService.DeepCopy()
s1.Spec.Selector[apps.DefaultDeploymentUniqueLabelKey] = "podtemplatehash-v1"
s2 := demoService.DeepCopy()
s2.Name = "echoserver-canary"
s2.Spec.Selector[apps.DefaultDeploymentUniqueLabelKey] = "podtemplatehash-v2"
return []*corev1.Service{s1, s2}, []*netv1.Ingress{demoIngress.DeepCopy()}
},
getRollout: func() (*v1beta1.Rollout, *util.Workload) {
obj := demoRollout.DeepCopy()
obj.Status.CanaryStatus.LastUpdateTime = &metav1.Time{Time: time.Now()}
return obj, &util.Workload{RevisionLabelKey: apps.DefaultDeploymentUniqueLabelKey}
},
expectObj: func() ([]*corev1.Service, []*netv1.Ingress) {
s1 := demoService.DeepCopy()
s1.Spec.Selector[apps.DefaultDeploymentUniqueLabelKey] = "podtemplatehash-v1"
s2 := demoService.DeepCopy()
s2.Name = "echoserver-canary"
s2.Spec.Selector[apps.DefaultDeploymentUniqueLabelKey] = "podtemplatehash-v2"
c1 := demoIngress.DeepCopy()
return []*corev1.Service{s1, s2}, []*netv1.Ingress{c1}
},
expectDone: false,
},
{
name: "DoTrafficRouting test3",
getObj: func() ([]*corev1.Service, []*netv1.Ingress) {
s1 := demoService.DeepCopy()
s1.Spec.Selector[apps.DefaultDeploymentUniqueLabelKey] = "podtemplatehash-v1"
s2 := demoService.DeepCopy()
s2.Name = "echoserver-canary"
s2.Spec.Selector[apps.DefaultDeploymentUniqueLabelKey] = "podtemplatehash-v2"
c1 := demoIngress.DeepCopy()
c2 := demoIngress.DeepCopy()
c2.Spec.Rules[0].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
c2.Name = "echoserver-canary"
c2.Annotations[fmt.Sprintf("%s/canary", nginxIngressAnnotationDefaultPrefix)] = "true"
c2.Annotations[fmt.Sprintf("%s/canary-weight", nginxIngressAnnotationDefaultPrefix)] = "0"
return []*corev1.Service{s1, s2}, []*netv1.Ingress{c1, c2}
},
getRollout: func() (*v1beta1.Rollout, *util.Workload) {
obj := demoRollout.DeepCopy()
obj.Status.CanaryStatus.LastUpdateTime = &metav1.Time{Time: time.Now().Add(-10 * time.Second)}
return obj, &util.Workload{RevisionLabelKey: apps.DefaultDeploymentUniqueLabelKey}
},
expectObj: func() ([]*corev1.Service, []*netv1.Ingress) {
s1 := demoService.DeepCopy()
s1.Spec.Selector[apps.DefaultDeploymentUniqueLabelKey] = "podtemplatehash-v1"
s2 := demoService.DeepCopy()
s2.Name = "echoserver-canary"
s2.Spec.Selector[apps.DefaultDeploymentUniqueLabelKey] = "podtemplatehash-v2"
c1 := demoIngress.DeepCopy()
c2 := demoIngress.DeepCopy()
c2.Name = "echoserver-canary"
c2.Annotations[fmt.Sprintf("%s/canary", nginxIngressAnnotationDefaultPrefix)] = "true"
c2.Annotations[fmt.Sprintf("%s/canary-weight", nginxIngressAnnotationDefaultPrefix)] = "5"
c2.Spec.Rules[0].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
return []*corev1.Service{s1, s2}, []*netv1.Ingress{c1, c2}
},
expectDone: false,
},
{
name: "DoTrafficRouting test4",
getObj: func() ([]*corev1.Service, []*netv1.Ingress) {
s1 := demoService.DeepCopy()
s1.Spec.Selector[apps.DefaultDeploymentUniqueLabelKey] = "podtemplatehash-v1"
s2 := demoService.DeepCopy()
s2.Name = "echoserver-canary"
s2.Spec.Selector[apps.DefaultDeploymentUniqueLabelKey] = "podtemplatehash-v2"
c1 := demoIngress.DeepCopy()
c2 := demoIngress.DeepCopy()
c2.Name = "echoserver-canary"
c2.Annotations[fmt.Sprintf("%s/canary", nginxIngressAnnotationDefaultPrefix)] = "true"
c2.Annotations[fmt.Sprintf("%s/canary-weight", nginxIngressAnnotationDefaultPrefix)] = "5"
c2.Spec.Rules[0].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
return []*corev1.Service{s1, s2}, []*netv1.Ingress{c1, c2}
},
getRollout: func() (*v1beta1.Rollout, *util.Workload) {
obj := demoRollout.DeepCopy()
obj.Status.CanaryStatus.LastUpdateTime = &metav1.Time{Time: time.Now().Add(-10 * time.Second)}
return obj, &util.Workload{RevisionLabelKey: apps.DefaultDeploymentUniqueLabelKey}
},
expectObj: func() ([]*corev1.Service, []*netv1.Ingress) {
s1 := demoService.DeepCopy()
s1.Spec.Selector[apps.DefaultDeploymentUniqueLabelKey] = "podtemplatehash-v1"
s2 := demoService.DeepCopy()
s2.Name = "echoserver-canary"
s2.Spec.Selector[apps.DefaultDeploymentUniqueLabelKey] = "podtemplatehash-v2"
c1 := demoIngress.DeepCopy()
c2 := demoIngress.DeepCopy()
c2.Name = "echoserver-canary"
c2.Annotations[fmt.Sprintf("%s/canary", nginxIngressAnnotationDefaultPrefix)] = "true"
c2.Annotations[fmt.Sprintf("%s/canary-weight", nginxIngressAnnotationDefaultPrefix)] = "5"
c2.Spec.Rules[0].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
return []*corev1.Service{s1, s2}, []*netv1.Ingress{c1, c2}
},
expectDone: true,
},
{
name: "DoTrafficRouting test5",
getObj: func() ([]*corev1.Service, []*netv1.Ingress) {
s1 := demoService.DeepCopy()
s1.Spec.Selector[apps.DefaultDeploymentUniqueLabelKey] = "podtemplatehash-v1"
s2 := demoService.DeepCopy()
s2.Name = "echoserver-canary"
s2.Spec.Selector[apps.DefaultDeploymentUniqueLabelKey] = "podtemplatehash-v2"
c1 := demoIngress.DeepCopy()
c2 := demoIngress.DeepCopy()
c2.Name = "echoserver-canary"
c2.Annotations[fmt.Sprintf("%s/canary", nginxIngressAnnotationDefaultPrefix)] = "true"
c2.Annotations[fmt.Sprintf("%s/canary-weight", nginxIngressAnnotationDefaultPrefix)] = "5"
c2.Spec.Rules[0].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
return []*corev1.Service{s1, s2}, []*netv1.Ingress{c1, c2}
},
getRollout: func() (*v1beta1.Rollout, *util.Workload) {
obj := demoRollout.DeepCopy()
obj.Status.CanaryStatus.LastUpdateTime = &metav1.Time{Time: time.Now().Add(-10 * time.Second)}
obj.Status.CanaryStatus.CurrentStepIndex = 2
return obj, &util.Workload{RevisionLabelKey: apps.DefaultDeploymentUniqueLabelKey}
},
expectObj: func() ([]*corev1.Service, []*netv1.Ingress) {
s1 := demoService.DeepCopy()
s1.Spec.Selector[apps.DefaultDeploymentUniqueLabelKey] = "podtemplatehash-v1"
s2 := demoService.DeepCopy()
s2.Name = "echoserver-canary"
s2.Spec.Selector[apps.DefaultDeploymentUniqueLabelKey] = "podtemplatehash-v2"
c1 := demoIngress.DeepCopy()
c2 := demoIngress.DeepCopy()
c2.Name = "echoserver-canary"
c2.Annotations[fmt.Sprintf("%s/canary", nginxIngressAnnotationDefaultPrefix)] = "true"
c2.Annotations[fmt.Sprintf("%s/canary-weight", nginxIngressAnnotationDefaultPrefix)] = "20"
c2.Spec.Rules[0].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
return []*corev1.Service{s1, s2}, []*netv1.Ingress{c1, c2}
},
expectDone: false,
},
}
for _, cs := range cases {
t.Run(cs.name, func(t *testing.T) {
ss, ig := cs.getObj()
client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(ig[0], ss[0], demoConf.DeepCopy()).Build()
if len(ss) == 2 {
_ = client.Create(context.TODO(), ss[1])
}
if len(ig) == 2 {
_ = client.Create(context.TODO(), ig[1])
}
rollout, workload := cs.getRollout()
newStatus := rollout.Status.DeepCopy()
currentStep := rollout.Spec.Strategy.Canary.Steps[newStatus.CanaryStatus.CurrentStepIndex-1]
c := &TrafficRoutingContext{
Key: fmt.Sprintf("Rollout(%s/%s)", rollout.Namespace, rollout.Name),
Namespace: rollout.Namespace,
ObjectRef: rollout.Spec.Strategy.Canary.TrafficRoutings,
Strategy: currentStep.TrafficRoutingStrategy,
OwnerRef: *metav1.NewControllerRef(rollout, v1beta1.SchemeGroupVersion.WithKind("Rollout")),
RevisionLabelKey: workload.RevisionLabelKey,
StableRevision: newStatus.CanaryStatus.StableRevision,
CanaryRevision: newStatus.CanaryStatus.PodTemplateHash,
LastUpdateTime: newStatus.CanaryStatus.LastUpdateTime,
}
manager := NewTrafficRoutingManager(client)
err := manager.InitializeTrafficRouting(c)
if err != nil {
t.Fatalf("InitializeTrafficRouting failed: %s", err)
}
done, err := manager.DoTrafficRouting(c)
if err != nil {
t.Fatalf("DoTrafficRouting failed: %s", err)
}
if cs.expectDone != done {
t.Fatalf("DoTrafficRouting expect(%v), but get(%v)", cs.expectDone, done)
}
ss, ig = cs.expectObj()
for _, obj := range ss {
checkObjEqual(client, t, obj)
}
for _, obj := range ig {
checkObjEqual(client, t, obj)
}
})
}
}
func TestDoTrafficRoutingWithIstio(t *testing.T) {
const (
OriginalSpecAnnotation = "rollouts.kruise.io/original-spec-configuration"
)
cases := []struct {
name string
getService func() []*corev1.Service
getUnstructureds func() []*unstructured.Unstructured
getRollout func() (*v1beta1.Rollout, *util.Workload)
expectUnstructureds func() []*unstructured.Unstructured
expectDone bool
}{
{
name: "Test with DisableGenerateCanaryService: false",
getService: func() []*corev1.Service {
s1 := demoService.DeepCopy()
s1.Spec.Selector[apps.DefaultDeploymentUniqueLabelKey] = "podtemplatehash-v1"
return []*corev1.Service{s1}
},
getUnstructureds: func() []*unstructured.Unstructured {
objects := make([]*unstructured.Unstructured, 0)
u := &unstructured.Unstructured{}
_ = u.UnmarshalJSON([]byte(virtualServiceDemo))
u.SetAPIVersion("networking.istio.io/v1alpha3")
objects = append(objects, u)
u = &unstructured.Unstructured{}
_ = u.UnmarshalJSON([]byte(destinationRuleDemo))
u.SetAPIVersion("networking.istio.io/v1alpha3")
objects = append(objects, u)
return objects
},
getRollout: func() (*v1beta1.Rollout, *util.Workload) {
obj := demoIstioRollout.DeepCopy()
obj.Status.CanaryStatus.LastUpdateTime = &metav1.Time{Time: time.Now().Add(-10 * time.Second)}
obj.Spec.Strategy.Canary.TrafficRoutings[0].GracePeriodSeconds = 1
return obj, &util.Workload{RevisionLabelKey: apps.DefaultDeploymentUniqueLabelKey}
},
expectUnstructureds: func() []*unstructured.Unstructured {
objects := make([]*unstructured.Unstructured, 0)
u := &unstructured.Unstructured{}
_ = u.UnmarshalJSON([]byte(virtualServiceDemo))
annotations := map[string]string{
OriginalSpecAnnotation: `{"spec":{"hosts":["echoserver.example.com"],"http":[{"route":[{"destination":{"host":"echoserver"}}]}]},"annotations":{"virtual":"test"}}`,
"virtual": "test",
}
u.SetAnnotations(annotations)
specStr := `{"hosts":["echoserver.example.com"],"http":[{"route":[{"destination":{"host":"echoserver"},"weight":95},{"destination":{"host":"echoserver-canary"},"weight":5}]}]}`
var spec interface{}
_ = json.Unmarshal([]byte(specStr), &spec)
u.Object["spec"] = spec
objects = append(objects, u)
u = &unstructured.Unstructured{}
_ = u.UnmarshalJSON([]byte(destinationRuleDemo))
annotations = map[string]string{
OriginalSpecAnnotation: `{"spec":{"host":"echoserver","subsets":[{"labels":{"version":"base"},"name":"version-base"}],"trafficPolicy":{"loadBalancer":{"simple":"ROUND_ROBIN"}}}}`,
}
u.SetAnnotations(annotations)
specStr = `{"host":"echoserver","subsets":[{"labels":{"version":"base"},"name":"version-base"},{"labels":{"istio.service.tag":"gray"},"name":"canary"}],"trafficPolicy":{"loadBalancer":{"simple":"ROUND_ROBIN"}}}`
_ = json.Unmarshal([]byte(specStr), &spec)
u.Object["spec"] = spec
objects = append(objects, u)
return objects
},
expectDone: true,
},
{
name: "Test with DisableGenerateCanaryService: true",
getService: func() []*corev1.Service {
s1 := demoService.DeepCopy()
s1.Spec.Selector[apps.DefaultDeploymentUniqueLabelKey] = "podtemplatehash-v1"
// s2 := demoService.DeepCopy()
// s2.Name = "echoserver-canary"
// s2.Spec.Selector[apps.DefaultDeploymentUniqueLabelKey] = "podtemplatehash-v2"
return []*corev1.Service{s1}
},
getUnstructureds: func() []*unstructured.Unstructured {
objects := make([]*unstructured.Unstructured, 0)
u := &unstructured.Unstructured{}
_ = u.UnmarshalJSON([]byte(virtualServiceDemo))
u.SetAPIVersion("networking.istio.io/v1alpha3")
objects = append(objects, u)
u = &unstructured.Unstructured{}
_ = u.UnmarshalJSON([]byte(destinationRuleDemo))
u.SetAPIVersion("networking.istio.io/v1alpha3")
objects = append(objects, u)
return objects
},
getRollout: func() (*v1beta1.Rollout, *util.Workload) {
obj := demoIstioRollout.DeepCopy()
// set DisableGenerateCanaryService as true
obj.Spec.Strategy.Canary.DisableGenerateCanaryService = true
obj.Spec.Strategy.Canary.TrafficRoutings[0].GracePeriodSeconds = 1
obj.Status.CanaryStatus.LastUpdateTime = &metav1.Time{Time: time.Now().Add(-10 * time.Second)}
return obj, &util.Workload{RevisionLabelKey: apps.DefaultDeploymentUniqueLabelKey}
},
expectUnstructureds: func() []*unstructured.Unstructured {
objects := make([]*unstructured.Unstructured, 0)
u := &unstructured.Unstructured{}
_ = u.UnmarshalJSON([]byte(virtualServiceDemo))
annotations := map[string]string{
OriginalSpecAnnotation: `{"spec":{"hosts":["echoserver.example.com"],"http":[{"route":[{"destination":{"host":"echoserver"}}]}]},"annotations":{"virtual":"test"}}`,
"virtual": "test",
}
u.SetAnnotations(annotations)
specStr := `{"hosts":["echoserver.example.com"],"http":[{"route":[{"destination":{"host":"echoserver"},"weight":95},{"destination":{"host":"echoserver","subset":"canary"},"weight":5}]}]}`
var spec interface{}
_ = json.Unmarshal([]byte(specStr), &spec)
u.Object["spec"] = spec
objects = append(objects, u)
u = &unstructured.Unstructured{}
_ = u.UnmarshalJSON([]byte(destinationRuleDemo))
annotations = map[string]string{
OriginalSpecAnnotation: `{"spec":{"host":"echoserver","subsets":[{"labels":{"version":"base"},"name":"version-base"}],"trafficPolicy":{"loadBalancer":{"simple":"ROUND_ROBIN"}}}}`,
}
u.SetAnnotations(annotations)
specStr = `{"host":"echoserver","subsets":[{"labels":{"version":"base"},"name":"version-base"},{"labels":{"istio.service.tag":"gray"},"name":"canary"}],"trafficPolicy":{"loadBalancer":{"simple":"ROUND_ROBIN"}}}`
_ = json.Unmarshal([]byte(specStr), &spec)
u.Object["spec"] = spec
objects = append(objects, u)
return objects
},
expectDone: true,
},
}
for _, cs := range cases {
t.Run(cs.name, func(t *testing.T) {
ss := cs.getService()
client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(ss[0], istioLuaConfig.DeepCopy()).Build()
for _, obj := range cs.getUnstructureds() {
err := client.Create(context.TODO(), obj)
if err != nil {
t.Fatalf("failed to create objects: %s", err.Error())
}
}
rollout, workload := cs.getRollout()
newStatus := rollout.Status.DeepCopy()
currentStep := rollout.Spec.Strategy.Canary.Steps[newStatus.CanaryStatus.CurrentStepIndex-1]
c := &TrafficRoutingContext{
Key: fmt.Sprintf("Rollout(%s/%s)", rollout.Namespace, rollout.Name),
Namespace: rollout.Namespace,
ObjectRef: rollout.Spec.Strategy.Canary.TrafficRoutings,
Strategy: currentStep.TrafficRoutingStrategy,
OwnerRef: *metav1.NewControllerRef(rollout, v1beta1.SchemeGroupVersion.WithKind("Rollout")),
RevisionLabelKey: workload.RevisionLabelKey,
StableRevision: newStatus.CanaryStatus.StableRevision,
CanaryRevision: newStatus.CanaryStatus.PodTemplateHash,
LastUpdateTime: newStatus.CanaryStatus.LastUpdateTime,
DisableGenerateCanaryService: rollout.Spec.Strategy.Canary.DisableGenerateCanaryService,
}
manager := NewTrafficRoutingManager(client)
err := manager.InitializeTrafficRouting(c)
if err != nil {
t.Fatalf("InitializeTrafficRouting failed: %s", err)
}
// now we need to wait at least 2x grace time to keep traffic stable:
// create the canary service -> grace time -> update the gateway -> grace time
// therefore, before both grace times are over, DoTrafficRouting should return false
// firstly, create the canary Service, before the grace time over, return false
_, err = manager.DoTrafficRouting(c)
if err != nil {
t.Fatalf("DoTrafficRouting failed: %s", err)
}
time.Sleep(1 * time.Second)
// secondly, update the gateway, before the grace time over, return false
_, err = manager.DoTrafficRouting(c)
if err != nil {
t.Fatalf("DoTrafficRouting failed: %s", err)
}
time.Sleep(1 * time.Second)
// now, both grace times are over, it should be true
done, err := manager.DoTrafficRouting(c)
if err != nil {
t.Fatalf("DoTrafficRouting failed: %s", err)
}
if cs.expectDone != done {
t.Fatalf("DoTrafficRouting expect(%v), but get(%v)", cs.expectDone, done)
}
unst := cs.expectUnstructureds()
for _, obj := range unst {
checkUnstructureEqual(client, t, obj)
}
})
}
}
func TestFinalisingTrafficRouting(t *testing.T) {
cases := []struct {
name string
getObj func() ([]*corev1.Service, []*netv1.Ingress)
getRollout func() (*v1beta1.Rollout, *util.Workload)
onlyRestoreStableService bool
expectObj func() ([]*corev1.Service, []*netv1.Ingress)
expectDone bool
}{
{
name: "FinalisingTrafficRouting test1",
getObj: func() ([]*corev1.Service, []*netv1.Ingress) {
s1 := demoService.DeepCopy()
s1.Spec.Selector[apps.DefaultDeploymentUniqueLabelKey] = "podtemplatehash-v1"
s2 := demoService.DeepCopy()
s2.Name = "echoserver-canary"
s2.Spec.Selector[apps.DefaultDeploymentUniqueLabelKey] = "podtemplatehash-v2"
c1 := demoIngress.DeepCopy()
c2 := demoIngress.DeepCopy()
c2.Name = "echoserver-canary"
c2.Annotations[fmt.Sprintf("%s/canary", nginxIngressAnnotationDefaultPrefix)] = "true"
c2.Annotations[fmt.Sprintf("%s/canary-weight", nginxIngressAnnotationDefaultPrefix)] = "100"
c2.Spec.Rules[0].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
return []*corev1.Service{s1, s2}, []*netv1.Ingress{c1, c2}
},
getRollout: func() (*v1beta1.Rollout, *util.Workload) {
obj := demoRollout.DeepCopy()
obj.Status.CanaryStatus.CurrentStepState = v1beta1.CanaryStepStateCompleted
obj.Status.CanaryStatus.CurrentStepIndex = 4
obj.Status.CanaryStatus.LastUpdateTime = &metav1.Time{Time: time.Now().Add(-time.Hour)}
return obj, &util.Workload{RevisionLabelKey: apps.DefaultDeploymentUniqueLabelKey}
},
onlyRestoreStableService: true,
expectObj: func() ([]*corev1.Service, []*netv1.Ingress) {
s1 := demoService.DeepCopy()
s2 := demoService.DeepCopy()
s2.Name = "echoserver-canary"
s2.Spec.Selector[apps.DefaultDeploymentUniqueLabelKey] = "podtemplatehash-v2"
c1 := demoIngress.DeepCopy()
c2 := demoIngress.DeepCopy()
c2.Name = "echoserver-canary"
c2.Annotations[fmt.Sprintf("%s/canary", nginxIngressAnnotationDefaultPrefix)] = "true"
c2.Annotations[fmt.Sprintf("%s/canary-weight", nginxIngressAnnotationDefaultPrefix)] = "100"
c2.Spec.Rules[0].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
return []*corev1.Service{s1, s2}, []*netv1.Ingress{c1, c2}
},
expectDone: false,
},
{
name: "FinalisingTrafficRouting test2",
getObj: func() ([]*corev1.Service, []*netv1.Ingress) {
s1 := demoService.DeepCopy()
s2 := demoService.DeepCopy()
s2.Name = "echoserver-canary"
s2.Spec.Selector[apps.DefaultDeploymentUniqueLabelKey] = "podtemplatehash-v2"
c1 := demoIngress.DeepCopy()
c2 := demoIngress.DeepCopy()
c2.Name = "echoserver-canary"
c2.Annotations[fmt.Sprintf("%s/canary", nginxIngressAnnotationDefaultPrefix)] = "true"
c2.Annotations[fmt.Sprintf("%s/canary-weight", nginxIngressAnnotationDefaultPrefix)] = "100"
c2.Spec.Rules[0].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
return []*corev1.Service{s1, s2}, []*netv1.Ingress{c1, c2}
},
getRollout: func() (*v1beta1.Rollout, *util.Workload) {
obj := demoRollout.DeepCopy()
obj.Status.CanaryStatus.CurrentStepState = v1beta1.CanaryStepStateCompleted
obj.Status.CanaryStatus.CurrentStepIndex = 4
obj.Status.CanaryStatus.LastUpdateTime = &metav1.Time{Time: time.Now().Add(time.Hour)}
return obj, &util.Workload{RevisionLabelKey: apps.DefaultDeploymentUniqueLabelKey}
},
onlyRestoreStableService: true,
expectObj: func() ([]*corev1.Service, []*netv1.Ingress) {
s1 := demoService.DeepCopy()
s2 := demoService.DeepCopy()
s2.Name = "echoserver-canary"
s2.Spec.Selector[apps.DefaultDeploymentUniqueLabelKey] = "podtemplatehash-v2"
c1 := demoIngress.DeepCopy()
c2 := demoIngress.DeepCopy()
c2.Name = "echoserver-canary"
c2.Annotations[fmt.Sprintf("%s/canary", nginxIngressAnnotationDefaultPrefix)] = "true"
c2.Annotations[fmt.Sprintf("%s/canary-weight", nginxIngressAnnotationDefaultPrefix)] = "100"
c2.Spec.Rules[0].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
return []*corev1.Service{s1, s2}, []*netv1.Ingress{c1, c2}
},
expectDone: false,
},
{
name: "FinalisingTrafficRouting test3",
getObj: func() ([]*corev1.Service, []*netv1.Ingress) {
s1 := demoService.DeepCopy()
s2 := demoService.DeepCopy()
s2.Name = "echoserver-canary"
s2.Spec.Selector[apps.DefaultDeploymentUniqueLabelKey] = "podtemplatehash-v2"
c1 := demoIngress.DeepCopy()
c2 := demoIngress.DeepCopy()
c2.Name = "echoserver-canary"
c2.Annotations[fmt.Sprintf("%s/canary", nginxIngressAnnotationDefaultPrefix)] = "true"
c2.Annotations[fmt.Sprintf("%s/canary-weight", nginxIngressAnnotationDefaultPrefix)] = "100"
c2.Spec.Rules[0].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
return []*corev1.Service{s1, s2}, []*netv1.Ingress{c1, c2}
},
getRollout: func() (*v1beta1.Rollout, *util.Workload) {
obj := demoRollout.DeepCopy()
obj.Status.CanaryStatus.CurrentStepState = v1beta1.CanaryStepStateCompleted
obj.Status.CanaryStatus.CurrentStepIndex = 4
obj.Status.CanaryStatus.LastUpdateTime = &metav1.Time{Time: time.Now().Add(-10 * time.Second)}
return obj, &util.Workload{RevisionLabelKey: apps.DefaultDeploymentUniqueLabelKey}
},
onlyRestoreStableService: true,
expectObj: func() ([]*corev1.Service, []*netv1.Ingress) {
s1 := demoService.DeepCopy()
s2 := demoService.DeepCopy()
s2.Name = "echoserver-canary"
s2.Spec.Selector[apps.DefaultDeploymentUniqueLabelKey] = "podtemplatehash-v2"
c1 := demoIngress.DeepCopy()
c2 := demoIngress.DeepCopy()
c2.Name = "echoserver-canary"
c2.Annotations[fmt.Sprintf("%s/canary", nginxIngressAnnotationDefaultPrefix)] = "true"
c2.Annotations[fmt.Sprintf("%s/canary-weight", nginxIngressAnnotationDefaultPrefix)] = "100"
c2.Spec.Rules[0].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
return []*corev1.Service{s1, s2}, []*netv1.Ingress{c1, c2}
},
expectDone: true,
},
{
name: "FinalisingTrafficRouting test4",
getObj: func() ([]*corev1.Service, []*netv1.Ingress) {
s1 := demoService.DeepCopy()
s2 := demoService.DeepCopy()
s2.Name = "echoserver-canary"
s2.Spec.Selector[apps.DefaultDeploymentUniqueLabelKey] = "podtemplatehash-v2"
c1 := demoIngress.DeepCopy()
c2 := demoIngress.DeepCopy()
c2.Name = "echoserver-canary"
c2.Annotations[fmt.Sprintf("%s/canary", nginxIngressAnnotationDefaultPrefix)] = "true"
c2.Annotations[fmt.Sprintf("%s/canary-weight", nginxIngressAnnotationDefaultPrefix)] = "100"
c2.Spec.Rules[0].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
return []*corev1.Service{s1, s2}, []*netv1.Ingress{c1, c2}
},
getRollout: func() (*v1beta1.Rollout, *util.Workload) {
obj := demoRollout.DeepCopy()
obj.Status.CanaryStatus.CurrentStepState = v1beta1.CanaryStepStateCompleted
obj.Status.CanaryStatus.CurrentStepIndex = 4
obj.Status.CanaryStatus.LastUpdateTime = &metav1.Time{Time: time.Now().Add(-3 * time.Second)}
return obj, &util.Workload{RevisionLabelKey: apps.DefaultDeploymentUniqueLabelKey}
},
onlyRestoreStableService: false,
expectObj: func() ([]*corev1.Service, []*netv1.Ingress) {
s1 := demoService.DeepCopy()
s2 := demoService.DeepCopy()
s2.Name = "echoserver-canary"
s2.Spec.Selector[apps.DefaultDeploymentUniqueLabelKey] = "podtemplatehash-v2"
c1 := demoIngress.DeepCopy()
c2 := demoIngress.DeepCopy()
c2.Name = "echoserver-canary"
c2.Annotations[fmt.Sprintf("%s/canary", nginxIngressAnnotationDefaultPrefix)] = "true"
c2.Annotations[fmt.Sprintf("%s/canary-weight", nginxIngressAnnotationDefaultPrefix)] = "0"
c2.Spec.Rules[0].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
return []*corev1.Service{s1, s2}, []*netv1.Ingress{c1, c2}
},
expectDone: false,
},
{
name: "FinalisingTrafficRouting test5",
getObj: func() ([]*corev1.Service, []*netv1.Ingress) {
s1 := demoService.DeepCopy()
s2 := demoService.DeepCopy()
s2.Name = "echoserver-canary"
s2.Spec.Selector[apps.DefaultDeploymentUniqueLabelKey] = "podtemplatehash-v2"
c1 := demoIngress.DeepCopy()
c2 := demoIngress.DeepCopy()
c2.Name = "echoserver-canary"
c2.Annotations[fmt.Sprintf("%s/canary", nginxIngressAnnotationDefaultPrefix)] = "true"
c2.Annotations[fmt.Sprintf("%s/canary-weight", nginxIngressAnnotationDefaultPrefix)] = "0"
c2.Spec.Rules[0].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
return []*corev1.Service{s1, s2}, []*netv1.Ingress{c1, c2}
},
getRollout: func() (*v1beta1.Rollout, *util.Workload) {
obj := demoRollout.DeepCopy()
obj.Status.CanaryStatus.CurrentStepState = v1beta1.CanaryStepStateCompleted
obj.Status.CanaryStatus.CurrentStepIndex = 4
obj.Status.CanaryStatus.LastUpdateTime = &metav1.Time{Time: time.Now().Add(-3 * time.Second)}
return obj, &util.Workload{RevisionLabelKey: apps.DefaultDeploymentUniqueLabelKey}
},
onlyRestoreStableService: false,
expectObj: func() ([]*corev1.Service, []*netv1.Ingress) {
s1 := demoService.DeepCopy()
c1 := demoIngress.DeepCopy()
return []*corev1.Service{s1}, []*netv1.Ingress{c1}
},
expectDone: true,
},
}
for _, cs := range cases {
t.Run(cs.name, func(t *testing.T) {
ss, ig := cs.getObj()
client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(ig[0], ss[0], demoConf.DeepCopy()).Build()
if len(ss) == 2 {
_ = client.Create(context.TODO(), ss[1])
}
if len(ig) == 2 {
_ = client.Create(context.TODO(), ig[1])
}
rollout, workload := cs.getRollout()
newStatus := rollout.Status.DeepCopy()
currentStep := rollout.Spec.Strategy.Canary.Steps[newStatus.CanaryStatus.CurrentStepIndex-1]
c := &TrafficRoutingContext{
Key: fmt.Sprintf("Rollout(%s/%s)", rollout.Namespace, rollout.Name),
Namespace: rollout.Namespace,
ObjectRef: rollout.Spec.Strategy.Canary.TrafficRoutings,
Strategy: currentStep.TrafficRoutingStrategy,
OwnerRef: *metav1.NewControllerRef(rollout, v1beta1.SchemeGroupVersion.WithKind("Rollout")),
RevisionLabelKey: workload.RevisionLabelKey,
StableRevision: newStatus.CanaryStatus.StableRevision,
CanaryRevision: newStatus.CanaryStatus.PodTemplateHash,
LastUpdateTime: newStatus.CanaryStatus.LastUpdateTime,
}
manager := NewTrafficRoutingManager(client)
done, err := manager.FinalisingTrafficRouting(c, cs.onlyRestoreStableService)
if err != nil {
t.Fatalf("DoTrafficRouting failed: %s", err)
}
if cs.expectDone != done {
t.Fatalf("DoTrafficRouting expect(%v), but get(%v)", cs.expectDone, done)
}
ss, ig = cs.expectObj()
for _, obj := range ss {
checkObjEqual(client, t, obj)
}
for _, obj := range ig {
checkObjEqual(client, t, obj)
}
})
}
}
func TestRestoreGateway(t *testing.T) {
cases := []struct {
name string
getObj func() ([]*corev1.Service, []*netv1.Ingress)
getRollout func() (*v1beta1.Rollout, *util.Workload)
onlyTrafficRouting bool
expectObj func() ([]*corev1.Service, []*netv1.Ingress)
expectNotFound func() ([]*corev1.Service, []*netv1.Ingress)
}{
{
name: "Restore Gateway test1",
getObj: func() ([]*corev1.Service, []*netv1.Ingress) {
s1 := demoService.DeepCopy()
s2 := demoService.DeepCopy()
s2.Name = "echoserver-canary"
s2.Spec.Selector[apps.DefaultDeploymentUniqueLabelKey] = "podtemplatehash-v2"
c1 := demoIngress.DeepCopy()
c2 := demoIngress.DeepCopy()
c2.Name = "echoserver-canary"
c2.Annotations[fmt.Sprintf("%s/canary", nginxIngressAnnotationDefaultPrefix)] = "true"
c2.Annotations[fmt.Sprintf("%s/canary-weight", nginxIngressAnnotationDefaultPrefix)] = "100"
c2.Spec.Rules[0].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
return []*corev1.Service{s1, s2}, []*netv1.Ingress{c1, c2}
},
getRollout: func() (*v1beta1.Rollout, *util.Workload) {
obj := demoRollout.DeepCopy()
obj.Status.CanaryStatus.CurrentStepState = v1beta1.CanaryStepStateCompleted
obj.Status.CanaryStatus.CurrentStepIndex = 4
obj.Status.CanaryStatus.LastUpdateTime = &metav1.Time{Time: time.Now().Add(-time.Hour)}
return obj, &util.Workload{RevisionLabelKey: apps.DefaultDeploymentUniqueLabelKey}
},
expectObj: func() ([]*corev1.Service, []*netv1.Ingress) {
// stable service and canary service remain unchanged
s1 := demoService.DeepCopy()
s2 := demoService.DeepCopy()
s2.Name = "echoserver-canary"
s2.Spec.Selector[apps.DefaultDeploymentUniqueLabelKey] = "podtemplatehash-v2"
c1 := demoIngress.DeepCopy()
return []*corev1.Service{s1, s2}, []*netv1.Ingress{c1}
},
expectNotFound: func() ([]*corev1.Service, []*netv1.Ingress) {
c2 := demoIngress.DeepCopy()
c2.Name = "echoserver-canary"
return nil, []*netv1.Ingress{c2}
},
},
}
for _, cs := range cases {
t.Run(cs.name, func(t *testing.T) {
ss, ig := cs.getObj()
cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(ig[0], ss[0], demoConf.DeepCopy()).Build()
if len(ss) == 2 {
_ = cli.Create(context.TODO(), ss[1])
}
if len(ig) == 2 {
_ = cli.Create(context.TODO(), ig[1])
}
rollout, workload := cs.getRollout()
newStatus := rollout.Status.DeepCopy()
currentStep := rollout.Spec.Strategy.Canary.Steps[newStatus.CanaryStatus.CurrentStepIndex-1]
c := &TrafficRoutingContext{
Key: fmt.Sprintf("Rollout(%s/%s)", rollout.Namespace, rollout.Name),
Namespace: rollout.Namespace,
ObjectRef: rollout.Spec.Strategy.Canary.TrafficRoutings,
Strategy: currentStep.TrafficRoutingStrategy,
OwnerRef: *metav1.NewControllerRef(rollout, v1beta1.SchemeGroupVersion.WithKind("Rollout")),
RevisionLabelKey: workload.RevisionLabelKey,
StableRevision: newStatus.CanaryStatus.StableRevision,
CanaryRevision: newStatus.CanaryStatus.PodTemplateHash,
LastUpdateTime: newStatus.CanaryStatus.LastUpdateTime,
OnlyTrafficRouting: cs.onlyTrafficRouting,
}
manager := NewTrafficRoutingManager(cli)
err := manager.RestoreGateway(c)
if err != nil {
t.Fatalf("DoTrafficRouting failed: %s", err)
}
ss, ig = cs.expectObj()
for _, obj := range ss {
checkObjEqual(cli, t, obj)
}
for _, obj := range ig {
checkObjEqual(cli, t, obj)
}
ss, ig = cs.expectNotFound()
for _, obj := range ss {
checkNotFound(cli, t, obj)
}
for _, obj := range ig {
checkNotFound(cli, t, obj)
}
})
}
}
func TestRemoveCanaryService(t *testing.T) {
cases := []struct {
name string
getObj func() ([]*corev1.Service, []*netv1.Ingress)
getRollout func() (*v1beta1.Rollout, *util.Workload)
onlyTrafficRouting bool
expectObj func() ([]*corev1.Service, []*netv1.Ingress)
expectNotFound func() ([]*corev1.Service, []*netv1.Ingress)
}{
{
name: "Restore Gateway test1",
getObj: func() ([]*corev1.Service, []*netv1.Ingress) {
s1 := demoService.DeepCopy()
s2 := demoService.DeepCopy()
s2.Name = "echoserver-canary"
s2.Spec.Selector[apps.DefaultDeploymentUniqueLabelKey] = "podtemplatehash-v2"
c1 := demoIngress.DeepCopy()
c2 := demoIngress.DeepCopy()
c2.Name = "echoserver-canary"
c2.Annotations[fmt.Sprintf("%s/canary", nginxIngressAnnotationDefaultPrefix)] = "true"
c2.Annotations[fmt.Sprintf("%s/canary-weight", nginxIngressAnnotationDefaultPrefix)] = "100"
c2.Spec.Rules[0].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
return []*corev1.Service{s1, s2}, []*netv1.Ingress{c1, c2}
},
getRollout: func() (*v1beta1.Rollout, *util.Workload) {
obj := demoRollout.DeepCopy()
obj.Status.CanaryStatus.CurrentStepState = v1beta1.CanaryStepStateCompleted
obj.Status.CanaryStatus.CurrentStepIndex = 4
obj.Status.CanaryStatus.LastUpdateTime = &metav1.Time{Time: time.Now().Add(-time.Hour)}
return obj, &util.Workload{RevisionLabelKey: apps.DefaultDeploymentUniqueLabelKey}
},
expectObj: func() ([]*corev1.Service, []*netv1.Ingress) {
// stable service and ingress remain unchanged
s1 := demoService.DeepCopy()
c1 := demoIngress.DeepCopy()
c2 := demoIngress.DeepCopy()
c2.Name = "echoserver-canary"
c2.Annotations[fmt.Sprintf("%s/canary", nginxIngressAnnotationDefaultPrefix)] = "true"
c2.Annotations[fmt.Sprintf("%s/canary-weight", nginxIngressAnnotationDefaultPrefix)] = "100"
c2.Spec.Rules[0].HTTP.Paths[0].Backend.Service.Name = "echoserver-canary"
return []*corev1.Service{s1}, []*netv1.Ingress{c1, c2}
},
expectNotFound: func() ([]*corev1.Service, []*netv1.Ingress) {
s2 := demoService.DeepCopy()
s2.Name = "echoserver-canary"
return []*corev1.Service{s2}, nil
},
},
}
for _, cs := range cases {
t.Run(cs.name, func(t *testing.T) {
ss, ig := cs.getObj()
cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(ig[0], ss[0], demoConf.DeepCopy()).Build()
if len(ss) == 2 {
_ = cli.Create(context.TODO(), ss[1])
}
if len(ig) == 2 {
_ = cli.Create(context.TODO(), ig[1])
}
rollout, workload := cs.getRollout()
newStatus := rollout.Status.DeepCopy()
currentStep := rollout.Spec.Strategy.Canary.Steps[newStatus.CanaryStatus.CurrentStepIndex-1]
c := &TrafficRoutingContext{
Key: fmt.Sprintf("Rollout(%s/%s)", rollout.Namespace, rollout.Name),
Namespace: rollout.Namespace,
ObjectRef: rollout.Spec.Strategy.Canary.TrafficRoutings,
Strategy: currentStep.TrafficRoutingStrategy,
OwnerRef: *metav1.NewControllerRef(rollout, v1beta1.SchemeGroupVersion.WithKind("Rollout")),
RevisionLabelKey: workload.RevisionLabelKey,
StableRevision: newStatus.CanaryStatus.StableRevision,
CanaryRevision: newStatus.CanaryStatus.PodTemplateHash,
LastUpdateTime: newStatus.CanaryStatus.LastUpdateTime,
OnlyTrafficRouting: cs.onlyTrafficRouting,
}
manager := NewTrafficRoutingManager(cli)
err := manager.RemoveCanaryService(c)
if err != nil {
t.Fatalf("DoTrafficRouting failed: %s", err)
}
ss, ig = cs.expectObj()
for _, obj := range ss {
checkObjEqual(cli, t, obj)
}
for _, obj := range ig {
checkObjEqual(cli, t, obj)
}
ss, ig = cs.expectNotFound()
for _, obj := range ss {
checkNotFound(cli, t, obj)
}
for _, obj := range ig {
checkNotFound(cli, t, obj)
}
})
}
}
func checkNotFound(c client.WithWatch, t *testing.T, expect client.Object) {
gvk := expect.GetObjectKind().GroupVersionKind()
obj := getEmptyObject(gvk)
err := c.Get(context.TODO(), client.ObjectKey{Namespace: expect.GetNamespace(), Name: expect.GetName()}, obj)
if !errors.IsNotFound(err) {
t.Fatalf("get object failed: %s", err.Error())
}
}
func checkObjEqual(c client.WithWatch, t *testing.T, expect client.Object) {
gvk := expect.GetObjectKind().GroupVersionKind()
obj := getEmptyObject(gvk)
err := c.Get(context.TODO(), client.ObjectKey{Namespace: expect.GetNamespace(), Name: expect.GetName()}, obj)
if err != nil {
t.Fatalf("get object failed: %s", err.Error())
}
switch gvk.Kind {
case "Service":
s1 := obj.(*corev1.Service)
s2 := expect.(*corev1.Service)
if !reflect.DeepEqual(s1.Spec, s2.Spec) {
t.Fatalf("expect(%s), but get object(%s)", util.DumpJSON(s2.Spec), util.DumpJSON(s1.Spec))
}
case "Ingress":
s1 := obj.(*netv1.Ingress)
s2 := expect.(*netv1.Ingress)
if !reflect.DeepEqual(s1.Spec, s2.Spec) || !reflect.DeepEqual(s1.Annotations, s2.Annotations) {
t.Fatalf("expect(%s), but get object(%s)", util.DumpJSON(s2), util.DumpJSON(s1))
}
}
}
func getEmptyObject(gvk schema.GroupVersionKind) client.Object {
switch gvk.Kind {
case "Service":
return &corev1.Service{}
case "Ingress":
return &netv1.Ingress{}
}
return nil
}
func checkUnstructureEqual(cli client.Client, t *testing.T, expect *unstructured.Unstructured) {
obj := &unstructured.Unstructured{}
obj.SetAPIVersion(expect.GetAPIVersion())
obj.SetKind(expect.GetKind())
if err := cli.Get(context.TODO(), types.NamespacedName{Namespace: expect.GetNamespace(), Name: expect.GetName()}, obj); err != nil {
t.Fatalf("Get object failed: %s", err.Error())
}
if !reflect.DeepEqual(obj.GetAnnotations(), expect.GetAnnotations()) {
fmt.Println(util.DumpJSON(obj.GetAnnotations()), util.DumpJSON(expect.GetAnnotations()))
t.Fatalf("expect(%s), but get(%s)", util.DumpJSON(expect.GetAnnotations()), util.DumpJSON(obj.GetAnnotations()))
}
if util.DumpJSON(expect.Object["spec"]) != util.DumpJSON(obj.Object["spec"]) {
t.Fatalf("expect(%s), but get(%s)", util.DumpJSON(expect.Object["spec"]), util.DumpJSON(obj.Object["spec"]))
}
}