From 7b4be6671ab3508fe32aa103fb598c9560341007 Mon Sep 17 00:00:00 2001 From: Evan Date: Mon, 23 May 2022 16:49:14 +0800 Subject: [PATCH] setpipeline supports detach mode Signed-off-by: Evan --- atc/builds/planner.go | 1 + atc/db/build.go | 12 ++- atc/db/build_in_memory_check.go | 2 +- atc/db/build_test.go | 16 ++-- atc/db/dbfakes/fake_build.go | 18 ++-- atc/db/dbfakes/fake_pipeline.go | 65 ++++++++++++++ atc/db/pipeline.go | 34 +++++++ atc/db/pipeline_lifecycle_test.go | 2 +- atc/exec/set_pipeline_step.go | 22 ++++- atc/exec/set_pipeline_step_test.go | 138 +++++++++++++++++++++++++++-- atc/plan.go | 1 + atc/steps.go | 1 + atc/steps_test.go | 2 + 13 files changed, 286 insertions(+), 28 deletions(-) diff --git a/atc/builds/planner.go b/atc/builds/planner.go index f0ceb005a91..7865a4d3019 100644 --- a/atc/builds/planner.go +++ b/atc/builds/planner.go @@ -264,6 +264,7 @@ func (visitor *planVisitor) VisitSetPipeline(step *atc.SetPipelineStep) error { Vars: step.Vars, VarFiles: step.VarFiles, InstanceVars: step.InstanceVars, + Detach: step.Detach, }) return nil diff --git a/atc/db/build.go b/atc/db/build.go index b6ac69bb786..f91766b33dc 100644 --- a/atc/db/build.go +++ b/atc/db/build.go @@ -212,6 +212,7 @@ type Build interface { config atc.Config, from ConfigVersion, initiallyPaused bool, + detach bool, ) (Pipeline, bool, error) ResourceCacheUser() ResourceCacheUser @@ -1799,6 +1800,7 @@ func (b *build) SavePipeline( config atc.Config, from ConfigVersion, initiallyPaused bool, + detach bool, ) (Pipeline, bool, error) { tx, err := b.conn.Begin() if err != nil { @@ -1807,8 +1809,14 @@ func (b *build) SavePipeline( defer Rollback(tx) - jobID := newNullInt64(b.jobID) - buildID := newNullInt64(b.id) + var jobID, buildID sql.NullInt64 + if detach { + jobID = sql.NullInt64{Valid: false} + buildID = sql.NullInt64{Valid: false} + } else { + jobID = newNullInt64(b.jobID) + buildID = newNullInt64(b.id) + } pipelineID, isNewPipeline, err := savePipeline(tx, pipelineRef, config, from, initiallyPaused, teamID, jobID, buildID) if err != nil { return nil, false, err diff --git a/atc/db/build_in_memory_check.go b/atc/db/build_in_memory_check.go index c746693e55e..74a22e91aa4 100644 --- a/atc/db/build_in_memory_check.go +++ b/atc/db/build_in_memory_check.go @@ -662,7 +662,7 @@ func (b *inMemoryCheckBuild) Start(atc.Plan) (bool, error) { func (b *inMemoryCheckBuild) ResourcesChecked() (bool, error) { return false, errors.New("not implemented for in memory build") } -func (b *inMemoryCheckBuild) SavePipeline(atc.PipelineRef, int, atc.Config, ConfigVersion, bool) (Pipeline, bool, error) { +func (b *inMemoryCheckBuild) SavePipeline(atc.PipelineRef, int, atc.Config, ConfigVersion, bool, bool) (Pipeline, bool, error) { return nil, false, errors.New("not implemented for in memory build") } func (b *inMemoryCheckBuild) AdoptInputsAndPipes() ([]BuildInput, bool, error) { diff --git a/atc/db/build_test.go b/atc/db/build_test.go index 43c89b2bd0f..6ca89349b5f 100644 --- a/atc/db/build_test.go +++ b/atc/db/build_test.go @@ -806,7 +806,7 @@ var _ = Describe("Build", func() { BeforeEach(func() { By("creating a child pipeline") build, _ := defaultJob.CreateBuild(defaultBuildCreatedBy) - childPipeline, _, _ = build.SavePipeline(atc.PipelineRef{Name: "child1-pipeline"}, defaultTeam.ID(), defaultPipelineConfig, db.ConfigVersion(0), false) + childPipeline, _, _ = build.SavePipeline(atc.PipelineRef{Name: "child1-pipeline"}, defaultTeam.ID(), defaultPipelineConfig, db.ConfigVersion(0), false, false) build.Finish(db.BuildStatusSucceeded) childPipeline.Reload() @@ -831,7 +831,7 @@ var _ = Describe("Build", func() { for i := 0; i < 5; i++ { job, _, _ := childPipeline.Job("some-job") build, _ := job.CreateBuild(defaultBuildCreatedBy) - childPipeline, _, _ = build.SavePipeline(atc.PipelineRef{Name: "child-pipeline-" + strconv.Itoa(i)}, defaultTeam.ID(), defaultPipelineConfig, db.ConfigVersion(0), false) + childPipeline, _, _ = build.SavePipeline(atc.PipelineRef{Name: "child-pipeline-" + strconv.Itoa(i)}, defaultTeam.ID(), defaultPipelineConfig, db.ConfigVersion(0), false, false) build.Finish(db.BuildStatusSucceeded) childPipelines = append(childPipelines, childPipeline) } @@ -2393,7 +2393,7 @@ var _ = Describe("Build", func() { }, }, }, - }, db.ConfigVersion(0), false) + }, db.ConfigVersion(0), false, false) Expect(err).ToNot(HaveOccurred()) Expect(pipeline.ParentJobID()).To(Equal(build.JobID())) Expect(pipeline.ParentBuildID()).To(Equal(build.ID())) @@ -2431,7 +2431,7 @@ var _ = Describe("Build", func() { }, }, }, - }, db.ConfigVersion(0), false) + }, db.ConfigVersion(0), false, false) Expect(err).ToNot(HaveOccurred()) Expect(pipeline.ParentJobID()).To(Equal(buildTwo.JobID())) Expect(pipeline.ParentBuildID()).To(Equal(buildTwo.ID())) @@ -2461,7 +2461,7 @@ var _ = Describe("Build", func() { }, }, }, - }, pipeline.ConfigVersion(), false) + }, pipeline.ConfigVersion(), false, false) Expect(err).To(Equal(db.ErrSetByNewerBuild)) }) @@ -2472,7 +2472,7 @@ var _ = Describe("Build", func() { Expect(err).ToNot(HaveOccurred()) By("re-saving the default pipeline with the build") - pipeline, _, err := build.SavePipeline(defaultPipelineRef, build.TeamID(), defaultPipelineConfig, db.ConfigVersion(1), false) + pipeline, _, err := build.SavePipeline(defaultPipelineRef, build.TeamID(), defaultPipelineConfig, db.ConfigVersion(1), false, false) Expect(err).ToNot(HaveOccurred()) Expect(pipeline.ParentJobID()).To(Equal(build.JobID())) Expect(pipeline.ParentBuildID()).To(Equal(build.ID())) @@ -2493,7 +2493,7 @@ var _ = Describe("Build", func() { By("setting the pipeline again via a build") build, err := defaultJob.CreateBuild(defaultBuildCreatedBy) Expect(err).ToNot(HaveOccurred()) - pipeline, _, err = build.SavePipeline(defaultPipelineRef, build.TeamID(), defaultPipelineConfig, pipeline.ConfigVersion(), false) + pipeline, _, err = build.SavePipeline(defaultPipelineRef, build.TeamID(), defaultPipelineConfig, pipeline.ConfigVersion(), false, false) Expect(err).ToNot(HaveOccurred()) Expect(pipeline.Paused()).To(BeFalse()) @@ -2513,7 +2513,7 @@ var _ = Describe("Build", func() { By("setting the pipeline again via a build") build, err := defaultJob.CreateBuild(defaultBuildCreatedBy) Expect(err).ToNot(HaveOccurred()) - pipeline, _, err = build.SavePipeline(defaultPipelineRef, build.TeamID(), defaultPipelineConfig, pipeline.ConfigVersion(), false) + pipeline, _, err = build.SavePipeline(defaultPipelineRef, build.TeamID(), defaultPipelineConfig, pipeline.ConfigVersion(), false, false) Expect(err).ToNot(HaveOccurred()) Expect(pipeline.Paused()).To(BeTrue()) diff --git a/atc/db/dbfakes/fake_build.go b/atc/db/dbfakes/fake_build.go index 74b4379c03c..6a6cfa72557 100644 --- a/atc/db/dbfakes/fake_build.go +++ b/atc/db/dbfakes/fake_build.go @@ -627,7 +627,7 @@ type FakeBuild struct { saveOutputReturnsOnCall map[int]struct { result1 error } - SavePipelineStub func(atc.PipelineRef, int, atc.Config, db.ConfigVersion, bool) (db.Pipeline, bool, error) + SavePipelineStub func(atc.PipelineRef, int, atc.Config, db.ConfigVersion, bool, bool) (db.Pipeline, bool, error) savePipelineMutex sync.RWMutex savePipelineArgsForCall []struct { arg1 atc.PipelineRef @@ -635,6 +635,7 @@ type FakeBuild struct { arg3 atc.Config arg4 db.ConfigVersion arg5 bool + arg6 bool } savePipelineReturns struct { result1 db.Pipeline @@ -3844,7 +3845,7 @@ func (fake *FakeBuild) SaveOutputReturnsOnCall(i int, result1 error) { }{result1} } -func (fake *FakeBuild) SavePipeline(arg1 atc.PipelineRef, arg2 int, arg3 atc.Config, arg4 db.ConfigVersion, arg5 bool) (db.Pipeline, bool, error) { +func (fake *FakeBuild) SavePipeline(arg1 atc.PipelineRef, arg2 int, arg3 atc.Config, arg4 db.ConfigVersion, arg5 bool, arg6 bool) (db.Pipeline, bool, error) { fake.savePipelineMutex.Lock() ret, specificReturn := fake.savePipelineReturnsOnCall[len(fake.savePipelineArgsForCall)] fake.savePipelineArgsForCall = append(fake.savePipelineArgsForCall, struct { @@ -3853,13 +3854,14 @@ func (fake *FakeBuild) SavePipeline(arg1 atc.PipelineRef, arg2 int, arg3 atc.Con arg3 atc.Config arg4 db.ConfigVersion arg5 bool - }{arg1, arg2, arg3, arg4, arg5}) + arg6 bool + }{arg1, arg2, arg3, arg4, arg5, arg6}) stub := fake.SavePipelineStub fakeReturns := fake.savePipelineReturns - fake.recordInvocation("SavePipeline", []interface{}{arg1, arg2, arg3, arg4, arg5}) + fake.recordInvocation("SavePipeline", []interface{}{arg1, arg2, arg3, arg4, arg5, arg6}) fake.savePipelineMutex.Unlock() if stub != nil { - return stub(arg1, arg2, arg3, arg4, arg5) + return stub(arg1, arg2, arg3, arg4, arg5, arg6) } if specificReturn { return ret.result1, ret.result2, ret.result3 @@ -3873,17 +3875,17 @@ func (fake *FakeBuild) SavePipelineCallCount() int { return len(fake.savePipelineArgsForCall) } -func (fake *FakeBuild) SavePipelineCalls(stub func(atc.PipelineRef, int, atc.Config, db.ConfigVersion, bool) (db.Pipeline, bool, error)) { +func (fake *FakeBuild) SavePipelineCalls(stub func(atc.PipelineRef, int, atc.Config, db.ConfigVersion, bool, bool) (db.Pipeline, bool, error)) { fake.savePipelineMutex.Lock() defer fake.savePipelineMutex.Unlock() fake.SavePipelineStub = stub } -func (fake *FakeBuild) SavePipelineArgsForCall(i int) (atc.PipelineRef, int, atc.Config, db.ConfigVersion, bool) { +func (fake *FakeBuild) SavePipelineArgsForCall(i int) (atc.PipelineRef, int, atc.Config, db.ConfigVersion, bool, bool) { fake.savePipelineMutex.RLock() defer fake.savePipelineMutex.RUnlock() argsForCall := fake.savePipelineArgsForCall[i] - return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4, argsForCall.arg5 + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4, argsForCall.arg5, argsForCall.arg6 } func (fake *FakeBuild) SavePipelineReturns(result1 db.Pipeline, result2 bool, result3 error) { diff --git a/atc/db/dbfakes/fake_pipeline.go b/atc/db/dbfakes/fake_pipeline.go index bfacb4e0c14..4defff84ca8 100644 --- a/atc/db/dbfakes/fake_pipeline.go +++ b/atc/db/dbfakes/fake_pipeline.go @@ -155,6 +155,16 @@ type FakePipeline struct { destroyReturnsOnCall map[int]struct { result1 error } + DetachParentStub func() error + detachParentMutex sync.RWMutex + detachParentArgsForCall []struct { + } + detachParentReturns struct { + result1 error + } + detachParentReturnsOnCall map[int]struct { + result1 error + } DisplayStub func() *atc.DisplayConfig displayMutex sync.RWMutex displayArgsForCall []struct { @@ -1318,6 +1328,59 @@ func (fake *FakePipeline) DestroyReturnsOnCall(i int, result1 error) { }{result1} } +func (fake *FakePipeline) DetachParent() error { + fake.detachParentMutex.Lock() + ret, specificReturn := fake.detachParentReturnsOnCall[len(fake.detachParentArgsForCall)] + fake.detachParentArgsForCall = append(fake.detachParentArgsForCall, struct { + }{}) + stub := fake.DetachParentStub + fakeReturns := fake.detachParentReturns + fake.recordInvocation("DetachParent", []interface{}{}) + fake.detachParentMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakePipeline) DetachParentCallCount() int { + fake.detachParentMutex.RLock() + defer fake.detachParentMutex.RUnlock() + return len(fake.detachParentArgsForCall) +} + +func (fake *FakePipeline) DetachParentCalls(stub func() error) { + fake.detachParentMutex.Lock() + defer fake.detachParentMutex.Unlock() + fake.DetachParentStub = stub +} + +func (fake *FakePipeline) DetachParentReturns(result1 error) { + fake.detachParentMutex.Lock() + defer fake.detachParentMutex.Unlock() + fake.DetachParentStub = nil + fake.detachParentReturns = struct { + result1 error + }{result1} +} + +func (fake *FakePipeline) DetachParentReturnsOnCall(i int, result1 error) { + fake.detachParentMutex.Lock() + defer fake.detachParentMutex.Unlock() + fake.DetachParentStub = nil + if fake.detachParentReturnsOnCall == nil { + fake.detachParentReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.detachParentReturnsOnCall[i] = struct { + result1 error + }{result1} +} + func (fake *FakePipeline) Display() *atc.DisplayConfig { fake.displayMutex.Lock() ret, specificReturn := fake.displayReturnsOnCall[len(fake.displayArgsForCall)] @@ -3609,6 +3672,8 @@ func (fake *FakePipeline) Invocations() map[string][][]interface{} { defer fake.deleteBuildEventsByBuildIDsMutex.RUnlock() fake.destroyMutex.RLock() defer fake.destroyMutex.RUnlock() + fake.detachParentMutex.RLock() + defer fake.detachParentMutex.RUnlock() fake.displayMutex.RLock() defer fake.displayMutex.RUnlock() fake.exposeMutex.RLock() diff --git a/atc/db/pipeline.go b/atc/db/pipeline.go index 2a85680b5ed..c5e7c82419d 100644 --- a/atc/db/pipeline.go +++ b/atc/db/pipeline.go @@ -127,6 +127,7 @@ type Pipeline interface { Variables(lager.Logger, creds.Secrets, creds.VarSourcePool) (vars.Variables, error) SetParentIDs(jobID, buildID int) error + DetachParent() error } type pipeline struct { @@ -1168,6 +1169,39 @@ func (p *pipeline) Variables(logger lager.Logger, globalSecrets creds.Secrets, v return allVars, nil } +func (p *pipeline) DetachParent() error { + tx, err := p.conn.Begin() + if err != nil { + return err + } + + defer Rollback(tx) + + nullID := sql.NullInt64{Valid: false} + result, err := psql.Update("pipelines"). + Set("parent_job_id", nullID). + Set("parent_build_id", nullID). + Where(sq.Eq{ + "id": p.id, + }). + RunWith(tx). + Exec() + + if err != nil { + return err + } + + rows, err := result.RowsAffected() + if err != nil { + return err + } + if rows == 0 { + return ErrSetByNewerBuild + } + + return tx.Commit() +} + func (p *pipeline) SetParentIDs(jobID, buildID int) error { if jobID <= 0 || buildID <= 0 { return errors.New("job and build id cannot be negative or zero-value") diff --git a/atc/db/pipeline_lifecycle_test.go b/atc/db/pipeline_lifecycle_test.go index 10bf34b9011..b91b08606bb 100644 --- a/atc/db/pipeline_lifecycle_test.go +++ b/atc/db/pipeline_lifecycle_test.go @@ -35,7 +35,7 @@ var _ = Describe("PipelineLifecycle", func() { BeforeEach(func() { setChildBuild, _ = defaultJob.CreateBuild(defaultBuildCreatedBy) - childPipeline, _, _ = setChildBuild.SavePipeline(atc.PipelineRef{Name: "child-pipeline"}, defaultTeam.ID(), defaultPipelineConfig, db.ConfigVersion(0), false) + childPipeline, _, _ = setChildBuild.SavePipeline(atc.PipelineRef{Name: "child-pipeline"}, defaultTeam.ID(), defaultPipelineConfig, db.ConfigVersion(0), false, false) setChildBuild.Finish(db.BuildStatusSucceeded) }) diff --git a/atc/exec/set_pipeline_step.go b/atc/exec/set_pipeline_step.go index c9d7f2a0cbe..a95b5a17656 100644 --- a/atc/exec/set_pipeline_step.go +++ b/atc/exec/set_pipeline_step.go @@ -133,8 +133,13 @@ func (step *SetPipelineStep) run(ctx context.Context, state RunState, delegate S } var team db.Team + var detach bool if step.plan.Team == "" { team = step.teamFactory.GetByID(step.metadata.TeamID) + // When team is not specified, target team equals to current team. + if team.Admin(){ + detach = step.plan.Detach + } } else { fmt.Fprintln(stderr, "\x1b[1;33mWARNING: specifying the team in a set_pipeline step is experimental and may be removed in the future!\x1b[0m") fmt.Fprintln(stderr, "") @@ -163,6 +168,7 @@ func (step *SetPipelineStep) run(ctx context.Context, state RunState, delegate S } if currentTeam.Admin() { permitted = true + detach = step.plan.Detach } if !permitted { return false, fmt.Errorf( @@ -174,6 +180,13 @@ func (step *SetPipelineStep) run(ctx context.Context, state RunState, delegate S team = targetTeam } + if detach != step.plan.Detach { + return false, fmt.Errorf( + "only %s team can set detached pipeline", + atc.DefaultTeamName, + ) + } + pipelineRef := atc.PipelineRef{ Name: step.plan.Name, InstanceVars: step.plan.InstanceVars, @@ -202,7 +215,12 @@ func (step *SetPipelineStep) run(ctx context.Context, state RunState, delegate S fmt.Fprintf(stdout, "no changes to apply.\n") if found { - err := pipeline.SetParentIDs(step.metadata.JobID, step.metadata.BuildID) + var err error + if detach { + err = pipeline.DetachParent() + } else { + err = pipeline.SetParentIDs(step.metadata.JobID, step.metadata.BuildID) + } if err != nil { return false, err } @@ -230,7 +248,7 @@ func (step *SetPipelineStep) run(ctx context.Context, state RunState, delegate S return false, fmt.Errorf("set_pipeline step not attached to a buildID") } - pipeline, _, err = parentBuild.SavePipeline(pipelineRef, team.ID(), atcConfig, fromVersion, false) + pipeline, _, err = parentBuild.SavePipeline(pipelineRef, team.ID(), atcConfig, fromVersion, false, detach) if err != nil { if err == db.ErrSetByNewerBuild { fmt.Fprintln(stderr, "\x1b[1;33mWARNING: the pipeline was not saved because it was already saved by a newer build\x1b[0m") diff --git a/atc/exec/set_pipeline_step_test.go b/atc/exec/set_pipeline_step_test.go index c9b8108bbec..b8a3b5540c4 100644 --- a/atc/exec/set_pipeline_step_test.go +++ b/atc/exec/set_pipeline_step_test.go @@ -296,12 +296,13 @@ jobs: It("should save the pipeline", func() { Expect(fakeBuild.SavePipelineCallCount()).To(Equal(1)) - ref, _, _, _, paused := fakeBuild.SavePipelineArgsForCall(0) + ref, _, _, _, paused, detach := fakeBuild.SavePipelineArgsForCall(0) Expect(ref).To(Equal(atc.PipelineRef{ Name: "some-pipeline", InstanceVars: atc.InstanceVars{"branch": "feature/foo"}, })) Expect(paused).To(BeFalse()) + Expect(detach).To(BeFalse()) }) It("should stdout have message", func() { @@ -393,12 +394,13 @@ jobs: It("should save the pipeline un-paused", func() { Expect(fakeBuild.SavePipelineCallCount()).To(Equal(1)) - ref, _, _, _, paused := fakeBuild.SavePipelineArgsForCall(0) + ref, _, _, _, paused, detach := fakeBuild.SavePipelineArgsForCall(0) Expect(ref).To(Equal(atc.PipelineRef{ Name: "some-pipeline", InstanceVars: atc.InstanceVars{"branch": "feature/foo"}, })) Expect(paused).To(BeFalse()) + Expect(detach).To(BeFalse()) }) It("should stdout have message", func() { @@ -426,17 +428,19 @@ jobs: It("should save the pipeline itself", func() { Expect(fakeBuild.SavePipelineCallCount()).To(Equal(1)) - pipelineRef, _, _, _, _ := fakeBuild.SavePipelineArgsForCall(0) + pipelineRef, _, _, _, _, detach := fakeBuild.SavePipelineArgsForCall(0) Expect(pipelineRef).To(Equal(atc.PipelineRef{ Name: "some-pipeline", InstanceVars: atc.InstanceVars{"branch": "feature/foo"}, })) + Expect(detach).To(BeFalse()) }) It("should save to the current team", func() { Expect(fakeBuild.SavePipelineCallCount()).To(Equal(1)) - _, teamId, _, _, _ := fakeBuild.SavePipelineArgsForCall(0) + _, teamId, _, _, _, detach := fakeBuild.SavePipelineArgsForCall(0) Expect(teamId).To(Equal(fakeTeam.ID())) + Expect(detach).To(BeFalse()) }) It("should print an experimental message", func() { @@ -508,11 +512,12 @@ jobs: }) It("should finish successfully", func() { - _, teamID, _, _, _ := fakeBuild.SavePipelineArgsForCall(0) + _, teamID, _, _, _, detach := fakeBuild.SavePipelineArgsForCall(0) Expect(teamID).To(Equal(fakeUserCurrentTeam.ID())) Expect(fakeDelegate.FinishedCallCount()).To(Equal(1)) _, succeeded := fakeDelegate.FinishedArgsForCall(0) Expect(succeeded).To(BeTrue()) + Expect(detach).To(BeFalse()) }) It("should print an experimental message", func() { @@ -540,11 +545,12 @@ jobs: }) It("should finish successfully", func() { - _, teamID, _, _, _ := fakeBuild.SavePipelineArgsForCall(0) + _, teamID, _, _, _, detach := fakeBuild.SavePipelineArgsForCall(0) Expect(teamID).To(Equal(fakeTeam.ID())) Expect(fakeDelegate.FinishedCallCount()).To(Equal(1)) _, succeeded := fakeDelegate.FinishedArgsForCall(0) Expect(succeeded).To(BeTrue()) + Expect(detach).To(BeFalse()) }) }) @@ -560,6 +566,126 @@ jobs: }) }) }) + + Context("when detach is configured", func() { + var fakeTargetTeam *dbfakes.FakeTeam + BeforeEach(func() { + fakeTargetTeam = new(dbfakes.FakeTeam) + fakeTargetTeam.IDReturns(211) + fakeTargetTeam.NameReturns("some-team") + fakeTargetTeam.AdminReturns(false) + fakeTargetTeam.PipelineReturns(fakePipeline, true, nil) + + fakePipeline.DetachParentReturns(nil) + fakeBuild.SavePipelineReturns(fakePipeline, true, nil) + + spPlan.Detach = true + }) + + Context("when team is not specified", func() { + BeforeEach(func() { + fakeTeamFactory.GetByIDReturnsOnCall(0, fakeTargetTeam) + }) + + Context("when target team is not main", func() { + It("should return error", func() { + Expect(stepErr).To(HaveOccurred()) + Expect(stepErr.Error()).To(Equal( + "only main team can set detached pipeline", + )) + }) + }) + + Context("when target team is main", func() { + BeforeEach(func() { + fakeTargetTeam.NameReturns("main") + fakeTargetTeam.AdminReturns(true) + }) + + Context("when no diff", func() { + BeforeEach(func() { + fakePipeline.ConfigReturns(pipelineObject, nil) + }) + + It("should not error", func() { + Expect(stepErr).ToNot(HaveOccurred()) + }) + + It("should detach parent", func() { + Expect(fakePipeline.DetachParentCallCount()).To(Equal(1)) + }) + }) + + Context("when there are some diff", func() { + BeforeEach(func() { + pipelineObject.Jobs[0].PlanSequence[0].Config.(*atc.TaskStep).Config.Run.Args = []string{"hello world"} + fakePipeline.ConfigReturns(pipelineObject, nil) + }) + + It("should not error", func() { + Expect(stepErr).ToNot(HaveOccurred()) + }) + + It("should save pipeline", func() { + Expect(fakeBuild.SavePipelineCallCount()).To(Equal(1)) + _, _, _, _, _, detach := fakeBuild.SavePipelineArgsForCall(0) + Expect(detach).To(BeTrue()) + }) + }) + }) + }) + + Context("when team is specified", func() { + var fakeCurrentUserTeam *dbfakes.FakeTeam + + BeforeEach(func() { + // If current user is not from the main team, then it should hit + // error "only main team can set another team's pipeline" first, + // which should have been covered by other test cases, thus here + // we only test when current user is from the main team. + fakeCurrentUserTeam = new(dbfakes.FakeTeam) + fakeCurrentUserTeam.IDReturns(111) + fakeCurrentUserTeam.NameReturns("main") + fakeCurrentUserTeam.AdminReturns(true) + + fakeTeamFactory.FindTeamReturnsOnCall(0, fakeCurrentUserTeam, true, nil) + fakeTeamFactory.FindTeamReturnsOnCall(1, fakeTargetTeam, true, nil) + + spPlan.Team = "some-team" + }) + + Context("when no diff", func() { + BeforeEach(func() { + fakePipeline.ConfigReturns(pipelineObject, nil) + }) + + It("should not error", func() { + Expect(stepErr).ToNot(HaveOccurred()) + }) + + It("should detach parent", func() { + Expect(fakePipeline.DetachParentCallCount()).To(Equal(1)) + }) + }) + + Context("when there are some diff", func() { + BeforeEach(func() { + pipelineObject.Jobs[0].PlanSequence[0].Config.(*atc.TaskStep).Config.Run.Args = []string{"hello world"} + fakePipeline.ConfigReturns(pipelineObject, nil) + }) + + It("should not error", func() { + Expect(stepErr).ToNot(HaveOccurred()) + }) + + It("should save pipeline", func() { + Expect(fakeBuild.SavePipelineCallCount()).To(Equal(1)) + _, _, _, _, _, detach := fakeBuild.SavePipelineArgsForCall(0) + Expect(detach).To(BeTrue()) + }) + }) + }) + }) }) }) }) diff --git a/atc/plan.go b/atc/plan.go index 20179b9d5bf..321ba54bf49 100644 --- a/atc/plan.go +++ b/atc/plan.go @@ -384,6 +384,7 @@ type SetPipelinePlan struct { Vars map[string]interface{} `json:"vars,omitempty"` VarFiles []string `json:"var_files,omitempty"` InstanceVars map[string]interface{} `json:"instance_vars,omitempty"` + Detach bool `json:"detach,omitempty"` } type LoadVarPlan struct { diff --git a/atc/steps.go b/atc/steps.go index a02f79bea48..bf2cde43c44 100644 --- a/atc/steps.go +++ b/atc/steps.go @@ -385,6 +385,7 @@ type SetPipelineStep struct { Vars Params `json:"vars,omitempty"` VarFiles []string `json:"var_files,omitempty"` InstanceVars InstanceVars `json:"instance_vars,omitempty"` + Detach bool `json:"detach,omitempty"` } func (step *SetPipelineStep) Visit(v StepVisitor) error { diff --git a/atc/steps_test.go b/atc/steps_test.go index 698e238aeef..d557025612c 100644 --- a/atc/steps_test.go +++ b/atc/steps_test.go @@ -204,6 +204,7 @@ var factoryTests = []StepTest{ vars: {some: vars} var_files: [file-1, file-2] instance_vars: {branch: feature/foo} + detach: true `, StepConfig: &atc.SetPipelineStep{ @@ -212,6 +213,7 @@ var factoryTests = []StepTest{ Vars: atc.Params{"some": "vars"}, VarFiles: []string{"file-1", "file-2"}, InstanceVars: atc.InstanceVars{"branch": "feature/foo"}, + Detach: true, }, }, {