package luavm import ( "context" "encoding/json" "fmt" "time" lua "github.com/yuin/gopher-lua" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/klog/v2" luajson "layeh.com/gopher-json" configv1alpha1 "github.com/karmada-io/karmada/pkg/apis/config/v1alpha1" workv1alpha2 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha2" "github.com/karmada-io/karmada/pkg/util/helper" "github.com/karmada-io/karmada/pkg/util/lifted" ) // VM Defines a struct that implements the luaVM. type VM struct { // UseOpenLibs flag to enable open libraries. Libraries are disabled by default while running, but enabled during testing to allow the use of print statements. UseOpenLibs bool } // GetReplicas returns the desired replicas of the object as well as the requirements of each replica by lua script. func (vm VM) GetReplicas(obj *unstructured.Unstructured, script string) (replica int32, requires *workv1alpha2.ReplicaRequirements, err error) { l := lua.NewState(lua.Options{ SkipOpenLibs: !vm.UseOpenLibs, }) defer l.Close() // Opens table library to allow access to functions to manipulate tables err = vm.setLib(l) if err != nil { return 0, nil, err } // preload our 'safe' version of the OS library. Allows the 'local os = require("os")' to work l.PreloadModule(lua.OsLibName, lifted.SafeOsLoader) ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() l.SetContext(ctx) err = l.DoString(script) f := l.GetGlobal("GetReplicas") if f.Type() == lua.LTNil { return 0, nil, fmt.Errorf("can't get function ReviseReplica pleace check the function name") } args := make([]lua.LValue, 1) args[0] = decodeValue(l, obj.Object) err = l.CallByParam(lua.P{Fn: f, NRet: 2, Protect: true}, args...) if err != nil { return 0, nil, err } replicaRequirementResult := l.Get(l.GetTop()) l.Pop(1) requires = &workv1alpha2.ReplicaRequirements{} if replicaRequirementResult.Type() == lua.LTTable { err = ConvertLuaResultInto(replicaRequirementResult, requires) if err != nil { klog.Errorf("ConvertLuaResultToReplicaRequirements err %v", err.Error()) return 0, nil, err } } else if replicaRequirementResult.Type() == lua.LTNil { requires = nil } else { return 0, nil, fmt.Errorf("expect the returned requires type is table but got %s", replicaRequirementResult.Type()) } luaReplica := l.Get(l.GetTop()) replica, err = ConvertLuaResultToInt(luaReplica) if err != nil { return 0, nil, err } return } // ReviseReplica revises the replica of the given object by lua. func (vm VM) ReviseReplica(object *unstructured.Unstructured, replica int64, script string) (*unstructured.Unstructured, error) { l := lua.NewState(lua.Options{ SkipOpenLibs: !vm.UseOpenLibs, }) defer l.Close() // Opens table library to allow access to functions to manipulate tables err := vm.setLib(l) if err != nil { return nil, err } // preload our 'safe' version of the OS library. Allows the 'local os = require("os")' to work l.PreloadModule(lua.OsLibName, lifted.SafeOsLoader) ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() l.SetContext(ctx) err = l.DoString(script) if err != nil { return nil, err } reviseReplicaLuaFunc := l.GetGlobal("ReviseReplica") if reviseReplicaLuaFunc.Type() == lua.LTNil { return nil, fmt.Errorf("can't get function ReviseReplica pleace check the function name") } args := make([]lua.LValue, 2) args[0] = decodeValue(l, object.Object) args[1] = decodeValue(l, replica) err = l.CallByParam(lua.P{Fn: reviseReplicaLuaFunc, NRet: 1, Protect: true}, args...) if err != nil { return nil, err } luaResult := l.Get(l.GetTop()) reviseReplicaResult := &unstructured.Unstructured{} if luaResult.Type() == lua.LTTable { err := ConvertLuaResultInto(luaResult, reviseReplicaResult) if err != nil { return nil, err } return reviseReplicaResult, nil } return nil, fmt.Errorf("expect the returned requires type is table but got %s", luaResult.Type()) } func (vm VM) setLib(l *lua.LState) error { for _, pair := range []struct { n string f lua.LGFunction }{ {lua.LoadLibName, lua.OpenPackage}, {lua.BaseLibName, lua.OpenBase}, {lua.TabLibName, lua.OpenTable}, // load our 'safe' version of the OS library {lua.OsLibName, lifted.OpenSafeOs}, } { if err := l.CallByParam(lua.P{ Fn: l.NewFunction(pair.f), NRet: 0, Protect: true, }, lua.LString(pair.n)); err != nil { return err } } return nil } // Retain returns the objects that based on the "desired" object but with values retained from the "observed" object by lua. func (vm VM) Retain(desired *unstructured.Unstructured, observed *unstructured.Unstructured, script string) (retained *unstructured.Unstructured, err error) { l := lua.NewState(lua.Options{ SkipOpenLibs: !vm.UseOpenLibs, }) defer l.Close() // Opens table library to allow access to functions to manipulate tables err = vm.setLib(l) if err != nil { return nil, err } // preload our 'safe' version of the OS library. Allows the 'local os = require("os")' to work l.PreloadModule(lua.OsLibName, lifted.SafeOsLoader) ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() l.SetContext(ctx) err = l.DoString(script) if err != nil { return nil, err } retainLuaFunc := l.GetGlobal("Retain") if retainLuaFunc.Type() == lua.LTNil { return nil, fmt.Errorf("can't get function Retatin pleace check the function ") } args := make([]lua.LValue, 2) args[0] = decodeValue(l, desired.Object) args[1] = decodeValue(l, observed.Object) err = l.CallByParam(lua.P{Fn: retainLuaFunc, NRet: 1, Protect: true}, args...) if err != nil { return nil, err } luaResult := l.Get(l.GetTop()) retainResult := &unstructured.Unstructured{} if luaResult.Type() == lua.LTTable { err := ConvertLuaResultInto(luaResult, retainResult) if err != nil { return nil, err } return retainResult, nil } return nil, fmt.Errorf("expect the returned requires type is table but got %s", luaResult.Type()) } // AggregateStatus returns the objects that based on the 'object' but with status aggregated by lua. func (vm VM) AggregateStatus(object *unstructured.Unstructured, item []map[string]interface{}, script string) (*unstructured.Unstructured, error) { l := lua.NewState(lua.Options{ SkipOpenLibs: !vm.UseOpenLibs, }) defer l.Close() // Opens table library to allow access to functions to manipulate tables err := vm.setLib(l) if err != nil { return nil, err } // preload our 'safe' version of the OS library. Allows the 'local os = require("os")' to work l.PreloadModule(lua.OsLibName, lifted.SafeOsLoader) ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() l.SetContext(ctx) err = l.DoString(script) if err != nil { return nil, err } f := l.GetGlobal("AggregateStatus") if f.Type() == lua.LTNil { return nil, fmt.Errorf("can't get function AggregateStatus pleace check the function ") } args := make([]lua.LValue, 2) args[0] = decodeValue(l, object.Object) args[1] = decodeValue(l, item) err = l.CallByParam(lua.P{Fn: f, NRet: 1, Protect: true}, args...) if err != nil { return nil, err } luaResult := l.Get(l.GetTop()) aggregateStatus := &unstructured.Unstructured{} if luaResult.Type() == lua.LTTable { err := ConvertLuaResultInto(luaResult, aggregateStatus) if err != nil { return nil, err } return aggregateStatus, nil } return nil, fmt.Errorf("expect the returned requires type is table but got %s", luaResult.Type()) } // InterpretHealth returns the health state of the object by lua. func (vm VM) InterpretHealth(object *unstructured.Unstructured, script string) (bool, error) { l := lua.NewState(lua.Options{ SkipOpenLibs: !vm.UseOpenLibs, }) defer l.Close() // Opens table library to allow access to functions to manipulate tables err := vm.setLib(l) if err != nil { return false, err } // preload our 'safe' version of the OS library. Allows the 'local os = require("os")' to work l.PreloadModule(lua.OsLibName, lifted.SafeOsLoader) ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() l.SetContext(ctx) err = l.DoString(script) if err != nil { return false, err } f := l.GetGlobal("InterpretHealth") if f.Type() == lua.LTNil { return false, fmt.Errorf("can't get function InterpretHealth pleace check the function ") } args := make([]lua.LValue, 1) args[0] = decodeValue(l, object.Object) err = l.CallByParam(lua.P{Fn: f, NRet: 1, Protect: true}, args...) if err != nil { return false, err } var health bool luaResult := l.Get(l.GetTop()) health, err = ConvertLuaResultToBool(luaResult) if err != nil { return false, err } return health, nil } // ReflectStatus returns the status of the object by lua. func (vm VM) ReflectStatus(object *unstructured.Unstructured, script string) (status *runtime.RawExtension, err error) { l := lua.NewState(lua.Options{ SkipOpenLibs: !vm.UseOpenLibs, }) defer l.Close() // Opens table library to allow access to functions to manipulate tables err = vm.setLib(l) if err != nil { return nil, err } // preload our 'safe' version of the OS library. Allows the 'local os = require("os")' to work l.PreloadModule(lua.OsLibName, lifted.SafeOsLoader) ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() l.SetContext(ctx) err = l.DoString(script) if err != nil { return nil, err } f := l.GetGlobal("ReflectStatus") if f.Type() == lua.LTNil { return nil, fmt.Errorf("can't get function ReflectStatus pleace check the function ") } args := make([]lua.LValue, 1) args[0] = decodeValue(l, object.Object) err = l.CallByParam(lua.P{Fn: f, NRet: 2, Protect: true}, args...) if err != nil { return nil, err } luaStatusResult := l.Get(l.GetTop()) l.Pop(1) if luaStatusResult.Type() != lua.LTTable { return nil, fmt.Errorf("expect the returned replica type is table but got %s", luaStatusResult.Type()) } luaExistResult := l.Get(l.GetTop()) var exist bool exist, err = ConvertLuaResultToBool(luaExistResult) if err != nil { return nil, err } if exist { resultMap := make(map[string]interface{}) jsonBytes, err := luajson.Encode(luaStatusResult) if err != nil { return nil, err } err = json.Unmarshal(jsonBytes, &resultMap) if err != nil { return nil, err } return helper.BuildStatusRawExtension(resultMap) } return nil, err } // GetDependencies returns the dependent resources of the given object by lua. func (vm VM) GetDependencies(object *unstructured.Unstructured, script string) (dependencies []configv1alpha1.DependentObjectReference, err error) { l := lua.NewState(lua.Options{ SkipOpenLibs: !vm.UseOpenLibs, }) defer l.Close() // Opens table library to allow access to functions to manipulate tables err = vm.setLib(l) if err != nil { return nil, err } // preload our 'safe' version of the OS library. Allows the 'local os = require("os")' to work l.PreloadModule(lua.OsLibName, lifted.SafeOsLoader) ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() l.SetContext(ctx) err = l.DoString(script) if err != nil { return nil, err } f := l.GetGlobal("GetDependencies") if f.Type() == lua.LTNil { return nil, fmt.Errorf("can't get function Retatin pleace check the function ") } args := make([]lua.LValue, 1) args[0] = decodeValue(l, object.Object) err = l.CallByParam(lua.P{Fn: f, NRet: 1, Protect: true}, args...) if err != nil { return nil, err } luaResult := l.Get(l.GetTop()) if luaResult.Type() == lua.LTTable { jsonBytes, err := luajson.Encode(luaResult) if err != nil { return nil, err } err = json.Unmarshal(jsonBytes, &dependencies) if err != nil { return nil, err } } else { return nil, fmt.Errorf("expect the returned requires type is table but got %s", luaResult.Type()) } return } // Took logic from the link below and added the int, int32, and int64 types since the value would have type int64 // while actually running in the controller and it was not reproducible through testing. // https://github.com/layeh/gopher-json/blob/97fed8db84274c421dbfffbb28ec859901556b97/json.go#L154 func decodeValue(L *lua.LState, value interface{}) lua.LValue { switch converted := value.(type) { case bool: return lua.LBool(converted) case float64: return lua.LNumber(converted) case string: return lua.LString(converted) case json.Number: return lua.LString(converted) case int: return lua.LNumber(converted) case int32: return lua.LNumber(converted) case int64: return lua.LNumber(converted) case []interface{}: arr := L.CreateTable(len(converted), 0) for _, item := range converted { arr.Append(decodeValue(L, item)) } return arr case []map[string]interface{}: arr := L.CreateTable(len(converted), 0) for _, item := range converted { arr.Append(decodeValue(L, item)) } return arr case map[string]interface{}: tbl := L.CreateTable(0, len(converted)) for key, item := range converted { tbl.RawSetH(lua.LString(key), decodeValue(L, item)) } return tbl case nil: return lua.LNil } return lua.LNil }