Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for custom network provider (part A) #172

Merged
merged 10 commits into from
Sep 25, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions api/v1alpha1/trafficrouting_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ type TrafficRoutingRef struct {
// Gateway holds Gateway specific configuration to route traffic
// Gateway configuration only supports >= v0.4.0 (v1alpha2).
Gateway *GatewayTrafficRouting `json:"gateway,omitempty"`
// CustomNetworkRefs hold a list of custom providers to route traffic
CustomNetworkRefs []CustomNetworkRef `json:"customNetworkRefs,omitempty"`
}

// IngressTrafficRouting configuration for ingress controller to control traffic routing
Expand Down Expand Up @@ -149,6 +151,12 @@ type TrafficRoutingList struct {
Items []TrafficRouting `json:"items"`
}

type CustomNetworkRef struct {
APIVersion string `json:"apiVersion"`
Kind string `json:"kind"`
Name string `json:"name"`
}

func init() {
SchemeBuilder.Register(&TrafficRouting{}, &TrafficRoutingList{})
}
20 changes: 20 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions config/crd/bases/rollouts.kruise.io_rollouts.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,23 @@ spec:
for supported service meshes to enable more fine-grained
traffic routing
properties:
customNetworkRefs:
description: CustomNetworkRefs hold a list of custom
providers to route traffic
items:
properties:
apiVersion:
type: string
kind:
type: string
name:
type: string
required:
- apiVersion
- kind
- name
type: object
type: array
gateway:
description: Gateway holds Gateway specific configuration
to route traffic Gateway configuration only supports
Expand Down
17 changes: 17 additions & 0 deletions config/crd/bases/rollouts.kruise.io_trafficroutings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,23 @@ spec:
for supported service meshes to enable more fine-grained traffic
routing
properties:
customNetworkRefs:
description: CustomNetworkRefs hold a list of custom providers
to route traffic
items:
properties:
apiVersion:
type: string
kind:
type: string
name:
type: string
required:
- apiVersion
- kind
- name
type: object
type: array
gateway:
description: Gateway holds Gateway specific configuration to
route traffic Gateway configuration only supports >= v0.4.0
Expand Down
225 changes: 225 additions & 0 deletions lua_configuration/lua.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
package main
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this source file a UT? can we run it when building the code?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file is more like a plugin to facilitate the development of Lua scripts.


import (
"fmt"
"io"
"os"
"path/filepath"
"strings"

"github.com/openkruise/rollouts/api/v1alpha1"
"github.com/openkruise/rollouts/pkg/trafficrouting/network/custom"
"github.com/openkruise/rollouts/pkg/util/luamanager"
lua "github.com/yuin/gopher-lua"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/yaml"

utilpointer "k8s.io/utils/pointer"
)

type TestCase struct {
Rollout *v1alpha1.Rollout `json:"rollout,omitempty"`
TrafficRouting *v1alpha1.TrafficRouting `json:"trafficRouting,omitempty"`
Original *unstructured.Unstructured `json:"original,omitempty"`
Expected []*unstructured.Unstructured `json:"expected,omitempty"`
}

// convert testdata to lua object for debugging
func main() {
err := PathWalk()
if err != nil {
fmt.Println(err)
}
}

func PathWalk() error {
err := filepath.Walk("./", func(path string, f os.FileInfo, err error) error {
if !strings.Contains(path, "trafficRouting.lua") {
return nil
}
if err != nil {
return fmt.Errorf("failed to walk path: %s", err.Error())
}
dir := filepath.Dir(path)
if _, err := os.Stat(filepath.Join(dir, "testdata")); err != nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is testdata directory included in this PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

testdata and lua scripts are not included in this PR. This function aims to convert testdata into lua objects. When the user wants to debug their lua scripts, they could use this function to generate lua objects and copy the generated objects to their lua scripts for debugging.

fmt.Printf("testdata not found in %s\n", dir)
return nil
}
err = filepath.Walk(filepath.Join(dir, "testdata"), func(path string, info os.FileInfo, err error) error {
if !info.IsDir() && filepath.Ext(path) == ".yaml" || filepath.Ext(path) == ".yml" {
fmt.Printf("--- walking path: %s ---\n", path)
err = ObjectToTable(path)
if err != nil {
return fmt.Errorf("failed to convert object to table: %s", err)
}
}
return nil
})
if err != nil {
return fmt.Errorf("failed to walk path: %s", err.Error())
}
return nil
})
if err != nil {
return fmt.Errorf("failed to walk path: %s", err)
}
return nil
}

