Skip to content

Commit e0d66b2

Browse files
authored
Merge pull request #121 from fluxcd/push-to-branch
Push to branch
2 parents f45e4a1 + f90846b commit e0d66b2

File tree

5 files changed

+200
-13
lines changed

5 files changed

+200
-13
lines changed

api/v1alpha1/imageupdateautomation_types.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,16 @@ type ImageUpdateAutomationSpec struct {
3939
// value.
4040
// +kubebuilder:default={"strategy":"Setters"}
4141
Update *UpdateStrategy `json:"update,omitempty"`
42-
// Commit specifies how to commit to the git repo
42+
// Commit specifies how to commit to the git repository.
4343
// +required
4444
Commit CommitSpec `json:"commit"`
4545

46+
// Push specifies how and where to push commits made by the
47+
// automation. If missing, commits are pushed (back) to
48+
// `.spec.checkout.branch`.
49+
// +optional
50+
Push *PushSpec `json:"push,omitempty"`
51+
4652
// Suspend tells the controller to not run this automation, until
4753
// it is unset (or set to false). Defaults to false.
4854
// +optional
@@ -54,7 +60,9 @@ type GitCheckoutSpec struct {
5460
// to a git repository to update files in.
5561
// +required
5662
GitRepositoryRef meta.LocalObjectReference `json:"gitRepositoryRef"`
57-
// Branch gives the branch to clone from the git repository.
63+
// Branch gives the branch to clone from the git repository. If
64+
// `.spec.push` is not supplied, commits will also be pushed to
65+
// this branch.
5866
// +required
5967
Branch string `json:"branch"`
6068
}
@@ -95,6 +103,15 @@ type CommitSpec struct {
95103
MessageTemplate string `json:"messageTemplate,omitempty"`
96104
}
97105

106+
// PushSpec specifies how and where to push commits.
107+
type PushSpec struct {
108+
// Branch specifies that commits should be pushed to the branch
109+
// named. The branch is created using `.spec.checkout.branch` as the
110+
// starting point, if it doesn't already exist.
111+
// +required
112+
Branch string `json:"branch"`
113+
}
114+
98115
// ImageUpdateAutomationStatus defines the observed state of ImageUpdateAutomation
99116
type ImageUpdateAutomationStatus struct {
100117
// LastAutomationRunTime records the last time the controller ran

api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/image.toolkit.fluxcd.io_imageupdateautomations.yaml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ spec:
4747
properties:
4848
branch:
4949
description: Branch gives the branch to clone from the git repository.
50+
If `.spec.push` is not supplied, commits will also be pushed
51+
to this branch.
5052
type: string
5153
gitRepositoryRef:
5254
description: GitRepositoryRef refers to the resource giving access
@@ -63,7 +65,7 @@ spec:
6365
- gitRepositoryRef
6466
type: object
6567
commit:
66-
description: Commit specifies how to commit to the git repo
68+
description: Commit specifies how to commit to the git repository.
6769
properties:
6870
authorEmail:
6971
description: AuthorEmail gives the email to provide when making
@@ -86,6 +88,18 @@ spec:
8688
description: Interval gives an lower bound for how often the automation
8789
run should be attempted.
8890
type: string
91+
push:
92+
description: Push specifies how and where to push commits made by
93+
the automation. If missing, commits are pushed (back) to `.spec.checkout.branch`.
94+
properties:
95+
branch:
96+
description: Branch specifies that commits should be pushed to
97+
the branch named. The branch is created using `.spec.checkout.branch`
98+
as the starting point, if it doesn't already exist.
99+
type: string
100+
required:
101+
- branch
102+
type: object
89103
suspend:
90104
description: Suspend tells the controller to not run this automation,
91105
until it is unset (or set to false). Defaults to false.

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)