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:
parent
39e999d617
commit
331fd64952
|
@ -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())
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
8
main.go
8
main.go
|
@ -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{
|
||||||
|
|
Loading…
Reference in New Issue