gogit: Add new ForceGoGitImplementation FeatureGate

ForceGoGitImplementation ignores the value set for gitImplementation
and ensures that go-git is used for all GitRepository objects.
This can be used to confirm that Flux instances won't break if/when
the libgit2 implementation was to be deprecated.

When enabled, libgit2 won't be initialized, nor will any git2go cgo
code be called.

Signed-off-by: Paulo Gomes <paulo.gomes@weave.works>
This commit is contained in:
Paulo Gomes 2022-11-10 14:24:09 +00:00
parent 39e999d617
commit 331fd64952
No known key found for this signature in database
GPG Key ID: 9995233870E99BEE
6 changed files with 69 additions and 31 deletions

View File

@ -422,8 +422,13 @@ func (r *GitRepositoryReconciler) reconcileStorage(ctx context.Context,
// change, it short-circuits the whole reconciliation with an early return. // change, it short-circuits the whole reconciliation with an early return.
func (r *GitRepositoryReconciler) reconcileSource(ctx context.Context, func (r *GitRepositoryReconciler) reconcileSource(ctx context.Context,
obj *sourcev1.GitRepository, commit *git.Commit, includes *artifactSet, dir string) (sreconcile.Result, error) { obj *sourcev1.GitRepository, commit *git.Commit, includes *artifactSet, dir string) (sreconcile.Result, error) {
gitImplementation := obj.Spec.GitImplementation
if goGitOnly, _ := r.features[features.ForceGoGitImplementation]; goGitOnly {
gitImplementation = sourcev1.GoGitImplementation
}
// Exit early, if we need to use libgit2 AND managed transport hasn't been intialized. // Exit early, if we need to use libgit2 AND managed transport hasn't been intialized.
if !r.Libgit2TransportInitialized() && obj.Spec.GitImplementation == sourcev1.LibGit2Implementation { if !r.Libgit2TransportInitialized() && gitImplementation == sourcev1.LibGit2Implementation {
return sreconcile.ResultEmpty, serror.NewStalling( return sreconcile.ResultEmpty, serror.NewStalling(
errors.New("libgit2 managed transport not initialized"), "Libgit2TransportNotEnabled", errors.New("libgit2 managed transport not initialized"), "Libgit2TransportNotEnabled",
) )
@ -499,7 +504,7 @@ func (r *GitRepositoryReconciler) reconcileSource(ctx context.Context,
optimizedClone = true optimizedClone = true
} }
c, err := r.gitCheckout(ctx, obj, authOpts, dir, optimizedClone) c, err := r.gitCheckout(ctx, obj, authOpts, dir, optimizedClone, gitImplementation)
if err != nil { if err != nil {
return sreconcile.ResultEmpty, err return sreconcile.ResultEmpty, err
} }
@ -533,7 +538,7 @@ func (r *GitRepositoryReconciler) reconcileSource(ctx context.Context,
// If we can't skip the reconciliation, checkout again without any // If we can't skip the reconciliation, checkout again without any
// optimization. // optimization.
c, err := r.gitCheckout(ctx, obj, authOpts, dir, false) c, err := r.gitCheckout(ctx, obj, authOpts, dir, false, gitImplementation)
if err != nil { if err != nil {
return sreconcile.ResultEmpty, err return sreconcile.ResultEmpty, err
} }
@ -725,7 +730,8 @@ func (r *GitRepositoryReconciler) reconcileInclude(ctx context.Context,
// gitCheckout builds checkout options with the given configurations and // gitCheckout builds checkout options with the given configurations and
// performs a git checkout. // performs a git checkout.
func (r *GitRepositoryReconciler) gitCheckout(ctx context.Context, func (r *GitRepositoryReconciler) gitCheckout(ctx context.Context,
obj *sourcev1.GitRepository, authOpts *git.AuthOptions, dir string, optimized bool) (*git.Commit, error) { obj *sourcev1.GitRepository, authOpts *git.AuthOptions, dir string,
optimized bool, gitImplementation string) (*git.Commit, error) {
// Configure checkout strategy. // Configure checkout strategy.
cloneOpts := git.CloneOptions{ cloneOpts := git.CloneOptions{
RecurseSubmodules: obj.Spec.RecurseSubmodules, RecurseSubmodules: obj.Spec.RecurseSubmodules,
@ -753,18 +759,18 @@ func (r *GitRepositoryReconciler) gitCheckout(ctx context.Context,
var gitReader git.RepositoryReader var gitReader git.RepositoryReader
var err error var err error
switch obj.Spec.GitImplementation { switch gitImplementation {
case sourcev1.LibGit2Implementation: case sourcev1.LibGit2Implementation:
gitReader, err = libgit2.NewClient(dir, authOpts) gitReader, err = libgit2.NewClient(dir, authOpts)
case sourcev1.GoGitImplementation: case sourcev1.GoGitImplementation:
gitReader, err = gogit.NewClient(dir, authOpts) gitReader, err = gogit.NewClient(dir, authOpts)
default: default:
err = fmt.Errorf("invalid Git implementation: %s", obj.Spec.GitImplementation) err = fmt.Errorf("invalid Git implementation: %s", gitImplementation)
} }
if err != nil { if err != nil {
// Do not return err as recovery without changes is impossible. // Do not return err as recovery without changes is impossible.
e := serror.NewStalling( e := serror.NewStalling(
fmt.Errorf("failed to create Git client for implementation '%s': %w", obj.Spec.GitImplementation, err), fmt.Errorf("failed to create Git client for implementation '%s': %w", gitImplementation, err),
sourcev1.GitOperationFailedReason, sourcev1.GitOperationFailedReason,
) )
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error()) conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())

View File

@ -498,10 +498,14 @@ func TestGitRepositoryReconciler_reconcileSource_authStrategy(t *testing.T) {
} }
r := &GitRepositoryReconciler{ r := &GitRepositoryReconciler{
Client: builder.Build(), Client: builder.Build(),
EventRecorder: record.NewFakeRecorder(32), EventRecorder: record.NewFakeRecorder(32),
Storage: testStorage, Storage: testStorage,
features: features.FeatureGates(), features: map[string]bool{
features.OptimizedGitClones: true,
// Ensure that both implementations are tested.
features.ForceGoGitImplementation: false,
},
Libgit2TransportInitialized: transport.Enabled, Libgit2TransportInitialized: transport.Enabled,
} }
@ -543,10 +547,12 @@ func TestGitRepositoryReconciler_reconcileSource_libgit2TransportUninitialized(t
g := NewWithT(t) g := NewWithT(t)
r := &GitRepositoryReconciler{ r := &GitRepositoryReconciler{
Client: fakeclient.NewClientBuilder().WithScheme(runtime.NewScheme()).Build(), Client: fakeclient.NewClientBuilder().WithScheme(runtime.NewScheme()).Build(),
EventRecorder: record.NewFakeRecorder(32), EventRecorder: record.NewFakeRecorder(32),
Storage: testStorage, Storage: testStorage,
features: features.FeatureGates(), features: map[string]bool{
features.ForceGoGitImplementation: false,
},
Libgit2TransportInitialized: mockTransportNotInitialized, Libgit2TransportInitialized: mockTransportNotInitialized,
} }
@ -732,6 +738,8 @@ func TestGitRepositoryReconciler_reconcileSource_checkoutStrategy(t *testing.T)
Storage: testStorage, Storage: testStorage,
features: map[string]bool{ features: map[string]bool{
features.OptimizedGitClones: true, features.OptimizedGitClones: true,
// Ensure that both implementations are tested.
features.ForceGoGitImplementation: false,
}, },
Libgit2TransportInitialized: transport.Enabled, Libgit2TransportInitialized: transport.Enabled,
} }

View File

@ -242,11 +242,15 @@ func TestMain(m *testing.M) {
} }
if err := (&GitRepositoryReconciler{ if err := (&GitRepositoryReconciler{
Client: testEnv, Client: testEnv,
EventRecorder: record.NewFakeRecorder(32), EventRecorder: record.NewFakeRecorder(32),
Metrics: testMetricsH, Metrics: testMetricsH,
Storage: testStorage, Storage: testStorage,
features: features.FeatureGates(), features: map[string]bool{
features.OptimizedGitClones: true,
// Ensure that both implementations are used during tests.
features.ForceGoGitImplementation: false,
},
Libgit2TransportInitialized: transport.Enabled, Libgit2TransportInitialized: transport.Enabled,
}).SetupWithManager(testEnv); err != nil { }).SetupWithManager(testEnv); err != nil {
panic(fmt.Sprintf("Failed to start GitRepositoryReconciler: %v", err)) panic(fmt.Sprintf("Failed to start GitRepositoryReconciler: %v", err))

View File

@ -385,6 +385,13 @@ resume.
### Git implementation ### Git implementation
> **_NOTE:_** `libgit2` is being deprecated. When it is used the controllers
are known to panic over long periods of time, or when under high GC pressure.
A new opt-out feature gate `ForceGoGitImplementation` was introduced, which will
use `go-git` regardless of the value defined at `.spec.gitImplementation`.
This can be disabled by starting the controller with the additional flag below:
`--feature-gates=ForceGoGitImplementation=false`.
`.spec.gitImplementation` is an optional field to change the client library `.spec.gitImplementation` is an optional field to change the client library
implementation used for Git operations (e.g. clone, checkout). The default implementation used for Git operations (e.g. clone, checkout). The default
value is `go-git`. value is `go-git`.
@ -396,14 +403,8 @@ drawbacks. For example, not being able to make use of shallow clones forces the
controller to fetch the whole Git history tree instead of a specific one, controller to fetch the whole Git history tree instead of a specific one,
resulting in an increase of disk space and traffic usage. resulting in an increase of disk space and traffic usage.
| Git Implementation | Shallow Clones | Git Submodules | V2 Protocol Support | **Note:** The `libgit2` implementation does not support shallow clones or
|--------------------|----------------|----------------|---------------------| Git submodules.
| `go-git` | true | true | false |
| `libgit2` | false | false | true |
Some Git providers like Azure DevOps _require_ the `libgit2` implementation, as
their Git servers provide only support for the
[v2 protocol](https://git-scm.com/docs/protocol-v2).
#### Optimized Git clones #### Optimized Git clones

View File

@ -30,12 +30,29 @@ const (
// the last revision is still the same at the target repository, // the last revision is still the same at the target repository,
// and if that is so, skips the reconciliation. // and if that is so, skips the reconciliation.
OptimizedGitClones = "OptimizedGitClones" OptimizedGitClones = "OptimizedGitClones"
// ForceGoGitImplementation ignores the value set for gitImplementation
// and ensures that go-git is used for all GitRepository objects.
//
// Libgit2 is built in C and we use the Go bindings provided by git2go
// to cross the C-GO chasm. Unfortunately, when libgit2 is being used the
// controllers are known to panic over long periods of time, or when
// under high GC pressure.
//
// This feature gate enables the gradual deprecation of libgit2 in favour
// of go-git, which so far is the most stable of the pair.
//
// When enabled, libgit2 won't be initialized, nor will any git2go CGO
// code be called.
ForceGoGitImplementation = "ForceGoGitImplementation"
) )
var features = map[string]bool{ var features = map[string]bool{
// OptimizedGitClones // OptimizedGitClones
// opt-out from v0.25 // opt-out from v0.25
OptimizedGitClones: true, OptimizedGitClones: true,
// ForceGoGitImplementation
// opt-out from v0.32
ForceGoGitImplementation: true,
} }
// DefaultFeatureGates contains a list of all supported feature gates and // DefaultFeatureGates contains a list of all supported feature gates and

View File

@ -204,9 +204,11 @@ func main() {
} }
storage := mustInitStorage(storagePath, storageAdvAddr, artifactRetentionTTL, artifactRetentionRecords, setupLog) storage := mustInitStorage(storagePath, storageAdvAddr, artifactRetentionTTL, artifactRetentionRecords, setupLog)
if err = transport.InitManagedTransport(); err != nil { if gogitOnly, _ := features.Enabled(features.ForceGoGitImplementation); !gogitOnly {
// Log the error, but don't exit so as to not block reconcilers that are healthy. if err = transport.InitManagedTransport(); err != nil {
setupLog.Error(err, "unable to initialize libgit2 managed transport") // Log the error, but don't exit so as to not block reconcilers that are healthy.
setupLog.Error(err, "unable to initialize libgit2 managed transport")
}
} }
if err = (&controllers.GitRepositoryReconciler{ if err = (&controllers.GitRepositoryReconciler{