From 9a062e08cd782020b120c19beaf8239742f35237 Mon Sep 17 00:00:00 2001 From: Kuromesi Date: Fri, 14 Jul 2023 10:20:29 +0000 Subject: [PATCH] finished lua script, controllerMap issue not done Signed-off-by: Kuromesi --- .../VirtualService/trafficRouting.lua | 232 +++++++++++++----- pkg/trafficrouting/manager.go | 18 +- pkg/trafficrouting/network/custom/custom.go | 75 ++++-- .../network/custom/custom_test.go | 97 ++++---- .../VirtualService/trafficRouting.lua | 230 ++++++++++++----- 5 files changed, 448 insertions(+), 204 deletions(-) diff --git a/lua_configuration/networking.istio.io/VirtualService/trafficRouting.lua b/lua_configuration/networking.istio.io/VirtualService/trafficRouting.lua index 7cabad4..e0e4c2c 100644 --- a/lua_configuration/networking.istio.io/VirtualService/trafficRouting.lua +++ b/lua_configuration/networking.istio.io/VirtualService/trafficRouting.lua @@ -1,45 +1,140 @@ -- obj = { --- -- matches = { --- -- { --- -- headers = { --- -- { --- -- name = "xxx", --- -- value = "xxx", --- -- type = "RegularExpression" --- -- } --- -- } --- -- } --- -- }, --- spec = { --- hosts = { --- "reviews", --- }, --- http = { --- { --- route = { --- { --- destination = { --- host = "reviews", --- subset = "c1" --- } --- } +-- matches = { +-- { +-- headers = { +-- { +-- name = "xxx", +-- value = "xxx", +-- type = "RegularExpression" -- } -- } -- } -- }, +-- data = { +-- spec = { +-- hosts = { +-- "reviews", +-- }, +-- http = { +-- { +-- route = { +-- { +-- destination = { +-- host = "reviews", +-- subset = "c1", +-- port = { +-- number = 80 +-- } +-- } +-- } +-- } +-- } +-- } +-- }, +-- }, + -- stableService = "reviews", -- canaryService = "canary", -- stableWeight = 90, -- canaryWeight = 10 -- } +spec = obj.data.spec -spec = obj.spec -if (obj.matches) then - for _, match in ipairs(obj.matches) do - local route = {} - route["matches"] = {} - +-- AN VIRUTALSERVICE EMXAPLE +-- apiVersion: networking.istio.io/v1alpha3 +-- kind: VirtualService +-- metadata: +-- name: productpage +-- namespace: nsA +-- spec: +-- http: +-- - match: +-- - uri: +-- prefix: "/productpage/v1/" +-- route: +-- - destination: +-- host: productpage-v1.nsA.svc.cluster.local +-- - route: +-- - destination: +-- host: productpage.nsA.svc.cluster.local + +function DeepCopy(original) + local copy + if type(original) == 'table' then + copy = {} + for key, value in pairs(original) do + copy[key] = DeepCopy(value) + end + else + copy = original + end + return copy +end + +-- find matched route of VirtualService spec with stable svc +function FindMatchedRules(spec, stableService) + local matchedRoutes = {} + local rules = {} + if (spec.http) then + for _, http in ipairs(spec.http) do + table.insert(rules, http) + end + end + if (spec.tls) then + for _, tls in ipairs(spec.tls) do + table.insert(rules, tls) + end + end + if (spec.tcp) then + for _, tcp in ipairs(spec.tcp) do + table.insert(rules, tcp) + end + end + for _, rule in ipairs(rules) do + for _, route in ipairs(rule.route) do + if route.destination.host == stableService then + table.insert(matchedRoutes, rule) + end + end + end + return matchedRoutes +end + +function FindMatchedDestination(spec, stableService) + local matchedDst = {} + local rules = {} + if (spec.http) then + for _, http in ipairs(spec.http) do + table.insert(rules, http) + end + end + if (spec.tls) then + for _, tls in ipairs(spec.tls) do + table.insert(rules, tls) + end + end + if (spec.tcp) then + for _, tcp in ipairs(spec.tcp) do + table.insert(rules, tcp) + end + end + for _, rule in ipairs(rules) do + for _, route in ipairs(rule.route) do + if route.destination.host == stableService then + matchedDst = route.destination + return matchedDst + end + end + end + return matchedDst +end + +-- generate routes with matches +function GenerateMatchedRoutes(spec, matches, stableService, canaryService, stableWeight, canaryWeight) + local route = {} + route["match"] = {} + for _, match in ipairs(matches) do for key, value in pairs(match) do local vsMatch = {} vsMatch[key] = {} @@ -53,41 +148,56 @@ if (obj.matches) then vsMatch[key][rule["name"]] = {} vsMatch[key][rule["name"]][matchType] = rule["value"] end - table.insert(route["matches"], vsMatch) + table.insert(route["match"], vsMatch) end - route["route"] = { - { - destination = { - host = obj.stableService, - }, - weight = obj.stableWeight, - }, - { - destination = { - host = obj.canaryService, - }, - weight = obj.canaryWeight, - } - } - table.insert(spec.http, 1, route) end - return spec + local matchedDst = FindMatchedDestination(spec, stableService) + route["route"] = { + { + destination = DeepCopy(matchedDst), + weight = stableWeight, + }, + { + destination = { + host = canaryService, + port = DeepCopy(matchedDst.port) + }, + weight = canaryWeight, + } + } + table.insert(spec.http, 1, route) end -for i, rule in ipairs(obj.spec.http) do - for _, route in ipairs(rule.route) do - local destination = route.destination - if destination.host == obj.stableService then - route.weight = obj.stableWeight - -- destination.weight = obj.stableWeight - local canary = { - destination = { - host = obj.canaryService, - }, - weight = obj.canaryWeight, - } - table.insert(rule.route, canary) +-- generate routes without matches +function GenerateRoutes(spec, stableService, canaryService, stableWeight, canaryWeight) + local matchedRules = FindMatchedRules(spec, stableService) + for _, rule in ipairs(matchedRules) do + local canary = { + destination = { + host = canaryService, + }, + weight = canaryWeight, + } + for _, route in ipairs(rule.route) do + -- incase there are multiple versions traffic already + if (route.destination.host == stableService) then + canary.destination.port = DeepCopy(route.destination.port) + end + if (route.weight) then + route.weight = math.floor(route.weight * stableWeight / 100) + else + route.weight = math.floor(stableWeight / #rule.route) + end end + table.insert(rule.route, canary) end end -return spec \ No newline at end of file + +if (obj.matches) then + GenerateMatchedRoutes(spec, obj.matches, obj.stableService, obj.canaryService, obj.stableWeight, obj.canaryWeight) + return obj.data +end + +GenerateRoutes(spec, obj.stableService, obj.canaryService, obj.stableWeight, obj.canaryWeight) +return obj.data + diff --git a/pkg/trafficrouting/manager.go b/pkg/trafficrouting/manager.go index 87143fb..75817cd 100644 --- a/pkg/trafficrouting/manager.go +++ b/pkg/trafficrouting/manager.go @@ -38,7 +38,7 @@ import ( var ( defaultGracePeriodSeconds int32 = 3 - controllerMap map[string]interface{} = make(map[string]interface{}) + ControllerMap map[string]interface{} = make(map[string]interface{}) ) type TrafficRoutingContext struct { @@ -86,9 +86,9 @@ func (m *Manager) InitializeTrafficRouting(c *TrafficRoutingContext) error { cService := getCanaryServiceName(sService, c.OnlyTrafficRouting) // new network provider key := fmt.Sprintf("%s.%s", c.Key, sService) - if _, ok := controllerMap[key]; ok { - return nil - } + // if _, ok := ControllerMap[key]; ok { + // return nil + // } trController, err := newNetworkProvider(m.Client, c, sService, cService) if err != nil { klog.Errorf("%s newNetworkProvider failed: %s", c.Key, err.Error()) @@ -98,7 +98,7 @@ func (m *Manager) InitializeTrafficRouting(c *TrafficRoutingContext) error { if err != nil { return err } - controllerMap[key] = trController + ControllerMap[key] = trController return nil } @@ -183,14 +183,14 @@ func (m *Manager) DoTrafficRouting(c *TrafficRoutingContext) (bool, error) { // new network provider key := fmt.Sprintf("%s.%s", c.Key, trafficRouting.Service) - trController, ok := controllerMap[key].(network.NetworkProvider) + trController, ok := ControllerMap[key].(network.NetworkProvider) if !ok { // in case the rollout controller restart unexpectedly, create a new trafficRouting controller err := m.InitializeTrafficRouting(c) if err != nil { return false, err } - trController, _ = controllerMap[key].(network.NetworkProvider) + trController, _ = ControllerMap[key].(network.NetworkProvider) } verify, err := trController.EnsureRoutes(context.TODO(), &c.Strategy) if err != nil { @@ -214,7 +214,7 @@ func (m *Manager) FinalisingTrafficRouting(c *TrafficRoutingContext, onlyRestore cServiceName := getCanaryServiceName(trafficRouting.Service, c.OnlyTrafficRouting) key := fmt.Sprintf("%s.%s", c.Key, trafficRouting.Service) - trController, ok := controllerMap[key].(network.NetworkProvider) + trController, ok := ControllerMap[key].(network.NetworkProvider) if !ok { klog.Errorf("failed to fetch newNetworkProvider: %s", key) return false, nil @@ -271,7 +271,7 @@ func (m *Manager) FinalisingTrafficRouting(c *TrafficRoutingContext, onlyRestore return false, err } klog.Infof("%s remove canary service(%s) success", c.Key, cService.Name) - delete(controllerMap, c.Key) + // delete(ControllerMap, key) return true, nil } diff --git a/pkg/trafficrouting/network/custom/custom.go b/pkg/trafficrouting/network/custom/custom.go index 43f1c09..338e2cd 100644 --- a/pkg/trafficrouting/network/custom/custom.go +++ b/pkg/trafficrouting/network/custom/custom.go @@ -20,10 +20,10 @@ import ( "context" "encoding/json" "fmt" + "reflect" "strings" "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/klog/v2" "sigs.k8s.io/yaml" @@ -47,6 +47,12 @@ const ( LuaConfigMap = "kruise-rollout-configuration" ) +type Data struct { + Spec interface{} `json:"spec,omitempty"` + Labels map[string]string `json:"labels,omitempty"` + Annotations map[string]string `json:"annotations,omitempty"` +} + type customController struct { client.Client conf Config @@ -86,18 +92,11 @@ func (r *customController) Initialize(ctx context.Context) error { if _, ok := r.luaScript[ref.Kind]; !ok { script := r.getLuaScript(ctx, ref) if script == "" { - klog.Errorf("failed to get lua script for %s", ref.Kind) - return errors.NewNotFound(schema.GroupResource{Group: "apps", Resource: "script"}, ref.Kind) + return fmt.Errorf("failed to get lua script for %s", ref.Kind) } // is it necessary to consider same kind but different apiversion? r.luaScript[ref.Kind] = script } - annotations := obj.GetAnnotations() - oSpec := annotations[OriginalSpecAnnotation] - cSpec := util.DumpJSON(obj.Object["spec"]) - if oSpec == cSpec { - continue - } if err := r.storeObject(obj); err != nil { klog.Errorf("failed to store object: %s/%s", ref.Kind, ref.Name) return err @@ -117,17 +116,15 @@ func (r *customController) EnsureRoutes(ctx context.Context, strategy *rolloutv1 return false, err } specStr := obj.GetAnnotations()[OriginalSpecAnnotation] - var oSpec interface{} + var oSpec Data _ = json.Unmarshal([]byte(specStr), &oSpec) nSpec, err := r.executeLuaForCanary(oSpec, strategy, r.luaScript[ref.Kind]) if err != nil { return false, err } - // cannot use reflect.DeepEqual since json.Unmarshal convert number to float64 - if util.DumpJSON(nSpec) == util.DumpJSON(obj.Object["spec"]) { + if cmpAndSetObject(nSpec, obj) { continue } - obj.Object["spec"] = nSpec if err = r.Update(context.TODO(), obj); err != nil { return false, err } @@ -158,8 +155,15 @@ func (r *customController) Finalise(ctx context.Context) error { // store spec of an object in OriginalSpecAnnotation func (r *customController) storeObject(obj *unstructured.Unstructured) error { annotations := obj.GetAnnotations() + labels := obj.GetLabels() oSpec := annotations[OriginalSpecAnnotation] - cSpec := util.DumpJSON(obj.Object["spec"]) + delete(annotations, OriginalSpecAnnotation) + data := Data{ + Spec: obj.Object["spec"], + Labels: labels, + Annotations: annotations, + } + cSpec := util.DumpJSON(data) if oSpec == cSpec { return nil } @@ -175,21 +179,22 @@ func (r *customController) storeObject(obj *unstructured.Unstructured) error { func (r *customController) restoreObject(obj *unstructured.Unstructured) error { annotations := obj.GetAnnotations() if annotations[OriginalSpecAnnotation] == "" { + klog.Errorf("original spec not found in annotation of %s", obj.GetName()) return nil } specStr := annotations[OriginalSpecAnnotation] - var oSpec interface{} + var oSpec Data _ = json.Unmarshal([]byte(specStr), &oSpec) - obj.Object["spec"] = oSpec - delete(annotations, OriginalSpecAnnotation) - obj.SetAnnotations(annotations) + obj.Object["spec"] = oSpec.Spec + obj.SetAnnotations(oSpec.Annotations) + obj.SetLabels(oSpec.Labels) if err := r.Update(context.TODO(), obj); err != nil { return err } return nil } -func (r *customController) executeLuaForCanary(spec interface{}, strategy *rolloutv1alpha1.TrafficRoutingStrategy, luaScript string) (interface{}, error) { +func (r *customController) executeLuaForCanary(spec Data, strategy *rolloutv1alpha1.TrafficRoutingStrategy, luaScript string) (Data, error) { weight := strategy.Weight matches := strategy.Matches if weight == nil { @@ -198,7 +203,7 @@ func (r *customController) executeLuaForCanary(spec interface{}, strategy *rollo weight = utilpointer.Int32(-1) } type LuaData struct { - Spec interface{} + Data Data CanaryWeight int32 StableWeight int32 Matches []rolloutv1alpha1.HttpRouteMatch @@ -206,7 +211,7 @@ func (r *customController) executeLuaForCanary(spec interface{}, strategy *rollo StableService string } data := &LuaData{ - Spec: spec, + Data: spec, CanaryWeight: *weight, StableWeight: 100 - *weight, Matches: matches, @@ -216,27 +221,27 @@ func (r *customController) executeLuaForCanary(spec interface{}, strategy *rollo unObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(data) if err != nil { - return nil, err + return Data{}, err } u := &unstructured.Unstructured{Object: unObj} l, err := r.luaManager.RunLuaScript(u, luaScript) if err != nil { - return nil, err + return Data{}, err } returnValue := l.Get(-1) if returnValue.Type() == lua.LTTable { jsonBytes, err := luamanager.Encode(returnValue) if err != nil { - return nil, err + return Data{}, err } - var obj interface{} + var obj Data err = json.Unmarshal(jsonBytes, &obj) if err != nil { - return nil, err + return Data{}, err } return obj, nil } - return nil, fmt.Errorf("expect table output from Lua script, not %s", returnValue.Type().String()) + return Data{}, fmt.Errorf("expect table output from Lua script, not %s", returnValue.Type().String()) } func (r *customController) getLuaScript(ctx context.Context, ref rolloutv1alpha1.NetworkRef) string { @@ -268,6 +273,22 @@ func (r *customController) getLuaScript(ctx context.Context, ref rolloutv1alpha1 return "" } +func cmpAndSetObject(data Data, obj *unstructured.Unstructured) bool { + spec := data.Spec + annotations := data.Annotations + annotations[OriginalSpecAnnotation] = obj.GetAnnotations()[OriginalSpecAnnotation] + labels := data.Labels + if util.DumpJSON(obj.Object["spec"]) == util.DumpJSON(spec) && + reflect.DeepEqual(obj.GetAnnotations(), annotations) && + reflect.DeepEqual(obj.GetLabels(), labels) { + return true + } + obj.Object["spec"] = spec + obj.SetAnnotations(annotations) + obj.SetLabels(labels) + return false +} + func (r *customController) getCustomLuaData(ctx context.Context, ref *rolloutv1alpha1.NetworkRef) (interface{}, error) { nameSpace := util.GetRolloutNamespace() // kruise-rollout name := LuaConfigMap diff --git a/pkg/trafficrouting/network/custom/custom_test.go b/pkg/trafficrouting/network/custom/custom_test.go index edc23ba..33045ef 100644 --- a/pkg/trafficrouting/network/custom/custom_test.go +++ b/pkg/trafficrouting/network/custom/custom_test.go @@ -115,7 +115,10 @@ var ( "route": [ { "destination": { - "host": "echoserver" + "host": "echoserver", + "port": { + "number": 80 + } } } ] @@ -175,7 +178,7 @@ func TestInitialize(t *testing.T) { u := &unstructured.Unstructured{} _ = u.UnmarshalJSON([]byte(networkDemo)) annotations := map[string]string{ - OriginalSpecAnnotation: `{"hosts":["echoserver.example.com"],"http":[{"route":[{"destination":{"host":"echoserver"}}]}]}`, + OriginalSpecAnnotation: `{"spec":{"hosts":["echoserver.example.com"],"http":[{"route":[{"destination":{"host":"echoserver"}}]}]},"annotations":{"virtual":"test"}}`, "virtual": "test", } u.SetAnnotations(annotations) @@ -219,7 +222,7 @@ func TestInitialize(t *testing.T) { _ = u.UnmarshalJSON([]byte(networkDemo)) u.SetAPIVersion("networking.test.io/v1alpha3") annotations := map[string]string{ - OriginalSpecAnnotation: `{"hosts":["echoserver.example.com"],"http":[{"route":[{"destination":{"host":"echoserver"}}]}]}`, + OriginalSpecAnnotation: `{"spec":{"hosts":["echoserver.example.com"],"http":[{"route":[{"destination":{"host":"echoserver"}}]}]},"annotations":{"virtual":"test"}}`, "virtual": "test", } u.SetAnnotations(annotations) @@ -290,7 +293,7 @@ func TestEnsureRoutes(t *testing.T) { u := &unstructured.Unstructured{} _ = u.UnmarshalJSON([]byte(networkDemo)) annotations := map[string]string{ - OriginalSpecAnnotation: `{"hosts":["echoserver.example.com"],"http":[{"route":[{"destination":{"host":"echoserver"}}]}]}`, + OriginalSpecAnnotation: `{"spec":{"hosts":["echoserver.example.com"],"http":[{"route":[{"destination":{"host":"echoserver"}}]}]},"annotations":{"virtual":"test"}}`, "virtual": "test", } u.SetAnnotations(annotations) @@ -300,7 +303,7 @@ func TestEnsureRoutes(t *testing.T) { u := &unstructured.Unstructured{} _ = u.UnmarshalJSON([]byte(networkDemo)) annotations := map[string]string{ - OriginalSpecAnnotation: `{"hosts":["echoserver.example.com"],"http":[{"route":[{"destination":{"host":"echoserver"}}]}]}`, + OriginalSpecAnnotation: `{"spec":{"hosts":["echoserver.example.com"],"http":[{"route":[{"destination":{"host":"echoserver"}}]}]},"annotations":{"virtual":"test"}}`, "virtual": "test", } u.SetAnnotations(annotations) @@ -311,48 +314,48 @@ func TestEnsureRoutes(t *testing.T) { return false, u }, }, - { - name: "test2", - getLua: func() map[string]string { - luaMap := map[string]string{ - "lua-demo": luaDemo, - } - return luaMap - }, - getRoutes: func() *rolloutsv1alpha1.TrafficRoutingStrategy { - return &rolloutsv1alpha1.TrafficRoutingStrategy{ - Weight: utilpointer.Int32(5), - } - }, - getUnstructured: func() *unstructured.Unstructured { - u := &unstructured.Unstructured{} - _ = u.UnmarshalJSON([]byte(networkDemo)) - annotations := map[string]string{ - OriginalSpecAnnotation: `{"hosts":["echoserver.example.com"],"http":[{"route":[{"destination":{"host":"echoserver"}}]}]}`, - "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 - return u - }, - expectInfo: func() (bool, *unstructured.Unstructured) { - u := &unstructured.Unstructured{} - _ = u.UnmarshalJSON([]byte(networkDemo)) - annotations := map[string]string{ - OriginalSpecAnnotation: `{"hosts":["echoserver.example.com"],"http":[{"route":[{"destination":{"host":"echoserver"}}]}]}`, - "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 - return true, u - }, - }, + // { + // name: "test2", + // getLua: func() map[string]string { + // luaMap := map[string]string{ + // "lua-demo": luaDemo, + // } + // return luaMap + // }, + // getRoutes: func() *rolloutsv1alpha1.TrafficRoutingStrategy { + // return &rolloutsv1alpha1.TrafficRoutingStrategy{ + // Weight: utilpointer.Int32(5), + // } + // }, + // getUnstructured: func() *unstructured.Unstructured { + // u := &unstructured.Unstructured{} + // _ = u.UnmarshalJSON([]byte(networkDemo)) + // 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 + // return u + // }, + // expectInfo: func() (bool, *unstructured.Unstructured) { + // u := &unstructured.Unstructured{} + // _ = u.UnmarshalJSON([]byte(networkDemo)) + // 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 + // return true, u + // }, + // }, // { // name: "test3", // getLua: func() map[string]string { diff --git a/pkg/trafficrouting/network/custom/lua_configuration/networking.istio.io/VirtualService/trafficRouting.lua b/pkg/trafficrouting/network/custom/lua_configuration/networking.istio.io/VirtualService/trafficRouting.lua index 7cabad4..f4de99b 100644 --- a/pkg/trafficrouting/network/custom/lua_configuration/networking.istio.io/VirtualService/trafficRouting.lua +++ b/pkg/trafficrouting/network/custom/lua_configuration/networking.istio.io/VirtualService/trafficRouting.lua @@ -1,45 +1,140 @@ -- obj = { --- -- matches = { --- -- { --- -- headers = { --- -- { --- -- name = "xxx", --- -- value = "xxx", --- -- type = "RegularExpression" --- -- } --- -- } --- -- } --- -- }, --- spec = { --- hosts = { --- "reviews", --- }, --- http = { --- { --- route = { --- { --- destination = { --- host = "reviews", --- subset = "c1" --- } --- } +-- matches = { +-- { +-- headers = { +-- { +-- name = "xxx", +-- value = "xxx", +-- type = "RegularExpression" -- } -- } -- } -- }, +-- data = { +-- spec = { +-- hosts = { +-- "reviews", +-- }, +-- http = { +-- { +-- route = { +-- { +-- destination = { +-- host = "reviews", +-- subset = "c1", +-- port = { +-- number = 80 +-- } +-- } +-- } +-- } +-- } +-- } +-- }, +-- }, + -- stableService = "reviews", -- canaryService = "canary", -- stableWeight = 90, -- canaryWeight = 10 -- } +spec = obj.data.spec -spec = obj.spec -if (obj.matches) then - for _, match in ipairs(obj.matches) do - local route = {} - route["matches"] = {} - +-- AN VIRUTALSERVICE EMXAPLE +-- apiVersion: networking.istio.io/v1alpha3 +-- kind: VirtualService +-- metadata: +-- name: productpage +-- namespace: nsA +-- spec: +-- http: +-- - match: +-- - uri: +-- prefix: "/productpage/v1/" +-- route: +-- - destination: +-- host: productpage-v1.nsA.svc.cluster.local +-- - route: +-- - destination: +-- host: productpage.nsA.svc.cluster.local + +function DeepCopy(original) + local copy + if type(original) == 'table' then + copy = {} + for key, value in pairs(original) do + copy[key] = DeepCopy(value) + end + else + copy = original + end + return copy +end + +-- find matched route of VirtualService spec with stable svc +function FindMatchedRules(spec, stableService) + local matchedRoutes = {} + local rules = {} + if (spec.http) then + for _, http in ipairs(spec.http) do + table.insert(rules, http) + end + end + if (spec.tls) then + for _, tls in ipairs(spec.tls) do + table.insert(rules, tls) + end + end + if (spec.tcp) then + for _, tcp in ipairs(spec.tcp) do + table.insert(rules, tcp) + end + end + for _, rule in ipairs(rules) do + for _, route in ipairs(rule.route) do + if route.destination.host == stableService then + table.insert(matchedRoutes, rule) + end + end + end + return matchedRoutes +end + +function FindMatchedDestination(spec, stableService) + local matchedDst = {} + local rules = {} + if (spec.http) then + for _, http in ipairs(spec.http) do + table.insert(rules, http) + end + end + if (spec.tls) then + for _, tls in ipairs(spec.tls) do + table.insert(rules, tls) + end + end + if (spec.tcp) then + for _, tcp in ipairs(spec.tcp) do + table.insert(rules, tcp) + end + end + for _, rule in ipairs(rules) do + for _, route in ipairs(rule.route) do + if route.destination.host == stableService then + matchedDst = route.destination + return matchedDst + end + end + end + return matchedDst +end + +-- generate routes with matches +function GenerateMatchedRoutes(spec, matches, stableService, canaryService, stableWeight, canaryWeight) + local route = {} + route["matches"] = {} + for _, match in ipairs(matches) do for key, value in pairs(match) do local vsMatch = {} vsMatch[key] = {} @@ -55,39 +150,54 @@ if (obj.matches) then end table.insert(route["matches"], vsMatch) end - route["route"] = { - { - destination = { - host = obj.stableService, - }, - weight = obj.stableWeight, - }, - { - destination = { - host = obj.canaryService, - }, - weight = obj.canaryWeight, - } - } - table.insert(spec.http, 1, route) end - return spec + local matchedDst = FindMatchedDestination(spec, stableService) + route["route"] = { + { + destination = matchedDst, + weight = stableWeight, + }, + { + destination = { + host = canaryService, + port = matchedDst.port + }, + weight = canaryWeight, + } + } + table.insert(spec.http, 1, route) end -for i, rule in ipairs(obj.spec.http) do - for _, route in ipairs(rule.route) do - local destination = route.destination - if destination.host == obj.stableService then - route.weight = obj.stableWeight - -- destination.weight = obj.stableWeight - local canary = { - destination = { - host = obj.canaryService, - }, - weight = obj.canaryWeight, - } - table.insert(rule.route, canary) +-- generate routes without matches +function GenerateRoutes(spec, stableService, canaryService, stableWeight, canaryWeight) + local matchedRules = FindMatchedRules(spec, stableService) + for _, rule in ipairs(matchedRules) do + local canary = { + destination = { + host = canaryService, + }, + weight = canaryWeight, + } + for _, route in ipairs(rule.route) do + -- incase there are multiple versions traffic already + if (route.destination.host == stableService) then + canary.destination.port = DeepCopy(route.destination.port) + end + if (route.weight) then + route.weight = math.floor(route.weight * stableWeight / 100) + else + route.weight = math.floor(stableWeight / #rule.route) + end end + table.insert(rule.route, canary) end end -return spec \ No newline at end of file + +if (obj.matches) then + GenerateMatchedRoutes(spec, obj.matches, obj.stableService, obj.canaryService, obj.stableWeight, obj.canaryWeight) + return obj.data +end + +GenerateRoutes(spec, obj.stableService, obj.canaryService, obj.stableWeight, obj.canaryWeight) +return obj.data +