Skip to content

Commit 9c1bbc4

Browse files
author
Paulo Gomes
authored
Merge pull request #665 from pjbgf/optimise-clone
Optimise clone operations
2 parents 5b4750b + 7f315f9 commit 9c1bbc4

File tree

13 files changed

+471
-101
lines changed

13 files changed

+471
-101
lines changed

controllers/gitrepository_controller.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import (
4848

4949
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
5050
serror "github.com/fluxcd/source-controller/internal/error"
51+
"github.com/fluxcd/source-controller/internal/features"
5152
sreconcile "github.com/fluxcd/source-controller/internal/reconcile"
5253
"github.com/fluxcd/source-controller/internal/reconcile/summarize"
5354
"github.com/fluxcd/source-controller/internal/util"
@@ -311,8 +312,9 @@ func (r *GitRepositoryReconciler) notify(oldObj, newObj *sourcev1.GitRepository,
311312
// reconcileStorage ensures the current state of the storage matches the
312313
// desired and previously observed state.
313314
//
314-
// All Artifacts for the object except for the current one in the Status are
315-
// garbage collected from the Storage.
315+
// The garbage collection is executed based on the flag based settings and
316+
// may remove files that are beyond their TTL or the maximum number of files
317+
// to survive a collection cycle.
316318
// If the Artifact in the Status of the object disappeared from the Storage,
317319
// it is removed from the object.
318320
// If the object does not have an Artifact in its Status, a Reconciling
@@ -411,6 +413,13 @@ func (r *GitRepositoryReconciler) reconcileSource(ctx context.Context,
411413
checkoutOpts.Tag = ref.Tag
412414
checkoutOpts.SemVer = ref.SemVer
413415
}
416+
417+
if oc, _ := features.Enabled(features.OptimizedGitClones); oc {
418+
if artifact := obj.GetArtifact(); artifact != nil {
419+
checkoutOpts.LastRevision = artifact.Revision
420+
}
421+
}
422+
414423
checkoutStrategy, err := strategy.CheckoutStrategyForImplementation(ctx,
415424
git.Implementation(obj.Spec.GitImplementation), checkoutOpts)
416425
if err != nil {
@@ -455,6 +464,12 @@ func (r *GitRepositoryReconciler) reconcileSource(ctx context.Context,
455464
defer cancel()
456465
c, err := checkoutStrategy.Checkout(gitCtx, dir, repositoryURL, authOpts)
457466
if err != nil {
467+
var v git.NoChangesError
468+
if errors.As(err, &v) {
469+
return sreconcile.ResultSuccess,
470+
&serror.Waiting{Err: v, Reason: v.Message, RequeueAfter: obj.GetRequeueAfter()}
471+
}
472+
458473
e := &serror.Event{
459474
Err: fmt.Errorf("failed to checkout and determine revision: %w", err),
460475
Reason: sourcev1.GitOperationFailedReason,
@@ -495,6 +510,7 @@ func (r *GitRepositoryReconciler) reconcileSource(ctx context.Context,
495510
// object are set, and the symlink in the Storage is updated to its path.
496511
func (r *GitRepositoryReconciler) reconcileArtifact(ctx context.Context,
497512
obj *sourcev1.GitRepository, commit *git.Commit, includes *artifactSet, dir string) (sreconcile.Result, error) {
513+
498514
// Create potential new artifact with current available metadata
499515
artifact := r.Storage.NewArtifactFor(obj.Kind, obj.GetObjectMeta(), commit.String(), fmt.Sprintf("%s.tar.gz", commit.Hash.String()))
500516

controllers/gitrepository_controller_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ func TestGitRepositoryReconciler_reconcileSource_authStrategy(t *testing.T) {
359359
},
360360
wantErr: true,
361361
assertConditions: []metav1.Condition{
362-
*conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.GitOperationFailedReason, "failed to checkout and determine revision: unable to clone '<url>': PEM CA bundle could not be appended to x509 certificate pool"),
362+
*conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.GitOperationFailedReason, "failed to checkout and determine revision: unable to fetch-connect to remote '<url>': PEM CA bundle could not be appended to x509 certificate pool"),
363363
},
364364
},
365365
{

docs/spec/v1beta2/gitrepositories.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,22 @@ transport being handled by the controller, instead of `libgit2`.
399399
This may lead to an increased number of timeout messages in the logs, however
400400
it will fix the bug in which Git operations make the controllers hang indefinitely.
401401

402+
#### Optimized Git clones
403+
404+
Optimized Git clones decreases resource utilization for GitRepository
405+
reconciliations. It supports both `go-git` and `libgit2` implementations
406+
when cloning repositories using branches or tags.
407+
408+
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.
411+
412+
This feature is enabled by default. It can be disabled by starting the
413+
controller with the argument `--feature-gates=OptimizedGitClones=false`.
414+
415+
NB: GitRepository objects configured for SemVer or Commit clones are
416+
not affected by this functionality.
417+
402418
#### Proxy support
403419

404420
When a proxy is configured in the source-controller Pod through the appropriate

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ require (
2525
github.com/fluxcd/pkg/gitutil v0.1.0
2626
github.com/fluxcd/pkg/helmtestserver v0.7.2
2727
github.com/fluxcd/pkg/lockedfile v0.1.0
28-
github.com/fluxcd/pkg/runtime v0.14.2
28+
github.com/fluxcd/pkg/runtime v0.15.1
2929
github.com/fluxcd/pkg/ssh v0.3.3
3030
github.com/fluxcd/pkg/testserver v0.2.0
3131
github.com/fluxcd/pkg/untar v0.1.0
@@ -185,7 +185,7 @@ require (
185185
github.com/shopspring/decimal v1.2.0 // indirect
186186
github.com/sirupsen/logrus v1.8.1 // indirect
187187
github.com/spf13/cast v1.4.1 // indirect
188-
github.com/spf13/cobra v1.3.0 // indirect
188+
github.com/spf13/cobra v1.4.0 // indirect
189189
github.com/stretchr/testify v1.7.1 // indirect
190190
github.com/xanzy/ssh-agent v0.3.1 // indirect
191191
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect

go.sum

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -364,8 +364,8 @@ github.com/fluxcd/pkg/helmtestserver v0.7.2/go.mod h1:WtUXBrfpJdwK54LX1Tqd8PpLJY
364364
github.com/fluxcd/pkg/lockedfile v0.1.0 h1:YsYFAkd6wawMCcD74ikadAKXA4s2sukdxrn7w8RB5eo=
365365
github.com/fluxcd/pkg/lockedfile v0.1.0/go.mod h1:EJLan8t9MiOcgTs8+puDjbE6I/KAfHbdvIy9VUgIjm8=
366366
github.com/fluxcd/pkg/runtime v0.13.0-rc.6/go.mod h1:4oKUO19TeudXrnCRnxCfMSS7EQTYpYlgfXwlQuDJ/Eg=
367-
github.com/fluxcd/pkg/runtime v0.14.2 h1:ktyUjcX4pHoC8DRoBmhEP6eMHbmR6+/MYoARe4YulZY=
368-
github.com/fluxcd/pkg/runtime v0.14.2/go.mod h1:NZr3PRK7xX2M1bl0LdtugvQyWkOmu2NcW3NrZH6U0is=
367+
github.com/fluxcd/pkg/runtime v0.15.1 h1:PKooYqlZM+KLhnNz10sQnBH0AHllS40PIDHtiRH/BGU=
368+
github.com/fluxcd/pkg/runtime v0.15.1/go.mod h1:TPAoOEgUFG60FXBA4ID41uaPldxuXCEI4jt3qfd5i5Q=
369369
github.com/fluxcd/pkg/ssh v0.3.3 h1:/tc7W7LO1VoVUI5jB+p9ZHCA+iQaXTkaSCDZJsxcZ9k=
370370
github.com/fluxcd/pkg/ssh v0.3.3/go.mod h1:+bKhuv0/pJy3HZwkK54Shz68sNv1uf5aI6wtPaEHaYk=
371371
github.com/fluxcd/pkg/testserver v0.2.0 h1:Mj0TapmKaywI6Fi5wvt1LAZpakUHmtzWQpJNKQ0Krt4=
@@ -990,8 +990,9 @@ github.com/spf13/cobra v0.0.6/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHN
990990
github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
991991
github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo=
992992
github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk=
993-
github.com/spf13/cobra v1.3.0 h1:R7cSvGu+Vv+qX0gW5R/85dx2kmmJT5z5NM8ifdYjdn0=
994993
github.com/spf13/cobra v1.3.0/go.mod h1:BrRVncBjOJa/eUcVVm9CE+oC6as8k+VYr4NY7WCi9V4=
994+
github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q=
995+
github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g=
995996
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
996997
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
997998
github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=

internal/features/features.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
Copyright 2022 The Flux authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
// Package features sets the feature gates that
18+
// source-controller supports, and their default
19+
// states.
20+
package features
21+
22+
import feathelper "github.com/fluxcd/pkg/runtime/features"
23+
24+
const (
25+
// OptimizedGitClones decreases resource utilization for GitRepository
26+
// reconciliations. It supports both go-git and libgit2 implementations
27+
// when cloning repositories using branches or tags.
28+
//
29+
// When enabled, avoids full clone operations by first checking whether
30+
// the last revision is still the same at the target repository,
31+
// and if that is so, skips the reconciliation.
32+
OptimizedGitClones = "OptimizedGitClones"
33+
)
34+
35+
var features = map[string]bool{
36+
// OptimizedGitClones
37+
// opt-out from v0.25
38+
OptimizedGitClones: true,
39+
}
40+
41+
// DefaultFeatureGates contains a list of all supported feature gates and
42+
// their default values.
43+
func FeatureGates() map[string]bool {
44+
return features
45+
}
46+
47+
// Enabled verifies whether the feature is enabled or not.
48+
//
49+
// This is only a wrapper around the Enabled func in
50+
// pkg/runtime/features, so callers won't need to import
51+
// both packages for checking whether a feature is enabled.
52+
func Enabled(feature string) (bool, error) {
53+
return feathelper.Enabled(feature)
54+
}

main.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,12 @@ import (
3636
"github.com/fluxcd/pkg/runtime/client"
3737
helper "github.com/fluxcd/pkg/runtime/controller"
3838
"github.com/fluxcd/pkg/runtime/events"
39+
feathelper "github.com/fluxcd/pkg/runtime/features"
3940
"github.com/fluxcd/pkg/runtime/leaderelection"
4041
"github.com/fluxcd/pkg/runtime/logger"
4142
"github.com/fluxcd/pkg/runtime/pprof"
4243
"github.com/fluxcd/pkg/runtime/probes"
44+
"github.com/fluxcd/source-controller/internal/features"
4345

4446
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
4547
"github.com/fluxcd/source-controller/controllers"
@@ -88,6 +90,7 @@ func main() {
8890
logOptions logger.Options
8991
leaderElectionOptions leaderelection.Options
9092
rateLimiterOptions helper.RateLimiterOptions
93+
featureGates feathelper.FeatureGates
9194
helmCacheMaxSize int
9295
helmCacheTTL string
9396
helmCachePurgeInterval string
@@ -136,11 +139,20 @@ func main() {
136139
logOptions.BindFlags(flag.CommandLine)
137140
leaderElectionOptions.BindFlags(flag.CommandLine)
138141
rateLimiterOptions.BindFlags(flag.CommandLine)
142+
featureGates.BindFlags(flag.CommandLine)
139143

140144
flag.Parse()
141145

142146
ctrl.SetLogger(logger.NewLogger(logOptions))
143147

148+
err := featureGates.WithLogger(setupLog).
149+
SupportedFeatures(features.FeatureGates())
150+
151+
if err != nil {
152+
setupLog.Error(err, "unable to load feature gates")
153+
os.Exit(1)
154+
}
155+
144156
// Set upper bound file size limits Helm
145157
helm.MaxIndexSize = helmIndexLimit
146158
helm.MaxChartSize = helmChartLimit

pkg/git/git.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,15 @@ func (c *Commit) ShortMessage() string {
106106
type CheckoutStrategy interface {
107107
Checkout(ctx context.Context, path, url string, config *AuthOptions) (*Commit, error)
108108
}
109+
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+
}

pkg/git/gogit/checkout.go

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,11 @@ import (
2626

2727
"github.com/Masterminds/semver/v3"
2828
extgogit "github.com/go-git/go-git/v5"
29+
"github.com/go-git/go-git/v5/config"
2930
"github.com/go-git/go-git/v5/plumbing"
3031
"github.com/go-git/go-git/v5/plumbing/object"
32+
"github.com/go-git/go-git/v5/plumbing/transport"
33+
"github.com/go-git/go-git/v5/storage/memory"
3134

3235
"github.com/fluxcd/pkg/gitutil"
3336
"github.com/fluxcd/pkg/version"
@@ -44,27 +47,44 @@ func CheckoutStrategyForOptions(_ context.Context, opts git.CheckoutOptions) git
4447
case opts.SemVer != "":
4548
return &CheckoutSemVer{SemVer: opts.SemVer, RecurseSubmodules: opts.RecurseSubmodules}
4649
case opts.Tag != "":
47-
return &CheckoutTag{Tag: opts.Tag, RecurseSubmodules: opts.RecurseSubmodules}
50+
return &CheckoutTag{Tag: opts.Tag, RecurseSubmodules: opts.RecurseSubmodules, LastRevision: opts.LastRevision}
4851
default:
4952
branch := opts.Branch
5053
if branch == "" {
5154
branch = git.DefaultBranch
5255
}
53-
return &CheckoutBranch{Branch: branch, RecurseSubmodules: opts.RecurseSubmodules}
56+
return &CheckoutBranch{Branch: branch, RecurseSubmodules: opts.RecurseSubmodules, LastRevision: opts.LastRevision}
5457
}
5558
}
5659

5760
type CheckoutBranch struct {
5861
Branch string
5962
RecurseSubmodules bool
63+
LastRevision string
6064
}
6165

6266
func (c *CheckoutBranch) Checkout(ctx context.Context, path, url string, opts *git.AuthOptions) (*git.Commit, error) {
6367
authMethod, err := transportAuth(opts)
6468
if err != nil {
6569
return nil, fmt.Errorf("failed to construct auth method with options: %w", err)
6670
}
71+
6772
ref := plumbing.NewBranchReferenceName(c.Branch)
73+
// check if previous revision has changed before attempting to clone
74+
if c.LastRevision != "" {
75+
currentRevision, err := getLastRevision(ctx, url, ref, opts, authMethod)
76+
if err != nil {
77+
return nil, err
78+
}
79+
80+
if currentRevision != "" && currentRevision == c.LastRevision {
81+
return nil, git.NoChangesError{
82+
Message: "no changes since last reconcilation",
83+
ObservedRevision: currentRevision,
84+
}
85+
}
86+
}
87+
6888
repo, err := extgogit.PlainCloneContext(ctx, path, false, &extgogit.CloneOptions{
6989
URL: url,
7090
Auth: authMethod,
@@ -92,9 +112,31 @@ func (c *CheckoutBranch) Checkout(ctx context.Context, path, url string, opts *g
92112
return buildCommitWithRef(cc, ref)
93113
}
94114

115+
func getLastRevision(ctx context.Context, url string, ref plumbing.ReferenceName, opts *git.AuthOptions, authMethod transport.AuthMethod) (string, error) {
116+
config := &config.RemoteConfig{
117+
Name: git.DefaultOrigin,
118+
URLs: []string{url},
119+
}
120+
rem := extgogit.NewRemote(memory.NewStorage(), config)
121+
listOpts := &extgogit.ListOptions{
122+
Auth: authMethod,
123+
}
124+
if opts != nil && opts.CAFile != nil {
125+
listOpts.CABundle = opts.CAFile
126+
}
127+
refs, err := rem.ListContext(ctx, listOpts)
128+
if err != nil {
129+
return "", fmt.Errorf("unable to list remote for '%s': %w", url, err)
130+
}
131+
132+
currentRevision := filterRefs(refs, ref)
133+
return currentRevision, nil
134+
}
135+
95136
type CheckoutTag struct {
96137
Tag string
97138
RecurseSubmodules bool
139+
LastRevision string
98140
}
99141

100142
func (c *CheckoutTag) Checkout(ctx context.Context, path, url string, opts *git.AuthOptions) (*git.Commit, error) {
@@ -103,6 +145,20 @@ func (c *CheckoutTag) Checkout(ctx context.Context, path, url string, opts *git.
103145
return nil, fmt.Errorf("failed to construct auth method with options: %w", err)
104146
}
105147
ref := plumbing.NewTagReferenceName(c.Tag)
148+
// check if previous revision has changed before attempting to clone
149+
if c.LastRevision != "" {
150+
currentRevision, err := getLastRevision(ctx, url, ref, opts, authMethod)
151+
if err != nil {
152+
return nil, err
153+
}
154+
155+
if currentRevision != "" && currentRevision == c.LastRevision {
156+
return nil, git.NoChangesError{
157+
Message: "no changes since last reconcilation",
158+
ObservedRevision: currentRevision,
159+
}
160+
}
161+
}
106162
repo, err := extgogit.PlainCloneContext(ctx, path, false, &extgogit.CloneOptions{
107163
URL: url,
108164
Auth: authMethod,
@@ -333,3 +389,13 @@ func recurseSubmodules(recurse bool) extgogit.SubmoduleRescursivity {
333389
}
334390
return extgogit.NoRecurseSubmodules
335391
}
392+
393+
func filterRefs(refs []*plumbing.Reference, currentRef plumbing.ReferenceName) string {
394+
for _, ref := range refs {
395+
if ref.Name().String() == currentRef.String() {
396+
return fmt.Sprintf("%s/%s", currentRef.Short(), ref.Hash().String())
397+
}
398+
}
399+
400+
return ""
401+
}

0 commit comments

Comments
 (0)