Skip to content

Commit 0fbc735

Browse files
committed
git clone optimization when artifact in storage
Update the GitRepositoryReconciler to attempt git clone optimization only when an artifact is already present in storage and do not skip reconciliation. When a partial commit is received, due to no-op clone, copy the existing artifact to the source build directory and populate all the associated fields in reconcileSource(). This ensure that the reconciliation is not skipped and allows other subreconcilers to operate on other parts of the GitRepo object like include, ignore, etc, when attributes related to them change but the remote repository has not changed. Remove git.NoChangeError as no-op clone now returns a partial commit and no error. Signed-off-by: Sunny <[email protected]>
1 parent 2393437 commit 0fbc735

File tree

5 files changed

+136
-46
lines changed

5 files changed

+136
-46
lines changed

api/v1beta2/condition_types.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,4 +100,7 @@ const (
100100

101101
// CacheOperationFailedReason signals a failure in cache operation.
102102
CacheOperationFailedReason string = "CacheOperationFailed"
103+
104+
// CopyOperationFailedReason signals a failure in copying files.
105+
CopyOperationFailedReason string = "CopyOperationFailed"
103106
)

controllers/gitrepository_controller.go

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,14 @@ func (r *GitRepositoryReconciler) notify(oldObj, newObj *sourcev1.GitRepository,
304304
oldChecksum = oldObj.GetArtifact().Checksum
305305
}
306306

307-
message := fmt.Sprintf("stored artifact for commit '%s'", commit.ShortMessage())
307+
// A partial commit due to no-op clone doesn't contain the commit
308+
// message information. Have separate message for it.
309+
var message string
310+
if git.IsConcreteCommit(commit) {
311+
message = fmt.Sprintf("stored artifact for commit '%s'", commit.ShortMessage())
312+
} else {
313+
message = fmt.Sprintf("stored artifact for commit hash '%s'", commit.Hash)
314+
}
308315

