github.com/argoproj/argo-cd@v1.8.7/util/lua/lua.go (about) 1 package lua 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "os" 8 "path/filepath" 9 "time" 10 11 "github.com/argoproj/gitops-engine/pkg/health" 12 "github.com/gobuffalo/packr" 13 lua "github.com/yuin/gopher-lua" 14 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 15 luajson "layeh.com/gopher-json" 16 17 appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1" 18 ) 19 20 const ( 21 incorrectReturnType = "expect %s output from Lua script, not %s" 22 invalidHealthStatus = "Lua returned an invalid health status" 23 resourceCustomizationBuiltInPath = "../../resource_customizations" 24 healthScriptFile = "health.lua" 25 actionScriptFile = "action.lua" 26 actionDiscoveryScriptFile = "discovery.lua" 27 ) 28 29 var ( 30 box packr.Box 31 ) 32 33 func init() { 34 box = packr.NewBox(resourceCustomizationBuiltInPath) 35 } 36 37 type ResourceHealthOverrides map[string]appv1.ResourceOverride 38 39 func (overrides ResourceHealthOverrides) GetResourceHealth(obj *unstructured.Unstructured) (*health.HealthStatus, error) { 40 luaVM := VM{ 41 ResourceOverrides: overrides, 42 } 43 script, err := luaVM.GetHealthScript(obj) 44 if err != nil { 45 return nil, err 46 } 47 if script == "" { 48 return nil, nil 49 } 50 result, err := luaVM.ExecuteHealthLua(obj, script) 51 if err != nil { 52 return nil, err 53 } 54 return result, nil 55 } 56 57 // VM Defines a struct that implements the luaVM 58 type VM struct { 59 ResourceOverrides map[string]appv1.ResourceOverride 60 // UseOpenLibs flag to enable open libraries. Libraries are always disabled while running, but enabled during testing to allow the use of print statements 61 UseOpenLibs bool 62 } 63 64 func (vm VM) runLua(obj *unstructured.Unstructured, script string) (*lua.LState, error) { 65 l := lua.NewState(lua.Options{ 66 SkipOpenLibs: !vm.UseOpenLibs, 67 }) 68 defer l.Close() 69 // Opens table library to allow access to functions to manipulate tables 70 for _, pair := range []struct { 71 n string 72 f lua.LGFunction 73 }{ 74 {lua.LoadLibName, lua.OpenPackage}, 75 {lua.BaseLibName, lua.OpenBase}, 76 {lua.TabLibName, lua.OpenTable}, 77 // load our 'safe' version of the os library 78 {lua.OsLibName, OpenSafeOs}, 79 } { 80 if err := l.CallByParam(lua.P{ 81 Fn: l.NewFunction(pair.f), 82 NRet: 0, 83 Protect: true, 84 }, lua.LString(pair.n)); err != nil { 85 panic(err) 86 } 87 } 88 // preload our 'safe' version of the os library. Allows the 'local os = require("os")' to work 89 l.PreloadModule(lua.OsLibName, SafeOsLoader) 90 91 ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) 92 defer cancel() 93 l.SetContext(ctx) 94 objectValue := decodeValue(l, obj.Object) 95 l.SetGlobal("obj", objectValue) 96 err := l.DoString(script) 97 return l, err 98 } 99 100 // ExecuteHealthLua runs the lua script to generate the health status of a resource 101 func (vm VM) ExecuteHealthLua(obj *unstructured.Unstructured, script string) (*health.HealthStatus, error) { 102 l, err := vm.runLua(obj, script) 103 if err != nil { 104 return nil, err 105 } 106 returnValue := l.Get(-1) 107 if returnValue.Type() == lua.LTTable { 108 jsonBytes, err := luajson.Encode(returnValue) 109 if err != nil { 110 return nil, err 111 } 112 healthStatus := &health.HealthStatus{} 113 err = json.Unmarshal(jsonBytes, healthStatus) 114 if err != nil { 115 return nil, err 116 } 117 if !isValidHealthStatusCode(healthStatus.Status) { 118 return &health.HealthStatus{ 119 Status: health.HealthStatusUnknown, 120 Message: invalidHealthStatus, 121 }, nil 122 } 123 124 return healthStatus, nil 125 } 126 return nil, fmt.Errorf(incorrectReturnType, "table", returnValue.Type().String()) 127 } 128 129 // GetHealthScript attempts to read lua script from config and then filesystem for that resource 130 func (vm VM) GetHealthScript(obj *unstructured.Unstructured) (string, error) { 131 key := getConfigMapKey(obj) 132 if script, ok := vm.ResourceOverrides[key]; ok && script.HealthLua != "" { 133 return script.HealthLua, nil 134 } 135 return vm.getPredefinedLuaScripts(key, healthScriptFile) 136 } 137 138 func (vm VM) ExecuteResourceAction(obj *unstructured.Unstructured, script string) (*unstructured.Unstructured, error) { 139 l, err := vm.runLua(obj, script) 140 if err != nil { 141 return nil, err 142 } 143 returnValue := l.Get(-1) 144 if returnValue.Type() == lua.LTTable { 145 jsonBytes, err := luajson.Encode(returnValue) 146 if err != nil { 147 return nil, err 148 } 149 newObj, err := appv1.UnmarshalToUnstructured(string(jsonBytes)) 150 if err != nil { 151 return nil, err 152 } 153 cleanedNewObj := cleanReturnedObj(newObj.Object, obj.Object) 154 newObj.Object = cleanedNewObj 155 return newObj, nil 156 } 157 return nil, fmt.Errorf(incorrectReturnType, "table", returnValue.Type().String()) 158 } 159 160 // cleanReturnedObj Lua cannot distinguish an empty table as an array or map, and the library we are using choose to 161 // decoded an empty table into an empty array. This function prevents the lua scripts from unintentionally changing an 162 // empty struct into empty arrays 163 func cleanReturnedObj(newObj, obj map[string]interface{}) map[string]interface{} { 164 mapToReturn := newObj 165 for key := range obj { 166 if newValueInterface, ok := newObj[key]; ok { 167 oldValueInterface, ok := obj[key] 168 if !ok { 169 continue 170 } 171 switch newValue := newValueInterface.(type) { 172 case map[string]interface{}: 173 if oldValue, ok := oldValueInterface.(map[string]interface{}); ok { 174 convertedMap := cleanReturnedObj(newValue, oldValue) 175 mapToReturn[key] = convertedMap 176 } 177 178 case []interface{}: 179 switch oldValue := oldValueInterface.(type) { 180 case map[string]interface{}: 181 if len(newValue) == 0 { 182 mapToReturn[key] = oldValue 183 } 184 case []interface{}: 185 newArray := cleanReturnedArray(newValue, oldValue) 186 mapToReturn[key] = newArray 187 } 188 } 189 } 190 } 191 return mapToReturn 192 } 193 194 // cleanReturnedArray allows Argo CD to recurse into nested arrays when checking for unintentional empty struct to 195 // empty array conversions. 196 func cleanReturnedArray(newObj, obj []interface{}) []interface{} { 197 arrayToReturn := newObj 198 for i := range newObj { 199 switch newValue := newObj[i].(type) { 200 case map[string]interface{}: 201 if oldValue, ok := obj[i].(map[string]interface{}); ok { 202 convertedMap := cleanReturnedObj(newValue, oldValue) 203 arrayToReturn[i] = convertedMap 204 } 205 case []interface{}: 206 if oldValue, ok := obj[i].([]interface{}); ok { 207 convertedMap := cleanReturnedArray(newValue, oldValue) 208 arrayToReturn[i] = convertedMap 209 } 210 } 211 } 212 return arrayToReturn 213 } 214 215 func (vm VM) ExecuteResourceActionDiscovery(obj *unstructured.Unstructured, script string) ([]appv1.ResourceAction, error) { 216 l, err := vm.runLua(obj, script) 217 if err != nil { 218 return nil, err 219 } 220 returnValue := l.Get(-1) 221 if returnValue.Type() == lua.LTTable { 222 223 jsonBytes, err := luajson.Encode(returnValue) 224 if err != nil { 225 return nil, err 226 } 227 availableActions := make([]appv1.ResourceAction, 0) 228 if noAvailableActions(jsonBytes) { 229 return availableActions, nil 230 } 231 availableActionsMap := make(map[string]interface{}) 232 err = json.Unmarshal(jsonBytes, &availableActionsMap) 233 if err != nil { 234 return nil, err 235 } 236 for key := range availableActionsMap { 237 value := availableActionsMap[key] 238 resourceAction := appv1.ResourceAction{Name: key, Disabled: isActionDisabled(value)} 239 if emptyResourceActionFromLua(value) { 240 availableActions = append(availableActions, resourceAction) 241 continue 242 } 243 resourceActionBytes, err := json.Marshal(value) 244 if err != nil { 245 return nil, err 246 } 247 248 err = json.Unmarshal(resourceActionBytes, &resourceAction) 249 if err != nil { 250 return nil, err 251 } 252 availableActions = append(availableActions, resourceAction) 253 } 254 return availableActions, err 255 } 256 257 return nil, fmt.Errorf(incorrectReturnType, "table", returnValue.Type().String()) 258 } 259 260 // Actions are enabled by default 261 func isActionDisabled(actionsMap interface{}) bool { 262 actions, ok := actionsMap.(map[string]interface{}) 263 if !ok { 264 return false 265 } 266 for key, val := range actions { 267 switch vv := val.(type) { 268 case bool: 269 if key == "disabled" { 270 return vv 271 } 272 } 273 } 274 return false 275 } 276 277 func emptyResourceActionFromLua(i interface{}) bool { 278 _, ok := i.([]interface{}) 279 return ok 280 } 281 282 func noAvailableActions(jsonBytes []byte) bool { 283 // When the Lua script returns an empty table, it is decoded as a empty array. 284 return string(jsonBytes) == "[]" 285 } 286 287 func (vm VM) GetResourceActionDiscovery(obj *unstructured.Unstructured) (string, error) { 288 key := getConfigMapKey(obj) 289 override, ok := vm.ResourceOverrides[key] 290 if ok && override.Actions != "" { 291 actions, err := override.GetActions() 292 if err != nil { 293 return "", err 294 } 295 return actions.ActionDiscoveryLua, nil 296 } 297 discoveryKey := fmt.Sprintf("%s/actions/", key) 298 discoveryScript, err := vm.getPredefinedLuaScripts(discoveryKey, actionDiscoveryScriptFile) 299 if err != nil { 300 return "", err 301 } 302 return discoveryScript, nil 303 } 304 305 // GetResourceAction attempts to read lua script from config and then filesystem for that resource 306 func (vm VM) GetResourceAction(obj *unstructured.Unstructured, actionName string) (appv1.ResourceActionDefinition, error) { 307 key := getConfigMapKey(obj) 308 override, ok := vm.ResourceOverrides[key] 309 if ok && override.Actions != "" { 310 actions, err := override.GetActions() 311 if err != nil { 312 return appv1.ResourceActionDefinition{}, err 313 } 314 for _, action := range actions.Definitions { 315 if action.Name == actionName { 316 return action, nil 317 } 318 } 319 } 320 321 actionKey := fmt.Sprintf("%s/actions/%s", key, actionName) 322 actionScript, err := vm.getPredefinedLuaScripts(actionKey, actionScriptFile) 323 if err != nil { 324 return appv1.ResourceActionDefinition{}, err 325 } 326 327 return appv1.ResourceActionDefinition{ 328 Name: actionName, 329 ActionLua: actionScript, 330 }, nil 331 } 332 333 func getConfigMapKey(obj *unstructured.Unstructured) string { 334 gvk := obj.GroupVersionKind() 335 if gvk.Group == "" { 336 return gvk.Kind 337 } 338 return fmt.Sprintf("%s/%s", gvk.Group, gvk.Kind) 339 340 } 341 342 func (vm VM) getPredefinedLuaScripts(objKey string, scriptFile string) (string, error) { 343 data, err := box.MustBytes(filepath.Join(objKey, scriptFile)) 344 if err != nil { 345 if os.IsNotExist(err) { 346 return "", nil 347 } 348 return "", err 349 } 350 return string(data), nil 351 } 352 353 func isValidHealthStatusCode(statusCode health.HealthStatusCode) bool { 354 switch statusCode { 355 case health.HealthStatusUnknown, health.HealthStatusProgressing, health.HealthStatusSuspended, health.HealthStatusHealthy, health.HealthStatusDegraded, health.HealthStatusMissing: 356 return true 357 } 358 return false 359 } 360 361 // Took logic from the link below and added the int, int32, and int64 types since the value would have type int64 362 // while actually running in the controller and it was not reproducible through testing. 363 // https://github.com/layeh/gopher-json/blob/97fed8db84274c421dbfffbb28ec859901556b97/json.go#L154 364 func decodeValue(L *lua.LState, value interface{}) lua.LValue { 365 switch converted := value.(type) { 366 case bool: 367 return lua.LBool(converted) 368 case float64: 369 return lua.LNumber(converted) 370 case string: 371 return lua.LString(converted) 372 case json.Number: 373 return lua.LString(converted) 374 case int: 375 return lua.LNumber(converted) 376 case int32: 377 return lua.LNumber(converted) 378 case int64: 379 return lua.LNumber(converted) 380 case []interface{}: 381 arr := L.CreateTable(len(converted), 0) 382 for _, item := range converted { 383 arr.Append(decodeValue(L, item)) 384 } 385 return arr 386 case map[string]interface{}: 387 tbl := L.CreateTable(0, len(converted)) 388 for key, item := range converted { 389 tbl.RawSetH(lua.LString(key), decodeValue(L, item)) 390 } 391 return tbl 392 case nil: 393 return lua.LNil 394 } 395 396 return lua.LNil 397 }