// convert a testcase object to lua table for debug
func ObjectToTable(path string) error {
dir, file := filepath.Split(path)
testCase, err := getLuaTestCase(path)
if err != nil {
return fmt.Errorf("failed to get lua testcase: %s", err)
}
uList := make(map[string]interface{})
rollout := testCase.Rollout
trafficRouting := testCase.TrafficRouting
if rollout != nil {
steps := rollout.Spec.Strategy.Canary.Steps
for i, step := range steps {
weight := step.TrafficRoutingStrategy.Weight
if step.TrafficRoutingStrategy.Weight == nil {
weight = utilpointer.Int32(-1)
}
var canaryService string
stableService := rollout.Spec.Strategy.Canary.TrafficRoutings[0].Service
canaryService = fmt.Sprintf("%s-canary", stableService)
data := &custom.LuaData{
Data: custom.Data{
Labels: testCase.Original.GetLabels(),
Annotations: testCase.Original.GetAnnotations(),
Spec: testCase.Original.Object["spec"],
},
Matches: step.TrafficRoutingStrategy.Matches,
CanaryWeight: *weight,
StableWeight: 100 - *weight,
CanaryService: canaryService,
StableService: stableService,
}
uList[fmt.Sprintf("step_%d", i)] = data
}
} else if trafficRouting != nil {
weight := trafficRouting.Spec.Strategy.Weight
if weight == nil {
weight = utilpointer.Int32(-1)
}
var canaryService string
stableService := trafficRouting.Spec.ObjectRef[0].Service
canaryService = stableService
data := &custom.LuaData{
Data: custom.Data{
Labels: testCase.Original.GetLabels(),
Annotations: testCase.Original.GetAnnotations(),
Spec: testCase.Original.Object["spec"],
},
Matches: trafficRouting.Spec.Strategy.Matches,
CanaryWeight: *weight,
StableWeight: 100 - *weight,
CanaryService: canaryService,
StableService: stableService,
}
uList["steps_0"] = data
} else {
return fmt.Errorf("neither rollout nor trafficRouting defined in test case: %s", path)
}

objStr, err := executeLua(uList)
if err != nil {
return fmt.Errorf("failed to execute lua: %s", err.Error())
}
filePath := fmt.Sprintf("%s%s_obj.lua", dir, strings.Split(file, ".")[0])
fileStream, err := os.OpenFile(filePath, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0666)
if err != nil {
return fmt.Errorf("failed to open file: %s", err)
}
defer fileStream.Close()
_, err = io.WriteString(fileStream, objStr)
if err != nil {
return fmt.Errorf("failed to WriteString %s", err)
}
return nil
}

func getLuaTestCase(path string) (*TestCase, error) {
yamlFile, err := os.ReadFile(path)
if err != nil {
return nil, err
}
luaTestCase := &TestCase{}
err = yaml.Unmarshal(yamlFile, luaTestCase)
if err != nil {
return nil, err
}
return luaTestCase, nil
}

