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  }