common/build.go (432 lines of code) (raw):

package common import ( "context" "errors" "fmt" "net/url" "os" "path" "strconv" "strings" "time" "github.com/Sirupsen/logrus" "gitlab.com/gitlab-org/gitlab-ci-multi-runner/helpers" ) type GitStrategy int const ( GitClone GitStrategy = iota GitFetch GitNone ) type SubmoduleStrategy int const ( SubmoduleInvalid SubmoduleStrategy = iota SubmoduleNone SubmoduleNormal SubmoduleRecursive ) type BuildRuntimeState string const ( BuildRunStatePending BuildRuntimeState = "pending" BuildRunRuntimeRunning BuildRuntimeState = "running" BuildRunRuntimeFinished BuildRuntimeState = "finished" BuildRunRuntimeCanceled BuildRuntimeState = "canceled" BuildRunRuntimeTerminated BuildRuntimeState = "terminated" BuildRunRuntimeTimedout BuildRuntimeState = "timedout" ) type BuildStage string const ( BuildStagePrepare BuildStage = "prepare_script" BuildStageGetSources BuildStage = "get_sources" BuildStageRestoreCache BuildStage = "restore_cache" BuildStageDownloadArtifacts BuildStage = "download_artifacts" BuildStageUserScript BuildStage = "build_script" BuildStageAfterScript BuildStage = "after_script" BuildStageArchiveCache BuildStage = "archive_cache" BuildStageUploadArtifacts BuildStage = "upload_artifacts" ) type Build struct { JobResponse `yaml:",inline"` SystemInterrupt chan os.Signal `json:"-" yaml:"-"` RootDir string `json:"-" yaml:"-"` BuildDir string `json:"-" yaml:"-"` CacheDir string `json:"-" yaml:"-"` Hostname string `json:"-" yaml:"-"` Runner *RunnerConfig `json:"runner"` ExecutorData ExecutorData // Unique ID for all running builds on this runner RunnerID int `json:"runner_id"` // Unique ID for all running builds on this runner and this project ProjectRunnerID int `json:"project_runner_id"` CurrentStage BuildStage CurrentState BuildRuntimeState executorStageResolver func() ExecutorStage } func (b *Build) Log() *logrus.Entry { return b.Runner.Log().WithField("job", b.ID).WithField("project", b.JobInfo.ProjectID) } func (b *Build) ProjectUniqueName() string { return fmt.Sprintf("runner-%s-project-%d-concurrent-%d", b.Runner.ShortDescription(), b.JobInfo.ProjectID, b.ProjectRunnerID) } func (b *Build) ProjectSlug() (string, error) { url, err := url.Parse(b.GitInfo.RepoURL) if err != nil { return "", err } if url.Host == "" { return "", errors.New("only URI reference supported") } slug := url.Path slug = strings.TrimSuffix(slug, ".git") slug = path.Clean(slug) if slug == "." { return "", errors.New("invalid path") } if strings.Contains(slug, "..") { return "", errors.New("it doesn't look like a valid path") } return slug, nil } func (b *Build) ProjectUniqueDir(sharedDir bool) string { dir, err := b.ProjectSlug() if err != nil { dir = fmt.Sprintf("project-%d", b.JobInfo.ProjectID) } // for shared dirs path is constructed like this: // <some-path>/runner-short-id/concurrent-id/group-name/project-name/ // ex.<some-path>/01234567/0/group/repo/ if sharedDir { dir = path.Join( fmt.Sprintf("%s", b.Runner.ShortDescription()), fmt.Sprintf("%d", b.ProjectRunnerID), dir, ) } return dir } func (b *Build) FullProjectDir() string { return helpers.ToSlash(b.BuildDir) } func (b *Build) StartBuild(rootDir, cacheDir string, sharedDir bool) { b.RootDir = rootDir b.BuildDir = path.Join(rootDir, b.ProjectUniqueDir(sharedDir)) b.CacheDir = path.Join(cacheDir, b.ProjectUniqueDir(false)) } func (b *Build) executeStage(ctx context.Context, buildStage BuildStage, executor Executor) error { b.CurrentStage = buildStage shell := executor.Shell() if shell == nil { return errors.New("No shell defined") } script, err := GenerateShellScript(buildStage, *shell) if err != nil { return err } // Nothing to execute if script == "" { return nil } cmd := ExecutorCommand{ Context: ctx, Script: script, } switch buildStage { case BuildStageUserScript, BuildStageAfterScript: // use custom build environment cmd.Predefined = false default: // all other stages use a predefined build environment cmd.Predefined = true } return executor.Run(cmd) } func (b *Build) executeUploadArtifacts(ctx context.Context, state error, executor Executor) (err error) { var uploadError error for _, artifact := range b.JobResponse.Artifacts { if artifact.ShouldUpload(state) { uploadError = b.executeStage(ctx, BuildStageUploadArtifacts, executor) } if uploadError != nil { err = uploadError } } return } func (b *Build) executeScript(ctx context.Context, executor Executor) error { // Prepare stage err := b.executeStage(ctx, BuildStagePrepare, executor) if err == nil { err = b.attemptExecuteStage(ctx, BuildStageGetSources, executor, b.GetGetSourcesAttempts()) } if err == nil { err = b.attemptExecuteStage(ctx, BuildStageRestoreCache, executor, b.GetRestoreCacheAttempts()) } if err == nil { err = b.attemptExecuteStage(ctx, BuildStageDownloadArtifacts, executor, b.GetDownloadArtifactsAttempts()) } if err == nil { // Execute user build script (before_script + script) err = b.executeStage(ctx, BuildStageUserScript, executor) // Execute after script (after_script) timeoutContext, timeoutCancel := context.WithTimeout(ctx, AfterScriptTimeout) defer timeoutCancel() b.executeStage(timeoutContext, BuildStageAfterScript, executor) } // Execute post script (cache store, artifacts upload) if err == nil { err = b.executeStage(ctx, BuildStageArchiveCache, executor) } jobState := err err = b.executeUploadArtifacts(ctx, jobState, executor) // Use job's error if set if jobState != nil { err = jobState } return err } func (b *Build) attemptExecuteStage(ctx context.Context, buildStage BuildStage, executor Executor, attempts int) (err error) { if attempts < 1 || attempts > 10 { return fmt.Errorf("Number of attempts out of the range [1, 10] for stage: %s", buildStage) } for attempt := 0; attempt < attempts; attempt++ { if err = b.executeStage(ctx, buildStage, executor); err == nil { return } } return } func (b *Build) GetBuildTimeout() time.Duration { buildTimeout := b.RunnerInfo.Timeout if buildTimeout <= 0 { buildTimeout = DefaultTimeout } return time.Duration(buildTimeout) * time.Second } func (b *Build) handleError(err error) error { switch err { case context.Canceled: b.CurrentState = BuildRunRuntimeCanceled return &BuildError{Inner: errors.New("canceled")} case context.DeadlineExceeded: b.CurrentState = BuildRunRuntimeTimedout return &BuildError{Inner: fmt.Errorf("execution took longer than %v seconds", b.GetBuildTimeout())} default: b.CurrentState = BuildRunRuntimeFinished return err } } func (b *Build) run(ctx context.Context, executor Executor) (err error) { b.CurrentState = BuildRunRuntimeRunning buildFinish := make(chan error, 1) runContext, runCancel := context.WithCancel(context.Background()) defer runCancel() // Run build script go func() { buildFinish <- b.executeScript(runContext, executor) }() // Wait for signals: cancel, timeout, abort or finish b.Log().Debugln("Waiting for signals...") select { case <-ctx.Done(): err = b.handleError(ctx.Err()) case signal := <-b.SystemInterrupt: err = fmt.Errorf("aborted: %v", signal) b.CurrentState = BuildRunRuntimeTerminated case err = <-buildFinish: b.CurrentState = BuildRunRuntimeFinished return err } b.Log().WithError(err).Debugln("Waiting for build to finish...") // Wait till we receive that build did finish runCancel() <-buildFinish return err } func (b *Build) retryCreateExecutor(options ExecutorPrepareOptions, provider ExecutorProvider, logger BuildLogger) (executor Executor, err error) { for tries := 0; tries < PreparationRetries; tries++ { executor = provider.Create() if executor == nil { err = errors.New("failed to create executor") return } b.executorStageResolver = executor.GetCurrentStage err = executor.Prepare(options) if err == nil { break } if executor != nil { executor.Cleanup() executor = nil } if _, ok := err.(*BuildError); ok { break } else if options.Context.Err() != nil { return nil, b.handleError(options.Context.Err()) } logger.SoftErrorln("Preparation failed:", err) logger.Infoln("Will be retried in", PreparationRetryInterval, "...") time.Sleep(PreparationRetryInterval) } return } func (b *Build) CurrentExecutorStage() ExecutorStage { if b.executorStageResolver == nil { b.executorStageResolver = func() ExecutorStage { return ExecutorStage("") } } return b.executorStageResolver() } func (b *Build) Run(globalConfig *Config, trace JobTrace) (err error) { var executor Executor logger := NewBuildLogger(trace, b.Log()) logger.Println(fmt.Sprintf("Running with %s\n on %s (%s)", AppVersion.Line(), b.Runner.Name, b.Runner.ShortDescription())) b.CurrentState = BuildRunStatePending defer func() { if _, ok := err.(*BuildError); ok { logger.SoftErrorln("Job failed:", err) trace.Fail(err) } else if err != nil { logger.Errorln("Job failed (system failure):", err) trace.Fail(err) } else { logger.Infoln("Job succeeded") trace.Success() } if executor != nil { executor.Cleanup() } }() context, cancel := context.WithTimeout(context.Background(), b.GetBuildTimeout()) defer cancel() trace.SetCancelFunc(cancel) options := ExecutorPrepareOptions{ Config: b.Runner, Build: b, Trace: trace, User: globalConfig.User, Context: context, } provider := GetExecutor(b.Runner.Executor) if provider == nil { return errors.New("executor not found") } executor, err = b.retryCreateExecutor(options, provider, logger) if err == nil { err = b.run(context, executor) } if executor != nil { executor.Finish(err) } return err } func (b *Build) String() string { return helpers.ToYAML(b) } func (b *Build) GetDefaultVariables() JobVariables { return JobVariables{ {Key: "CI_PROJECT_DIR", Value: b.FullProjectDir(), Public: true, Internal: true, File: false}, {Key: "CI_SERVER", Value: "yes", Public: true, Internal: true, File: false}, } } func (b *Build) GetCITLSVariables() JobVariables { variables := JobVariables{} if b.TLSCAChain != "" { variables = append(variables, JobVariable{"CI_SERVER_TLS_CA_FILE", b.TLSCAChain, true, true, true}) } if b.TLSAuthCert != "" && b.TLSAuthKey != "" { variables = append(variables, JobVariable{"CI_SERVER_TLS_CERT_FILE", b.TLSAuthCert, true, true, true}) variables = append(variables, JobVariable{"CI_SERVER_TLS_KEY_FILE", b.TLSAuthKey, true, true, true}) } return variables } func (b *Build) GetGitTLSVariables() JobVariables { variables := JobVariables{} if b.TLSCAChain != "" { variables = append(variables, JobVariable{"GIT_SSL_CAINFO", b.TLSCAChain, true, true, true}) } if b.TLSAuthCert != "" && b.TLSAuthKey != "" { variables = append(variables, JobVariable{"GIT_SSL_CERT", b.TLSAuthCert, true, true, true}) variables = append(variables, JobVariable{"GIT_SSL_KEY", b.TLSAuthKey, true, true, true}) } return variables } func (b *Build) GetAllVariables() (variables JobVariables) { if b.Runner != nil { variables = append(variables, b.Runner.GetVariables()...) } variables = append(variables, b.GetDefaultVariables()...) variables = append(variables, b.GetCITLSVariables()...) variables = append(variables, b.Variables...) return variables.Expand() } func (b *Build) GetGitDepth() string { return b.GetAllVariables().Get("GIT_DEPTH") } func (b *Build) GetGitStrategy() GitStrategy { switch b.GetAllVariables().Get("GIT_STRATEGY") { case "clone": return GitClone case "fetch": return GitFetch case "none": return GitNone default: if b.AllowGitFetch { return GitFetch } return GitClone } } func (b *Build) GetGitCheckout() bool { if b.GetGitStrategy() == GitNone { return false } strCheckout := b.GetAllVariables().Get("GIT_CHECKOUT") if len(strCheckout) == 0 { return true } checkout, err := strconv.ParseBool(strCheckout) if err != nil { return true } return checkout } func (b *Build) GetSubmoduleStrategy() SubmoduleStrategy { if b.GetGitStrategy() == GitNone { return SubmoduleNone } switch b.GetAllVariables().Get("GIT_SUBMODULE_STRATEGY") { case "normal": return SubmoduleNormal case "recursive": return SubmoduleRecursive case "none", "": // Default (legacy) behavior is to not update/init submodules return SubmoduleNone default: // Will cause an error in AbstractShell) writeSubmoduleUpdateCmds return SubmoduleInvalid } } func (b *Build) IsDebugTraceEnabled() bool { trace, err := strconv.ParseBool(b.GetAllVariables().Get("CI_DEBUG_TRACE")) if err != nil { return false } return trace } func (b *Build) GetDockerAuthConfig() string { return b.GetAllVariables().Get("DOCKER_AUTH_CONFIG") } func (b *Build) GetGetSourcesAttempts() int { retries, err := strconv.Atoi(b.GetAllVariables().Get("GET_SOURCES_ATTEMPTS")) if err != nil { return DefaultGetSourcesAttempts } return retries } func (b *Build) GetDownloadArtifactsAttempts() int { retries, err := strconv.Atoi(b.GetAllVariables().Get("ARTIFACT_DOWNLOAD_ATTEMPTS")) if err != nil { return DefaultArtifactDownloadAttempts } return retries } func (b *Build) GetRestoreCacheAttempts() int { retries, err := strconv.Atoi(b.GetAllVariables().Get("RESTORE_CACHE_ATTEMPTS")) if err != nil { return DefaultRestoreCacheAttempts } return retries } func (b *Build) GetCacheRequestTimeout() int { timeout, err := strconv.Atoi(b.GetAllVariables().Get("CACHE_REQUEST_TIMEOUT")) if err != nil { return DefaultCacheRequestTimeout } return timeout }