github.com/upcmd/up@v0.8.1-0.20230108151705-ad8b797bf04f/biz/impl/step.go (about) 1 // Ultimate Provisioner: UP cmd 2 // Copyright (c) 2019 Stephen Cheng and contributors 3 4 /* This Source Code Form is subject to the terms of the Mozilla Public 5 * License, v. 2.0. If a copy of the MPL was not distributed with this 6 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 7 8 package impl 9 10 import ( 11 "github.com/fatih/color" 12 "github.com/imdario/mergo" 13 ms "github.com/mitchellh/mapstructure" 14 "github.com/mohae/deepcopy" 15 "github.com/upcmd/up/biz" 16 "github.com/upcmd/up/model/core" 17 u "github.com/upcmd/up/utils" 18 ee "github.com/upcmd/up/utils/error" 19 "github.com/xlab/treeprint" 20 "os" 21 "path" 22 "reflect" 23 "strconv" 24 "strings" 25 ) 26 27 type Step struct { 28 Name string 29 Do interface{} //FuncImpl 30 Dox interface{} 31 Func string 32 Vars core.Cache 33 Dvars Dvars 34 Desc string 35 Reg string 36 Flags []string 37 If string 38 Else interface{} 39 Loop interface{} 40 Until string 41 RefDir string 42 VarsFile string 43 Timeout int //milli seconds, only for shell func 44 Finally interface{} 45 Rescue bool 46 } 47 48 type Steps []Step 49 50 /* 51 ExecbaseVars is the the scope containing passed in caller vars 52 local vars will be merged depending if it is a callee task 53 merge taskvars before final merge as task vars will be the calculated result of current stack 54 */ 55 func (step *Step) getRuntimeExecVars(fromBlock bool) *core.Cache { 56 var execvars core.Cache 57 var resultVars *core.Cache 58 59 if u.Contains(step.Flags, "pure") { 60 execvars = *core.NewCache() 61 } else { 62 execvars = deepcopy.Copy(*TaskRuntime().ExecbaseVars).(core.Cache) 63 } 64 65 mergo.Merge(&execvars, UpRunTimeVars, mergo.WithOverride) 66 67 taskVars := TaskRuntime().TaskVars 68 mergo.Merge(&execvars, taskVars, mergo.WithOverride) 69 70 if step.VarsFile != "" { 71 refdir := ConfigRuntime().RefDir 72 if step.RefDir != "" { 73 raw := step.RefDir 74 refdir = Render(raw, execvars) 75 } 76 77 var varsfile string 78 if step.VarsFile != "" { 79 raw := step.VarsFile 80 varsfile = Render(raw, execvars) 81 } 82 83 filepath := path.Join(refdir, varsfile) 84 if _, err := os.Stat(filepath); !os.IsNotExist(err) { 85 yamlvarsroot := u.YamlLoader("varsfile", refdir, varsfile) 86 filevars := loadRefVars(yamlvarsroot) 87 mergo.Merge(filevars, &step.Vars, mergo.WithOverride) 88 step.Vars = *filevars 89 } else { 90 u.LogWarn("varsfile is not loaded, ignored", u.Spf("%s does not exist", filepath)) 91 } 92 } 93 94 if fromBlock { 95 blockvars := BlockStack().GetTop().(*BlockRuntimeContext).BlockBaseVars 96 mergo.Merge(&execvars, blockvars, mergo.WithOverride) 97 mergo.Merge(&execvars, &step.Vars, mergo.WithOverride) 98 resultVars = &execvars 99 } else { 100 if IsCalledTask() { 101 //u.Ptmpdebug("if", "if") 102 if step.Vars != nil { 103 mergo.Merge(&step.Vars, &execvars, mergo.WithOverride) 104 resultVars = &step.Vars 105 } else { 106 resultVars = &execvars 107 } 108 } else { 109 //u.Ptmpdebug("else", "else") 110 mergo.Merge(&execvars, &step.Vars, mergo.WithOverride) 111 resultVars = &execvars 112 } 113 } 114 u.Pfvvvv("current exec runtime vars:") 115 u.Ppmsgvvvv(secureCache(resultVars)) 116 117 StepRuntime().ContextVars = resultVars 118 //so far the execvars includes: scope vars + scope dvars + global runtime vars + task vars 119 varsWithDvars := VarsMergedWithDvars("local", resultVars, &step.Dvars, resultVars) 120 121 //the processed varsWithDvars must merge with result vars: from varsWithDvars to resultVars 122 //care taken to register new vars to dvar processing phase 123 mergo.Merge(resultVars, varsWithDvars, mergo.WithOverride) 124 125 //so far the resultVars includes: the local vars + dvars rendered using execvars 126 u.Ppmsgvvvhint(u.Spf("%s: final context exec vars:", ConfigRuntime().ModuleName), secureCache(resultVars)) 127 //debugVars() 128 return resultVars 129 } 130 131 type LoopItem struct { 132 Index int 133 Index1 int 134 Item interface{} 135 } 136 137 func chainAction(action *biz.Do) { 138 (*action).Adapt() 139 (*action).Exec() 140 } 141 142 func validation(vars *core.Cache) { 143 identified := false 144 145 for k, _ := range *vars { 146 if k == "" { 147 u.InvalidAndPanic("validating var name", "var name can not be empty") 148 } 149 if u.CharIsNum(k[0:1]) != -1 { 150 identified = true 151 u.InvalidAndPanic("validating var name", u.Spf("var name (%s) can not start with number", k)) 152 } 153 } 154 155 if identified { 156 u.InvalidAndPanic("vars validation", "please fix all validation before continue") 157 } 158 } 159 160 func (step *Step) Exec(fromBlock bool) { 161 var action biz.Do 162 u.StepPanicCount += 1 163 defer func() { 164 if step.Finally != nil && step.Finally != "" { 165 u.PlnBlue("Step Finally:") 166 u.Ppmsgvvvvv(StepRuntime().Result) 167 } 168 169 paniced := false 170 if step.Vars == nil { 171 step.Vars = *core.NewCache() 172 } 173 174 step.Vars.Put(UP_RUNTIME_SHELL_EXEC_RESULT, StepRuntime().Result) 175 //debugVars() 176 var panicInfo interface{} 177 if r := recover(); r != nil { 178 u.PlnBlue(u.Spf("Recovered from: %s", r)) 179 paniced = true 180 panicInfo = r 181 } 182 183 if step.Finally != nil && step.Finally != "" { 184 execFinally(step.Finally, &step.Vars) 185 } 186 187 step.Vars.Delete(UP_RUNTIME_SHELL_EXEC_RESULT) 188 189 if paniced && step.Rescue == false { 190 u.LogWarn("Not rescued in step level", "please assess the panic problem and cause, fix it before re-run the task") 191 panic(panicInfo) 192 } else if paniced { 193 u.LogWarn("Rescued in step level, but not advised!", "setting rescue to yes/true to continue is not recommended\nit is advised to locate root cause of the problem, fix it and re-run the task again\nit is the best practice to test the execution in your ci pipeline to eliminate problems rather than dynamically fix using rescue") 194 } 195 u.StepPanicCount -= 1 196 }() 197 198 var bizErr *ee.Error = ee.New() 199 var stepExecVars *core.Cache 200 201 stepExecVars = step.getRuntimeExecVars(fromBlock) 202 validation(stepExecVars) 203 204 if step.Flags != nil && u.Contains(step.Flags, "pause") { 205 pause(stepExecVars) 206 } 207 208 routeFuncType := func(loopItem *LoopItem) { 209 if loopItem != nil { 210 stepExecVars.Put("loopitem", loopItem.Item) 211 stepExecVars.Put("loopindex", loopItem.Index) 212 stepExecVars.Put("loopindex1", loopItem.Index1) 213 } 214 215 switch step.Func { 216 case FUNC_SHELL: 217 funcAction := ShellFuncAction{ 218 Do: step.Do, 219 Vars: stepExecVars, 220 } 221 action = biz.Do(&funcAction) 222 223 case FUNC_CALL: 224 funcAction := CallFuncAction{ 225 Do: step.Do, 226 Vars: stepExecVars, 227 } 228 action = biz.Do(&funcAction) 229 230 case FUNC_BLOCK: 231 funcAction := BlockFuncAction{ 232 Do: step.Do, 233 Vars: stepExecVars, 234 } 235 action = biz.Do(&funcAction) 236 237 case FUNC_CMD: 238 funcAction := CmdFuncAction{ 239 Do: step.Do, 240 Vars: stepExecVars, 241 } 242 action = biz.Do(&funcAction) 243 244 case "": 245 u.InvalidAndPanic("Step dispatch", "func name is empty and not defined") 246 bizErr.Mark = "func name not implemented" 247 248 default: 249 u.InvalidAndPanic("Step dispatch", u.Spf("func name(%s) is not recognised and implemented", step.Func)) 250 bizErr.Mark = "func name not implemented" 251 } 252 } 253 254 dryRunOrContinue := func() { 255 //example to stop further steps 256 //f := u.MustConditionToContinueFunc(func() bool { 257 // return action != nil 258 //}) 259 // 260 //u.DryRunOrExit("Step Exec", f, "func name must be valid") 261 262 alloweErrors := []string{ 263 "func name not implemented", 264 } 265 266 DryRunAndSkip( 267 bizErr.Mark, 268 alloweErrors, 269 ContinueFunc( 270 func() { 271 if step.Loop != nil { 272 rawUtil := step.Until 273 func() { 274 //loop points to a var name which is a slice 275 if reflect.TypeOf(step.Loop).Kind() == reflect.String { 276 loopVarName := Render(step.Loop.(string), stepExecVars) 277 loopObj := stepExecVars.Get(loopVarName) 278 if loopObj == nil { 279 u.InvalidAndPanic("Evaluating loop var and object", u.Spf("Please use a correct varname:(%s) containing a list of values", loopVarName)) 280 } 281 if reflect.TypeOf(loopObj).Kind() == reflect.Slice { 282 switch loopObj.(type) { 283 case []interface{}: 284 for idx, item := range loopObj.([]interface{}) { 285 routeFuncType(&LoopItem{idx, idx + 1, item}) 286 if rawUtil != "" { 287 if TaskRuntime().ReturnVars != nil { 288 mergo.Merge(stepExecVars, TaskRuntime().ReturnVars, mergo.WithOverride) 289 } 290 untilEval := Render(rawUtil, stepExecVars) 291 toBreak, err := strconv.ParseBool(untilEval) 292 u.LogErrorAndPanic("evaluate until condition", err, u.Spf("please fix until condition evaluation: [%s]", untilEval)) 293 if toBreak { 294 u.Pvvvv("loop util conditional break") 295 break 296 } else { 297 chainAction(&action) 298 } 299 } else { 300 chainAction(&action) 301 } 302 } 303 304 case []string: 305 for idx, item := range loopObj.([]string) { 306 routeFuncType(&LoopItem{idx, idx + 1, item}) 307 if rawUtil != "" { 308 if TaskRuntime().ReturnVars != nil { 309 mergo.Merge(stepExecVars, TaskRuntime().ReturnVars, mergo.WithOverride) 310 } 311 untilEval := Render(rawUtil, stepExecVars) 312 toBreak, err := strconv.ParseBool(untilEval) 313 u.LogErrorAndPanic("evaluate until condition", err, u.Spf("please fix until condition evaluation: [%s]", untilEval)) 314 if toBreak { 315 u.Pvvvv("loop util conditional break") 316 break 317 } else { 318 chainAction(&action) 319 } 320 } else { 321 chainAction(&action) 322 } 323 } 324 325 case []int64: 326 for idx, item := range loopObj.([]int64) { 327 routeFuncType(&LoopItem{idx, idx + 1, item}) 328 if rawUtil != "" { 329 if TaskRuntime().ReturnVars != nil { 330 mergo.Merge(stepExecVars, TaskRuntime().ReturnVars, mergo.WithOverride) 331 } 332 untilEval := Render(rawUtil, stepExecVars) 333 toBreak, err := strconv.ParseBool(untilEval) 334 u.LogErrorAndPanic("evaluate until condition", err, u.Spf("please fix until condition evaluation: [%s]", untilEval)) 335 if toBreak { 336 u.Pvvvv("loop util conditional break") 337 break 338 } else { 339 chainAction(&action) 340 } 341 } else { 342 chainAction(&action) 343 } 344 } 345 346 default: 347 u.LogWarn("loop item evaluation", "Loop item type is not supported yet!") 348 u.LogWarn("supported:", "[]interface, []string, []int64") 349 u.LogWarn("got:", u.Spf("%T\n", loopObj)) 350 } 351 } else { 352 u.InvalidAndPanic("evaluate loop var", "loop var is not a array/list/slice") 353 } 354 } else if reflect.TypeOf(step.Loop).Kind() == reflect.Slice { 355 //loop itself is a slice 356 for idx, item := range step.Loop.([]interface{}) { 357 routeFuncType(&LoopItem{idx, idx + 1, item}) 358 if rawUtil != "" { 359 //make the return value could be evaluated in until 360 if TaskRuntime().ReturnVars != nil { 361 mergo.Merge(stepExecVars, TaskRuntime().ReturnVars, mergo.WithOverride) 362 } 363 untilEval := Render(rawUtil, stepExecVars) 364 toBreak, err := strconv.ParseBool(untilEval) 365 u.LogErrorAndPanic("evaluate until condition", err, u.Spf("please fix until condition evaluation: [%s]", untilEval)) 366 if toBreak { 367 u.Pvvvv("loop util conditional break") 368 break 369 } else { 370 chainAction(&action) 371 } 372 } else { 373 chainAction(&action) 374 } 375 } 376 } else { 377 u.InvalidAndPanic("evaluate loop items", "please either use a list or a template evaluation which could result in a value of a list") 378 } 379 }() 380 381 } else { 382 routeFuncType(nil) 383 chainAction(&action) 384 } 385 386 }), 387 nil, 388 ) 389 } 390 391 func() { 392 if step.If != NONE_VALUE && step.If != "" { 393 IfEval := Render(step.If, stepExecVars) 394 if IfEval != NONE_VALUE { 395 goahead, err := strconv.ParseBool(IfEval) 396 u.LogErrorAndPanic("evaluate condition", err, u.Spf("please fix if condition evaluation: [%s]", IfEval)) 397 if goahead { 398 dryRunOrContinue() 399 } else { 400 if step.Else != nil && step.Else != "" { 401 doElse(step.Else, stepExecVars) 402 } else { 403 u.Pvvv("condition failed, skip executing step", step.Name) 404 } 405 } 406 } else { 407 u.Pvvv("condition failed, skip executing step", step.Name) 408 } 409 } else { 410 dryRunOrContinue() 411 } 412 413 }() 414 415 } 416 417 func doElse(elseCalls interface{}, execVars *core.Cache) { 418 var taskname string 419 var tasknames []string 420 var flow Steps 421 422 switch elseCalls.(type) { 423 case string: 424 taskname = elseCalls.(string) 425 tasknames = append(tasknames, taskname) 426 427 case []interface{}: 428 elseStr := u.Spf("%s", elseCalls) 429 if strings.Index(elseStr, "map") != -1 && strings.Index(elseStr, "func:") != -1 { 430 err := ms.Decode(elseCalls, &flow) 431 u.LogErrorAndPanic("load steps in else", err, "steps has configuration problem, please fix it") 432 BlockFlowRun(&flow, execVars) 433 } else { 434 err := ms.Decode(elseCalls, &tasknames) 435 u.LogErrorAndPanic("call func alias: else", err, "please ref to a task name only") 436 } 437 438 default: 439 u.LogWarn("else ..", "Not implemented or void for no action!") 440 } 441 442 if len(tasknames) > 0 { 443 for _, tmptaskname := range tasknames { 444 taskname := Render(tmptaskname, execVars) 445 u.PpmsgvvvvvhintHigh(u.Spf("else caller vars to task (%s):", taskname), execVars) 446 ExecTask(taskname, execVars) 447 } 448 } 449 450 } 451 452 func execFinally(finally interface{}, execVars *core.Cache) { 453 var taskname string 454 var tasknames []string 455 var flow Steps 456 switch finally.(type) { 457 case string: 458 taskname = finally.(string) 459 tasknames = append(tasknames, taskname) 460 461 case []interface{}: 462 elseStr := u.Spf("%s", finally) 463 if strings.Index(elseStr, "map") != -1 && strings.Index(elseStr, "func:") != -1 { 464 err := ms.Decode(finally, &flow) 465 u.LogErrorAndPanic("load steps in finally", err, "steps/flow has configuration problem, please fix it") 466 BlockFlowRun(&flow, execVars) 467 } else { 468 err := ms.Decode(finally, &tasknames) 469 u.LogErrorAndPanic("load task names in finally", err, "please ref to a task name only") 470 } 471 472 default: 473 u.LogWarn("finally ..", "Not implemented or void for no action!") 474 } 475 476 if len(tasknames) > 0 { 477 for _, tmptaskname := range tasknames { 478 taskname := Render(tmptaskname, execVars) 479 u.PpmsgvvvvvhintHigh(u.Spf("finally caller vars to task (%s):", taskname), execVars) 480 ExecTask(taskname, execVars) 481 } 482 } 483 484 } 485 486 func (steps *Steps) InspectSteps(tree treeprint.Tree, level *int) bool { 487 for _, step := range *steps { 488 descRaw := strings.Split(step.Desc, "\n")[0] 489 desc := Render(descRaw, TaskerRuntime().Tasker.RuntimeVarsAndDvarsMerged) 490 if step.Func == FUNC_CALL { 491 branch := tree.AddMetaBranch(func() string { 492 if step.Loop != "" { 493 return step.Name + color.HiYellowString("%s", " /call.") 494 } else { 495 return step.Name 496 } 497 }(), desc) 498 var callee string 499 switch t := step.Do.(type) { 500 case string: 501 callee = step.Do.(string) 502 if !TaskerRuntime().Tasker.InspectTask(callee, branch, level) { 503 break 504 } 505 *level -= 1 506 case []interface{}: 507 calleeTasknames := step.Do.([]interface{}) 508 breakFlag := false 509 for _, x := range calleeTasknames { 510 callee = x.(string) 511 if !TaskerRuntime().Tasker.InspectTask(callee, branch, level) { 512 breakFlag = true 513 break 514 } 515 *level -= 1 516 } 517 if breakFlag { 518 break 519 } 520 default: 521 u.Pf("type: %T", t) 522 } 523 524 } else if step.Func == FUNC_BLOCK { 525 branch := tree.AddMetaBranch(func() string { 526 if step.Loop != "" { 527 return step.Name + color.HiYellowString("%s", " /block.") 528 } else { 529 return step.Name 530 } 531 }(), desc) 532 533 switch t := step.Do.(type) { 534 case string: 535 rawFlowname := step.Do.(string) 536 tree.AddNode(u.Spf("%s %s", color.HiYellowString("%s", " ..flow ->"), rawFlowname)) 537 538 case []interface{}: 539 //detailed steps 540 var steps Steps 541 err := ms.Decode(step.Do, &steps) 542 u.LogErrorAndPanic("load steps", err, "configuration problem, please fix it") 543 steps.InspectSteps(branch, level) 544 545 default: 546 u.Pf("type: %T", t) 547 } 548 549 } else { 550 tree.AddNode(u.Spf("%s: %s", step.Name, desc)) 551 } 552 } 553 return true 554 } 555 556 func (steps *Steps) Exec(fromBlock bool) { 557 if fromBlock { 558 if TaskRuntime() != nil && TaskRuntime().ExecbaseVars != nil { 559 TaskRuntime().ExecbaseVars.Delete(LAST_RESULT) 560 } 561 if StepRuntime() != nil && StepRuntime().ContextVars != nil { 562 StepRuntime().ContextVars.Delete(LAST_RESULT) 563 } 564 } 565 566 for idx, step := range *steps { 567 taskLayerCnt := TaskerRuntime().Tasker.TaskStack.GetLen() 568 desc := Render(step.Desc, TaskRuntime().ExecbaseVars) 569 u.LogDesc("step", idx+1, taskLayerCnt, step.Name, desc) 570 u.Ppmsgvvvvv(step) 571 572 execStep := func() { 573 rtContext := StepRuntimeContext{ 574 Stepname: step.Name, 575 Timeout: step.Timeout, 576 Flags: &step.Flags, 577 } 578 StepStack().Push(&rtContext) 579 580 if step.Do == nil && step.Dox != nil { 581 u.LogWarn("*", "Step is deactivated!") 582 } else { 583 step.Exec(fromBlock) 584 } 585 586 result := StepRuntime().Result 587 taskname := TaskRuntime().Taskname 588 589 if u.Contains([]string{FUNC_SHELL, FUNC_CALL}, step.Func) { 590 if step.Reg == "auto" { 591 if step.Name == "" { 592 TaskRuntime().ExecbaseVars.Put(u.Spf("%s_%d_result", taskname, idx), result) 593 } else { 594 TaskRuntime().ExecbaseVars.Put(u.Spf("%s_%s_result", taskname, step.Name), result) 595 } 596 } else if step.Reg != "" { 597 TaskRuntime().ExecbaseVars.Put(u.Spf("%s", step.Reg), result) 598 } 599 if step.Func == FUNC_SHELL { 600 TaskRuntime().ExecbaseVars.Put(LAST_RESULT, result) 601 } 602 } 603 604 func() { 605 result := StepRuntime().Result 606 607 if result != nil && result.Code == 0 { 608 u.LogOk(".") 609 } 610 611 if !u.Contains(step.Flags, "ignoreError") { 612 if result != nil && result.Code != 0 { 613 u.InvalidAndPanic("Failed And Not Ignored!", "You may want to continue and ignore the error") 614 } 615 } else { 616 if result != nil && result.Code != 0 { 617 u.LogWarn("ignoreError:", "Error ignored!!!") 618 } 619 } 620 621 }() 622 623 StepStack().Pop() 624 } 625 626 if !TaskerRuntime().Tasker.TaskBreak { 627 execStep() 628 } else { 629 TaskerRuntime().Tasker.TaskBreak = false 630 u.LogWarn("break", "client chose to break") 631 break 632 } 633 634 } 635 636 }