github.com/swaros/contxt/module/taskrun@v0.0.0-20240305083542-3dbd4436ac40/template.go (about)

     1  // Copyright (c) 2020 Thomas Ziegler <thomas.zglr@googlemail.com>. All rights reserved.
     2  //
     3  // # Licensed under the MIT License
     4  //
     5  // Permission is hereby granted, free of charge, to any person obtaining a copy
     6  // of this software and associated documentation files (the "Software"), to deal
     7  // in the Software without restriction, including without limitation the rights
     8  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     9  // copies of the Software, and to permit persons to whom the Software is
    10  // furnished to do so, subject to the following conditions:
    11  //
    12  // The above copyright notice and this permission notice shall be included in all
    13  // copies or substantial portions of the Software.
    14  //
    15  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    16  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    17  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    18  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    19  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    20  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    21  // SOFTWARE.
    22  package taskrun
    23  
    24  import (
    25  	"errors"
    26  	"fmt"
    27  
    28  	"os"
    29  	"os/user"
    30  	"path/filepath"
    31  	"regexp"
    32  	"strconv"
    33  	"strings"
    34  
    35  	"github.com/imdario/mergo"
    36  
    37  	"github.com/swaros/contxt/module/configure"
    38  	"github.com/swaros/contxt/module/dirhandle"
    39  	"github.com/swaros/contxt/module/systools"
    40  	"github.com/swaros/manout"
    41  	"gopkg.in/yaml.v2"
    42  )
    43  
    44  const (
    45  	incFileParseError  = 105
    46  	mainFileParseError = 104
    47  )
    48  
    49  // FindTemplate searchs for Template files in different spaces
    50  func FindTemplate() (string, bool) {
    51  	// 1. looking in user mirror path
    52  	usr, err := user.Current()
    53  	if err != nil {
    54  		GetLogger().Fatal(err)
    55  	}
    56  
    57  	homeDirYml := usr.HomeDir + configure.DefaultPath + configure.MirrorPath + DefaultExecYaml
    58  	exists, exerr := dirhandle.Exists(homeDirYml)
    59  	if exerr == nil && exists {
    60  		return homeDirYml, true
    61  	}
    62  
    63  	// 2. looking in current path with user name as prefix
    64  	dir, curerr := dirhandle.Current()
    65  	if curerr == nil {
    66  		userYml := dir + string(os.PathSeparator) + usr.Username + defaultExecYamlName
    67  		exists, exerr = dirhandle.Exists(userYml)
    68  		if exerr == nil && exists {
    69  			return userYml, true
    70  		}
    71  
    72  		// 3. plain template in current dir
    73  		regularPath := dir + DefaultExecYaml
    74  		exists, exerr = dirhandle.Exists(regularPath)
    75  		if exerr == nil && exists {
    76  			return regularPath, true
    77  		}
    78  	}
    79  
    80  	return "", false
    81  }
    82  
    83  // GetTemplate return current template, the absolute path,if it exists, any error
    84  func GetTemplate() (configure.RunConfig, string, bool, error) {
    85  
    86  	foundPath, success := FindTemplate()
    87  	var template configure.RunConfig
    88  	if !success {
    89  		return template, "", false, errors.New("template not found or have failures")
    90  	}
    91  	ctemplate, err := GetPwdTemplate(foundPath)
    92  	if err == nil {
    93  		// checking required shared templates
    94  		// and merge them into the current template
    95  		if len(ctemplate.Config.Require) > 0 {
    96  			for _, reqSource := range ctemplate.Config.Require {
    97  				GetLogger().WithField("path", reqSource).Debug("handle required ")
    98  				fullPath, pathError := CheckOrCreateUseConfig(reqSource)
    99  				if pathError == nil {
   100  					GetLogger().WithField("path", fullPath).Info("require path resolved")
   101  					subTemplate, tError := GetPwdTemplate(fullPath + string(os.PathSeparator) + DefaultExecYaml)
   102  					if tError == nil {
   103  						mergo.Merge(&ctemplate, subTemplate, mergo.WithOverride, mergo.WithAppendSlice)
   104  						GetLogger().WithField("template", ctemplate).Debug("merged")
   105  					} else {
   106  						return template, "", false, tError
   107  					}
   108  				} else {
   109  					return template, "", false, pathError
   110  				}
   111  			}
   112  		} else {
   113  			GetLogger().Debug("no required files configured")
   114  		}
   115  
   116  		return ctemplate, foundPath, true, nil
   117  	}
   118  
   119  	return template, "", false, err
   120  }
   121  
   122  func getIncludeConfigPath(path string) (string, string, bool) {
   123  	fullPath := filepath.Dir(path)
   124  	checkIncPath := fullPath + string(os.PathSeparator) + ".inc.contxt.yml"
   125  	existing, fileerror := dirhandle.Exists(checkIncPath)
   126  	if fileerror != nil || !existing {
   127  		return checkIncPath, fullPath, false
   128  	}
   129  	GetLogger().WithField("include-config", checkIncPath).Debug("found include setting")
   130  	return checkIncPath, fullPath, true
   131  }
   132  
   133  func getIncludeConfig(path string) (configure.IncludePaths, string, string, bool) {
   134  	var importTemplate configure.IncludePaths
   135  	checkIncPath, fullPath, existing := getIncludeConfigPath(path)
   136  	if !existing {
   137  		return importTemplate, checkIncPath, fullPath, false
   138  	}
   139  
   140  	file, ferr := os.ReadFile(checkIncPath)
   141  	if ferr != nil {
   142  		return importTemplate, checkIncPath, fullPath, false
   143  	}
   144  	err := yaml.Unmarshal(file, &importTemplate)
   145  	if err != nil {
   146  		fmt.Println(manout.MessageCln(manout.ForeRed, "error reading include config file: ", manout.ForeWhite, checkIncPath), err)
   147  		systools.Exit(incFileParseError)
   148  	}
   149  	return importTemplate, checkIncPath, fullPath, true
   150  }
   151  
   152  // LoadIncTempalte check if .inc.contxt.yml files exists
   153  // and if this is the case the content will be loaded and all defined paths
   154  // used to get values for parsing the template file
   155  func LoadIncTempalte(path string) (string, bool) {
   156  	importTemplate, _, fullPath, existing := getIncludeConfig(path)
   157  	if !existing {
   158  		return "", false
   159  	}
   160  	// imports by Include.Folders
   161  	if len(importTemplate.Include.Folders) > 0 || importTemplate.Include.Basedir {
   162  		GetLogger().WithField("file", path).Info("parsing task-file")
   163  		var dirs []string = importTemplate.Include.Folders
   164  		if importTemplate.Include.Basedir {
   165  			GetLogger().WithField("dir", fullPath).Debug("add parsing source dir")
   166  			dirs = append(dirs, fullPath)
   167  		}
   168  
   169  		parsedTemplate, perr := ImportFolders(path, dirs...)
   170  		if perr != nil {
   171  			fmt.Println(perr)
   172  			fmt.Println(manout.MessageCln(manout.ForeRed, "error parsing files from path: ", manout.ForeWhite, path), perr)
   173  			systools.Exit(incFileParseError)
   174  		}
   175  		return parsedTemplate, true
   176  	}
   177  	return "", false
   178  }
   179  
   180  // GetParsedTemplateSource Returns the soucecode of the template
   181  // including parsing placeholders
   182  func GetParsedTemplateSource(path string) (string, error) {
   183  	existing, fileerror := dirhandle.Exists(path)
   184  	if fileerror != nil {
   185  		return "", fileerror
   186  	}
   187  	if existing {
   188  		// first check if includes exists
   189  		templateSource, inExists := LoadIncTempalte(path)
   190  		if inExists {
   191  			return templateSource, nil
   192  		}
   193  		// no imports .... load template file
   194  		file, ferr := os.ReadFile(path)
   195  		if ferr != nil {
   196  			return "", ferr
   197  		}
   198  		return string(file), nil
   199  	}
   200  	notExistsErr := errors.New("file not exists")
   201  	return "", notExistsErr
   202  }
   203  
   204  // GetPwdTemplate returns the template path if exists.
   205  // it also parses the content of the template
   206  // against imports and handles them
   207  func GetPwdTemplate(path string) (configure.RunConfig, error) {
   208  	var template configure.RunConfig
   209  	source, err := GetParsedTemplateSource(path)
   210  	if err != nil {
   211  		return template, err
   212  	}
   213  
   214  	err2 := yaml.Unmarshal([]byte(source), &template)
   215  
   216  	if err2 != nil {
   217  		printErrSource(err2, source)
   218  		return template, err2
   219  	}
   220  	return template, nil
   221  }
   222  
   223  func getLineNr(str string) (int, error) {
   224  	re := regexp.MustCompile("[0-9]+")
   225  	found := re.FindAllString(str, -1)
   226  	if len(found) > 0 {
   227  		return strconv.Atoi(found[0])
   228  	}
   229  	return -1, errors.New("no line number found in message " + str)
   230  }
   231  
   232  func printErrSource(err error, source string) {
   233  	errPlain := err.Error()
   234  	errParts := strings.Split(errPlain, ":")
   235  
   236  	if len(errParts) == 3 { // this is depending an regular error message from yaml. like: yaml: line 3: mapping values are not allowed in this context
   237  		if lineNr, lErr := getLineNr(errParts[1]); lErr == nil {
   238  			sourceParts := strings.Split(source, "\n")
   239  			if len(sourceParts) >= lineNr && lineNr >= 0 {
   240  				min := lineNr - 3
   241  				max := lineNr + 3
   242  				if min < 0 {
   243  					min = 0
   244  				}
   245  				if max > len(sourceParts) {
   246  					max = len(sourceParts)
   247  				}
   248  				for i := min; i < max; i++ {
   249  					nrback := manout.BackWhite
   250  					nrFore := manout.ForeBlue
   251  					msgFore := manout.ForeCyan
   252  					msgBack := ""
   253  					msg := ""
   254  					if i == lineNr {
   255  						nrback = manout.BackLightRed
   256  						nrFore = manout.ForeRed
   257  						msgFore = manout.ForeWhite
   258  						msgBack = manout.BackRed
   259  						msg = errParts[2]
   260  					}
   261  
   262  					padLineNr := fmt.Sprintf("%4d |", i)
   263  					outstr := manout.MessageCln(
   264  						nrback,
   265  						nrFore,
   266  						" ",
   267  						padLineNr,
   268  						manout.CleanTag,
   269  						msgFore,
   270  						msgBack,
   271  						sourceParts[i],
   272  						manout.CleanTag,
   273  						manout.ForeLightYellow,
   274  						" ",
   275  						msg)
   276  
   277  					fmt.Println(outstr)
   278  				}
   279  			} else {
   280  				fmt.Println("source parsing faliure", sourceParts)
   281  			}
   282  		} else {
   283  			fmt.Println(lErr)
   284  		}
   285  	} else {
   286  		fmt.Println("unexpected message format ", len(errParts), " ", errParts)
   287  	}
   288  }