Skip to content

Commit f90846b

Browse files
committed
Implement .spec.push.branch most simply
This adapts the controller so that it will honour the `.spec.push.branch` field. The behaviour _without_ that field is to check out the branch given in `.spec.checkout.branch`, commit, and push to the origin. With `.spec.push.branch` present, it will try to check out that branch; if it doesn't exist, it'll create it, starting from `.spec.checkout.branch`. Either way it'll commit to that branch and push to the origin. The effect is that all automation will happen on the "push" branch, and (most likely) not be applied into the cluster until merged into whichever branch is synced. When the push branch is deleted, it'll be created anew; otherwise, commits will pile up there as more changes are made. Signed-off-by: Michael Bridgen <[email protected]>
1 parent 97c7510 commit f90846b

File tree

2 files changed

+146
-10
lines changed

2 files changed

+146
-10
lines changed

controllers/imageupdateautomation_controller.go

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,14 @@ func (r *ImageUpdateAutomationReconciler) Reconcile(ctx context.Context, req ctr
176176
return failWithError(err)
177177
}
178178

179+
// When there's a push spec, the pushed-to branch is where commits
180+
// shall be made
181+
if auto.Spec.Push != nil {
182+
if err := switchBranch(repo, auto.Spec.Push.Branch); err != nil {
183+
return failWithError(err)
184+
}
185+
}
186+
179187
log.V(debug).Info("cloned git repository", "gitrepository", originName, "branch", auto.Spec.Checkout.Branch, "working", tmp)
180188

181189
switch {
@@ -221,15 +229,19 @@ func (r *ImageUpdateAutomationReconciler) Reconcile(ctx context.Context, req ctr
221229
return failWithError(err)
222230
}
223231
} else {
224-
if err := push(ctx, tmp, repo, auto.Spec.Checkout.Branch, access, origin.Spec.GitImplementation); err != nil {
232+
pushBranch := auto.Spec.Checkout.Branch
233+
if auto.Spec.Push != nil {
234+
pushBranch = auto.Spec.Push.Branch
235+
}
236+
if err := push(ctx, tmp, repo, pushBranch, access, origin.Spec.GitImplementation); err != nil {
225237
return failWithError(err)
226238
}
227239

228-
r.event(ctx, auto, events.EventSeverityInfo, "committed and pushed change "+rev)
229-
log.Info("pushed commit to origin", "revision", rev)
240+
r.event(ctx, auto, events.EventSeverityInfo, "committed and pushed change "+rev+" to "+pushBranch)
241+
log.Info("pushed commit to origin", "revision", rev, "branch", pushBranch)
230242
auto.Status.LastPushCommit = rev
231243
auto.Status.LastPushTime = &metav1.Time{Time: now}
232-
statusMessage = "committed and pushed " + rev
244+
statusMessage = "committed and pushed " + rev + " to " + pushBranch
233245
}
234246

235247
// Getting to here is a successful run.
@@ -358,6 +370,45 @@ func cloneInto(ctx context.Context, access repoAccess, branch, path, impl string
358370
return gogit.PlainOpen(path)
359371
}
360372

373+
// switchBranch switches the repo from the current branch to the
374+
// branch given. If the branch does not exist, it is created using the
375+
// head as the starting point.
376+
func switchBranch(repo *gogit.Repository, pushBranch string) error {
377+
remoteBranch := plumbing.NewRemoteReferenceName(originRemote, pushBranch)
378+
localBranch := plumbing.NewBranchReferenceName(pushBranch)
379+
380+
// is the remote branch already present?
381+
branchHead, err := repo.Reference(remoteBranch, false)
382+
switch {
383+
case err == plumbing.ErrReferenceNotFound:
384+
// make a new branch, starting at HEAD
385+
head, err := repo.Head()
386+
if err != nil {
387+
return err
388+
}
389+
branchRef := plumbing.NewHashReference(localBranch, head.Hash())
390+
if err = repo.Storer.SetReference(branchRef); err != nil {
391+
return err
392+
}
393+
case err != nil:
394+
return err
395+
default:
396+
// make a local branch that references the remote branch
397+
branchRef := plumbing.NewHashReference(localBranch, branchHead.Hash())
398+
if err = repo.Storer.SetReference(branchRef); err != nil {
399+
return err
400+
}
401+
}
402+
403+
tree, err := repo.Worktree()
404+
if err != nil {
405+
return err
406+
}
407+
return tree.Checkout(&gogit.CheckoutOptions{
408+
Branch: localBranch,
409+
})
410+
}
411+
361412
var errNoChanges error = errors.New("no changes made to working directory")
362413

363414
func commitAll(ctx context.Context, repo *gogit.Repository, commit *imagev1.CommitSpec, values TemplateData) (string, error) {

controllers/update_test.go

Lines changed: 91 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,62 @@ Images:
390390
Expect(gitServer.StopSSH()).To(Succeed())
391391
})
392392

