396 lines
11 KiB
Go
396 lines
11 KiB
Go
/*
|
|
Copyright 2022 The Karmada 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 luavm
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"reflect"
|
|
"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/fixedpool"
|
|
lualifted "github.com/karmada-io/karmada/pkg/util/lifted/lua"
|
|
)
|
|
|
|
// 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
|
|
Pool *fixedpool.FixedPool
|
|
}
|
|
|
|
// New creates a manager for lua VM
|
|
func New(useOpenLibs bool, poolSize int) *VM {
|
|
vm := &VM{
|
|
UseOpenLibs: useOpenLibs,
|
|
}
|
|
vm.Pool = fixedpool.New(
|
|
"luavm",
|
|
func() (any, error) { return vm.NewLuaState() },
|
|
func(a any) { a.(*lua.LState).Close() },
|
|
poolSize)
|
|
return vm
|
|
}
|
|
|
|
// NewLuaState creates a new lua state.
|
|
func (vm *VM) NewLuaState() (*lua.LState, error) {
|
|
l := lua.NewState(lua.Options{
|
|
SkipOpenLibs: !vm.UseOpenLibs,
|
|
})
|
|
// Opens table library to allow access to functions to manipulate tables
|
|
err := vm.setLib(l)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// preload kube library. Allows the 'local kube = require("kube")' to work
|
|
l.PreloadModule(KubeLibName, KubeLoader)
|
|
return l, err
|
|
}
|
|
|
|
// RunScript got a lua vm from pool, and execute script with given arguments.
|
|
func (vm *VM) RunScript(script string, fnName string, nRets int, args ...interface{}) ([]lua.LValue, error) {
|
|
a, err := vm.Pool.Get()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer vm.Pool.Put(a)
|
|
|
|
l := a.(*lua.LState)
|
|
l.Pop(l.GetTop())
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
|
defer cancel()
|
|
l.SetContext(ctx)
|
|
|
|
err = l.DoString(script)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
vArgs := make([]lua.LValue, len(args))
|
|
for i, arg := range args {
|
|
vArgs[i], err = decodeValue(l, arg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
f := l.GetGlobal(fnName)
|
|
if f.Type() == lua.LTNil {
|
|
return nil, fmt.Errorf("not found function %v", fnName)
|
|
}
|
|
if f.Type() != lua.LTFunction {
|
|
return nil, fmt.Errorf("%s is not a function: %s", fnName, f.Type())
|
|
}
|
|
|
|
err = l.CallByParam(lua.P{
|
|
Fn: f,
|
|
NRet: nRets,
|
|
Protect: true,
|
|
}, vArgs...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// get rets from stack: [ret1, ret2, ret3 ...]
|
|
rets := make([]lua.LValue, nRets)
|
|
for i := range rets {
|
|
rets[i] = l.Get(i + 1)
|
|
}
|
|
// pop all the values in stack
|
|
l.Pop(l.GetTop())
|
|
return rets, nil
|
|
}
|
|
|
|
// 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) {
|
|
results, err := vm.RunScript(script, "GetReplicas", 2, obj)
|
|
if err != nil {
|
|
return 0, nil, err
|
|
}
|
|
|
|
replica, err = ConvertLuaResultToInt(results[0])
|
|
if err != nil {
|
|
return 0, nil, err
|
|
}
|
|
|
|
replicaRequirementResult := results[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())
|
|
}
|
|
|
|
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) {
|
|
results, err := vm.RunScript(script, "ReviseReplica", 1, object, replica)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
luaResult := results[0]
|
|
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, lualifted.OpenPackage},
|
|
{lua.BaseLibName, lua.OpenBase},
|
|
{lua.TabLibName, lua.OpenTable},
|
|
{lua.StringLibName, lua.OpenString},
|
|
{lua.MathLibName, lua.OpenMath},
|
|
// load our 'safe' version of the OS library
|
|
{lua.OsLibName, lualifted.OpenSafeOs},
|
|
} {
|
|
if err := l.CallByParam(lua.P{
|
|
Fn: l.NewFunction(pair.f),
|
|
NRet: 0,
|
|
Protect: true,
|
|
}, lua.LString(pair.n)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Set potentially unsafe basic functions to nil, prohibiting users from
|
|
// calling these functions in custom Lua scripts.
|
|
// For the safety analysis of Sandbox in Lua, please refer to http://lua-users.org/wiki/SandBoxes
|
|
// For users, these functions are not needed for parsing resource templates,
|
|
// so disabling these functions has no impact on functionality.
|
|
for _, value := range []string{
|
|
"collectgarbage",
|
|
"dofile",
|
|
"getfenv",
|
|
"getmetatable",
|
|
"load",
|
|
"loadfile",
|
|
"loadstring",
|
|
"rawequal",
|
|
"rawget",
|
|
"rawset",
|
|
"setfenv",
|
|
"setmetatable",
|
|
"module",
|
|
"newproxy",
|
|
} {
|
|
overrideFunc := "function %s() error('call function %s is not supported in karmada customized resourceinterpreter.') end"
|
|
err := l.DoString(fmt.Sprintf(overrideFunc, value, value))
|
|
if 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) {
|
|
results, err := vm.RunScript(script, "Retain", 1, desired, observed)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
luaResult := results[0]
|
|
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, items []workv1alpha2.AggregatedStatusItem, script string) (*unstructured.Unstructured, error) {
|
|
results, err := vm.RunScript(script, "AggregateStatus", 1, object, items)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
luaResult := results[0]
|
|
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) {
|
|
results, err := vm.RunScript(script, "InterpretHealth", 1, object)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
var health bool
|
|
health, err = ConvertLuaResultToBool(results[0])
|
|
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) {
|
|
results, err := vm.RunScript(script, "ReflectStatus", 1, object)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
luaStatusResult := results[0]
|
|
if luaStatusResult.Type() != lua.LTTable {
|
|
return nil, fmt.Errorf("expect the returned replica type is table but got %s", luaStatusResult.Type())
|
|
}
|
|
|
|
status = &runtime.RawExtension{}
|
|
err = ConvertLuaResultInto(luaStatusResult, status)
|
|
return status, 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) {
|
|
results, err := vm.RunScript(script, "GetDependencies", 1, object)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
luaResult := results[0]
|
|
|
|
if luaResult.Type() != lua.LTTable {
|
|
return nil, fmt.Errorf("expect the returned requires type is table but got %s", luaResult.Type())
|
|
}
|
|
err = ConvertLuaResultInto(luaResult, &dependencies)
|
|
return
|
|
}
|
|
|
|
// NewWithContext creates a lua VM with the given context.
|
|
func NewWithContext(ctx context.Context) (*lua.LState, error) {
|
|
vm := VM{}
|
|
l := lua.NewState(lua.Options{
|
|
SkipOpenLibs: !vm.UseOpenLibs,
|
|
})
|
|
// Opens table library to allow access to functions to manipulate tables
|
|
err := vm.setLib(l)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// preload kube library. Allows the 'local kube = require("kube")' to work
|
|
l.PreloadModule(KubeLibName, KubeLoader)
|
|
if ctx != nil {
|
|
l.SetContext(ctx)
|
|
}
|
|
return l, nil
|
|
}
|
|
|
|
// nolint:gocyclo
|
|
func decodeValue(L *lua.LState, value interface{}) (lua.LValue, error) {
|
|
// We handle simple type without json for better performance.
|
|
switch converted := value.(type) {
|
|
case []interface{}:
|
|
arr := L.CreateTable(len(converted), 0)
|
|
for _, item := range converted {
|
|
v, err := decodeValue(L, item)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
arr.Append(v)
|
|
}
|
|
return arr, nil
|
|
case map[string]interface{}:
|
|
tbl := L.CreateTable(0, len(converted))
|
|
for key, item := range converted {
|
|
v, err := decodeValue(L, item)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tbl.RawSetString(key, v)
|
|
}
|
|
return tbl, nil
|
|
case nil:
|
|
return lua.LNil, nil
|
|
}
|
|
|
|
v := reflect.ValueOf(value)
|
|
switch {
|
|
case v.CanInt():
|
|
return lua.LNumber(v.Int()), nil
|
|
case v.CanUint():
|
|
return lua.LNumber(v.Uint()), nil
|
|
case v.CanFloat():
|
|
return lua.LNumber(v.Float()), nil
|
|
}
|
|
|
|
switch t := v.Type(); t.Kind() {
|
|
case reflect.String:
|
|
return lua.LString(v.String()), nil
|
|
case reflect.Bool:
|
|
return lua.LBool(v.Bool()), nil
|
|
case reflect.Pointer:
|
|
if v.IsNil() {
|
|
return lua.LNil, nil
|
|
}
|
|
}
|
|
|
|
// Other types can't be handled, ask for help from json
|
|
data, err := json.Marshal(value)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("json Marshal obj %#v error: %v", value, err)
|
|
}
|
|
|
|
lv, err := luajson.Decode(L, data)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("lua Decode obj %#v error: %v", value, err)
|
|
}
|
|
return lv, nil
|
|
}
|