309316
// Notify on new artifact and failure recovery.
310317
if oldChecksum != newObj.GetArtifact().Checksum {
@@ -425,8 +432,13 @@ func (r *GitRepositoryReconciler) reconcileSource(ctx context.Context,
425432
}
426433

427434
if val, ok := r.features[features.OptimizedGitClones]; ok && val {
428-
if artifact := obj.GetArtifact(); artifact != nil {
429-
checkoutOpts.LastRevision = artifact.Revision
435+
// Only if the object has an existing artifact in storage, attempt to
436+
// short-circuit clone operation. reconcileStorage has already verified
437+
// that the artifact exists.
438+
if conditions.IsTrue(obj, sourcev1.ArtifactInStorageCondition) {
439+
if artifact := obj.GetArtifact(); artifact != nil {
440+
checkoutOpts.LastRevision = artifact.Revision
441+
}
430442
}
431443
}
432444

@@ -474,12 +486,6 @@ func (r *GitRepositoryReconciler) reconcileSource(ctx context.Context,
474486
defer cancel()
475487
c, err := checkoutStrategy.Checkout(gitCtx, dir, repositoryURL, authOpts)
476488
if err != nil {
477-
var v git.NoChangesError
478-
if errors.As(err, &v) {
479-
return sreconcile.ResultSuccess,
480-
&serror.Waiting{Err: v, Reason: v.Message, RequeueAfter: obj.GetRequeueAfter()}
481-
}
482-
483489
e := &serror.Event{
484490
Err: fmt.Errorf("failed to checkout and determine revision: %w", err),
485491
Reason: sourcev1.GitOperationFailedReason,
@@ -490,8 +496,40 @@ func (r *GitRepositoryReconciler) reconcileSource(ctx context.Context,
490496
}
491497
// Assign the commit to the shared commit reference.
492498
*commit = *c
499+
500+
// If it's a partial commit obtained from an existing artifact, copy the
501+
// artifact content into new source directory.
502+
if !git.IsConcreteCommit(*commit) {
503+
ctrl.LoggerFrom(ctx).V(logger.DebugLevel).Info(fmt.Sprintf(
504+
"no changes since last reconciliation, observed revision '%s'", commit.String()))
505+
506+
// Remove the target directory, as CopyToPath() renames another
507+
// directory to which the artifact is unpacked into the target
508+
// directory. At this point, the target directory is empty, safe to
509+
// remove.
510+
if err := os.RemoveAll(dir); err != nil {
511+
ctrl.LoggerFrom(ctx).Error(err, "failed to remove source build directory")
512+
}
513+
if err := r.Storage.CopyToPath(obj.GetArtifact(), "/", dir); err != nil {
514+
e := &serror.Event{
515+
Err: fmt.Errorf("failed to copy existing artifact to source dir: %w", err),
516+
Reason: sourcev1.CopyOperationFailedReason,
517+
}
518+
conditions.MarkTrue(obj, sourcev1.StorageOperationFailedCondition, e.Reason, e.Err.Error())
519+
return sreconcile.ResultEmpty, e
520+
}
521+
conditions.Delete(obj, sourcev1.FetchFailedCondition)
522+
conditions.Delete(obj, sourcev1.StorageOperationFailedCondition)
523+
524+
return sreconcile.ResultSuccess, nil
525+
}
526+
493527
ctrl.LoggerFrom(ctx).V(logger.DebugLevel).Info("git repository checked out", "url", obj.Spec.URL, "revision", commit.String())
494528
conditions.Delete(obj, sourcev1.FetchFailedCondition)
529+
// In case no-op clone resulted in a failure and in the subsequent
530+
// reconciliation a new remote revision was observed, delete any stale
531+
// StorageOperationFailedCondition.
532+
conditions.Delete(obj, sourcev1.StorageOperationFailedCondition)
495533

496534
// Verify commit signature
497535
if result, err := r.verifyCommitSignature(ctx, obj, *commit); err != nil || result == sreconcile.ResultEmpty {

controllers/gitrepository_controller_test.go

Lines changed: 84 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -547,30 +547,35 @@ func TestGitRepositoryReconciler_reconcileSource_checkoutStrategy(t *testing.T)
547547
name string
548548
skipForImplementation string
549549
reference *sourcev1.GitRepositoryRef
550+
beforeFunc func(obj *sourcev1.GitRepository, latestRev string)
550551
want sreconcile.Result
551552
wantErr bool
552553
wantRevision string
554+
wantArtifactOutdated bool
553555
}{
554556
{
555-
name: "Nil reference (default branch)",
556-
want: sreconcile.ResultSuccess,
557-
wantRevision: "master/<commit>",
557+
name: "Nil reference (default branch)",
558+
want: sreconcile.ResultSuccess,
559+
wantRevision: "master/<commit>",
560+
wantArtifactOutdated: true,
558561
},
559562
{
560563
name: "Branch",
561564
reference: &sourcev1.GitRepositoryRef{
562565
Branch: "staging",
563566
},
564-
want: sreconcile.ResultSuccess,
565-
wantRevision: "staging/<commit>",
567+
want: sreconcile.ResultSuccess,
568+
wantRevision: "staging/<commit>",
569+
wantArtifactOutdated: true,
566570
},
567571
{
568572
name: "Tag",
569573
reference: &sourcev1.GitRepositoryRef{
570574
Tag: "v0.1.0",
571575
},
572-
want: sreconcile.ResultSuccess,
573-
wantRevision: "v0.1.0/<commit>",
576+
want: sreconcile.ResultSuccess,
577+
wantRevision: "v0.1.0/<commit>",
578+
wantArtifactOutdated: true,
574579
},
575580
{
576581
name: "Branch commit",
@@ -579,8 +584,9 @@ func TestGitRepositoryReconciler_reconcileSource_checkoutStrategy(t *testing.T)
579584
Branch: "staging",
580585
Commit: "<commit>",
581586
},
582-
want: sreconcile.ResultSuccess,
583-
wantRevision: "staging/<commit>",
587+
want: sreconcile.ResultSuccess,
588+
wantRevision: "staging/<commit>",
589+
wantArtifactOutdated: true,
584590
},
585591
{
586592
name: "Branch commit",
@@ -589,32 +595,56 @@ func TestGitRepositoryReconciler_reconcileSource_checkoutStrategy(t *testing.T)
589595
Branch: "staging",
590596
Commit: "<commit>",
591597
},
592-
want: sreconcile.ResultSuccess,
593-
wantRevision: "HEAD/<commit>",
598+
want: sreconcile.ResultSuccess,
599+
wantRevision: "HEAD/<commit>",
600+
wantArtifactOutdated: true,
594601
},
595602
{
596603
name: "SemVer",
597604
reference: &sourcev1.GitRepositoryRef{
598605
SemVer: "*",
599606
},
600-
want: sreconcile.ResultSuccess,
601-
wantRevision: "v2.0.0/<commit>",
607+
want: sreconcile.ResultSuccess,
608+
wantRevision: "v2.0.0/<commit>",
609+
wantArtifactOutdated: true,
602610
},
603611
{
604612
name: "SemVer range",
605613
reference: &sourcev1.GitRepositoryRef{
606614
SemVer: "<v0.2.1",
607615
},
608-
want: sreconcile.ResultSuccess,
609-
wantRevision: "0.2.0/<commit>",
616+
want: sreconcile.ResultSuccess,
617+
wantRevision: "0.2.0/<commit>",
618+
wantArtifactOutdated: true,
610619
},
611620
{
612621
name: "SemVer prerelease",
613622
reference: &sourcev1.GitRepositoryRef{
614623
SemVer: ">=1.0.0-0 <1.1.0-0",
615624
},
616-
wantRevision: "v1.0.0-alpha/<commit>",
617-
want: sreconcile.ResultSuccess,
625+
wantRevision: "v1.0.0-alpha/<commit>",
626+
want: sreconcile.ResultSuccess,
627+
wantArtifactOutdated: true,
628+
},
629+
{
630+
name: "Optimized clone",
631+
reference: &sourcev1.GitRepositoryRef{
632+
Branch: "staging",
633+
},
634+
beforeFunc: func(obj *sourcev1.GitRepository, latestRev string) {
635+
// Add existing artifact on the object and storage.
636+
obj.Status = sourcev1.GitRepositoryStatus{
637+
Artifact: &sourcev1.Artifact{
638+
Revision: "staging/" + latestRev,
639+
Path: randStringRunes(10),
640+
},
641+
}
642+
testStorage.Archive(obj.GetArtifact(), "testdata/git/repository", nil)
643+
conditions.MarkTrue(obj, sourcev1.ArtifactInStorageCondition, meta.SucceededReason, "foo")
644+
},
645+
want: sreconcile.ResultSuccess,
646+
wantRevision: "staging/<commit>",
647+
wantArtifactOutdated: false,
618648
},
619649
}
620650

@@ -677,6 +707,10 @@ func TestGitRepositoryReconciler_reconcileSource_checkoutStrategy(t *testing.T)
677707
obj := obj.DeepCopy()
678708
obj.Spec.GitImplementation = i
679709

710+
if tt.beforeFunc != nil {
711+
tt.beforeFunc(obj, headRef.Hash().String())
712+
}
713+
680714
var commit git.Commit
681715
var includes artifactSet
682716
got, err := r.reconcileSource(ctx, obj, &commit, &includes, tmpDir)
@@ -685,10 +719,10 @@ func TestGitRepositoryReconciler_reconcileSource_checkoutStrategy(t *testing.T)
685719
}
686720
g.Expect(err != nil).To(Equal(tt.wantErr))
687721
g.Expect(got).To(Equal(tt.want))
688-
if tt.wantRevision != "" {
722+
if tt.wantRevision != "" && !tt.wantErr {
689723
revision := strings.ReplaceAll(tt.wantRevision, "<commit>", headRef.Hash().String())
690724
g.Expect(commit.String()).To(Equal(revision))
691-
g.Expect(conditions.IsTrue(obj, sourcev1.ArtifactOutdatedCondition)).To(BeTrue())
725+
g.Expect(conditions.IsTrue(obj, sourcev1.ArtifactOutdatedCondition)).To(Equal(tt.wantArtifactOutdated))
692726
}
693727
})
694728
}
@@ -1782,10 +1816,20 @@ func TestGitRepositoryReconciler_statusConditions(t *testing.T) {
17821816
}
17831817

