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 }