diff --git a/commands/exec.go b/commands/exec.go index eecf3a275493b728b2b558e60bf28a68c016c0fe..f41faa0d6056cc79804544f14e341548f30d66f6 100644 --- a/commands/exec.go +++ b/commands/exec.go @@ -82,7 +82,7 @@ func (c *ExecCommand) createBuild(repoURL string, abortSignal chan os.Signal) (b RunnerSettings: c.RunnerSettings, } - build = common.NewBuild(jobResponse, runner, abortSignal, nil) + build, err = common.NewBuild(jobResponse, runner, abortSignal, nil) return } diff --git a/commands/multi.go b/commands/multi.go index af57965fc55814e68fc4c9806e063628c7a8b81a..4f45f7cff6e72a7b4d1a6d8709b735f85da23a9d 100644 --- a/commands/multi.go +++ b/commands/multi.go @@ -121,15 +121,21 @@ func (mr *RunCommand) feedRunners(runners chan *common.RunnerConfig) { } } -func (mr *RunCommand) requestJob(runner *common.RunnerConfig, sessionInfo *common.SessionInfo) (*common.JobResponse, bool) { +// requestJob will check if the runner can send another concurrent request to +// GitLab, if not the return value is nil. +func (mr *RunCommand) requestJob(runner *common.RunnerConfig, sessionInfo *common.SessionInfo) *common.JobResponse { if !mr.buildsHelper.acquireRequest(runner) { - return nil, false + mr.log().WithField("runner", runner.ShortDescription()). + Debugln("Failed to request job: runner requestConcurrency meet") + + return nil } defer mr.buildsHelper.releaseRequest(runner) jobData, healthy := mr.network.RequestJob(*runner, sessionInfo) mr.makeHealthy(runner.UniqueID(), healthy) - return jobData, true + + return jobData } func (mr *RunCommand) processRunner(id int, runner *common.RunnerConfig, runners chan *common.RunnerConfig) (err error) { @@ -138,20 +144,16 @@ func (mr *RunCommand) processRunner(id int, runner *common.RunnerConfig, runners return } - context, err := provider.Acquire(runner) + executorData, releaseFn, err := mr.acquireRunnerResources(provider, runner) if err != nil { - logrus.Warningln("Failed to update executor", runner.Executor, "for", runner.ShortDescription(), err) + mr.log().WithFields(logrus.Fields{ + "runner": runner.ShortDescription(), + "executor": runner.Executor, + }).WithError(err). + Warn("Failed to acquire runner resource") return } - defer provider.Release(runner, context) - - // Acquire build slot - if !mr.buildsHelper.acquireBuild(runner) { - mr.log().WithField("runner", runner.ShortDescription()). - Debugln("Failed to request job: runner limit meet") - return - } - defer mr.buildsHelper.releaseBuild(runner) + defer releaseFn() var features common.FeaturesInfo provider.GetFeatures(&features) @@ -161,12 +163,7 @@ func (mr *RunCommand) processRunner(id int, runner *common.RunnerConfig, runners } // Receive a new build - jobData, result := mr.requestJob(runner, sessionInfo) - if !result { - mr.log().WithField("runner", runner.ShortDescription()). - Debugln("Failed to request job: runner requestConcurrency meet") - return - } + jobData := mr.requestJob(runner, sessionInfo) if jobData == nil { return } @@ -177,12 +174,22 @@ func (mr *RunCommand) processRunner(id int, runner *common.RunnerConfig, runners Token: jobData.Token, } trace := mr.network.ProcessJob(*runner, jobCredentials) - defer trace.Fail(err, common.NoneFailure) + defer func() { + if err != nil { + fmt.Fprintln(trace, err.Error()) + trace.Fail(err, common.RunnerSystemFailure) + } else { + trace.Fail(nil, common.NoneFailure) + } + }() trace.SetFailuresCollector(mr.failuresCollector) // Create a new build - build := common.NewBuild(*jobData, runner, mr.abortBuilds, context) + build, err := common.NewBuild(*jobData, runner, mr.abortBuilds, executorData) + if err != nil { + return + } build.Session = buildSession // Add build to list of builds to assign numbers @@ -203,6 +210,32 @@ func (mr *RunCommand) processRunner(id int, runner *common.RunnerConfig, runners return build.Run(mr.config, trace) } +func (mr *RunCommand) acquireRunnerResources(provider common.ExecutorProvider, runner *common.RunnerConfig) (common.ExecutorData, func(), error) { + executorData, err := provider.Acquire(runner) + if err != nil { + return nil, func() {}, fmt.Errorf("failed to update executor: %v", err) + } + + releaseProviderFn := func() { + err := provider.Release(runner, executorData) + if err != nil { + logrus.WithError(err).Error("Failed to release executor") + } + } + + if !mr.buildsHelper.acquireBuild(runner) { + releaseProviderFn() + return nil, nil, errors.New("failed to request job, runner limit met") + } + + releaseFn := func() { + mr.buildsHelper.releaseBuild(runner) + releaseProviderFn() + } + + return executorData, releaseFn, nil +} + func (mr *RunCommand) createSession(features common.FeaturesInfo) (*session.Session, *common.SessionInfo, error) { if mr.sessionServer == nil || !features.Session { return nil, nil, nil @@ -227,7 +260,10 @@ func (mr *RunCommand) processRunners(id int, stopWorker chan bool, runners chan for mr.stopSignal == nil { select { case runner := <-runners: - mr.processRunner(id, runner, runners) + err := mr.processRunner(id, runner, runners) + if err != nil { + logrus.WithError(err).Error("Failed to process runner") + } // force GC cycle after processing build runtime.GC() diff --git a/commands/single.go b/commands/single.go index 7d2268cab540decce280de596b0fd8bacdd1903c..d62323c5ec7ed18df02ac18e33a533a28c9fec88 100644 --- a/commands/single.go +++ b/commands/single.go @@ -87,7 +87,10 @@ func (r *RunSingleCommand) processBuild(data common.ExecutorData, abortSignal ch } config := common.NewConfig() - newBuild := common.NewBuild(*jobData, &r.RunnerConfig, abortSignal, data) + newBuild, err := common.NewBuild(*jobData, &r.RunnerConfig, abortSignal, data) + if err != nil { + return + } jobCredentials := &common.JobCredentials{ ID: jobData.ID, @@ -148,7 +151,11 @@ func (r *RunSingleCommand) Execute(c *cli.Context) { log.Warningln("Executor update:", err) } - r.processBuild(data, abortSignal) + pErr := r.processBuild(data, abortSignal) + if pErr != nil { + log.WithError(pErr).Error("Failed to process build") + } + r.checkFinishedConditions() executorProvider.Release(&r.RunnerConfig, data) } diff --git a/common/build.go b/common/build.go index 48be28ca8a76485c08288b26f0f1fe6c82541937..1a4ac91bb7259975db4b68586a0174d981e71c8d 100644 --- a/common/build.go +++ b/common/build.go @@ -706,14 +706,20 @@ func (b *Build) Duration() time.Duration { return time.Since(b.createdAt) } -func NewBuild(jobData JobResponse, runnerConfig *RunnerConfig, systemInterrupt chan os.Signal, executorData ExecutorData) *Build { +func NewBuild(jobData JobResponse, runnerConfig *RunnerConfig, systemInterrupt chan os.Signal, executorData ExecutorData) (*Build, error) { + // Attempt to perform a deep copy of the RunnerConfig + runnerConfigCopy, err := runnerConfig.DeepCopy() + if err != nil { + return nil, fmt.Errorf("deep copy of runner config failed: %v", err) + } + return &Build{ JobResponse: jobData, - Runner: runnerConfig, + Runner: runnerConfigCopy, SystemInterrupt: systemInterrupt, ExecutorData: executorData, createdAt: time.Now(), - } + }, nil } func (b *Build) IsFeatureFlagOn(name string) bool { diff --git a/common/build_test.go b/common/build_test.go index 241b9e5e5f41e1194ae03c5755c7019a220ecdb2..8a4f3f8a5a90be501cf4bd5aa62eb620a56598c3 100644 --- a/common/build_test.go +++ b/common/build_test.go @@ -18,6 +18,7 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + "gitlab.com/gitlab-org/gitlab-runner/helpers/docker" "gitlab.com/gitlab-org/gitlab-runner/session" "gitlab.com/gitlab-org/gitlab-runner/session/terminal" ) @@ -75,6 +76,63 @@ func TestBuildRun(t *testing.T) { assert.NoError(t, err) } +func TestBuildRunNoModifyConfig(t *testing.T) { + e := MockExecutor{} + defer e.AssertExpectations(t) + + p := MockExecutorProvider{} + defer p.AssertExpectations(t) + + // Create executor only once + p.On("CanCreate").Return(true).Once() + p.On("GetDefaultShell").Return("bash").Once() + p.On("GetFeatures", mock.Anything).Return(nil).Twice() + p.On("Create").Return(&e).Once() + + // Attempt to modify the Config object + e.On("Prepare", mock.Anything, mock.Anything, mock.Anything). + Return(func(options ExecutorPrepareOptions) error { + options.Config.Docker.DockerCredentials.Host = "10.0.0.2" + return nil + }).Once() + + // We run everything else once + e.On("Finish", nil).Return().Once() + e.On("Cleanup").Return().Once() + + // Run script successfully + e.On("Shell").Return(&ShellScriptInfo{Shell: "script-shell"}) + e.On("Run", matchBuildStage(BuildStagePrepare)).Return(nil).Once() + e.On("Run", matchBuildStage(BuildStageGetSources)).Return(nil).Once() + e.On("Run", matchBuildStage(BuildStageRestoreCache)).Return(nil).Once() + e.On("Run", matchBuildStage(BuildStageDownloadArtifacts)).Return(nil).Once() + e.On("Run", matchBuildStage(BuildStageUserScript)).Return(nil).Once() + e.On("Run", matchBuildStage(BuildStageAfterScript)).Return(nil).Once() + e.On("Run", matchBuildStage(BuildStageArchiveCache)).Return(nil).Once() + e.On("Run", matchBuildStage(BuildStageUploadOnSuccessArtifacts)).Return(nil).Once() + + RegisterExecutor("build-run-nomodify-test", &p) + + successfulBuild, err := GetSuccessfulBuild() + assert.NoError(t, err) + rc := &RunnerConfig{ + RunnerSettings: RunnerSettings{ + Executor: "build-run-nomodify-test", + Docker: &DockerConfig{ + DockerCredentials: docker_helpers.DockerCredentials{ + Host: "10.0.0.1", + }, + }, + }, + } + build, err := NewBuild(successfulBuild, rc, nil, nil) + assert.NoError(t, err) + + err = build.Run(&Config{}, &Trace{Writer: os.Stdout}) + assert.NoError(t, err) + assert.Equal(t, "10.0.0.1", rc.Docker.DockerCredentials.Host) +} + func TestRetryPrepare(t *testing.T) { PreparationRetryInterval = 0 diff --git a/common/config.go b/common/config.go index 5746938aa4c26a95a4ace987586dc9a6bce0fe0d..735cdfd4ac32a8a520be9c5845a3b084f555de20 100644 --- a/common/config.go +++ b/common/config.go @@ -3,6 +3,7 @@ package common import ( "bufio" "bytes" + "encoding/json" "errors" "fmt" "io/ioutil" @@ -646,6 +647,23 @@ func (c *RunnerConfig) GetVariables() JobVariables { return variables } +// DeepCopy attempts to make a deep clone of the object +func (c *RunnerConfig) DeepCopy() (*RunnerConfig, error) { + var r RunnerConfig + + bytes, err := json.Marshal(c) + if err != nil { + return nil, fmt.Errorf("serialization of runner config failed: %v", err) + } + + err = json.Unmarshal(bytes, &r) + if err != nil { + return nil, fmt.Errorf("deserialization of runner config failed: %v", err) + } + + return &r, err +} + func NewConfig() *Config { return &Config{ Concurrent: 1,