Skip to content

Commit aa9edfd

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 aa9edfd

File tree

5 files changed

+97
-41
lines changed

5 files changed

+97
-41
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: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -425,8 +425,13 @@ func (r *GitRepositoryReconciler) reconcileSource(ctx context.Context,
425425
}
426426

427427
if val, ok := r.features[features.OptimizedGitClones]; ok && val {
428-
if artifact := obj.GetArtifact(); artifact != nil {
429-
checkoutOpts.LastRevision = artifact.Revision
428+
// Only if the object has an existing artifact in storage, attempt to
429+
// short-circuit clone operation. reconcileStorage has already verified
430+
// that the artifact exists.
431+
if conditions.IsTrue(obj, sourcev1.ArtifactInStorageCondition) {
432+
if artifact := obj.GetArtifact(); artifact != nil {
433+
checkoutOpts.LastRevision = artifact.Revision
434+
}
430435
}
431436
}
432437

@@ -474,12 +479,6 @@ func (r *GitRepositoryReconciler) reconcileSource(ctx context.Context,
474479
defer cancel()
475480
c, err := checkoutStrategy.Checkout(gitCtx, dir, repositoryURL, authOpts)
476481
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-
483482
e := &serror.Event{
484483
Err: fmt.Errorf("failed to checkout and determine revision: %w", err),
485484
Reason: sourcev1.GitOperationFailedReason,
@@ -490,8 +489,40 @@ func (r *GitRepositoryReconciler) reconcileSource(ctx context.Context,
490489
}
491490
// Assign the commit to the shared commit reference.
492491
*commit = *c
492+
493+
// If it's a partial commit obtained from an existing artifact, copy the
494+
// artifact content into new source directory.
495+
if !git.IsConcreteCommit(*commit) {
496+
ctrl.LoggerFrom(ctx).V(logger.DebugLevel).Info(fmt.Sprintf(
497+
"no changes since last reconciliation, observed revision '%s'", commit.String()))
498+
499+
// Remove the target directory, as CopyToPath() renames another
500+
// directory to which the artifact is unpacked into the target
501+
// directory. At this point, the target directory is empty, safe to
502+
// remove.
503+
if err := os.RemoveAll(dir); err != nil {
504+
ctrl.LoggerFrom(ctx).Error(err, "failed to remove source build directory")
505+
}
506+
if err := r.Storage.CopyToPath(obj.GetArtifact(), "/", dir); err != nil {
507+
e := &serror.Event{
508+
Err: fmt.Errorf("failed to copy existing artifact to source dir: %w", err),
509+
Reason: sourcev1.CopyOperationFailedReason,
510+
}
511+
conditions.MarkTrue(obj, sourcev1.StorageOperationFailedCondition, e.Reason, e.Err.Error())
512+
return sreconcile.ResultEmpty, e
513+
}
514+
conditions.Delete(obj, sourcev1.FetchFailedCondition)
515+
conditions.Delete(obj, sourcev1.StorageOperationFailedCondition)
516+
517+
return sreconcile.ResultSuccess, nil
518+
}
519+
493520
ctrl.LoggerFrom(ctx).V(logger.DebugLevel).Info("git repository checked out", "url", obj.Spec.URL, "revision", commit.String())
494521
conditions.Delete(obj, sourcev1.FetchFailedCondition)
522+
// In case no-op clone resulted in a failure and in the subsequent
523+
// reconciliation a new remote revision was observed, delete any stale
524+
// StorageOperationFailedCondition.
525+
conditions.Delete(obj, sourcev1.StorageOperationFailedCondition)
495526

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

controllers/gitrepository_controller_test.go

Lines changed: 53 additions & 19 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
}

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)