Skip to content

Commit 68ad71b

Browse files
committed
git: add push.refspec to push using a refspec
Add `.spec.git.push.refspec` to allow specifying a refspec to be used for performing a push operation. If specified alongside `.spec.git.push.branch`, two push operations, one for each specified push configuration will be performed. Signed-off-by: Sanskar Jaiswal <[email protected]>
1 parent e127374 commit 68ad71b

File tree

5 files changed

+241
-77
lines changed

5 files changed

+241
-77
lines changed

api/v1beta1/git.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,14 @@ type PushSpec struct {
8585
// Branch specifies that commits should be pushed to the branch
8686
// named. The branch is created using `.spec.checkout.branch` as the
8787
// starting point, if it doesn't already exist.
88-
// +required
89-
Branch string `json:"branch"`
88+
// +optional
89+
Branch string `json:"branch,omitempty"`
90+
91+
// Refspec specifies the Git Refspec to use for a push operation.
92+
// If both Branch and Refspec are provided, then the commit is pushed
93+
// to the branch and also using the specified refspec.
94+
// For more details about Git Refspecs, see:
95+
// https://git-scm.com/book/en/v2/Git-Internals-The-Refspec
96+
// +optional
97+
Refspec string `json:"refspec,omitempty"`
9098
}

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,13 @@ spec:
135135
to the branch named. The branch is created using `.spec.checkout.branch`
136136
as the starting point, if it doesn't already exist.
137137
type: string
138-
required:
139-
- branch
138+
refspec:
139+
description: 'Refspec specifies the Git Refspec to use for
140+
a push operation. If both Branch and Refspec are provided,
141+
then the commit is pushed to the branch and also using the
142+
specified refspec. For more details about Git Refspecs,
143+
see: https://git-scm.com/book/en/v2/Git-Internals-The-Refspec'
144+
type: string
140145
type: object
141146
required:
142147
- commit

docs/api/v1beta1/image-automation.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -638,11 +638,28 @@ string
638638
</em>
639639
</td>
640640
<td>
641+
<em>(Optional)</em>
641642
<p>Branch specifies that commits should be pushed to the branch
642643
named. The branch is created using <code>.spec.checkout.branch</code> as the
643644
starting point, if it doesn&rsquo;t already exist.</p>
644645
</td>
645646
</tr>
647+
<tr>
648+
<td>
649+
<code>refspec</code><br>
650+
<em>
651+
string
652+
</em>
653+
</td>
654+
<td>
655+
<em>(Optional)</em>
656+
<p>Refspec specifies the Git Refspec to use for a push operation.
657+
If both Branch and Refspec are provided, then the commit is pushed
658+
to the branch and also using the specified refspec.
659+
For more details about Git Refspecs, see:
660+
<a href="https://git-scm.com/book/en/v2/Git-Internals-The-Refspec">https://git-scm.com/book/en/v2/Git-Internals-The-Refspec</a></p>
661+
</td>
662+
</tr>
646663
</tbody>
647664
</table>
648665
</div>

internal/controller/imageupdateautomation_controller.go

Lines changed: 119 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -214,30 +214,15 @@ func (r *ImageUpdateAutomationReconciler) Reconcile(ctx context.Context, req ctr
214214
}
215215

216216
// validate the git spec and default any values needed later, before proceeding
217-
var ref *sourcev1.GitRepositoryRef
217+
var checkoutRef *sourcev1.GitRepositoryRef
218218
if gitSpec.Checkout != nil {
219-
ref = &gitSpec.Checkout.Reference
220-
tracelog.Info("using git repository ref from .spec.git.checkout", "ref", ref)
219+
checkoutRef = &gitSpec.Checkout.Reference
220+
tracelog.Info("using git repository ref from .spec.git.checkout", "ref", checkoutRef)
221221
} else if r := origin.Spec.Reference; r != nil {
222-
ref = r
223-
tracelog.Info("using git repository ref from GitRepository spec", "ref", ref)
222+
checkoutRef = r
223+
tracelog.Info("using git repository ref from GitRepository spec", "ref", checkoutRef)
224224
} // else remain as `nil` and git.DefaultBranch will be used.
225225

