github.com/upcmd/up@v0.8.1-0.20230108151705-ad8b797bf04f/biz/impl/tasker.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  	"bufio"
    12  	"bytes"
    13  	"github.com/fatih/color"
    14  	"github.com/imdario/mergo"
    15  	ms "github.com/mitchellh/mapstructure"
    16  	"github.com/mohae/deepcopy"
    17  	"github.com/spf13/viper"
    18  	"github.com/upcmd/up/model"
    19  	"github.com/upcmd/up/model/core"
    20  	"github.com/upcmd/up/model/stack"
    21  	u "github.com/upcmd/up/utils"
    22  	"github.com/xlab/treeprint"
    23  	"io/ioutil"
    24  	"os"
    25  	"os/exec"
    26  	"path"
    27  	"path/filepath"
    28  	"strconv"
    29  	"strings"
    30  )
    31  
    32  func InitDefaultSkeleton() {
    33  	filepath := path.Join(".", "upconfig.yml")
    34  	ioutil.WriteFile(filepath, []byte(u.DEFAULT_CONFIG), 0644)
    35  	filepath = path.Join(".", "up.yml")
    36  	ioutil.WriteFile(filepath, []byte(u.DEFAULT_UP_TASK_YML), 0644)
    37  }
    38  
    39  type Tasker struct {
    40  	TaskYmlRoot      *viper.Viper
    41  	Tasks            *model.Tasks
    42  	InstanceName     string
    43  	ExecProfilename  string
    44  	Dryrun           bool
    45  	TaskStack        *stack.ExecStack
    46  	StepStack        *stack.ExecStack
    47  	BlockStack       *stack.ExecStack
    48  	FinallyStack     *stack.ExecStack
    49  	TaskBreak        bool
    50  	Config           *u.UpConfig
    51  	GroupMembersList []string
    52  	MemberGroupMap   map[string]string
    53  	//expanded context only contains group and global scope, but not each instance vars
    54  	ExpandedContext ScopeContext
    55  	ScopeProfiles   *Scopes
    56  	ExecProfiles    *ExecProfiles
    57  	//this is the merged vars from within scope: global, groups level (if there is), instance varss, then global runtime vars
    58  	RuntimeVarsMerged  *core.Cache
    59  	ExecProfileEnvVars *core.Cache
    60  	SecretVars         *core.Cache
    61  	//this is the merged vars and dvars to a vars cache from within scope: global, groups level (if there is), instance varss, then global runtime vars
    62  	//this vars should be used instead of RuntimeVarsMerged as it include both runtime vars and dvars except the local vars and dvars
    63  	RuntimeVarsAndDvarsMerged *core.Cache
    64  	RuntimeGlobalVars         *core.Cache
    65  	RuntimeGlobalDvars        *Dvars
    66  	InFinalExec               bool
    67  }
    68  
    69  type ScopeContext map[string]*core.Cache
    70  type ContextInstances []ScopeContext
    71  
    72  type TaskerRuntimeContext struct {
    73  	Tasker       *Tasker
    74  	TaskerCaller *Tasker
    75  }
    76  
    77  func NewTasker(instanceId string, eprofiename string, cfg *u.UpConfig) *Tasker {
    78  	priorityLoadingTaskFile := filepath.Join(".", cfg.TaskFile)
    79  	refDir := "."
    80  	if _, err := os.Stat(priorityLoadingTaskFile); err != nil {
    81  		refDir = cfg.RefDir
    82  	}
    83  
    84  	taskYmlRoot := u.YamlLoader("Task", refDir, cfg.TaskFile)
    85  	tasker := &Tasker{
    86  		TaskYmlRoot:      taskYmlRoot,
    87  		MemberGroupMap:   map[string]string{},
    88  		GroupMembersList: []string{},
    89  		ExpandedContext:  ScopeContext{},
    90  	}
    91  	tasker.Config = cfg
    92  	tasker.initRuntime()
    93  
    94  	taskerContext := TaskerRuntimeContext{
    95  		Tasker: tasker,
    96  	}
    97  
    98  	TaskerStack.Push(&taskerContext)
    99  
   100  	tasker.initSecureVault()
   101  	tasker.loadPureEnv()
   102  	tasker.loadExecProfiles()
   103  	tasker.setInstanceName(instanceId, eprofiename)
   104  	tasker.loadExecProfileEnvVars()
   105  	tasker.loadScopes()
   106  	tasker.loadInstancesContext()
   107  	tasker.loadRuntimeGlobalVars()
   108  	tasker.loadRuntimeGlobalDvars()
   109  	tasker.MergeUptoRuntimeGlobalVars()
   110  	tasker.MergeRuntimeGlobalDvars()
   111  	tasker.loadTasks()
   112  	tasker.validateTasks()
   113  	return tasker
   114  }
   115  
   116  /*
   117  Get the merged vars for specific scope instance
   118  Validate the scopes
   119  1. for the scope name equal to global, there should be no value for members, otherwise errors
   120  2. for the scope with group members, it is a group itself
   121  3. for the scope with no members and name is not global, it is a final instance
   122  */
   123  
   124  func (t *Tasker) loadInstancesContext() {
   125  	ss := t.ScopeProfiles
   126  	//validation
   127  	for idx, s := range *ss {
   128  
   129  		if s.Ref != "" && s.Vars != nil {
   130  			u.Dvvvvv(s)
   131  			u.InvalidAndPanic("verify scope ref and member coexistence", "ref and members can not both exist")
   132  		}
   133  		refdir := ConfigRuntime().RefDir
   134  		if s.Ref != "" {
   135  			if s.RefDir != "" {
   136  				refdir = s.RefDir
   137  			}
   138  			yamlvarsroot := u.YamlLoader("ref vars", refdir, s.Ref)
   139  			vars := *loadRefVars(yamlvarsroot)
   140  			u.Pvvvv("loading vars from:", s.Ref)
   141  			u.Ppmsgvvvv(vars)
   142  			(*ss)[idx].Vars = vars
   143  		}
   144  
   145  	}
   146  
   147  	u.Pvvvvv("-------full vars in scopes------")
   148  	u.Dvvvvv(ss)
   149  
   150  	var globalScope *Scope
   151  	for idx, s := range *ss {
   152  		if s.Name == "global" {
   153  			if s.Members != nil {
   154  				u.InvalidAndPanic("scope expand", "global scope should not contains members")
   155  			}
   156  			globalScope = &(*ss)[idx]
   157  		}
   158  	}
   159  
   160  	//expand dvars into global scope's vars space
   161  	var globalvarsMergedWithDvars *core.Cache
   162  	if globalScope != nil {
   163  		globalvarsMergedWithDvars = GlobalVarsMergedWithDvars(globalScope)
   164  	} else {
   165  		globalvarsMergedWithDvars = core.NewCache()
   166  	}
   167  
   168  	for idx, s := range *ss {
   169  		if s.Members != nil {
   170  			for _, m := range s.Members {
   171  				if u.Contains(t.GroupMembersList, m) {
   172  					u.InvalidAndPanic("scope expand", u.Spfv("duplicated member: %s\n", m))
   173  				}
   174  				t.GroupMembersList = append(t.GroupMembersList, m)
   175  				t.MemberGroupMap[m] = s.Name
   176  			}
   177  
   178  			var groupvars core.Cache = deepcopy.Copy(*globalvarsMergedWithDvars).(core.Cache)
   179  			mergo.Merge(&groupvars, s.Vars, mergo.WithOverride)
   180  
   181  			//expand dvars into group scope's vars space
   182  			groupScope := &(*ss)[idx]
   183  			var groupvarsMergedWithDvars *core.Cache = ScopeVarsMergedWithDvars(groupScope, &groupvars)
   184  
   185  			t.ExpandedContext[s.Name] = groupvarsMergedWithDvars
   186  		}
   187  	}
   188  
   189  	t.ExpandedContext["global"] = globalvarsMergedWithDvars
   190  	func() {
   191  		u.Pvvvv("---------group vars----------")
   192  		for k, v := range t.ExpandedContext {
   193  			u.Pfvvvv("%s: %s", k, u.Sppmsg(secureCache(v)))
   194  		}
   195  		u.Pfvvvv("groups members:%s\n", t.GroupMembersList)
   196  
   197  	}()
   198  
   199  }
   200  
   201  func (t *Tasker) MergeRuntimeGlobalDvars() {
   202  	var mergedVars core.Cache
   203  	mergedVars = deepcopy.Copy(*t.RuntimeVarsMerged).(core.Cache)
   204  
   205  	expandedVars := t.RuntimeGlobalDvars.Expand("runtime global", t.RuntimeVarsMerged)
   206  
   207  	if t.RuntimeGlobalDvars != nil {
   208  		mergo.Merge(&mergedVars, *expandedVars, mergo.WithOverride)
   209  	}
   210  
   211  	t.RuntimeVarsAndDvarsMerged = &mergedVars
   212  	u.Ppmsgvvvvhint("-------runtime global final merged with dvars-------", secureCache(&mergedVars))
   213  }
   214  
   215  func (t *Tasker) loadExecProfileEnvVars() {
   216  	var envVars *core.Cache = core.NewCache()
   217  	var evars *EnvVars
   218  	if p := t.getExecProfile(t.ExecProfilename); p != nil {
   219  
   220  		if p.Ref != "" && p.Evars != nil {
   221  			u.InvalidAndPanic("exec proile validation", "You can only setup either ref file to load the env vars or use evars tag to config env vars, but not both")
   222  		}
   223  
   224  		refdir := ConfigRuntime().RefDir
   225  
   226  		if p.Ref != "" {
   227  			if p.RefDir != "" {
   228  				refdir = p.RefDir
   229  			}
   230  			yamlevarsroot := u.YamlLoader("ref evars", refdir, p.Ref)
   231  			evars = loadRefEvars(yamlevarsroot)
   232  			u.Pvvvv("loading vars from:", p.Ref)
   233  			u.Ppmsgvvvv(evars)
   234  		}
   235  
   236  		if p.Evars != nil {
   237  			evars = &p.Evars
   238  		}
   239  
   240  		if p.Taskname != "" {
   241  			u.MainConfig.EntryTask = p.Taskname
   242  		}
   243  
   244  		alternativeEntryTaskname := os.Getenv(UP_ENTRY_TASK_NAME)
   245  		if alternativeEntryTaskname != "" {
   246  			u.PlnBlue(u.Spf("entry task: %s", alternativeEntryTaskname))
   247  			u.MainConfig.EntryTask = alternativeEntryTaskname
   248  		}
   249  
   250  		if p.Pure {
   251  			pureEnv()
   252  		}
   253  
   254  		if p.Verbose != "" {
   255  			u.MainConfig.Verbose = p.Verbose
   256  		}
   257  
   258  		if evars != nil {
   259  			for _, v := range *evars {
   260  				envvarName := u.Spf("%s_%s", "envVar", v.Name)
   261  				envVars.Put(envvarName, v.Value)
   262  				os.Setenv(v.Name, v.Value)
   263  			}
   264  		}
   265  	}
   266  
   267  	t.ExecProfileEnvVars = envVars
   268  	u.Ppmsgvvvhint(u.Spf("profile - %s envVars:", t.ExecProfilename), envVars)
   269  }
   270  
   271  func pureEnv() {
   272  	sourceContent := `
   273  set -e
   274  echo '<<<ENVIRONMENT>>>'
   275  env
   276  `
   277  
   278  	cmd := exec.Command(u.MainConfig.ShellType, "-c", sourceContent)
   279  	bs, err := cmd.CombinedOutput()
   280  	if err != nil {
   281  		u.LogErrorAndExit("set pure env", err, "something is wrong obtaining system env vars")
   282  	}
   283  	venv := func() model.Venv {
   284  		s := bufio.NewScanner(bytes.NewReader(bs))
   285  		start := false
   286  		output := bytes.NewBufferString("")
   287  		venv := model.Venv{}
   288  		for s.Scan() {
   289  			if s.Text() == "<<<ENVIRONMENT>>>" {
   290  				start = true
   291  			} else if start {
   292  				kv := strings.SplitN(s.Text(), "=", 2)
   293  				if len(kv) == 2 {
   294  					k := kv[0]
   295  					v := kv[1]
   296  					os.Setenv(k, v)
   297  					venv = append(venv, model.Env{
   298  						Name:  k,
   299  						Value: v,
   300  					})
   301  				}
   302  			} else if !start {
   303  				output.WriteString(s.Text() + "\n")
   304  			}
   305  		}
   306  		return venv
   307  	}()
   308  
   309  	for _, x := range venv {
   310  		os.Unsetenv(x.Name)
   311  	}
   312  	u.PlnInfoHighlight("-set pure env context done.")
   313  }
   314  
   315  //clear up everything in scope and cache
   316  func (t *Tasker) Unset() {
   317  	t.ExpandedContext = ScopeContext{}
   318  	t.GroupMembersList = []string{}
   319  	t.MemberGroupMap = map[string]string{}
   320  	t.ScopeProfiles = nil
   321  	t.RuntimeVarsMerged = nil
   322  	t.RuntimeVarsAndDvarsMerged = nil
   323  	t.RuntimeGlobalVars = nil
   324  	t.RuntimeGlobalDvars = nil
   325  	TaskerStack = stack.New("tasker")
   326  }
   327  
   328  /*
   329  This will generate a one off vars merged from top level down to runtime
   330  global and merge them all together,the result vars will be used to finally
   331  merge with local func vars to be used in runtime execution time
   332  
   333  pass in runtime id, if runtime id is in member list, eg dev -> nonprod
   334  then merge runtimevars to group(nonprod)'s varss,
   335  
   336  if runtime id (nonname) is not in member list,
   337  then merge runtimevars to global varss,
   338  
   339  This has chained dvar expansion through global to group then to instance level
   340  and finally merge with global var, except the global dvars
   341  */
   342  func (t *Tasker) MergeUptoRuntimeGlobalVars() {
   343  
   344  	var runtimevars core.Cache
   345  	runtimevars = deepcopy.Copy(*t.ExpandedContext["global"]).(core.Cache)
   346  
   347  	if u.Contains(t.GroupMembersList, t.InstanceName) {
   348  		groupname := t.MemberGroupMap[t.InstanceName]
   349  		//TODO: t.ExpandedContext[groupname] should have already merge to global vars, double check to confirm
   350  		mergo.Merge(&runtimevars, *t.ExpandedContext[groupname], mergo.WithOverride)
   351  		instanceVars := t.ScopeProfiles.GetInstanceVars(t.InstanceName)
   352  		if instanceVars != nil {
   353  			mergo.Merge(&runtimevars, instanceVars, mergo.WithOverride)
   354  		}
   355  	}
   356  
   357  	//merge dvars for the instance
   358  	//TODO: is this a duplication of: GetInstanceVars above?
   359  	var instanceScope *Scope
   360  	for idx, s := range *t.ScopeProfiles {
   361  		if s.Name == t.InstanceName {
   362  			instanceScope = &(*t.ScopeProfiles)[idx]
   363  		}
   364  	}
   365  
   366  	var instanceVarsMergedWithDvars *core.Cache
   367  	if instanceScope != nil {
   368  		instanceVarsMergedWithDvars = VarsMergedWithDvars(instanceScope.Name, &instanceScope.Vars, &instanceScope.Dvars, &runtimevars)
   369  		//merge back the expanded merged scope vars and dvars
   370  		mergo.Merge(&runtimevars, *instanceVarsMergedWithDvars, mergo.WithOverride)
   371  	}
   372  
   373  	//merge with global vars
   374  	mergo.Merge(&runtimevars, *t.RuntimeGlobalVars, mergo.WithOverride)
   375  
   376  	u.Pfvvvv("merged[ %s ] runtime vars:", t.InstanceName)
   377  	u.Ppmsgvvvv(secureCache(&runtimevars))
   378  	u.Dvvvvv(secureCache(&runtimevars))
   379  
   380  	t.RuntimeVarsMerged = &runtimevars
   381  }
   382  
   383  func (t *Tasker) setInstanceName(id, eprofilename string) {
   384  	t.ExecProfilename = eprofilename
   385  	instanceName := "nonamed"
   386  	if id != "" {
   387  		instanceName = id
   388  	} else {
   389  		if p := t.getExecProfile(eprofilename); p != nil {
   390  			if p.Instance != "" {
   391  				instanceName = p.Instance
   392  			}
   393  		}
   394  	}
   395  
   396  	t.InstanceName = instanceName
   397  	u.Pf("module: [%s], instance id: [%s], exec profile: [%s]\n", ConfigRuntime().ModuleName, t.InstanceName, t.ExecProfilename)
   398  	if t.InstanceName == "nonamed" && t.ExecProfilename == "" {
   399  		u.LogWarn("*be aware*", "both instance id and exec profile are not set")
   400  	}
   401  }
   402  
   403  func (t *Tasker) initRuntime() {
   404  	t.TaskStack = stack.New("task")
   405  	t.StepStack = stack.New("step")
   406  	t.BlockStack = stack.New("block")
   407  	t.FinallyStack = stack.New("block")
   408  }
   409  
   410  func (t *Tasker) ListTasks() {
   411  	caps := "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
   412  	u.Pln("-task list")
   413  	maxlen := 0
   414  	for _, task := range *t.Tasks {
   415  		tasknamelen := len(task.Name)
   416  		if tasknamelen > maxlen {
   417  			maxlen = tasknamelen
   418  		}
   419  	}
   420  	format := "  %4d  | %" + u.Spf("%d", maxlen) + "s |%9s| %s "
   421  	for idx, task := range *t.Tasks {
   422  		start := task.Name[0:1]
   423  		desc := strings.Split(task.Desc, "\n")[0]
   424  		if strings.Contains(caps, start) {
   425  			color.HiGreen("%s", u.Spf(format, idx+1, task.Name, "public", desc))
   426  		} else {
   427  			color.Yellow("%s", u.Spf(format, idx+1, task.Name, "protected", desc))
   428  		}
   429  
   430  		u.Ppmsgvvvv(task)
   431  	}
   432  	u.Pln("-\n")
   433  }
   434  
   435  func (t *Tasker) GetUiTasks() []model.Task {
   436  	caps := "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
   437  	tasks := []model.Task{}
   438  	for _, task := range *t.Tasks {
   439  		start := task.Name[0:1]
   440  		if strings.Contains(caps, start) {
   441  			task.Public = true
   442  		} else {
   443  			task.Public = false
   444  		}
   445  		tasks = append(tasks, task)
   446  	}
   447  
   448  	return tasks
   449  }
   450  
   451  func (t *Tasker) ListAllTasks() {
   452  	u.Pln("-inspect all tasks:")
   453  	for _, task := range *t.Tasks {
   454  		t.ListTask(task.Name)
   455  	}
   456  }
   457  
   458  func (t *Tasker) LockModules() {
   459  	if !t.ValidateAllModules() {
   460  		u.InvalidAndPanic("modules configuration is not valid", "please fix the problem and try again")
   461  	}
   462  	u.Pln("-lock repos:")
   463  
   464  	lockMap := u.ModuleLockMap{}
   465  
   466  	mlist := (*ConfigRuntime()).Modules
   467  	if mlist != nil {
   468  		for _, m := range mlist {
   469  			m.Normalize()
   470  			m.ShowDetails()
   471  			gitdir := path.Join(m.Dir, ".git")
   472  			if _, err := os.Stat(gitdir); !os.IsNotExist(err) {
   473  				rev := u.GetHeadRev(m.Dir)
   474  				lockMap[m.Alias] = rev
   475  			}
   476  		}
   477  	}
   478  
   479  	u.Pln("versions:")
   480  	u.Ppmsg(lockMap)
   481  	lockYml := core.ObjToYaml(lockMap)
   482  	ioutil.WriteFile("./modlock.yml", []byte(lockYml), 0644)
   483  	u.Pf("Please check in: [%s] into code repo", "modlock.yml")
   484  }
   485  
   486  func (t *Tasker) CleanModules() {
   487  
   488  	if !t.ValidateAllModules() {
   489  		u.InvalidAndPanic("modules configuration is not valid", "please fix the problem and try again")
   490  	}
   491  	u.Pln("-clean repos:")
   492  }
   493  
   494  func (t *Tasker) PullModules() {
   495  	if !t.ValidateAllModules() {
   496  		u.InvalidAndPanic("modules configuration is not valid", "please fix the problem and try again")
   497  	}
   498  
   499  	u.Pln("-pull repos:")
   500  
   501  	mainMods := listModules("-main direct modules:", "%s/%s")
   502  	clonedMainModNames := mainMods.PullMainModules()
   503  	clonedSubModNames := append(clonedMainModNames, []string{}...)
   504  	mainMods.PullCascadedModules(&clonedMainModNames, &clonedSubModNames)
   505  }
   506  
   507  func (t *Tasker) ValidateAllModules() bool {
   508  	u.Pln("-validate all modules:")
   509  	mlist := (*ConfigRuntime()).Modules
   510  
   511  	namelist := []string{}
   512  	policies := []string{"manual", "always", "skip"}
   513  	errCnt := 0
   514  	for idx, m := range mlist {
   515  		m.Normalize()
   516  		if u.Contains(namelist, m.Alias) {
   517  			u.LogErrorMsg("alias duplication error", u.Spf("%d:%s", idx+1, m.Alias))
   518  			errCnt += 1
   519  		} else {
   520  			namelist = append(namelist, m.Alias)
   521  		}
   522  
   523  		if m.Repo != "" && !u.Contains(policies, m.PullPolicy) {
   524  			u.LogErrorMsg("pullpolicy error", u.Spf("%d:%s", idx+1, "must be one of: manual | always | skip"))
   525  			errCnt += 1
   526  		}
   527  
   528  		if m.Repo != "" && m.Subdir != "" && m.Alias == "" {
   529  			u.LogErrorMsg("alias must be set", u.Spf("%d:%s", idx+1, "alias is needed to avoid confusion"))
   530  			errCnt += 1
   531  		}
   532  	}
   533  
   534  	if errCnt == 0 {
   535  		return true
   536  	} else {
   537  		return false
   538  	}
   539  
   540  }
   541  
   542  //list tasker modules
   543  func (t *Tasker) ListMainModules() {
   544  	u.Pln("-list all modules:")
   545  	mlist := ConfigRuntime().Modules
   546  	mlist.ReportModules()
   547  	t.ValidateAllModules()
   548  }
   549  
   550  //probing modules list all modules, including the main direct modules and the all indirect modules
   551  func ListAllModules() {
   552  	u.Pln("-list all modules:")
   553  	mods := listModules("-main direct modules:", "%s/%s")
   554  	u.Pln("- Insights:")
   555  	mods.ReportModules()
   556  	u.Pln("")
   557  	mods = listModules("-indirect sub modules:", "%s/.upmodules/*/%s")
   558  	u.Pln("- Insights:")
   559  	mods.ReportModules()
   560  }
   561  
   562  func listModules(desc, pattern string) *u.Modules {
   563  	cfgname := "upconfig.yml"
   564  	filelist := []string{}
   565  	match := u.Spfv(pattern, u.MainConfig.AbsWorkDir, cfgname)
   566  	files, err := filepath.Glob(match)
   567  	u.LogError("list upconfig.yml", err)
   568  	filelist = append(filelist, files...)
   569  
   570  	modlist := u.Modules{}
   571  	for _, f := range filelist {
   572  		cfg := u.NewUpConfig(path.Dir(f), cfgname)
   573  		modlist = append(modlist, cfg.Modules...)
   574  	}
   575  	u.Pf("\n%s\n", desc)
   576  	yml := core.ObjToYaml(modlist)
   577  	u.Pln(yml)
   578  
   579  	return &modlist
   580  }
   581  
   582  func (tasker *Tasker) ListTask(taskname string) {
   583  	var tree = treeprint.New()
   584  	//u.Pln("\ninspect task:")
   585  	level := 0
   586  	for _, task := range *tasker.Tasks {
   587  		if task.Name == taskname {
   588  			desc := strings.Split(task.Desc, "\n")[0]
   589  			u.Pf("%s: %s", color.BlueString("%s", task.Name), desc)
   590  			var steps Steps
   591  			err := ms.Decode(task.Task, &steps)
   592  			u.LogErrorAndExit("decode steps:", err, "please fix data type in yaml config")
   593  			steps.InspectSteps(tree, &level)
   594  		}
   595  	}
   596  	u.Pln(tree.String())
   597  }
   598  
   599  func (tasker *Tasker) InspectTask(taskname string, branch treeprint.Tree, level *int) bool {
   600  	*level += 1
   601  	maxLayers, _ := strconv.Atoi(ConfigRuntime().MaxCallLayers)
   602  	if *level > maxLayers {
   603  		u.LogWarn("evaluate max task stack layer", "please setup max MaxCallLayers correctly, or fix recursive cycle calls")
   604  		return false
   605  	}
   606  	for _, task := range *tasker.Tasks {
   607  		if task.Name == taskname {
   608  			desc := strings.Split(task.Desc, "\n")[0]
   609  			br := branch.AddMetaBranch(color.BlueString("%s", task.Name), desc)
   610  			var steps Steps
   611  			err := ms.Decode(task.Task, &steps)
   612  			u.LogErrorAndExit("decode steps:", err, "please fix data type in yaml config")
   613  
   614  			for _, step := range steps {
   615  				descRaw := strings.Split(step.Desc, "\n")[0]
   616  				desc := Render(descRaw, TaskerRuntime().Tasker.RuntimeVarsAndDvarsMerged)
   617  				if step.Func == FUNC_CALL {
   618  					var callee string
   619  					switch t := step.Do.(type) {
   620  					case string:
   621  
   622  						brnode := br.AddMetaBranch(func() string {
   623  							if step.Loop != nil {
   624  								return step.Name + color.HiYellowString("%s", " /loop..")
   625  							} else {
   626  								return step.Name
   627  							}
   628  						}(), desc)
   629  
   630  						callee = step.Do.(string)
   631  						tasker.InspectTask(callee, brnode, level)
   632  					case []interface{}:
   633  						calleeTasknames := step.Do.([]interface{})
   634  						for _, x := range calleeTasknames {
   635  							brnode := br.AddMetaBranch(func() string {
   636  								if step.Loop != "" {
   637  									return step.Name + color.HiYellowString("%s", " /loop..")
   638  								} else {
   639  									return step.Name
   640  								}
   641  							}(), desc)
   642  
   643  							callee = x.(string)
   644  							tasker.InspectTask(callee, brnode, level)
   645  						}
   646  					default:
   647  						u.Pf("type: %T", t)
   648  					}
   649  				} else if step.Func == FUNC_BLOCK {
   650  					branch := branch.AddMetaBranch(func() string {
   651  						if step.Loop != "" {
   652  							return step.Name + color.HiYellowString("%s", " /block.")
   653  						} else {
   654  							return step.Name
   655  						}
   656  					}(), desc)
   657  
   658  					switch t := step.Do.(type) {
   659  					case string:
   660  						rawFlowname := step.Do.(string)
   661  						branch.AddNode(u.Spf("%s %s", color.HiYellowString("%s", " ..flow ->"), rawFlowname))
   662  
   663  					case []interface{}:
   664  						//detailed steps
   665  						var steps Steps
   666  						err := ms.Decode(step.Do, &steps)
   667  						u.LogErrorAndExit("load steps", err, "configuration problem, please fix it")
   668  						steps.InspectSteps(branch, level)
   669  
   670  					default:
   671  						u.Pf("type: %T", t)
   672  					}
   673  
   674  				} else {
   675  					br.AddNode(u.Spf("%s: %s", step.Name, desc))
   676  				}
   677  			}
   678  		}
   679  	}
   680  	return true
   681  }
   682  
   683  func (t *Tasker) DryrunTask(taskname string) {
   684  	SetDryrun()
   685  	t.ExecTask(taskname, nil, false)
   686  }
   687  
   688  func ExecTask(fulltaskname string, callerVars *core.Cache) {
   689  	var modname string
   690  	var taskname string
   691  
   692  	func() {
   693  		subnames := strings.Split(fulltaskname, ".")
   694  		if len(subnames) > 2 {
   695  			u.InvalidAndPanic("task name validation", "task naming pattern: modulename.taskname")
   696  		}
   697  
   698  		if len(subnames) == 1 {
   699  			modname = "self"
   700  			taskname = subnames[0]
   701  		} else if len(subnames) == 2 {
   702  			modname = subnames[0]
   703  			taskname = subnames[1]
   704  		}
   705  	}()
   706  
   707  	if modname == "self" {
   708  		TaskerRuntime().Tasker.ExecTask(taskname, callerVars, false)
   709  	} else {
   710  		if modname == GetBaseModuleName() {
   711  			u.InvalidAndPanic("module name should not be the same as the main caller", "please check your task configuration")
   712  		} else {
   713  
   714  			cwd, err := os.Getwd()
   715  
   716  			if err != nil {
   717  				u.LogErrorAndPanic("cwd", err, "working directory error")
   718  			}
   719  
   720  			mods := TaskerRuntime().Tasker.Config.Modules
   721  			if mods != nil {
   722  				mod := TaskerRuntime().Tasker.Config.Modules.LocateModule(modname)
   723  
   724  				if mod != nil {
   725  					func() {
   726  						//TODO: exclude the subdir case
   727  						var modpath string
   728  						if path.IsAbs(mod.Dir) {
   729  							modpath = mod.Dir
   730  						} else {
   731  							modpath = path.Clean(path.Join(BaseDir, mod.Dir))
   732  						}
   733  						os.Chdir(modpath)
   734  						if _, err := os.Stat(modpath); !os.IsNotExist(err) {
   735  							/*
   736  								in module loading, since you can not pass in the cli options, so:
   737  								version: will not be used at all
   738  								Verbose: determined by caller, so not relevant
   739  								MaxCallLayers: determined by caller
   740  								RefDir: applied
   741  								TaskFile: applied
   742  								ConfigDir: will not be used at all since no cli option to override this, it will be always be current dir .
   743  								ConfigFile: will not be used at all since no cli option to override this, it will be always be upconfig.yml from default
   744  							*/
   745  							mcfg := u.NewUpConfig("", "")
   746  							mcfg.SetModulename(modname)
   747  							mcfg.InitConfig()
   748  							taskerCaller := TaskerRuntime().Tasker
   749  							mTasker := NewTasker(mod.Iid, "", mcfg)
   750  							TaskerRuntime().TaskerCaller = taskerCaller
   751  							u.Pf("=>call module: [%s] task: [%s]\n", modname, taskname)
   752  							//u.Ptmpdebug("55", callerVars)
   753  
   754  							func() {
   755  								taskerLayer := TaskerStack.GetLen()
   756  								UpRunTimeVars.Put(UP_RUNTIME_TASKER_LAYER_NUMBER, taskerLayer)
   757  								u.Pvvvv("Executing tasker layer:", taskerLayer)
   758  								maxLayers, err := strconv.Atoi(u.MainConfig.MaxModuelCallLayers)
   759  								u.LogErrorAndPanic("evaluate max tasker module call layer", err, "please setup max MaxModuelCallLayers properly for your case")
   760  
   761  								if maxLayers != 0 && taskerLayer > maxLayers {
   762  									u.InvalidAndPanic("Module call layer check:", u.Spf("Too many layers of recursive module executions, max allowed(%d), please fix your recursive call", maxLayers))
   763  								}
   764  							}()
   765  
   766  							mTasker.ExecTask(taskname, callerVars, true)
   767  							TaskerStack.Pop()
   768  							os.Chdir(cwd)
   769  						} else {
   770  							//TODO: put the reasoning into the doco: not to auto update to avoid evil code injection problem
   771  							u.InvalidAndPanic(u.Spf("module dir: [%s] does not exist under: [%s]\n", mod.Dir, cwd), "double check if you have change your module configuration, then you will probably need to update module again")
   772  						}
   773  					}()
   774  				} else {
   775  					u.LogWarn("locating module name failed", u.Spf("module name: [%s] does not exist", modname))
   776  					TaskerRuntime().Tasker.ListMainModules()
   777  				}
   778  
   779  			} else {
   780  				callerName := TaskerRuntime().Tasker.Config.ModuleName
   781  				u.InvalidAndPanic(u.Spf("caller Module [%s] is not configured,", callerName), u.Spf("module: [%s], task: [%s]", modname, taskname))
   782  			}
   783  		}
   784  
   785  	}
   786  
   787  }
   788  
   789  func (t *Tasker) validateTasks() {
   790  	for _, x := range *t.Tasks {
   791  		if x.Name == "" {
   792  			continue
   793  		}
   794  		var cnt int = 0
   795  		for _, y := range *t.Tasks {
   796  			if y.Name == x.Name {
   797  				cnt += 1
   798  			}
   799  		}
   800  
   801  		if cnt > 1 {
   802  			u.InvalidAndPanic("Duplicated task name", u.Spf("task name:[%s]", x.Name))
   803  		}
   804  	}
   805  }
   806  
   807  func execTask(t *Tasker, taskname string, idx int, task model.Task, callerVars *core.Cache, isExternalCall bool) {
   808  	var rtContextFinal *TaskRuntimeContext
   809  	u.TaskPanicCount += 1
   810  	defer func(currentTask *model.Task) {
   811  		TaskerRuntime().Tasker.InFinalExec = true
   812  		TaskFinallyStack().Push(rtContextFinal)
   813  		if currentTask.Finally != nil && currentTask.Finally != "" {
   814  			u.PlnBlue("task Finally:")
   815  		}
   816  
   817  		paniced := false
   818  		var panicInfo interface{}
   819  		if r := recover(); r != nil {
   820  			u.Pvvvvv(u.Spf("Recovered from: %s", r))
   821  			paniced = true
   822  			panicInfo = r
   823  		}
   824  
   825  		if currentTask.Finally != nil && currentTask.Finally != "" {
   826  			execFinally(currentTask.Finally, core.NewCache())
   827  		}
   828  
   829  		if paniced && currentTask.Rescue == false {
   830  			u.LogWarn("Not rescued in task level", "please assess the panic problem and cause, fix it before re-run the task")
   831  			u.PanicExit("task finally", panicInfo)
   832  		} else if paniced {
   833  			u.LogWarn("Rescued in task 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")
   834  		}
   835  		TaskerRuntime().Tasker.InFinalExec = false
   836  		TaskFinallyStack().Pop()
   837  		u.TaskPanicCount -= 1
   838  	}(&task)
   839  
   840  	u.Pfvvvv("  located task-> %d [%s]: \n", idx+1, task.Name)
   841  
   842  	var ctxCallerTaskname string
   843  
   844  	if isExternalCall {
   845  		ctxCallerTaskname = "TODO: Main Caller Taskname"
   846  	} else {
   847  		if IsAtRootTaskLevel() {
   848  			ctxCallerTaskname = taskname
   849  		} else {
   850  			ctxCallerTaskname = TaskRuntime().TasknameLayered
   851  		}
   852  	}
   853  
   854  	taskLayerCnt := TaskerRuntime().Tasker.TaskStack.GetLen()
   855  	desc := Render(task.Desc, TaskerRuntime().Tasker.RuntimeVarsAndDvarsMerged)
   856  
   857  	u.LogDesc("task", idx+1, taskLayerCnt, u.Spf("%s ==> %s", ctxCallerTaskname, taskname), desc)
   858  
   859  	var steps Steps
   860  	err := ms.Decode(task.Task, &steps)
   861  
   862  	u.LogErrorAndPanic("decode steps:", err, "please fix data type in yaml config")
   863  	func() {
   864  		//step name validation
   865  		invalidNames := []string{}
   866  		for _, step := range steps {
   867  			if strings.Contains(step.Name, "-") {
   868  				invalidNames = append(invalidNames, step.Name)
   869  			}
   870  		}
   871  
   872  		if len(invalidNames) > 0 {
   873  			u.InvalidAndPanic(u.Spf("validating step name fails: %s ", invalidNames), "task name can not contain '-', please use '_' instead, failed names:")
   874  		}
   875  	}()
   876  
   877  	func() {
   878  		rtContext := TaskRuntimeContext{
   879  			Taskname:           taskname,
   880  			TaskVars:           core.NewCache(),
   881  			IsCalledExternally: isExternalCall,
   882  		}
   883  		rtContextFinal = &rtContext
   884  
   885  		u.Pdebugvvvvvvv(callerVars)
   886  		if isExternalCall {
   887  			var passinvars core.Cache
   888  			passinvars = deepcopy.Copy(*t.RuntimeVarsAndDvarsMerged).(core.Cache)
   889  			mergo.Merge(&passinvars, callerVars, mergo.WithOverride)
   890  			rtContext.ExecbaseVars = &passinvars
   891  			rtContext.TasknameLayered = u.Spf("%s/%s", "TODO: Main Caller Taskname", taskname)
   892  		} else {
   893  			if IsAtRootTaskLevel() {
   894  				rtContext.ExecbaseVars = t.RuntimeVarsAndDvarsMerged
   895  				rtContext.TasknameLayered = taskname
   896  			} else {
   897  				rtContext.ExecbaseVars = func() *core.Cache {
   898  					if *callerVars == nil {
   899  						return core.NewCache()
   900  					} else {
   901  						return callerVars
   902  					}
   903  				}()
   904  
   905  				rtContext.TasknameLayered = u.Spf("%s/%s", TaskRuntime().TasknameLayered, taskname)
   906  			}
   907  		}
   908  		u.Pdebugvvvvvvv(rtContext.ExecbaseVars)
   909  
   910  		func() {
   911  			UpRunTimeVars.Put(UP_RUNTIME_TASK_LAYER_NUMBER, TaskerRuntime().Tasker.TaskStack.GetLen())
   912  			TaskerRuntime().Tasker.TaskStack.Push(&rtContext)
   913  			u.Pvvvv("Executing task stack layer:", TaskerRuntime().Tasker.TaskStack.GetLen())
   914  			maxLayers, err := strconv.Atoi(ConfigRuntime().MaxCallLayers)
   915  			u.LogErrorAndPanic("evaluate max task stack layer", err, "please setup max MaxCallLayers correctly")
   916  
   917  			if maxLayers != 0 && TaskerRuntime().Tasker.TaskStack.GetLen() > maxLayers {
   918  				u.InvalidAndPanic("Task exec stack layer check:", u.Spf("Too many layers of task executions, max allowed(%d), please fix your recursive call", maxLayers))
   919  			}
   920  		}()
   921  
   922  		steps.Exec(false)
   923  
   924  		returnVars := TaskRuntime().ReturnVars
   925  
   926  		TaskerRuntime().Tasker.TaskStack.Pop()
   927  		func() {
   928  			//this will ensure the local caller vars are synced with return values, typically useful for chained tasks in call func
   929  			if returnVars != nil {
   930  				mergo.Merge(callerVars, returnVars, mergo.WithOverride)
   931  			}
   932  
   933  			if isExternalCall {
   934  				if returnVars != nil {
   935  					callerExecBaseVars := TaskerRuntime().TaskerCaller.TaskStack.GetTop().(*TaskRuntimeContext).ExecbaseVars
   936  					mergo.Merge(callerExecBaseVars, returnVars, mergo.WithOverride)
   937  				}
   938  			} else {
   939  				if !IsAtRootTaskLevel() && returnVars != nil {
   940  					mergo.Merge(TaskRuntime().ExecbaseVars, returnVars, mergo.WithOverride)
   941  				} else if IsAtRootTaskLevel() && returnVars != nil {
   942  					mergo.Merge(t.RuntimeVarsAndDvarsMerged, returnVars, mergo.WithOverride)
   943  				}
   944  			}
   945  
   946  		}()
   947  
   948  	}()
   949  
   950  }
   951  
   952  func (t *Tasker) ExecTask(taskname string, callerVars *core.Cache, isExternalCall bool) {
   953  	found := false
   954  	for idx, task := range *t.Tasks {
   955  		if taskname == task.Name {
   956  			found = true
   957  			execTask(t, taskname, idx, task, callerVars, isExternalCall)
   958  		}
   959  	}
   960  
   961  	if !found {
   962  		u.Pferror("Task %s is not defined!", taskname)
   963  		t.ListTasks()
   964  		u.InvalidAndPanic("Task call failed", "Task does not exist")
   965  	}
   966  }
   967  
   968  func (t *Tasker) validateAndLoadTaskRef() {
   969  	//validation
   970  
   971  	invalidNames := []string{}
   972  	for idx, task := range *t.Tasks {
   973  		start := task.Name[0:1]
   974  		if strings.Contains(u.CapsChars, start) {
   975  			task.Public = true
   976  		} else {
   977  			task.Public = false
   978  		}
   979  
   980  		if strings.Contains(task.Name, "-") {
   981  			invalidNames = append(invalidNames, task.Name)
   982  		}
   983  
   984  		if task.Task != nil && task.Ref != "" {
   985  			u.InvalidAndPanic("validate task node and ref", "task and ref can not coexist")
   986  		}
   987  
   988  		//load ref task
   989  		refdir := ConfigRuntime().RefDir
   990  
   991  		if task.Ref != "" {
   992  			if task.RefDir != "" {
   993  				rawdir := task.RefDir
   994  				refdir = Render(rawdir, t.RuntimeVarsAndDvarsMerged)
   995  			}
   996  
   997  			rawref := task.Ref
   998  			ref := Render(rawref, t.RuntimeVarsAndDvarsMerged)
   999  
  1000  			yamlflowroot := u.YamlLoader("flow ref", refdir, ref)
  1001  			flow := loadRefFlow(yamlflowroot)
  1002  			(*t.Tasks)[idx].Task = flow
  1003  		}
  1004  	}
  1005  
  1006  	if len(invalidNames) > 0 {
  1007  		u.InvalidAndPanic(u.Spf("validating task name fails: %s ", invalidNames), "task name can not contain '-', please use '_' instead, failed names:")
  1008  	}
  1009  }
  1010  
  1011  func (t *Tasker) loadRefTasks() {
  1012  	tasksRefList := t.TaskYmlRoot.Get("tasksref")
  1013  	if tasksRefList != nil {
  1014  		for _, ref := range tasksRefList.([]interface{}) {
  1015  			tasksYamlName := ref.(string)
  1016  			tasksYmlRoot := u.YamlLoader(tasksYamlName, ConfigRuntime().RefDir, tasksYamlName)
  1017  
  1018  			var tasks model.Tasks
  1019  			tasksData := tasksYmlRoot.Get("tasks")
  1020  			err := ms.Decode(tasksData, &tasks)
  1021  			u.LogErrorAndPanic(u.Spf("decode tasks:%s", tasksYamlName), err, "please fix configuration in tasks yaml file")
  1022  			for _, task := range tasks {
  1023  				*t.Tasks = append(*t.Tasks, task)
  1024  			}
  1025  		}
  1026  	}
  1027  }
  1028  
  1029  func (t *Tasker) loadTasks() error {
  1030  	tasksData := t.TaskYmlRoot.Get("tasks")
  1031  	var tasks model.Tasks
  1032  	err := ms.Decode(tasksData, &tasks)
  1033  	u.LogErrorAndPanic("decode tasks:main", err, "please fix configuration in tasks yaml file")
  1034  	t.Tasks = &tasks
  1035  	t.loadRefTasks()
  1036  	t.validateAndLoadTaskRef()
  1037  	return err
  1038  }
  1039  
  1040  func loadRefFlow(yamlroot *viper.Viper) *Steps {
  1041  	flowData := yamlroot.Get("flow")
  1042  	var flow Steps
  1043  	err := ms.Decode(flowData, &flow)
  1044  	u.LogErrorAndPanic("load ref flow", err, "flow of the steps has configuration problem, please fix it")
  1045  	return &flow
  1046  }
  1047  
  1048  func (t *Tasker) loadScopes() {
  1049  	scopesData := t.TaskYmlRoot.Get("scopes")
  1050  	var scopes Scopes
  1051  	err := ms.Decode(scopesData, &scopes)
  1052  	t.ScopeProfiles = &scopes
  1053  
  1054  	u.LogErrorAndPanic("load full scopes", err, "please assess your scope configuration carefully")
  1055  }
  1056  
  1057  func (t *Tasker) loadExecProfiles() {
  1058  	eprofileData := t.TaskYmlRoot.Get("eprofiles")
  1059  	var eprofiles ExecProfiles
  1060  	err := ms.Decode(eprofileData, &eprofiles)
  1061  	t.ExecProfiles = &eprofiles
  1062  
  1063  	u.LogErrorAndPanic("load exec profiles", err, "please assess your exec profiles configuration carefully")
  1064  }
  1065  
  1066  func (t *Tasker) loadPureEnv() {
  1067  	if u.MainConfig.Pure {
  1068  		pureEnv()
  1069  	}
  1070  
  1071  }
  1072  
  1073  func (t *Tasker) initSecureVault() {
  1074  	t.SecretVars = core.NewCache()
  1075  }
  1076  
  1077  func (t *Tasker) getExecProfile(pname string) *ExecProfile {
  1078  	var ep *ExecProfile
  1079  	if t.ExecProfiles != nil {
  1080  		for _, p := range *t.ExecProfiles {
  1081  			if p.Name == pname {
  1082  				ep = &p
  1083  				break
  1084  			}
  1085  		}
  1086  	}
  1087  	return ep
  1088  }
  1089  
  1090  func (t *Tasker) reportContextualEnvVars(vars *core.Cache) string {
  1091  	var envs EnvVars = EnvVars{}
  1092  	pname := TaskerRuntime().Tasker.ExecProfilename
  1093  	eprofile := TaskerRuntime().Tasker.getExecProfile(pname)
  1094  	if eprofile != nil {
  1095  		evars := eprofile.Evars
  1096  		for _, x := range evars {
  1097  			envs = append(envs, EnvVar{
  1098  				Name:  x.Name,
  1099  				Value: x.Value,
  1100  			})
  1101  		}
  1102  	}
  1103  
  1104  	for k, v := range *vars {
  1105  		if strings.HasPrefix(k, "envVar_") {
  1106  			envs = append(envs, EnvVar{
  1107  				Name:  strings.TrimPrefix(k, "envVar_"),
  1108  				Value: v.(string),
  1109  			})
  1110  		}
  1111  	}
  1112  
  1113  	var maxlen int
  1114  	for _, x := range envs {
  1115  		if l := len(x.Name); l > maxlen {
  1116  			maxlen = l
  1117  		}
  1118  	}
  1119  
  1120  	fs := `%3d: %` + strconv.Itoa(maxlen) + `s = %s`
  1121  	var expStr = bytes.NewBufferString("")
  1122  	for idx, x := range envs {
  1123  		u.PlnInfoHighlight(u.Spf(fs, idx+1, x.Name, x.Value))
  1124  		expStr.WriteString(u.Spf("export %s=\"%s\"\n", x.Name, x.Value))
  1125  	}
  1126  
  1127  	return expStr.String()
  1128  }
  1129  
  1130  func (t *Tasker) loadRuntimeGlobalVars() {
  1131  	varsData := t.TaskYmlRoot.Get("vars")
  1132  	var vars core.Cache
  1133  	err := ms.Decode(varsData, &vars)
  1134  	u.LogError("loadRuntimeGlobalVars", err)
  1135  	t.RuntimeGlobalVars = &vars
  1136  }
  1137  
  1138  func (t *Tasker) loadRuntimeGlobalDvars() {
  1139  	dvarsData := t.TaskYmlRoot.Get("dvars")
  1140  	var dvars Dvars
  1141  	err := ms.Decode(dvarsData, &dvars)
  1142  	u.LogErrorAndPanic("loadRuntimeGlobalDvars",
  1143  		err,
  1144  		"You must fix the data type to be\n string for a dvar value and try again. possible problems:\nthe name can not be single character 'y' or 'n' ",
  1145  	)
  1146  	t.RuntimeGlobalDvars = &dvars
  1147  }