Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gogit: Add new ForceGoGitImplementation FeatureGate #945

Merged
merged 3 commits into from
Nov 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 15 additions & 15 deletions controllers/gitrepository_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,12 +141,7 @@ func (r *GitRepositoryReconciler) SetupWithManagerAndOptions(mgr ctrl.Manager, o
r.requeueDependency = opts.DependencyRequeueInterval

if r.features == nil {
r.features = map[string]bool{}
}

// Check and enable gated features.
if oc, _ := features.Enabled(features.OptimizedGitClones); oc {
r.features[features.OptimizedGitClones] = true
r.features = features.FeatureGates()
}

return ctrl.NewControllerManagedBy(mgr).
Expand Down Expand Up @@ -427,8 +422,13 @@ func (r *GitRepositoryReconciler) reconcileStorage(ctx context.Context,
// change, it short-circuits the whole reconciliation with an early return.
func (r *GitRepositoryReconciler) reconcileSource(ctx context.Context,
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.
if !r.Libgit2TransportInitialized() && obj.Spec.GitImplementation == sourcev1.LibGit2Implementation {
if !r.Libgit2TransportInitialized() && gitImplementation == sourcev1.LibGit2Implementation {
return sreconcile.ResultEmpty, serror.NewStalling(
errors.New("libgit2 managed transport not initialized"), "Libgit2TransportNotEnabled",
)
Expand Down Expand Up @@ -504,7 +504,7 @@ func (r *GitRepositoryReconciler) reconcileSource(ctx context.Context,
optimizedClone = true
}

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

// If we can't skip the reconciliation, checkout again without any
// optimization.
c, err := r.gitCheckout(ctx, obj, authOpts, dir, false)
c, err := r.gitCheckout(ctx, obj, authOpts, dir, false, gitImplementation)
if err != nil {
return sreconcile.ResultEmpty, err
}
Expand Down Expand Up @@ -730,7 +730,8 @@ func (r *GitRepositoryReconciler) reconcileInclude(ctx context.Context,
// gitCheckout builds checkout options with the given configurations and
// performs a git checkout.
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.
cloneOpts := git.CloneOptions{
RecurseSubmodules: obj.Spec.RecurseSubmodules,
Expand Down Expand Up @@ -758,18 +759,17 @@ func (r *GitRepositoryReconciler) gitCheckout(ctx context.Context,
var gitReader git.RepositoryReader
var err error

switch obj.Spec.GitImplementation {
switch gitImplementation {
case sourcev1.LibGit2Implementation:
gitReader, err = libgit2.NewClient(dir, authOpts)
case sourcev1.GoGitImplementation:
gitReader, err = gogit.NewClient(dir, authOpts)
default:
err = fmt.Errorf("invalid Git implementation: %s", obj.Spec.GitImplementation)
err = fmt.Errorf("invalid Git implementation: %s", gitImplementation)
}
if err != nil {
// Do not return err as recovery without changes is impossible.
e := serror.NewStalling(
fmt.Errorf("failed to create Git client for implementation '%s': %w", obj.Spec.GitImplementation, err),
e := serror.NewGeneric(
fmt.Errorf("failed to create Git client for implementation '%s': %w", gitImplementation, err),
darkowlzz marked this conversation as resolved.
Show resolved Hide resolved
sourcev1.GitOperationFailedReason,
)
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
Expand Down
34 changes: 22 additions & 12 deletions controllers/gitrepository_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -498,10 +498,14 @@ func TestGitRepositoryReconciler_reconcileSource_authStrategy(t *testing.T) {
}

r := &GitRepositoryReconciler{
Client: builder.Build(),
EventRecorder: record.NewFakeRecorder(32),
Storage: testStorage,
features: features.FeatureGates(),
Client: builder.Build(),
EventRecorder: record.NewFakeRecorder(32),
Storage: testStorage,
features: map[string]bool{
features.OptimizedGitClones: true,
// Ensure that both implementations are tested.
features.ForceGoGitImplementation: false,
darkowlzz marked this conversation as resolved.
Show resolved Hide resolved
},
Libgit2TransportInitialized: transport.Enabled,
}

Expand Down Expand Up @@ -543,10 +547,12 @@ func TestGitRepositoryReconciler_reconcileSource_libgit2TransportUninitialized(t
g := NewWithT(t)

r := &GitRepositoryReconciler{
Client: fakeclient.NewClientBuilder().WithScheme(runtime.NewScheme()).Build(),
EventRecorder: record.NewFakeRecorder(32),
Storage: testStorage,
features: features.FeatureGates(),
Client: fakeclient.NewClientBuilder().WithScheme(runtime.NewScheme()).Build(),
EventRecorder: record.NewFakeRecorder(32),
Storage: testStorage,
features: map[string]bool{
features.ForceGoGitImplementation: false,
},
Libgit2TransportInitialized: mockTransportNotInitialized,
}

Expand Down Expand Up @@ -727,10 +733,14 @@ func TestGitRepositoryReconciler_reconcileSource_checkoutStrategy(t *testing.T)
}

r := &GitRepositoryReconciler{
Client: fakeclient.NewClientBuilder().WithScheme(runtime.NewScheme()).Build(),
EventRecorder: record.NewFakeRecorder(32),
Storage: testStorage,
features: features.FeatureGates(),
Client: fakeclient.NewClientBuilder().WithScheme(runtime.NewScheme()).Build(),
EventRecorder: record.NewFakeRecorder(32),
Storage: testStorage,
features: map[string]bool{
features.OptimizedGitClones: true,
// Ensure that both implementations are tested.
features.ForceGoGitImplementation: false,
},
darkowlzz marked this conversation as resolved.
Show resolved Hide resolved
Libgit2TransportInitialized: transport.Enabled,
}

Expand Down
14 changes: 9 additions & 5 deletions controllers/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,11 +242,15 @@ func TestMain(m *testing.M) {
}

if err := (&GitRepositoryReconciler{
Client: testEnv,
EventRecorder: record.NewFakeRecorder(32),
Metrics: testMetricsH,
Storage: testStorage,
features: features.FeatureGates(),
Client: testEnv,
EventRecorder: record.NewFakeRecorder(32),
Metrics: testMetricsH,
Storage: testStorage,
features: map[string]bool{
features.OptimizedGitClones: true,
// Ensure that both implementations are used during tests.
features.ForceGoGitImplementation: false,
},
Libgit2TransportInitialized: transport.Enabled,
}).SetupWithManager(testEnv); err != nil {
panic(fmt.Sprintf("Failed to start GitRepositoryReconciler: %v", err))
Expand Down
17 changes: 9 additions & 8 deletions docs/spec/v1beta2/gitrepositories.md
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,13 @@ resume.

### 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
implementation used for Git operations (e.g. clone, checkout). The default
value is `go-git`.
Expand All @@ -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,
resulting in an increase of disk space and traffic usage.

| Git Implementation | Shallow Clones | Git Submodules | V2 Protocol Support |
|--------------------|----------------|----------------|---------------------|
| `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).
**Note:** The `libgit2` implementation does not support shallow clones or
Git submodules.

#### Optimized Git clones

Expand Down
17 changes: 17 additions & 0 deletions internal/features/features.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,29 @@ const (
// the last revision is still the same at the target repository,
// and if that is so, skips the reconciliation.
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"
darkowlzz marked this conversation as resolved.
Show resolved Hide resolved
)

var features = map[string]bool{
// OptimizedGitClones
// opt-out from v0.25
OptimizedGitClones: true,
// ForceGoGitImplementation
// opt-out from v0.32
ForceGoGitImplementation: true,
}

// DefaultFeatureGates contains a list of all supported feature gates and
Expand Down
8 changes: 5 additions & 3 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,9 +204,11 @@ func main() {
}
storage := mustInitStorage(storagePath, storageAdvAddr, artifactRetentionTTL, artifactRetentionRecords, setupLog)

if err = transport.InitManagedTransport(); err != nil {
// 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 gogitOnly, _ := features.Enabled(features.ForceGoGitImplementation); !gogitOnly {
if err = transport.InitManagedTransport(); err != nil {
// 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{
Expand Down