226-
var pushBranch string
227-
if gitSpec.Push != nil {
228-
pushBranch = gitSpec.Push.Branch
229-
tracelog.Info("using push branch from .spec.push.branch", "branch", pushBranch)
230-
} else {
231-
// Here's where it gets constrained. If there's no push branch
232-
// given, then the checkout ref must include a branch, and
233-
// that can be used.
234-
if ref == nil || ref.Branch == "" {
235-
return failWithError(fmt.Errorf("Push branch not given explicitly, and cannot be inferred from .spec.git.checkout.ref or GitRepository .spec.ref"))
236-
}
237-
pushBranch = ref.Branch
238-
tracelog.Info("using push branch from $ref.branch", "branch", pushBranch)
239-
}
240-
241226
tmp, err := os.MkdirTemp("", fmt.Sprintf("%s-%s", originName.Namespace, originName.Name))
242227
if err != nil {
243228
return failWithError(err)
@@ -248,42 +233,43 @@ func (r *ImageUpdateAutomationReconciler) Reconcile(ctx context.Context, req ctr
248233
}
249234
}()
250235

251-
debuglog.Info("attempting to clone git repository", "gitrepository", originName, "ref", ref, "working", tmp)
252-
253-
authOpts, err := r.getAuthOpts(ctx, &origin)
254-
if err != nil {
255-
return failWithError(err)
256-
}
257-
258-
clientOpts := []gogit.ClientOption{gogit.WithDiskStorage()}
259-
if authOpts.Transport == git.HTTP {
260-
clientOpts = append(clientOpts, gogit.WithInsecureCredentialsOverHTTP())
236+
var pushBranch string
237+
var switchBranch bool
238+
if gitSpec.Push != nil {
239+
// We only need to switch branches when a branch has been specified in
240+
// the push spec and it is different than the one in the checkout ref.
241+
if gitSpec.Push.Branch != "" && gitSpec.Push.Branch != checkoutRef.Branch {
242+
pushBranch = gitSpec.Push.Branch
243+
switchBranch = true
244+
tracelog.Info("using push branch from .spec.push.branch", "branch", pushBranch)
245+
}
246+
} else {
247+
// Here's where it gets constrained. If there's no push branch
248+
// given, then the checkout ref must include a branch, and
249+
// that can be used.
250+
if checkoutRef == nil || checkoutRef.Branch == "" {
251+
return failWithError(
252+
fmt.Errorf("Push spec not provided, and cannot be inferred from .spec.git.checkout.ref or GitRepository .spec.ref"),
253+
)
254+
}
255+
pushBranch = checkoutRef.Branch
256+
tracelog.Info("using push branch from $ref.branch", "branch", pushBranch)
261257
}
262258

263-
// If the push branch is different from the checkout ref, we need to
264-
// have all the references downloaded at clone time, to ensure that
265-
// SwitchBranch will have access to the target branch state. fluxcd/flux2#3384
266-
//
267-
// To always overwrite the push branch, the feature gate
268-
// GitAllBranchReferences can be set to false, which will cause
269-
// the SwitchBranch operation to ignore the remote branch state.
270-
allReferences := r.features[features.GitAllBranchReferences]
271-
if pushBranch != ref.Branch {
272-
clientOpts = append(clientOpts, gogit.WithSingleBranch(!allReferences))
273-
}
259+
debuglog.Info("attempting to clone git repository", "gitrepository", originName, "ref", checkoutRef, "working", tmp)
274260

275-
gitClient, err := gogit.NewClient(tmp, authOpts, clientOpts...)
261+
gitClient, err := r.constructGitClient(ctx, &origin, tmp, switchBranch)
276262
if err != nil {
277263
return failWithError(err)
278264
}
279265
defer gitClient.Close()
280266

281267
opts := repository.CloneConfig{}
282-
if ref != nil {
283-
opts.Tag = ref.Tag
284-
opts.SemVer = ref.SemVer
285-
opts.Commit = ref.Commit
286-
opts.Branch = ref.Branch
268+
if checkoutRef != nil {
269+
opts.Tag = checkoutRef.Tag
270+
opts.SemVer = checkoutRef.SemVer
271+
opts.Commit = checkoutRef.Commit
272+
opts.Branch = checkoutRef.Branch
287273
}
288274

289275
if enabled, _ := r.features[features.GitShallowClone]; enabled {
@@ -297,9 +283,9 @@ func (r *ImageUpdateAutomationReconciler) Reconcile(ctx context.Context, req ctr
297283
return failWithError(err)
298284
}
299285

300-
// When there's a push spec, the pushed-to branch is where commits
286+
// When there's a push branch specified, the pushed-to branch is where commits
301287
// shall be made
302-
if gitSpec.Push != nil && !(ref != nil && ref.Branch == pushBranch) {
288+
if switchBranch {
303289
// Use the git operations timeout for the repo.
304290
fetchCtx, cancel := context.WithTimeout(ctx, origin.Spec.Timeout.Duration)
305291
defer cancel()
@@ -352,7 +338,6 @@ func (r *ImageUpdateAutomationReconciler) Reconcile(ctx context.Context, req ctr
352338

353339
debuglog.Info("ran updates to working dir", "working", tmp)
354340

355-
var statusMessage string
356341
var signingEntity *openpgp.Entity
357342
if gitSpec.Commit.SigningKey != nil {
358343
if signingEntity, err = r.getSigningEntity(ctx, auto); err != nil {
@@ -386,40 +371,80 @@ func (r *ImageUpdateAutomationReconciler) Reconcile(ctx context.Context, req ctr
386371
err = extgogit.ErrEmptyCommit
387372
}
388373

374+
var statusMessage strings.Builder
389375
if err != nil {
390376
if !errors.Is(err, git.ErrNoStagedFiles) && !errors.Is(err, extgogit.ErrEmptyCommit) {
391377
return failWithError(err)
392378
}
393379

394380
log.Info("no changes made in working directory; no commit")
395-
statusMessage = "no updates made"
381+
statusMessage.WriteString("no updates made")
396382

397383
if auto.Status.LastPushTime != nil && len(auto.Status.LastPushCommit) >= 7 {
398-
statusMessage = fmt.Sprintf("%s; last commit %s at %s", statusMessage, auto.Status.LastPushCommit[:7], auto.Status.LastPushTime.Format(time.RFC3339))
384+
statusMessage.WriteString(fmt.Sprintf("; last commit %s at %s",
385+
auto.Status.LastPushCommit[:7], auto.Status.LastPushTime.Format(time.RFC3339)))
399386
}
400387
} else {
401388
// Use the git operations timeout for the repo.
402389
pushCtx, cancel := context.WithTimeout(ctx, origin.Spec.Timeout.Duration)
403390
defer cancel()
404-
opts := repository.PushConfig{}
405-
forcePush := r.features[features.GitForcePushBranch]
406-
if forcePush && pushBranch != ref.Branch {
407-
opts.Force = true
391+
392+
var pushToBranch bool
393+
var pushWithRefspec bool
394+
// If a refspec is specified, then we need to perform a push using
395+
// that refspec.
396+
if gitSpec.Push != nil && gitSpec.Push.Refspec != "" {
397+
pushWithRefspec = true
408398
}
409-
if err := gitClient.Push(pushCtx, opts); err != nil {
410-
return failWithError(err)
399+
// We need to push the commit to the push branch if one was specified, or if
400+
// no push config was specified, then we need to push to the branch we checked
401+
// out to.
402+
if (gitSpec.Push != nil && gitSpec.Push.Branch != "") || gitSpec.Push == nil {
403+
pushToBranch = true
404+
}
405+
406+
if pushToBranch {
407+
// If the force push feature flag is true and we are pushing to a
408+
// different branch than the one we checked out to, then force push
409+
// these changes.
410+
var pushConfig repository.PushConfig
411+
forcePush := r.features[features.GitForcePushBranch]
412+
if forcePush && switchBranch {
413+
pushConfig.Force = true
414+
}
415+
416+
if err := gitClient.Push(pushCtx, pushConfig); err != nil {
417+
return failWithError(err)
418+
}
419+
log.Info("pushed commit to origin", "revision", rev, "branch", pushBranch)
420+
statusMessage.WriteString(fmt.Sprintf("commited and pushed commit '%s' to branch '%s'", rev, pushBranch))
411421
}
412422

413-
r.event(ctx, auto, eventv1.EventSeverityInfo, fmt.Sprintf("Committed and pushed change %s to %s\n%s", rev, pushBranch, message))
414-
log.Info("pushed commit to origin", "revision", rev, "branch", pushBranch)
423+
if pushWithRefspec {
424+
pushConfig := repository.PushConfig{
425+
Refspecs: []string{gitSpec.Push.Refspec},
426+
}
427+
if err := gitClient.Push(pushCtx, pushConfig); err != nil {
428+
return failWithError(err)
429+
}
430+
log.Info("pushed commit to origin", "revision", rev, "refspec", gitSpec.Push.Refspec)
431+
432+
if pushToBranch {
433+
statusMessage.WriteString(fmt.Sprintf(" and using refspec '%s'", gitSpec.Push.Refspec))
434+
} else {
435+
statusMessage.WriteString(fmt.Sprintf("committed and pushed commit '%s' using refspec '%s'", rev, gitSpec.Push.Refspec))
436+
}
437+
}
438+
439+
r.event(ctx, auto, eventv1.EventSeverityInfo, fmt.Sprintf("%s\n%s", statusMessage.String(), message))
440+
415441
auto.Status.LastPushCommit = rev
416442
auto.Status.LastPushTime = &metav1.Time{Time: start}
417-
statusMessage = "committed and pushed " + rev + " to " + pushBranch
418443
}
419444

420445
// Getting to here is a successful run.
421446
auto.Status.LastAutomationRunTime = &metav1.Time{Time: start}
422-
imagev1.SetImageUpdateAutomationReadiness(&auto, metav1.ConditionTrue, imagev1.ReconciliationSucceededReason, statusMessage)
447+
imagev1.SetImageUpdateAutomationReadiness(&auto, metav1.ConditionTrue, imagev1.ReconciliationSucceededReason, statusMessage.String())
423448
if err := r.patchStatus(ctx, req, auto.Status); err != nil {
424449
return ctrl.Result{Requeue: true}, err
425450
}
@@ -545,6 +570,38 @@ func (r *ImageUpdateAutomationReconciler) getAuthOpts(ctx context.Context, repos
545570
return opts, nil
546571
}
547572

573+
// constructGitClient constructs and returns a new gogit client.
574+
func (r *ImageUpdateAutomationReconciler) constructGitClient(ctx context.Context,
575+
origin *sourcev1.GitRepository, repoDir string, switchBranch bool) (*gogit.Client, error) {
576+
authOpts, err := r.getAuthOpts(ctx, origin)
577+
if err != nil {
578+
return nil, err
579+
}
580+
581+
clientOpts := []gogit.ClientOption{gogit.WithDiskStorage()}
582+
if authOpts.Transport == git.HTTP {
583+
clientOpts = append(clientOpts, gogit.WithInsecureCredentialsOverHTTP())
584+
}
585+
586+
// If the push branch is different from the checkout ref, we need to
587+
// have all the references downloaded at clone time, to ensure that
588+
// SwitchBranch will have access to the target branch state. fluxcd/flux2#3384
589+
//
590+
// To always overwrite the push branch, the feature gate
591+
// GitAllBranchReferences can be set to false, which will cause
592+
// the SwitchBranch operation to ignore the remote branch state.
593+
allReferences := r.features[features.GitAllBranchReferences]
594+
if switchBranch {
595+
clientOpts = append(clientOpts, gogit.WithSingleBranch(!allReferences))
596+
}
597+
598+
gitClient, err := gogit.NewClient(repoDir, authOpts, clientOpts...)
599+
if err != nil {
600+
return nil, err
601+
}
602+
return gitClient, nil
603+
}
604+
548605
// getSigningEntity retrieves an OpenPGP entity referenced by the
549606
// provided imagev1.ImageUpdateAutomation for git commit signing
550607
func (r *ImageUpdateAutomationReconciler) getSigningEntity(ctx context.Context, auto imagev1.ImageUpdateAutomation) (*openpgp.Entity, error) {

0 commit comments

Comments
 (0)