393+
Context("with PushSpec", func() {
394+
395+
var (
396+
update *imagev1.ImageUpdateAutomation
397+
pushBranch string
398+
)
399+
400+
BeforeEach(func() {
401+
commitInRepo(cloneLocalRepoURL, branch, "Install setter marker", func(tmp string) {
402+
replaceMarker(tmp, policyKey)
403+
})
404+
waitForNewHead(localRepo, branch)
405+
406+
pushBranch = "pr-" + randStringRunes(5)
407+
408+
update = &imagev1.ImageUpdateAutomation{
409+
Spec: imagev1.ImageUpdateAutomationSpec{
410+
Interval: metav1.Duration{Duration: 2 * time.Hour},
411+
Checkout: imagev1.GitCheckoutSpec{
412+
GitRepositoryRef: meta.LocalObjectReference{
413+
Name: gitRepoKey.Name,
414+
},
415+
Branch: branch,
416+
},
417+
Update: &imagev1.UpdateStrategy{
418+
Strategy: imagev1.UpdateStrategySetters,
419+
},
420+
Commit: imagev1.CommitSpec{
421+
MessageTemplate: commitMessage,
422+
},
423+
Push: &imagev1.PushSpec{
424+
Branch: pushBranch,
425+
},
426+
},
427+
}
428+
update.Name = "update-" + randStringRunes(5)
429+
update.Namespace = namespace.Name
430+
431+
Expect(k8sClient.Create(context.Background(), update)).To(Succeed())
432+
})
433+
434+
It("creates and pushes the push branch", func() {
435+
waitForNewHead(localRepo, pushBranch)
436+
head, err := localRepo.Reference(plumbing.NewRemoteReferenceName(originRemote, pushBranch), true)
437+
Expect(err).NotTo(HaveOccurred())
438+
commit, err := localRepo.CommitObject(head.Hash())
439+
Expect(err).ToNot(HaveOccurred())
440+
Expect(commit.Message).To(Equal(commitMessage))
441+
})
442+
443+
AfterEach(func() {
444+
Expect(k8sClient.Delete(context.Background(), update)).To(Succeed())
445+
})
446+
447+
})
448+
393449
Context("with Setters", func() {
394450

395451
var (
@@ -586,20 +642,49 @@ func setterRef(name types.NamespacedName) string {
586642
return fmt.Sprintf(`{"%s": "%s:%s"}`, update.SetterShortHand, name.Namespace, name.Name)
587643
}
588644

645+
// waitForHead fetches the remote branch given until it differs from
646+
// the remote ref locally (or if there's no ref locally, until it has
647+
// fetched the remote branch). It resets the working tree head to the
648+
// remote branch ref.
589649
func waitForNewHead(repo *git.Repository, branch string) {
590-
head, _ := repo.Head()
591-
headHash := head.Hash().String()
592650
working, err := repo.Worktree()
593651
Expect(err).ToNot(HaveOccurred())
652+
653+
// Try to find the remote branch in the repo locally; this will
654+
// fail if we're on a branch that didn't exist when we cloned the
655+
// repo (e.g., if the automation is pushing to another branch).
656+
remoteHeadHash := ""
657+
remoteBranch := plumbing.NewRemoteReferenceName(originRemote, branch)
658+
remoteHead, err := repo.Reference(remoteBranch, false)
659+
if err != plumbing.ErrReferenceNotFound {
660+
Expect(err).ToNot(HaveOccurred())
661+
}
662+
if err == nil {
663+
remoteHeadHash = remoteHead.Hash().String()
664+
} // otherwise, any reference fetched will do.
665+
666+
// Now try to fetch new commits from that remote branch
594667
Eventually(func() bool {
595-
if working.Pull(&git.PullOptions{
596-
ReferenceName: plumbing.NewBranchReferenceName(branch),
668+
if err := repo.Fetch(&git.FetchOptions{
669+
RefSpecs: []config.RefSpec{
670+
config.RefSpec("refs/heads/" + branch + ":refs/remotes/origin/" + branch),
671+
},
597672
}); err != nil {
598673
return false
599674
}
600-
h, _ := repo.Head()
601-
return headHash != h.Hash().String()
675+
remoteHead, err = repo.Reference(remoteBranch, false)
676+
if err != nil {
677+
return false
678+
}
679+
return remoteHead.Hash().String() != remoteHeadHash
602680
}, timeout, time.Second).Should(BeTrue())
681+
682+
// New commits in the remote branch -- reset the working tree head
683+
// to that. Note this does not create a local branch tracking the
684+
// remote, so it is a detached head.
685+
Expect(working.Reset(&git.ResetOptions{
686+
Commit: remoteHead.Hash(),
687+
})).To(Succeed())
603688
}
604689

605690
func compareRepoWithExpected(repoURL, branch, fixture string, changeFixture func(tmp string)) {

0 commit comments

Comments
 (0)