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

     1  // MIT License
     2  //
     3  // Copyright (c) 2020 Thomas Ziegler <thomas.zglr@googlemail.com>. All rights reserved.
     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  
    23  // AINC-NOTE-0815
    24  
    25  package runner
    26  
    27  import (
    28  	"encoding/json"
    29  	"fmt"
    30  	"log"
    31  	"os"
    32  	"path/filepath"
    33  	"strings"
    34  
    35  	"github.com/imdario/mergo"
    36  	"github.com/swaros/contxt/module/configure"
    37  	"github.com/swaros/contxt/module/ctemplate"
    38  	"github.com/swaros/contxt/module/dirhandle"
    39  	"github.com/swaros/contxt/module/mimiclog"
    40  	"github.com/swaros/contxt/module/systools"
    41  	"github.com/swaros/contxt/module/tasks"
    42  	"github.com/swaros/manout"
    43  )
    44  
    45  const (
    46  	DefaultSubPath     = "/.contxt/shared/"
    47  	DefaultVersionConf = "version.conf"
    48  	DefaultExecYaml    = string(os.PathSeparator) + ".contxt.yml"
    49  )
    50  
    51  // SharedHelper is a helper to handle shared content
    52  // that is hosted on github
    53  type SharedHelper struct {
    54  	basePath       string
    55  	defaultSubPath string
    56  	versionConf    string
    57  	logger         mimiclog.Logger
    58  }
    59  
    60  // NewSharedHelper returns a new instance of the SharedHelper depending on the user home dir
    61  func NewSharedHelper() *SharedHelper {
    62  	if path, err := os.UserHomeDir(); err != nil {
    63  		panic(err)
    64  	} else {
    65  		return NewSharedHelperWithPath(path)
    66  	}
    67  }
    68  
    69  // NewSharedHelperWithPath returns a new instance of the SharedHelper depending on the given path
    70  func NewSharedHelperWithPath(basePath string) *SharedHelper {
    71  	return &SharedHelper{basePath, DefaultSubPath, DefaultVersionConf, mimiclog.NewNullLogger()}
    72  }
    73  
    74  // SetLogger sets the logger for the shared helper. the default is a null logger
    75  func (sh *SharedHelper) SetLogger(logger mimiclog.Logger) {
    76  	sh.logger = logger
    77  }
    78  
    79  // GetBasePath returns the base path of the shared folder
    80  func (sh *SharedHelper) GetBasePath() string {
    81  	return sh.basePath
    82  }
    83  
    84  // GetSharedPath returns the full path of the given shared name
    85  func (sh *SharedHelper) GetSharedPath(sharedName string) string {
    86  	fileName := systools.SanitizeFilename(sharedName, true) // make sure we have an valid filename
    87  	return filepath.Clean(filepath.FromSlash(sh.basePath + sh.defaultSubPath + fileName))
    88  }
    89  
    90  // CheckOrCreateUseConfig get a usecase like swaros/ctx-git and checks
    91  // if a local copy of them exists.
    92  // if they not exists it creates the local directoy and uses git to
    93  // clone the content.
    94  // afterwards it writes a version.conf, in the forlder above of content,
    95  // and stores the current hashes
    96  func (sh *SharedHelper) CheckOrCreateUseConfig(externalUseCase string) (string, error) {
    97  	sh.logger.Info("trying to solve usecase", externalUseCase)
    98  	path := ""                                      // just as default
    99  	var defaultError error                          // just as default
   100  	sharedPath := sh.GetSharedPath(externalUseCase) // get the main path for shared content
   101  	if sharedPath != "" {                           // no error and not an empty path
   102  		isThere, dirError := dirhandle.Exists(sharedPath) // do we have the main shared directory?
   103  		sh.logger.Info("using shared contxt tasks", sharedPath)
   104  		if dirError != nil { // this is NOT related to not exists. it is an error while checking if the path exists
   105  			return "", dirError
   106  		} else {
   107  			if !isThere { // directory not exists
   108  				sh.logger.Info("shared directory not exists. try to checkout by git (github)")
   109  				path, defaultError = sh.createUseByGit(externalUseCase, sharedPath) // create dirs and checkout content if possible. fit the path also
   110  				if defaultError != nil {
   111  					sh.logger.Error("unable to create shared usecase", externalUseCase, defaultError)
   112  					return "", defaultError
   113  				}
   114  
   115  			} else { // directory exists
   116  				path = sh.getSourcePath(sharedPath)
   117  				exists, _ := dirhandle.Exists(path)
   118  				if !exists {
   119  					manout.Error("USE Error", "shared usecase not exist and can not be downloaded", " ", path)
   120  					systools.Exit(systools.ErrorBySystem)
   121  				}
   122  				sh.logger.Debug("shared directory exists. use them", path)
   123  			}
   124  		}
   125  	}
   126  	return path, nil
   127  }
   128  
   129  // createUseByGit creates the local directory and uses git to clone the content
   130  // the version.conf will be created also and the current hashes will be stored
   131  // in them.
   132  // if the git checkout fails, it will check if the local directory exists
   133  // and uses them instead
   134  // if the local directory also not exists, it will exit with an error
   135  func (sh *SharedHelper) createUseByGit(usecase, pathTouse string) (string, error) {
   136  	usecase, version := sh.GetUseInfo(usecase, pathTouse) // get needed git ref and usecase by the requested usage (like from swaros/ctx-gt@v0.0.1)
   137  	sh.logger.Info("trying to checkout", usecase, "by git.", pathTouse, " version:", version)
   138  	path := ""
   139  	gitCmd := "git ls-remote --refs https://github.com/" + usecase
   140  
   141  	var gitInfo []string
   142  	shellRunner := tasks.GetShellRunner()
   143  	internalExitCode, cmdError, _ := shellRunner.Exec(gitCmd, func(feed string, e error) bool {
   144  		gitInfo = strings.Split(feed, "\t")
   145  		if len(gitInfo) >= 2 && gitInfo[1] == version {
   146  			sh.logger.Debug("found matching version")
   147  			cfg, versionErr := sh.getOrCreateRepoConfig(gitInfo[1], gitInfo[0], usecase, pathTouse)
   148  			if versionErr == nil {
   149  				cfg = sh.takeCareAboutRepo(pathTouse, cfg)
   150  				path = cfg.Path
   151  			}
   152  		}
   153  		return true
   154  	}, func(process *os.Process) {
   155  		pidStr := fmt.Sprintf("%d", process.Pid)
   156  		sh.logger.Debug("git process id", pidStr)
   157  	})
   158  
   159  	if internalExitCode != systools.ExitOk {
   160  		// git info was failing. so we did not create anything right now by using git
   161  		// so now we have to check if this is a local repository
   162  		sh.logger.Warn("failed get version info from git", internalExitCode, cmdError)
   163  		exists, _ := dirhandle.Exists(pathTouse)
   164  		if exists {
   165  			existsSource, _ := dirhandle.Exists(sh.getSourcePath(pathTouse))
   166  			if existsSource {
   167  				return sh.getSourcePath(pathTouse), nil
   168  			}
   169  		}
   170  		// this is not working at all. so we exit with a error
   171  		sh.logger.Critical("Local Usage folder not exists (+ ./source)", pathTouse)
   172  		return "", fmt.Errorf("invalid github repository and local usage folder not exists (+ ./source) [%s]", pathTouse)
   173  	}
   174  	return path, nil
   175  }
   176  
   177  // GetUseInfo returns the usecase and the version from the given usecase-string
   178  func (sh *SharedHelper) GetUseInfo(usecase, _ string) (string, string) {
   179  	parts := strings.Split(usecase, "@")
   180  	version := "refs/heads/main"
   181  	if len(parts) > 1 {
   182  		usecase = parts[0]
   183  		version = "refs/tags/" + parts[1]
   184  	}
   185  	return usecase, version
   186  }
   187  
   188  func (sh *SharedHelper) GetSharedPathForUseCase(usecase string) string {
   189  	return sh.GetSharedPath(usecase)
   190  }
   191  
   192  func (sh *SharedHelper) getSourcePath(pathTouse string) string {
   193  	return fmt.Sprintf("%s%s%s", pathTouse, string(os.PathSeparator), "source")
   194  }
   195  
   196  func (sh *SharedHelper) getVersionOsPath(pathTouse string) string {
   197  	return fmt.Sprintf("%s%s%s", pathTouse, string(os.PathSeparator), sh.versionConf)
   198  }
   199  
   200  func (sh *SharedHelper) getOrCreateRepoConfig(ref, hash, usecase, pathTouse string) (configure.GitVersionInfo, error) {
   201  	var versionConf configure.GitVersionInfo
   202  	versionFilename := sh.getVersionOsPath(pathTouse)
   203  
   204  	// check if the useage folder exists and create them if not
   205  	if pathWErr := sh.createSharedUsageDir(pathTouse); pathWErr != nil {
   206  		return versionConf, pathWErr
   207  	}
   208  
   209  	hashChk, hashError := dirhandle.Exists(versionFilename)
   210  	if hashError != nil {
   211  		return versionConf, hashError
   212  	} else if !hashChk {
   213  
   214  		versionConf.Repositiory = usecase
   215  		versionConf.HashUsed = hash
   216  		versionConf.Reference = ref
   217  
   218  		sh.logger.Info("try to create version info", versionFilename)
   219  		if werr := sh.writeGitConfig(versionFilename, versionConf); werr != nil {
   220  			sh.logger.Error("unable to create version info ", versionFilename, werr)
   221  			return versionConf, werr
   222  		}
   223  		sh.logger.Debug("created version info", versionConf)
   224  	} else {
   225  		versionConf, vErr := sh.loadGitConfig(versionFilename, versionConf)
   226  		sh.logger.Debug("loaded version info", versionConf)
   227  		return versionConf, vErr
   228  	}
   229  	return versionConf, nil
   230  }
   231  
   232  func (sh *SharedHelper) createSharedUsageDir(sharedPath string) error {
   233  	exists, _ := dirhandle.Exists(sharedPath)
   234  	if !exists {
   235  		// create dir
   236  		sh.logger.Info("shared directory not exists. try to create them", sharedPath)
   237  		err := os.MkdirAll(sharedPath, os.ModePerm)
   238  		if err != nil {
   239  			log.Fatal(err)
   240  			return err
   241  		}
   242  	}
   243  	sh.logger.Info("shared directory exists already", sharedPath)
   244  	return nil
   245  }
   246  
   247  func (sh *SharedHelper) HandleUsecase(externalUseCase string) string {
   248  	path, _ := sh.CheckOrCreateUseConfig(externalUseCase)
   249  	return path
   250  }
   251  
   252  func (sh *SharedHelper) StripContxtUseDir(path string) string {
   253  	sep := fmt.Sprintf("%c", os.PathSeparator)
   254  	newpath := strings.TrimSuffix(path, sep)
   255  
   256  	parts := strings.Split(newpath, sep)
   257  	cleanDir := ""
   258  	if len(parts) > 1 && parts[len(parts)-1] == "source" {
   259  		parts = parts[:len(parts)-1]
   260  	}
   261  	for _, subpath := range parts {
   262  		if subpath != "" {
   263  			cleanDir = cleanDir + sep + subpath
   264  		}
   265  
   266  	}
   267  	return cleanDir
   268  }
   269  
   270  func (sh *SharedHelper) UpdateUseCase(fullPath string) {
   271  	//usecase, version := getUseInfo("", fullPath)
   272  	exists, config, _ := sh.getRepoConfig(fullPath)
   273  	if exists {
   274  		sh.logger.Debug("update shared usecase", fullPath, config)
   275  		fmt.Println(manout.MessageCln(" remote:", manout.ForeLightBlue, " ", config.Repositiory))
   276  		sh.updateGitRepo(config, true, fullPath)
   277  
   278  	} else {
   279  		fmt.Println(manout.MessageCln(" local shared:", manout.ForeYellow, " ", fullPath, manout.ForeDarkGrey, "(not updatable. ignored)"))
   280  	}
   281  }
   282  
   283  // ListUseCases returns a list of all available shared usecases
   284  func (sh *SharedHelper) ListUseCases(fullPath bool) ([]string, error) {
   285  	var sharedDirs []string
   286  	sharedPath := sh.GetSharedPath("")
   287  
   288  	errWalk := filepath.Walk(sharedPath, func(path string, info os.FileInfo, err error) error {
   289  		if err != nil {
   290  			return err
   291  		}
   292  
   293  		if !info.IsDir() {
   294  			var basename = filepath.Base(path)
   295  			var directory = filepath.Dir(path)
   296  
   297  			if basename == ".contxt.yml" {
   298  				if fullPath {
   299  					sharedDirs = append(sharedDirs, sh.StripContxtUseDir(directory))
   300  				} else {
   301  					releative := strings.Replace(sh.StripContxtUseDir(directory), sharedPath, "", 1)
   302  					sharedDirs = append(sharedDirs, releative)
   303  				}
   304  			}
   305  		}
   306  		return nil
   307  	})
   308  	return sharedDirs, errWalk
   309  
   310  }
   311  
   312  func (sh *SharedHelper) getRepoConfig(pathTouse string) (bool, configure.GitVersionInfo, error) {
   313  	hashChk, hashError := dirhandle.Exists(sh.getVersionOsPath(pathTouse))
   314  	var versionConf configure.GitVersionInfo
   315  	if hashError != nil {
   316  		return false, versionConf, hashError
   317  	} else if hashChk {
   318  		versionConf, err := sh.loadGitConfig(sh.getVersionOsPath(pathTouse), versionConf)
   319  		return err == nil, versionConf, err
   320  	}
   321  	sh.logger.Warn("no version info. seems to be a local shared.", pathTouse)
   322  	return false, versionConf, nil
   323  }
   324  
   325  func (sh *SharedHelper) loadGitConfig(path string, config configure.GitVersionInfo) (configure.GitVersionInfo, error) {
   326  
   327  	file, _ := os.Open(path)
   328  	defer file.Close()
   329  	decoder := json.NewDecoder(file)
   330  
   331  	err := decoder.Decode(&config)
   332  	return config, err
   333  
   334  }
   335  
   336  func (sh *SharedHelper) updateGitRepo(config configure.GitVersionInfo, doUpdate bool, workDir string) bool {
   337  	if config.Repositiory != "" {
   338  		fmt.Print(manout.MessageCln(" Reference:", manout.ForeLightBlue, " ", config.Reference))
   339  		fmt.Print(manout.MessageCln(" Current:", manout.ForeLightBlue, " ", config.HashUsed))
   340  		returnBool := false
   341  		sh.checkGitVersionInfo(config.Repositiory, func(hash, reference string) {
   342  			if reference == config.Reference {
   343  				fmt.Print(manout.MessageCln(manout.ForeLightGreen, "[EXISTS]"))
   344  				if hash == config.HashUsed {
   345  					fmt.Print(manout.MessageCln(manout.ForeLightGreen, " [up to date]"))
   346  				} else {
   347  					fmt.Print(manout.MessageCln(manout.ForeYellow, " [update found]"))
   348  					if doUpdate {
   349  						gCode := sh.executeGitUpdate(sh.getSourcePath(workDir))
   350  						if gCode == systools.ExitOk {
   351  							config.HashUsed = hash
   352  							if werr := sh.writeGitConfig(workDir+"/"+sh.versionConf, config); werr != nil {
   353  								manout.Error("unable to create version info", werr)
   354  								returnBool = false
   355  							} else {
   356  								returnBool = true
   357  							}
   358  						}
   359  					}
   360  				}
   361  			}
   362  		})
   363  		fmt.Println(".")
   364  		return returnBool
   365  	}
   366  	return false
   367  }
   368  
   369  func (sh *SharedHelper) checkGitVersionInfo(usecase string, callback func(string, string)) (int, int, error) {
   370  	gitCmd := "git ls-remote --refs https://github.com/" + usecase
   371  	shellRunner := tasks.GetShellRunner()
   372  	internalExitCode, cmdError, err := shellRunner.Exec(gitCmd, func(feed string, e error) bool {
   373  		gitInfo := strings.Split(feed, "\t")
   374  		if len(gitInfo) >= 2 {
   375  			callback(gitInfo[0], gitInfo[1])
   376  		}
   377  		return true
   378  	}, func(process *os.Process) {
   379  		pidStr := fmt.Sprintf("%d", process.Pid)
   380  		sh.logger.Debug("git process id", pidStr)
   381  	})
   382  	return internalExitCode, cmdError, err
   383  }
   384  
   385  func (sh *SharedHelper) executeGitUpdate(path string) int {
   386  	currentDir, _ := dirhandle.Current()
   387  	os.Chdir(path)
   388  	gitCmd := "git pull"
   389  
   390  	shellRunner := tasks.GetShellRunner()
   391  	exitCode, _, _ := shellRunner.Exec(gitCmd, func(feed string, e error) bool {
   392  		fmt.Println(manout.MessageCln("\tgit: ", manout.ForeLightYellow, feed))
   393  		return true
   394  	}, func(process *os.Process) {
   395  		pidStr := fmt.Sprintf("%d", process.Pid)
   396  		sh.logger.Debug("git process id", pidStr)
   397  	})
   398  	os.Chdir(currentDir)
   399  	return exitCode
   400  }
   401  
   402  func (sh *SharedHelper) writeGitConfig(path string, config configure.GitVersionInfo) error {
   403  	b, _ := json.MarshalIndent(config, "", " ")
   404  	if err := os.WriteFile(path, b, 0644); err != nil {
   405  		sh.logger.Error("can not create file ", path, " ", err)
   406  		return err
   407  	}
   408  	return nil
   409  }
   410  
   411  func (sh *SharedHelper) takeCareAboutRepo(pathTouse string, config configure.GitVersionInfo) configure.GitVersionInfo {
   412  	exists, _ := dirhandle.Exists(sh.getSourcePath(pathTouse))
   413  	if !exists { // source folder not exists
   414  		if config.Repositiory != "" { // no repository info exists
   415  			sh.createSharedUsageDir(pathTouse) // check if the usage folder exists and create them if not
   416  			gitCmd := "git clone https://github.com/" + config.Repositiory + ".git " + sh.getSourcePath(pathTouse)
   417  			sh.logger.Info("using git to create new checkout from repo", gitCmd)
   418  			shellRunner := tasks.GetShellRunner()
   419  			codeInt, codeCmd, err := shellRunner.Exec(gitCmd, func(feed string, e error) bool {
   420  				fmt.Println(manout.MessageCln("\tgit: ", manout.ForeLightYellow, feed))
   421  				return true
   422  			}, func(process *os.Process) {
   423  				pidStr := fmt.Sprintf("%d", process.Pid)
   424  				sh.logger.Debug("git process id", pidStr)
   425  			})
   426  			sh.logger.Debug("git execution result", codeInt, codeCmd, err)
   427  		} else {
   428  			sh.logger.Debug("no repository info exists. seems to be a local shared.", pathTouse)
   429  		}
   430  	}
   431  	config.Path = sh.getSourcePath(pathTouse)
   432  	return config
   433  }
   434  
   435  // Merged the required paths into the given template.
   436  // this is loading the .contxt.yml from the required path, located in the shared folder
   437  // and merges them into the given template.
   438  // so the current template will be extended by the content of these files.
   439  func (sh *SharedHelper) MergeRequiredPaths(ctemplate *configure.RunConfig, templateHandler *ctemplate.Template) error {
   440  	if len(ctemplate.Config.Require) > 0 {
   441  		// we have to check if the required files exists
   442  		for _, reqSource := range ctemplate.Config.Require {
   443  			sh.logger.Info("shared: handle required ", reqSource)
   444  			fullPath, pathError := sh.CheckOrCreateUseConfig(reqSource)
   445  			if pathError == nil {
   446  				sh.logger.Debug("shared: merge required", fullPath)
   447  				subTemplate, tError := templateHandler.LoadV2ByAbsolutePath(fullPath + string(os.PathSeparator) + DefaultExecYaml)
   448  				if tError == nil {
   449  					return mergo.Merge(ctemplate, subTemplate, mergo.WithOverride, mergo.WithAppendSlice)
   450  				} else {
   451  					return tError
   452  				}
   453  			} else {
   454  				return pathError
   455  			}
   456  		}
   457  	} else {
   458  		sh.logger.Debug("shared: there are no files defined for requirement")
   459  	}
   460  	return nil
   461  }