17841818
func TestGitRepositoryReconciler_notify(t *testing.T) {
1819+
concreteCommit := git.Commit{
1820+
Hash: git.Hash("some-hash"),
1821+
Message: "test commit",
1822+
Encoded: []byte("commit content"),
1823+
}
1824+
partialCommit := git.Commit{
1825+
Hash: git.Hash("some-hash"),
1826+
}
1827+
17851828
tests := []struct {
17861829
name string
17871830
res sreconcile.Result
17881831
resErr error
1832+
commit git.Commit
17891833
oldObjBeforeFunc func(obj *sourcev1.GitRepository)
17901834
newObjBeforeFunc func(obj *sourcev1.GitRepository)
17911835
wantEvent string
@@ -1799,6 +1843,7 @@ func TestGitRepositoryReconciler_notify(t *testing.T) {
17991843
name: "new artifact",
18001844
res: sreconcile.ResultSuccess,
18011845
resErr: nil,
1846+
commit: concreteCommit,
18021847
newObjBeforeFunc: func(obj *sourcev1.GitRepository) {
18031848
obj.Status.Artifact = &sourcev1.Artifact{Revision: "xxx", Checksum: "yyy"}
18041849
},
@@ -1808,6 +1853,7 @@ func TestGitRepositoryReconciler_notify(t *testing.T) {
18081853
name: "recovery from failure",
18091854
res: sreconcile.ResultSuccess,
18101855
resErr: nil,
1856+
commit: concreteCommit,
18111857
oldObjBeforeFunc: func(obj *sourcev1.GitRepository) {
18121858
obj.Status.Artifact = &sourcev1.Artifact{Revision: "xxx", Checksum: "yyy"}
18131859
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, sourcev1.GitOperationFailedReason, "fail")
@@ -1823,6 +1869,7 @@ func TestGitRepositoryReconciler_notify(t *testing.T) {
18231869
name: "recovery and new artifact",
18241870
res: sreconcile.ResultSuccess,
18251871
resErr: nil,
1872+
commit: concreteCommit,
18261873
oldObjBeforeFunc: func(obj *sourcev1.GitRepository) {
18271874
obj.Status.Artifact = &sourcev1.Artifact{Revision: "xxx", Checksum: "yyy"}
18281875
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, sourcev1.GitOperationFailedReason, "fail")
@@ -1838,6 +1885,7 @@ func TestGitRepositoryReconciler_notify(t *testing.T) {
18381885
name: "no updates",
18391886
res: sreconcile.ResultSuccess,
18401887
resErr: nil,
1888+
commit: concreteCommit,
18411889
oldObjBeforeFunc: func(obj *sourcev1.GitRepository) {
18421890
obj.Status.Artifact = &sourcev1.Artifact{Revision: "xxx", Checksum: "yyy"}
18431891
conditions.MarkTrue(obj, meta.ReadyCondition, meta.SucceededReason, "ready")
@@ -1847,6 +1895,22 @@ func TestGitRepositoryReconciler_notify(t *testing.T) {
18471895
conditions.MarkTrue(obj, meta.ReadyCondition, meta.SucceededReason, "ready")
18481896
},
18491897
},
1898+
{
1899+
name: "recovery with no-op clone commit",
1900+
res: sreconcile.ResultSuccess,
1901+
resErr: nil,
1902+
commit: partialCommit,
1903+
oldObjBeforeFunc: func(obj *sourcev1.GitRepository) {
1904+
obj.Status.Artifact = &sourcev1.Artifact{Revision: "xxx", Checksum: "yyy"}
1905+
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, sourcev1.GitOperationFailedReason, "fail")
1906+
conditions.MarkFalse(obj, meta.ReadyCondition, meta.FailedReason, "foo")
1907+
},
1908+
newObjBeforeFunc: func(obj *sourcev1.GitRepository) {
1909+
obj.Status.Artifact = &sourcev1.Artifact{Revision: "aaa", Checksum: "bbb"}
1910+
conditions.MarkTrue(obj, meta.ReadyCondition, meta.SucceededReason, "ready")
1911+
},
1912+
wantEvent: "Normal NewArtifact stored artifact for commit hash",
1913+
},
18501914
}
18511915

18521916
for _, tt := range tests {
@@ -1868,10 +1932,7 @@ func TestGitRepositoryReconciler_notify(t *testing.T) {
18681932
EventRecorder: recorder,
18691933
features: features.FeatureGates(),
18701934
}
1871-
commit := &git.Commit{
1872-
Message: "test commit",
1873-
}
1874-
reconciler.notify(oldObj, newObj, *commit, tt.res, tt.resErr)
1935+
reconciler.notify(oldObj, newObj, tt.commit, tt.res, tt.resErr)
18751936

18761937
select {
18771938
case x, ok := <-recorder.Events:

docs/spec/v1beta2/gitrepositories.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -406,8 +406,8 @@ reconciliations. It supports both `go-git` and `libgit2` implementations
406406
when cloning repositories using branches or tags.
407407

408408
When enabled, avoids full clone operations by first checking whether
409-
the last revision is still the same at the target repository,
410-
and if that is so, skips the reconciliation.
409+
the revision of the last stored artifact is still the head of the remote
410+
repository, and if that is so, the local artifact is reused.
411411

412412
This feature is enabled by default. It can be disabled by starting the
413413
controller with the argument `--feature-gates=OptimizedGitClones=false`.

pkg/git/git.go

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -107,18 +107,6 @@ type CheckoutStrategy interface {
107107
Checkout(ctx context.Context, path, url string, config *AuthOptions) (*Commit, error)
108108
}
109109

110-
// NoChangesError represents the case in which a Git clone operation
111-
// is attempted, but cancelled as the revision is still the same as
112-
// the one observed on the last successful reconciliation.
113-
type NoChangesError struct {
114-
Message string
115-
ObservedRevision string
116-
}
117-
118-
func (e NoChangesError) Error() string {
119-
return fmt.Sprintf("%s: observed revision '%s'", e.Message, e.ObservedRevision)
120-
}
121-
122110
// IsConcreteCommit returns if a given commit is a concrete commit. Concrete
123111
// commits have most of commit metadata and commit content. In contrast, a
124112
// partial commit may only have some metadata and no commit content.

0 commit comments

Comments
 (0)