func executeLua(steps map[string]interface{}) (string, error) {
luaManager := &luamanager.LuaManager{}
unObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&steps)
if err != nil {
return "", fmt.Errorf("failed to convert to unstructured: %s", err)
}
u := &unstructured.Unstructured{Object: unObj}
script := `
function serialize(obj, isKey)
local lua = ""
local t = type(obj)
if t == "number" then
lua = lua .. obj
elseif t == "boolean" then
lua = lua .. tostring(obj)
elseif t == "string" then
if isKey then
lua = lua .. string.format("%s", obj)
else
lua = lua .. string.format("%q", obj)
end
elseif t == "table" then
lua = lua .. "{"
for k, v in pairs(obj) do
if type(k) == "string" then
lua = lua .. serialize(k, true) .. "=" .. serialize(v, false) .. ","
else
lua = lua .. serialize(v, false) .. ","
end
end
local metatable = getmetatable(obj)
if metatable ~= nil and type(metatable.__index) == "table" then
for k, v in pairs(metatable.__index) do
if type(k) == "string" then
lua = lua .. serialize(k, true) .. "=" .. serialize(v, false) .. ","
else
lua = lua .. serialize(v, false) .. ","
end
end
end
lua = lua .. "}"
elseif t == "nil" then
return nil
else
error("can not serialize a " .. t .. " type.")
end
return lua
end

function table2string(tablevalue)
local stringtable = "steps=" .. serialize(tablevalue)
print(stringtable)
return stringtable
end
return table2string(obj)
`
l, err := luaManager.RunLuaScript(u, script)
if err != nil {
return "", fmt.Errorf("failed to run lua script: %s", err)
}
returnValue := l.Get(-1)
if returnValue.Type() == lua.LTString {
return returnValue.String(), nil
} else {
return "", fmt.Errorf("unexpected lua output type")
}
}
5 changes: 3 additions & 2 deletions pkg/controller/trafficrouting/trafficrouting_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ func (r *TrafficRoutingReconciler) Reconcile(ctx context.Context, req ctrl.Reque
newStatus := tr.Status.DeepCopy()
if newStatus.Phase == "" {
newStatus.Phase = v1alpha1.TrafficRoutingPhaseInitial
newStatus.Message = "TrafficRouting is Initializing"
}
if !tr.DeletionTimestamp.IsZero() {
newStatus.Phase = v1alpha1.TrafficRoutingPhaseTerminating
Expand Down Expand Up @@ -124,8 +125,8 @@ func (r *TrafficRoutingReconciler) Reconcile(ctx context.Context, req ctrl.Reque
case v1alpha1.TrafficRoutingPhaseFinalizing:
done, err = r.trafficRoutingManager.FinalisingTrafficRouting(newTrafficRoutingContext(tr), false)
if done {
newStatus.Phase = v1alpha1.TrafficRoutingPhaseHealthy
newStatus.Message = "TrafficRouting is Healthy"
newStatus.Phase = v1alpha1.TrafficRoutingPhaseInitial
newStatus.Message = "TrafficRouting is Initializing"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why changing routing to initializing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing the status to Healthy works fine for gateway and ingress since they only check if the resource exists when initializing, however, for custom network providers, the original configuration is stored in annotation in initialization, and the annotation will be removed after finalising, without this annotation, customController can't do EnsureRoutes(). So it is necessary to first initialize after finalising, or next time customController will not work.

}
case v1alpha1.TrafficRoutingPhaseTerminating:
done, err = r.trafficRoutingManager.FinalisingTrafficRouting(newTrafficRoutingContext(tr), false)
Expand Down
11 changes: 11 additions & 0 deletions pkg/trafficrouting/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (

"github.com/openkruise/rollouts/api/v1alpha1"
"github.com/openkruise/rollouts/pkg/trafficrouting/network"
"github.com/openkruise/rollouts/pkg/trafficrouting/network/custom"
"github.com/openkruise/rollouts/pkg/trafficrouting/network/gateway"
"github.com/openkruise/rollouts/pkg/trafficrouting/network/ingress"
"github.com/openkruise/rollouts/pkg/util"
Expand Down Expand Up @@ -263,6 +264,16 @@ func (m *Manager) FinalisingTrafficRouting(c *TrafficRoutingContext, onlyRestore

func newNetworkProvider(c client.Client, con *TrafficRoutingContext, sService, cService string) (network.NetworkProvider, error) {
trafficRouting := con.ObjectRef[0]
if trafficRouting.CustomNetworkRefs != nil {
return custom.NewCustomController(c, custom.Config{
Key: con.Key,
RolloutNs: con.Namespace,
CanaryService: cService,
StableService: sService,
TrafficConf: trafficRouting.CustomNetworkRefs,
OwnerRef: con.OwnerRef,
})
}
if trafficRouting.Ingress != nil {
return ingress.NewIngressTrafficRouting(c, ingress.Config{
Key: con.Key,
Expand Down
Loading
Loading