Skip to content

Commit 396e3cb

Browse files
author
Paulo Gomes
authored
Merge pull request #945 from pjbgf/feature-nolibgit2
gogit: Add new ForceGoGitImplementation FeatureGate
2 parents 3fee9a5 + bdcf708 commit 396e3cb

File tree

6 files changed

+77
-43
lines changed

6 files changed

+77
-43
lines changed

controllers/gitrepository_controller.go

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -141,12 +141,7 @@ func (r *GitRepositoryReconciler) SetupWithManagerAndOptions(mgr ctrl.Manager, o
141141
r.requeueDependency = opts.DependencyRequeueInterval
142142

143143
if r.features == nil {
144-
r.features = map[string]bool{}
145-
}
146-
147-
// Check and enable gated features.
148-
if oc, _ := features.Enabled(features.OptimizedGitClones); oc {
149-
r.features[features.OptimizedGitClones] = true
144+
r.features = features.FeatureGates()
150145
}
151146

152147
return ctrl.NewControllerManagedBy(mgr).
@@ -427,8 +422,13 @@ func (r *GitRepositoryReconciler) reconcileStorage(ctx context.Context,
427422
// change, it short-circuits the whole reconciliation with an early return.
428423
func (r *GitRepositoryReconciler) reconcileSource(ctx context.Context,
429424
obj *sourcev1.GitRepository, commit *git.Commit, includes *artifactSet, dir string) (sreconcile.Result, error) {
425+
gitImplementation := obj.Spec.GitImplementation
426+
if goGitOnly, _ := r.features[features.ForceGoGitImplementation]; goGitOnly {
427+
gitImplementation = sourcev1.GoGitImplementation
428+
}
429+
430430
// Exit early, if we need to use libgit2 AND managed transport hasn't been intialized.
431-
if !r.Libgit2TransportInitialized() && obj.Spec.GitImplementation == sourcev1.LibGit2Implementation {
431+
if !r.Libgit2TransportInitialized() && gitImplementation == sourcev1.LibGit2Implementation {
432432
return sreconcile.ResultEmpty, serror.NewStalling(
433433
errors.New("libgit2 managed transport not initialized"), "Libgit2TransportNotEnabled",
434434
)
@@ -504,7 +504,7 @@ func (r *GitRepositoryReconciler) reconcileSource(ctx context.Context,
504504
optimizedClone = true
505505
}
506506

507-
c, err := r.gitCheckout(ctx, obj, authOpts, dir, optimizedClone)
507+
c, err := r.gitCheckout(ctx, obj, authOpts, dir, optimizedClone, gitImplementation)
508508
if err != nil {
509509
return sreconcile.ResultEmpty, err
510510
}
@@ -538,7 +538,7 @@ func (r *GitRepositoryReconciler) reconcileSource(ctx context.Context,
538538

539539
// If we can't skip the reconciliation, checkout again without any
540540
// optimization.
541-
c, err := r.gitCheckout(ctx, obj, authOpts, dir, false)
541+
c, err := r.gitCheckout(ctx, obj, authOpts, dir, false, gitImplementation)
542542
if err != nil {
543543
return sreconcile.ResultEmpty, err
544544
}
@@ -730,7 +730,8 @@ func (r *GitRepositoryReconciler) reconcileInclude(ctx context.Context,
730730
// gitCheckout builds checkout options with the given configurations and
731731
// performs a git checkout.
732732
func (r *GitRepositoryReconciler) gitCheckout(ctx context.Context,
733-
obj *sourcev1.GitRepository, authOpts *git.AuthOptions, dir string, optimized bool) (*git.Commit, error) {
733+
obj *sourcev1.GitRepository, authOpts *git.AuthOptions, dir string,
734+
optimized bool, gitImplementation string) (*git.Commit, error) {
734735
// Configure checkout strategy.
735736
cloneOpts := git.CloneOptions{
736737
RecurseSubmodules: obj.Spec.RecurseSubmodules,
@@ -758,18 +759,17 @@ func (r *GitRepositoryReconciler) gitCheckout(ctx context.Context,
758759
var gitReader git.RepositoryReader
759760
var err error
760761

761-
switch obj.Spec.GitImplementation {
762+
switch gitImplementation {
762763
case sourcev1.LibGit2Implementation:
763764
gitReader, err = libgit2.NewClient(dir, authOpts)
764765
case sourcev1.GoGitImplementation:
765766
gitReader, err = gogit.NewClient(dir, authOpts)
766767
default:
767-
err = fmt.Errorf("invalid Git implementation: %s", obj.Spec.GitImplementation)
768+
err = fmt.Errorf("invalid Git implementation: %s", gitImplementation)
768769
}
769770
if err != nil {
770-
// Do not return err as recovery without changes is impossible.
771-
e := serror.NewStalling(
772-
fmt.Errorf("failed to create Git client for implementation '%s': %w", obj.Spec.GitImplementation, err),
771+
e := serror.NewGeneric(
772+
fmt.Errorf("failed to create Git client for implementation '%s': %w", gitImplementation, err),
773773
sourcev1.GitOperationFailedReason,
774774
)
775775
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())

controllers/gitrepository_controller_test.go

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -498,10 +498,14 @@ func TestGitRepositoryReconciler_reconcileSource_authStrategy(t *testing.T) {
498498
}
499499

500500
r := &GitRepositoryReconciler{
501-
Client: builder.Build(),
502-
EventRecorder: record.NewFakeRecorder(32),
503-
Storage: testStorage,
504-
features: features.FeatureGates(),
501+
Client: builder.Build(),
502+
EventRecorder: record.NewFakeRecorder(32),
503+
Storage: testStorage,
504+
features: map[string]bool{
505+
features.OptimizedGitClones: true,
506+
// Ensure that both implementations are tested.
507+
features.ForceGoGitImplementation: false,
508+
},
505509
Libgit2TransportInitialized: transport.Enabled,
506510
}
507511

@@ -543,10 +547,12 @@ func TestGitRepositoryReconciler_reconcileSource_libgit2TransportUninitialized(t
543547
g := NewWithT(t)
544548

545549
r := &GitRepositoryReconciler{
546-
Client: fakeclient.NewClientBuilder().WithScheme(runtime.NewScheme()).Build(),
547-
EventRecorder: record.NewFakeRecorder(32),
548-
Storage: testStorage,
549-
features: features.FeatureGates(),
550+
Client: fakeclient.NewClientBuilder().WithScheme(runtime.NewScheme()).Build(),
551+
EventRecorder: record.NewFakeRecorder(32),
552+
Storage: testStorage,
553+
features: map[string]bool{
554+
features.ForceGoGitImplementation: false,
555+
},
550556
Libgit2TransportInitialized: mockTransportNotInitialized,
551557
}
552558

@@ -727,10 +733,14 @@ func TestGitRepositoryReconciler_reconcileSource_checkoutStrategy(t *testing.T)
727733
}
728734

729735
r := &GitRepositoryReconciler{
730-
Client: fakeclient.NewClientBuilder().WithScheme(runtime.NewScheme()).Build(),
731-
EventRecorder: record.NewFakeRecorder(32),
732-
Storage: testStorage,
733-
features: features.FeatureGates(),
736+
Client: fakeclient.NewClientBuilder().WithScheme(runtime.NewScheme()).Build(),
737+
EventRecorder: record.NewFakeRecorder(32),
738+
Storage: testStorage,
739+
features: map[string]bool{
740+
features.OptimizedGitClones: true,
741+
// Ensure that both implementations are tested.
742+
features.ForceGoGitImplementation: false,
743+
},
734744
Libgit2TransportInitialized: transport.Enabled,
735745
}
736746

controllers/suite_test.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -242,11 +242,15 @@ func TestMain(m *testing.M) {
242242
}
243243

244244
if err := (&GitRepositoryReconciler{
245-
Client: testEnv,
246-
EventRecorder: record.NewFakeRecorder(32),
247-
Metrics: testMetricsH,
248-
Storage: testStorage,
249-
features: features.FeatureGates(),
245+
Client: testEnv,
246+
EventRecorder: record.NewFakeRecorder(32),
247+
Metrics: testMetricsH,
248+
Storage: testStorage,
249+
features: map[string]bool{
250+
features.OptimizedGitClones: true,
251+
// Ensure that both implementations are used during tests.
252+
features.ForceGoGitImplementation: false,
253+
},
250254
Libgit2TransportInitialized: transport.Enabled,
251255
}).SetupWithManager(testEnv); err != nil {
252256
panic(fmt.Sprintf("Failed to start GitRepositoryReconciler: %v", err))

docs/spec/v1beta2/gitrepositories.md

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,13 @@ resume.
385385

386386
### Git implementation
387387

388+
> **_NOTE:_** `libgit2` is being deprecated. When it is used the controllers
389+
are known to panic over long periods of time, or when under high GC pressure.
390+
A new opt-out feature gate `ForceGoGitImplementation` was introduced, which will
391+
use `go-git` regardless of the value defined at `.spec.gitImplementation`.
392+
This can be disabled by starting the controller with the additional flag below:
393+
`--feature-gates=ForceGoGitImplementation=false`.
394+
388395
`.spec.gitImplementation` is an optional field to change the client library
389396
implementation used for Git operations (e.g. clone, checkout). The default
390397
value is `go-git`.
@@ -396,14 +403,8 @@ drawbacks. For example, not being able to make use of shallow clones forces the
396403
controller to fetch the whole Git history tree instead of a specific one,
397404
resulting in an increase of disk space and traffic usage.
398405

399-
| Git Implementation | Shallow Clones | Git Submodules | V2 Protocol Support |
400-
|--------------------|----------------|----------------|---------------------|
401-
| `go-git` | true | true | false |
402-
| `libgit2` | false | false | true |
403-
404-
Some Git providers like Azure DevOps _require_ the `libgit2` implementation, as
405-
their Git servers provide only support for the
406-
[v2 protocol](https://git-scm.com/docs/protocol-v2).
406+
**Note:** The `libgit2` implementation does not support shallow clones or
407+
Git submodules.
407408

408409
#### Optimized Git clones
409410

internal/features/features.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,29 @@ const (
3030
// the last revision is still the same at the target repository,
3131
// and if that is so, skips the reconciliation.
3232
OptimizedGitClones = "OptimizedGitClones"
33+
// ForceGoGitImplementation ignores the value set for gitImplementation
34+
// and ensures that go-git is used for all GitRepository objects.
35+
//
36+
// Libgit2 is built in C and we use the Go bindings provided by git2go
37+
// to cross the C-GO chasm. Unfortunately, when libgit2 is being used the
38+
// controllers are known to panic over long periods of time, or when
39+
// under high GC pressure.
40+
//
41+
// This feature gate enables the gradual deprecation of libgit2 in favour
42+
// of go-git, which so far is the most stable of the pair.
43+
//
44+
// When enabled, libgit2 won't be initialized, nor will any git2go CGO
45+
// code be called.
46+
ForceGoGitImplementation = "ForceGoGitImplementation"
3347
)
3448

3549
var features = map[string]bool{
3650
// OptimizedGitClones
3751
// opt-out from v0.25
3852
OptimizedGitClones: true,
53+
// ForceGoGitImplementation
54+
// opt-out from v0.32
55+
ForceGoGitImplementation: true,
3956
}
4057

4158
// DefaultFeatureGates contains a list of all supported feature gates and

main.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -204,9 +204,11 @@ func main() {
204204
}
205205
storage := mustInitStorage(storagePath, storageAdvAddr, artifactRetentionTTL, artifactRetentionRecords, setupLog)
206206

207-
if err = transport.InitManagedTransport(); err != nil {
208-
// Log the error, but don't exit so as to not block reconcilers that are healthy.
209-
setupLog.Error(err, "unable to initialize libgit2 managed transport")
207+
if gogitOnly, _ := features.Enabled(features.ForceGoGitImplementation); !gogitOnly {
208+
if err = transport.InitManagedTransport(); err != nil {
209+
// Log the error, but don't exit so as to not block reconcilers that are healthy.
210+
setupLog.Error(err, "unable to initialize libgit2 managed transport")
211+
}
210212
}
211213

212214
if err = (&controllers.GitRepositoryReconciler{

0 commit comments

Comments
 (0)