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