github.com/alloyci/alloy-runner@v1.0.1-0.20180222164613-925503ccafd6/shells/abstract.go (about) 1 package shells 2 3 import ( 4 "errors" 5 "fmt" 6 "net/url" 7 "path" 8 "path/filepath" 9 "strconv" 10 "strings" 11 12 "gitlab.com/gitlab-org/gitlab-runner/common" 13 "gitlab.com/gitlab-org/gitlab-runner/helpers/tls" 14 ) 15 16 type AbstractShell struct { 17 } 18 19 func (b *AbstractShell) GetFeatures(features *common.FeaturesInfo) { 20 features.Artifacts = true 21 features.Cache = true 22 } 23 24 func (b *AbstractShell) writeCdBuildDir(w ShellWriter, info common.ShellScriptInfo) { 25 w.Cd(info.Build.FullProjectDir()) 26 } 27 28 func (b *AbstractShell) writeExports(w ShellWriter, info common.ShellScriptInfo) { 29 for _, variable := range info.Build.GetAllVariables() { 30 w.Variable(variable) 31 } 32 } 33 34 func (b *AbstractShell) writeGitSSLConfig(w ShellWriter, build *common.Build, where []string) { 35 repoURL, err := url.Parse(build.Runner.URL) 36 if err != nil { 37 w.Warning("git SSL config: Can't parse repository URL. %s", err) 38 return 39 } 40 41 repoURL.Path = "" 42 host := repoURL.String() 43 variables := build.GetCITLSVariables() 44 args := append([]string{"config"}, where...) 45 46 for variable, config := range map[string]string{ 47 tls.VariableCAFile: "sslCAInfo", 48 tls.VariableCertFile: "sslCert", 49 tls.VariableKeyFile: "sslKey", 50 } { 51 if variables.Get(variable) == "" { 52 continue 53 } 54 55 key := fmt.Sprintf("http.%s.%s", host, config) 56 value := w.TmpFile(variable) 57 w.Command("git", append(args, key, value)...) 58 } 59 60 return 61 } 62 63 func (b *AbstractShell) writeCloneCmd(w ShellWriter, build *common.Build, projectDir string) { 64 templateDir := w.MkTmpDir("git-template") 65 args := []string{"clone", "--no-checkout", build.GetRemoteURL(), projectDir, "--template", templateDir} 66 67 w.RmDir(projectDir) 68 templateFile := path.Join(templateDir, "config") 69 w.Command("git", "config", "-f", templateFile, "fetch.recurseSubmodules", "false") 70 if build.IsSharedEnv() { 71 b.writeGitSSLConfig(w, build, []string{"-f", templateFile}) 72 } 73 74 if depth := build.GetGitDepth(); depth != "" { 75 w.Notice("Cloning repository for %s with git depth set to %s...", build.GitInfo.Ref, depth) 76 args = append(args, "--depth", depth, "--branch", build.GitInfo.Ref) 77 } else { 78 w.Notice("Cloning repository...") 79 } 80 81 w.Command("git", args...) 82 w.Cd(projectDir) 83 } 84 85 func (b *AbstractShell) writeFetchCmd(w ShellWriter, build *common.Build, projectDir string, gitDir string) { 86 depth := build.GetGitDepth() 87 88 w.IfDirectory(gitDir) 89 if depth != "" { 90 w.Notice("Fetching changes for %s with git depth set to %s...", build.GitInfo.Ref, depth) 91 } else { 92 w.Notice("Fetching changes...") 93 } 94 w.Cd(projectDir) 95 w.Command("git", "config", "fetch.recurseSubmodules", "false") 96 97 if build.IsSharedEnv() { 98 b.writeGitSSLConfig(w, build, nil) 99 } 100 101 // Remove .git/{index,shallow,HEAD}.lock files from .git, which can fail the fetch command 102 // The file can be left if previous build was terminated during git operation 103 w.RmFile(".git/index.lock") 104 w.RmFile(".git/shallow.lock") 105 w.RmFile(".git/HEAD.lock") 106 107 w.IfFile(".git/hooks/post-checkout") 108 w.RmFile(".git/hooks/post-checkout") 109 w.EndIf() 110 111 w.Command("git", "clean", "-ffdx") 112 w.Command("git", "reset", "--hard") 113 w.Command("git", "remote", "set-url", "origin", build.GetRemoteURL()) 114 if depth != "" { 115 var refspec string 116 if build.GitInfo.RefType == common.RefTypeTag { 117 refspec = "+refs/tags/" + build.GitInfo.Ref + ":refs/tags/" + build.GitInfo.Ref 118 } else { 119 refspec = "+refs/heads/" + build.GitInfo.Ref + ":refs/remotes/origin/" + build.GitInfo.Ref 120 } 121 w.Command("git", "fetch", "--depth", depth, "origin", "--prune", refspec) 122 } else { 123 w.Command("git", "fetch", "origin", "--prune", "+refs/heads/*:refs/remotes/origin/*", "+refs/tags/*:refs/tags/*", "+refs/pull/*:refs/remotes/origin/*") 124 } 125 w.Else() 126 b.writeCloneCmd(w, build, projectDir) 127 w.EndIf() 128 } 129 130 func (b *AbstractShell) writeCheckoutCmd(w ShellWriter, build *common.Build) { 131 w.Notice("Checking out %s as %s...", build.GitInfo.Sha[0:8], build.GitInfo.Ref) 132 w.Command("git", "checkout", "-f", "-q", build.GitInfo.Sha) 133 } 134 135 func (b *AbstractShell) writeSubmoduleUpdateCmd(w ShellWriter, build *common.Build, recursive bool) { 136 if recursive { 137 w.Notice("Updating/initializing submodules recursively...") 138 } else { 139 w.Notice("Updating/initializing submodules...") 140 } 141 142 // Sync .git/config to .gitmodules in case URL changes (e.g. new build token) 143 args := []string{"submodule", "sync"} 144 if recursive { 145 args = append(args, "--recursive") 146 } 147 w.Command("git", args...) 148 149 // Update / initialize submodules 150 updateArgs := []string{"submodule", "update", "--init"} 151 foreachArgs := []string{"submodule", "foreach"} 152 if recursive { 153 updateArgs = append(updateArgs, "--recursive") 154 foreachArgs = append(foreachArgs, "--recursive") 155 } 156 157 // Clean changed files in submodules 158 // "git submodule update --force" option not supported in Git 1.7.1 (shipped with CentOS 6) 159 w.Command("git", append(foreachArgs, "git", "reset", "--hard")...) 160 w.Command("git", updateArgs...) 161 } 162 163 func (b *AbstractShell) cacheFile(build *common.Build, userKey string) (key, file string) { 164 if build.CacheDir == "" { 165 return 166 } 167 168 // Deduce cache key 169 key = path.Join(build.JobInfo.Name, build.GitInfo.Ref) 170 if userKey != "" { 171 key = build.GetAllVariables().ExpandValue(userKey) 172 } 173 174 // Ignore cache without the key 175 if key == "" { 176 return 177 } 178 179 file = path.Join(build.CacheDir, key, "cache.zip") 180 file, err := filepath.Rel(build.BuildDir, file) 181 if err != nil { 182 return "", "" 183 } 184 return 185 } 186 187 func (b *AbstractShell) guardRunnerCommand(w ShellWriter, runnerCommand string, action string, f func()) { 188 if runnerCommand == "" { 189 w.Warning("%s is not supported by this executor.", action) 190 return 191 } 192 193 w.IfCmd(runnerCommand, "--version") 194 f() 195 w.Else() 196 w.Warning("Missing %s. %s is disabled.", runnerCommand, action) 197 w.EndIf() 198 } 199 200 func (b *AbstractShell) cacheExtractor(w ShellWriter, info common.ShellScriptInfo) error { 201 for _, cacheOptions := range info.Build.Cache { 202 203 // Create list of files to extract 204 archiverArgs := []string{} 205 for _, path := range cacheOptions.Paths { 206 archiverArgs = append(archiverArgs, "--path", path) 207 } 208 209 if cacheOptions.Untracked { 210 archiverArgs = append(archiverArgs, "--untracked") 211 } 212 213 // Skip restoring cache if no cache is defined 214 if len(archiverArgs) < 1 { 215 continue 216 } 217 218 // Skip extraction if no cache is defined 219 cacheKey, cacheFile := b.cacheFile(info.Build, cacheOptions.Key) 220 if cacheKey == "" { 221 continue 222 } 223 224 if ok, err := cacheOptions.CheckPolicy(common.CachePolicyPull); err != nil { 225 return fmt.Errorf("%s for %s", err, cacheKey) 226 } else if !ok { 227 w.Notice("Not downloading cache %s due to policy", cacheKey) 228 continue 229 } 230 231 args := []string{ 232 "cache-extractor", 233 "--file", cacheFile, 234 "--timeout", strconv.Itoa(info.Build.GetCacheRequestTimeout()), 235 } 236 237 // Generate cache download address 238 if url := getCacheDownloadURL(info.Build, cacheKey); url != nil { 239 args = append(args, "--url", url.String()) 240 } 241 242 // Execute cache-extractor command. Failure is not fatal. 243 b.guardRunnerCommand(w, info.RunnerCommand, "Extracting cache", func() { 244 w.Notice("Checking cache for %s...", cacheKey) 245 w.IfCmdWithOutput(info.RunnerCommand, args...) 246 w.Notice("Successfully extracted cache") 247 w.Else() 248 w.Warning("Failed to extract cache") 249 w.EndIf() 250 }) 251 } 252 253 return nil 254 } 255 256 func (b *AbstractShell) downloadArtifacts(w ShellWriter, job common.Dependency, info common.ShellScriptInfo) { 257 args := []string{ 258 "artifacts-downloader", 259 "--url", 260 info.Build.Runner.URL, 261 "--token", 262 job.Token, 263 "--id", 264 strconv.Itoa(job.ID), 265 } 266 267 w.Notice("Downloading artifacts for %s (%d)...", job.Name, job.ID) 268 w.Command(info.RunnerCommand, args...) 269 } 270 271 func (b *AbstractShell) jobArtifacts(info common.ShellScriptInfo) (otherJobs []common.Dependency) { 272 for _, otherJob := range info.Build.Dependencies { 273 if otherJob.ArtifactsFile.Filename == "" { 274 continue 275 } 276 277 otherJobs = append(otherJobs, otherJob) 278 } 279 return 280 } 281 282 func (b *AbstractShell) downloadAllArtifacts(w ShellWriter, info common.ShellScriptInfo) { 283 otherJobs := b.jobArtifacts(info) 284 if len(otherJobs) == 0 { 285 return 286 } 287 288 b.guardRunnerCommand(w, info.RunnerCommand, "Artifacts downloading", func() { 289 for _, otherJob := range otherJobs { 290 b.downloadArtifacts(w, otherJob, info) 291 } 292 }) 293 } 294 295 func (b *AbstractShell) writePrepareScript(w ShellWriter, info common.ShellScriptInfo) (err error) { 296 return nil 297 } 298 299 func (b *AbstractShell) writeCloneFetchCmds(w ShellWriter, info common.ShellScriptInfo) (err error) { 300 build := info.Build 301 projectDir := build.FullProjectDir() 302 gitDir := path.Join(build.FullProjectDir(), ".git") 303 304 switch info.Build.GetGitStrategy() { 305 case common.GitFetch: 306 b.writeFetchCmd(w, build, projectDir, gitDir) 307 case common.GitClone: 308 b.writeCloneCmd(w, build, projectDir) 309 case common.GitNone: 310 w.Notice("Skipping Git repository setup") 311 w.MkDir(projectDir) 312 default: 313 return errors.New("unknown GIT_STRATEGY") 314 } 315 316 if info.Build.GetGitCheckout() { 317 b.writeCheckoutCmd(w, build) 318 } else { 319 w.Notice("Skippping Git checkout") 320 } 321 322 return nil 323 } 324 325 func (b *AbstractShell) writeSubmoduleUpdateCmds(w ShellWriter, info common.ShellScriptInfo) (err error) { 326 build := info.Build 327 328 switch build.GetSubmoduleStrategy() { 329 case common.SubmoduleNormal: 330 b.writeSubmoduleUpdateCmd(w, build, false) 331 332 case common.SubmoduleRecursive: 333 b.writeSubmoduleUpdateCmd(w, build, true) 334 335 case common.SubmoduleNone: 336 w.Notice("Skipping Git submodules setup") 337 338 default: 339 return errors.New("unknown GIT_SUBMODULE_STRATEGY") 340 } 341 342 return nil 343 } 344 345 func (b *AbstractShell) writeGetSourcesScript(w ShellWriter, info common.ShellScriptInfo) (err error) { 346 b.writeExports(w, info) 347 348 if !info.Build.IsSharedEnv() { 349 b.writeGitSSLConfig(w, info.Build, []string{"--global"}) 350 } 351 352 if info.PreCloneScript != "" && info.Build.GetGitStrategy() != common.GitNone { 353 b.writeCommands(w, info.PreCloneScript) 354 } 355 356 if err := b.writeCloneFetchCmds(w, info); err != nil { 357 return err 358 } 359 360 return b.writeSubmoduleUpdateCmds(w, info) 361 } 362 363 func (b *AbstractShell) writeRestoreCacheScript(w ShellWriter, info common.ShellScriptInfo) (err error) { 364 b.writeExports(w, info) 365 b.writeCdBuildDir(w, info) 366 367 // Try to restore from main cache, if not found cache for master 368 return b.cacheExtractor(w, info) 369 } 370 371 func (b *AbstractShell) writeDownloadArtifactsScript(w ShellWriter, info common.ShellScriptInfo) (err error) { 372 b.writeExports(w, info) 373 b.writeCdBuildDir(w, info) 374 375 // Process all artifacts 376 b.downloadAllArtifacts(w, info) 377 return nil 378 } 379 380 // Write the given string of commands using the provided ShellWriter object. 381 func (b *AbstractShell) writeCommands(w ShellWriter, commands ...string) { 382 for _, command := range commands { 383 command = strings.TrimSpace(command) 384 if command != "" { 385 lines := strings.SplitN(command, "\n", 2) 386 if len(lines) > 1 { 387 // TODO: this should be collapsable once we introduce that in GitLab 388 w.Notice("$ %s # collapsed multi-line command", lines[0]) 389 } else { 390 w.Notice("$ %s", lines[0]) 391 } 392 } else { 393 w.EmptyLine() 394 } 395 w.Line(command) 396 w.CheckForErrors() 397 } 398 } 399 400 func (b *AbstractShell) writeUserScript(w ShellWriter, info common.ShellScriptInfo) (err error) { 401 var scriptStep *common.Step 402 for _, step := range info.Build.Steps { 403 if step.Name == common.StepNameScript { 404 scriptStep = &step 405 break 406 } 407 } 408 409 if scriptStep == nil { 410 return nil 411 } 412 413 b.writeExports(w, info) 414 b.writeCdBuildDir(w, info) 415 416 if info.PreBuildScript != "" { 417 b.writeCommands(w, info.PreBuildScript) 418 } 419 420 b.writeCommands(w, scriptStep.Script...) 421 422 if info.PostBuildScript != "" { 423 b.writeCommands(w, info.PostBuildScript) 424 } 425 426 return nil 427 } 428 429 func (b *AbstractShell) cacheArchiver(w ShellWriter, info common.ShellScriptInfo) error { 430 for _, cacheOptions := range info.Build.Cache { 431 // Skip archiving if no cache is defined 432 cacheKey, cacheFile := b.cacheFile(info.Build, cacheOptions.Key) 433 if cacheKey == "" { 434 continue 435 } 436 437 if ok, err := cacheOptions.CheckPolicy(common.CachePolicyPush); err != nil { 438 return fmt.Errorf("%s for %s", err, cacheKey) 439 } else if !ok { 440 w.Notice("Not uploading cache %s due to policy", cacheKey) 441 continue 442 } 443 444 args := []string{ 445 "cache-archiver", 446 "--file", cacheFile, 447 "--timeout", strconv.Itoa(info.Build.GetCacheRequestTimeout()), 448 } 449 450 // Create list of files to archive 451 archiverArgs := []string{} 452 for _, path := range cacheOptions.Paths { 453 archiverArgs = append(archiverArgs, "--path", path) 454 } 455 456 if cacheOptions.Untracked { 457 archiverArgs = append(archiverArgs, "--untracked") 458 } 459 460 if len(archiverArgs) < 1 { 461 // Skip creating archive 462 continue 463 } 464 args = append(args, archiverArgs...) 465 466 // Generate cache upload address 467 if url := getCacheUploadURL(info.Build, cacheKey); url != nil { 468 args = append(args, "--url", url.String()) 469 } 470 471 // Execute cache-archiver command. Failure is not fatal. 472 b.guardRunnerCommand(w, info.RunnerCommand, "Creating cache", func() { 473 w.Notice("Creating cache %s...", cacheKey) 474 w.IfCmdWithOutput(info.RunnerCommand, args...) 475 w.Notice("Created cache") 476 w.Else() 477 w.Warning("Failed to create cache") 478 w.EndIf() 479 }) 480 } 481 482 return nil 483 } 484 485 func (b *AbstractShell) uploadArtifacts(w ShellWriter, info common.ShellScriptInfo) { 486 if info.Build.Runner.URL == "" { 487 return 488 } 489 490 for _, artifacts := range info.Build.Artifacts { 491 args := []string{ 492 "artifacts-uploader", 493 "--url", 494 info.Build.Runner.URL, 495 "--token", 496 info.Build.Token, 497 "--id", 498 strconv.Itoa(info.Build.ID), 499 } 500 501 // Create list of files to archive 502 archiverArgs := []string{} 503 for _, path := range artifacts.Paths { 504 archiverArgs = append(archiverArgs, "--path", path) 505 } 506 507 if artifacts.Untracked { 508 archiverArgs = append(archiverArgs, "--untracked") 509 } 510 511 if len(archiverArgs) < 1 { 512 // Skip creating archive 513 continue 514 } 515 args = append(args, archiverArgs...) 516 517 if artifacts.Name != "" { 518 args = append(args, "--name", artifacts.Name) 519 } 520 521 if artifacts.ExpireIn != "" { 522 args = append(args, "--expire-in", artifacts.ExpireIn) 523 } 524 525 b.guardRunnerCommand(w, info.RunnerCommand, "Uploading artifacts", func() { 526 w.Notice("Uploading artifacts...") 527 w.Command(info.RunnerCommand, args...) 528 }) 529 } 530 } 531 532 func (b *AbstractShell) writeAfterScript(w ShellWriter, info common.ShellScriptInfo) error { 533 var afterScriptStep *common.Step 534 for _, step := range info.Build.Steps { 535 if step.Name == common.StepNameAfterScript { 536 afterScriptStep = &step 537 break 538 } 539 } 540 541 if afterScriptStep == nil { 542 return nil 543 } 544 545 if len(afterScriptStep.Script) == 0 { 546 return nil 547 } 548 549 b.writeExports(w, info) 550 b.writeCdBuildDir(w, info) 551 552 w.Notice("Running after script...") 553 b.writeCommands(w, afterScriptStep.Script...) 554 return nil 555 } 556 557 func (b *AbstractShell) writeArchiveCacheScript(w ShellWriter, info common.ShellScriptInfo) (err error) { 558 b.writeExports(w, info) 559 b.writeCdBuildDir(w, info) 560 561 // Find cached files and archive them 562 return b.cacheArchiver(w, info) 563 } 564 565 func (b *AbstractShell) writeUploadArtifactsScript(w ShellWriter, info common.ShellScriptInfo) (err error) { 566 b.writeExports(w, info) 567 b.writeCdBuildDir(w, info) 568 569 // Upload artifacts 570 b.uploadArtifacts(w, info) 571 return 572 } 573 574 func (b *AbstractShell) writeScript(w ShellWriter, buildStage common.BuildStage, info common.ShellScriptInfo) error { 575 methods := map[common.BuildStage]func(ShellWriter, common.ShellScriptInfo) error{ 576 common.BuildStagePrepare: b.writePrepareScript, 577 common.BuildStageGetSources: b.writeGetSourcesScript, 578 common.BuildStageRestoreCache: b.writeRestoreCacheScript, 579 common.BuildStageDownloadArtifacts: b.writeDownloadArtifactsScript, 580 common.BuildStageUserScript: b.writeUserScript, 581 common.BuildStageAfterScript: b.writeAfterScript, 582 common.BuildStageArchiveCache: b.writeArchiveCacheScript, 583 common.BuildStageUploadArtifacts: b.writeUploadArtifactsScript, 584 } 585 586 fn := methods[buildStage] 587 if fn == nil { 588 return errors.New("Not supported script type: " + string(buildStage)) 589 } 590 591 return fn(w, info) 592 }