diff --git a/controllers/gitrepository_controller.go b/controllers/gitrepository_controller.go index 6161d041..d76690e1 100644 --- a/controllers/gitrepository_controller.go +++ b/controllers/gitrepository_controller.go @@ -50,6 +50,7 @@ import ( "github.com/fluxcd/source-controller/internal/reconcile/summarize" "github.com/fluxcd/source-controller/internal/util" "github.com/fluxcd/source-controller/pkg/git" + "github.com/fluxcd/source-controller/pkg/git/libgit2/managed" "github.com/fluxcd/source-controller/pkg/git/strategy" "github.com/fluxcd/source-controller/pkg/sourceignore" ) @@ -369,10 +370,37 @@ func (r *GitRepositoryReconciler) reconcileSource(ctx context.Context, return sreconcile.ResultEmpty, e } + repositoryURL := obj.Spec.URL + // managed GIT transport only affects the libgit2 implementation + if managed.Enabled() && obj.Spec.GitImplementation == sourcev1.LibGit2Implementation { + // At present only HTTP connections have the ability to define remote options. + // Although this can be easily extended by ensuring that the fake URL below uses the + // target ssh scheme, and the libgit2/managed/ssh.go pulls that information accordingly. + // + // This is due to the fact the key libgit2 remote callbacks do not take place for HTTP + // whilst most still work for SSH. + if strings.HasPrefix(repositoryURL, "http") { + // Due to the lack of the callback feature, a fake target URL is created to allow + // for the smart sub transport be able to pick the options specific for this + // GitRepository object. + // The URL should use unique information that do not collide in a multi tenant + // deployment. + repositoryURL = fmt.Sprintf("http://%s/%s/%d", obj.Name, obj.UID, obj.Generation) + managed.AddTransportOptions(repositoryURL, + managed.TransportOptions{ + TargetUrl: obj.Spec.URL, + CABundle: authOpts.CAFile, + }) + + // We remove the options from memory, to avoid accumulating unused options over time. + defer managed.RemoveTransportOptions(repositoryURL) + } + } + // Checkout HEAD of reference in object gitCtx, cancel := context.WithTimeout(ctx, obj.Spec.Timeout.Duration) defer cancel() - c, err := checkoutStrategy.Checkout(gitCtx, dir, obj.Spec.URL, authOpts) + c, err := checkoutStrategy.Checkout(gitCtx, dir, repositoryURL, authOpts) if err != nil { e := &serror.Event{ Err: fmt.Errorf("failed to checkout and determine revision: %w", err), diff --git a/main.go b/main.go index 19e6c35e..120c83d5 100644 --- a/main.go +++ b/main.go @@ -45,6 +45,7 @@ import ( sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" "github.com/fluxcd/source-controller/controllers" "github.com/fluxcd/source-controller/internal/helm" + "github.com/fluxcd/source-controller/pkg/git/libgit2/managed" // +kubebuilder:scaffold:imports ) @@ -226,6 +227,10 @@ func main() { startFileServer(storage.BasePath, storageAddr, setupLog) }() + if managed.Enabled() { + managed.InitManagedTransport() + } + setupLog.Info("starting manager") if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { setupLog.Error(err, "problem running manager") diff --git a/pkg/git/libgit2/managed/flag.go b/pkg/git/libgit2/managed/flag.go new file mode 100644 index 00000000..7901a711 --- /dev/null +++ b/pkg/git/libgit2/managed/flag.go @@ -0,0 +1,18 @@ +package managed + +import ( + "os" + "strings" +) + +// Enabled defines whether the use of Managed Transport should be enabled. +// This is only affects git operations that uses libgit2 implementation. +// +// True is returned when the environment variable `EXPERIMENTAL_GIT_TRANSPORT` +// is detected with the value of `true` or `1`. +func Enabled() bool { + if v, ok := os.LookupEnv("EXPERIMENTAL_GIT_TRANSPORT"); ok { + return strings.ToLower(v) == "true" || v == "1" + } + return false +} diff --git a/pkg/git/libgit2/managed/http.go b/pkg/git/libgit2/managed/http.go new file mode 100644 index 00000000..cd2f65f6 --- /dev/null +++ b/pkg/git/libgit2/managed/http.go @@ -0,0 +1,331 @@ +/* +Copyright 2022 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +This was inspired and contains part of: +https://github.com/libgit2/git2go/blob/eae00773cce87d5282a8ac7c10b5c1961ee6f9cb/http.go + +The MIT License + +Copyright (c) 2013 The git2go contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +package managed + +import ( + "crypto/tls" + "crypto/x509" + "errors" + "fmt" + "io" + "io/ioutil" + "net" + "net/http" + "net/url" + "sync" + "time" + + git2go "github.com/libgit2/git2go/v33" +) + +// registerManagedHTTP registers a Go-native implementation of an +// HTTP(S) transport that doesn't rely on any lower-level libraries +// such as OpenSSL. +func registerManagedHTTP() error { + for _, protocol := range []string{"http", "https"} { + _, err := git2go.NewRegisteredSmartTransport(protocol, true, httpSmartSubtransportFactory) + if err != nil { + return fmt.Errorf("failed to register transport for %q: %v", protocol, err) + } + } + return nil +} + +func httpSmartSubtransportFactory(remote *git2go.Remote, transport *git2go.Transport) (git2go.SmartSubtransport, error) { + sst := &httpSmartSubtransport{ + transport: transport, + } + + return sst, nil +} + +type httpSmartSubtransport struct { + transport *git2go.Transport +} + +func (t *httpSmartSubtransport) Action(targetUrl string, action git2go.SmartServiceAction) (git2go.SmartSubtransportStream, error) { + var req *http.Request + var err error + + var proxyFn func(*http.Request) (*url.URL, error) + proxyOpts, err := t.transport.SmartProxyOptions() + if err != nil { + return nil, err + } + switch proxyOpts.Type { + case git2go.ProxyTypeNone: + proxyFn = nil + case git2go.ProxyTypeAuto: + proxyFn = http.ProxyFromEnvironment + case git2go.ProxyTypeSpecified: + parsedUrl, err := url.Parse(proxyOpts.Url) + if err != nil { + return nil, err + } + + proxyFn = http.ProxyURL(parsedUrl) + } + + httpTransport := &http.Transport{ + // Add the proxy to the http transport. + Proxy: proxyFn, + + // Set reasonable timeouts to ensure connections are not + // left open in an idle state, nor they hang indefinitely. + // + // These are based on the official go http.DefaultTransport: + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).DialContext, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + } + + finalUrl := targetUrl + opts, found := transportOptions(targetUrl) + if found && opts.TargetUrl != "" { + // override target URL only if options are found and a new targetURL + // is provided. + finalUrl = opts.TargetUrl + } + + // Add any provided certificate to the http transport. + if len(opts.CABundle) > 0 { + cap := x509.NewCertPool() + if ok := cap.AppendCertsFromPEM(opts.CABundle); !ok { + return nil, fmt.Errorf("failed to use certificate from PEM") + } + httpTransport.TLSClientConfig = &tls.Config{ + RootCAs: cap, + } + } + + client := &http.Client{Transport: httpTransport, Timeout: fullHttpClientTimeOut} + + switch action { + case git2go.SmartServiceActionUploadpackLs: + req, err = http.NewRequest("GET", finalUrl+"/info/refs?service=git-upload-pack", nil) + + case git2go.SmartServiceActionUploadpack: + req, err = http.NewRequest("POST", finalUrl+"/git-upload-pack", nil) + if err != nil { + break + } + req.Header.Set("Content-Type", "application/x-git-upload-pack-request") + + case git2go.SmartServiceActionReceivepackLs: + req, err = http.NewRequest("GET", finalUrl+"/info/refs?service=git-receive-pack", nil) + + case git2go.SmartServiceActionReceivepack: + req, err = http.NewRequest("POST", finalUrl+"/git-receive-pack", nil) + if err != nil { + break + } + req.Header.Set("Content-Type", "application/x-git-receive-pack-request") + + default: + err = errors.New("unknown action") + } + + if err != nil { + return nil, err + } + + req.Header.Set("User-Agent", "git/2.0 (git2go)") + + stream := newManagedHttpStream(t, req, client) + if req.Method == "POST" { + stream.recvReply.Add(1) + stream.sendRequestBackground() + } + + return stream, nil +} + +func (t *httpSmartSubtransport) Close() error { + return nil +} + +func (t *httpSmartSubtransport) Free() { +} + +type httpSmartSubtransportStream struct { + owner *httpSmartSubtransport + client *http.Client + req *http.Request + resp *http.Response + reader *io.PipeReader + writer *io.PipeWriter + sentRequest bool + recvReply sync.WaitGroup + httpError error +} + +func newManagedHttpStream(owner *httpSmartSubtransport, req *http.Request, client *http.Client) *httpSmartSubtransportStream { + r, w := io.Pipe() + return &httpSmartSubtransportStream{ + owner: owner, + client: client, + req: req, + reader: r, + writer: w, + } +} + +func (self *httpSmartSubtransportStream) Read(buf []byte) (int, error) { + if !self.sentRequest { + self.recvReply.Add(1) + if err := self.sendRequest(); err != nil { + return 0, err + } + } + + if err := self.writer.Close(); err != nil { + return 0, err + } + + self.recvReply.Wait() + + if self.httpError != nil { + return 0, self.httpError + } + + return self.resp.Body.Read(buf) +} + +func (self *httpSmartSubtransportStream) Write(buf []byte) (int, error) { + if self.httpError != nil { + return 0, self.httpError + } + return self.writer.Write(buf) +} + +func (self *httpSmartSubtransportStream) Free() { + if self.resp != nil { + self.resp.Body.Close() + } +} + +func (self *httpSmartSubtransportStream) sendRequestBackground() { + go func() { + self.httpError = self.sendRequest() + }() + self.sentRequest = true +} + +func (self *httpSmartSubtransportStream) sendRequest() error { + defer self.recvReply.Done() + self.resp = nil + + var resp *http.Response + var err error + var userName string + var password string + + // Obtain the credentials and use them if available. + cred, err := self.owner.transport.SmartCredentials("", git2go.CredentialTypeUserpassPlaintext) + if err != nil { + // Passthrough error indicates that no credentials were provided. + // Continue without credentials. + if err.Error() != git2go.ErrorCodePassthrough.String() { + return err + } + } else { + userName, password, err = cred.GetUserpassPlaintext() + if err != nil { + return err + } + defer cred.Free() + } + + for { + req := &http.Request{ + Method: self.req.Method, + URL: self.req.URL, + Header: self.req.Header, + } + if req.Method == "POST" { + req.Body = self.reader + req.ContentLength = -1 + } + + req.SetBasicAuth(userName, password) + resp, err = self.client.Do(req) + if err != nil { + return err + } + + if resp.StatusCode == http.StatusOK { + break + } + + if resp.StatusCode == http.StatusUnauthorized { + resp.Body.Close() + + cred, err := self.owner.transport.SmartCredentials("", git2go.CredentialTypeUserpassPlaintext) + if err != nil { + return err + } + defer cred.Free() + + userName, password, err = cred.GetUserpassPlaintext() + if err != nil { + return err + } + + continue + } + + io.Copy(ioutil.Discard, resp.Body) + resp.Body.Close() + return fmt.Errorf("Unhandled HTTP error %s", resp.Status) + } + + self.sentRequest = true + self.resp = resp + return nil +} diff --git a/pkg/git/libgit2/managed/init.go b/pkg/git/libgit2/managed/init.go new file mode 100644 index 00000000..8df4a9ae --- /dev/null +++ b/pkg/git/libgit2/managed/init.go @@ -0,0 +1,61 @@ +/* +Copyright 2022 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package managed + +import ( + "sync" + "time" +) + +var ( + once sync.Once + + // sshConnectionTimeOut defines the timeout used for when + // creating ssh.ClientConfig, which translates in the timeout + // for stablishing the SSH TCP connections. + sshConnectionTimeOut time.Duration = 30 * time.Second + + // fullHttpClientTimeOut defines the maximum amount of + // time a http client may take before timing out, + // regardless of the current operation (i.e. connection, + // handshake, put/get). + fullHttpClientTimeOut time.Duration = 10 * time.Minute +) + +// InitManagedTransport initialises HTTP(S) and SSH managed transport +// for git2go, and therefore only impact git operations using the +// libgit2 implementation. +// +// This must run after git2go.init takes place, hence this is not executed +// within a init(). +// Regardless of the state in libgit2/git2go, this will replace the +// built-in transports. +// +// This function will only register managed transports once, subsequent calls +// leads to no-op. +func InitManagedTransport() error { + var err error + once.Do(func() { + if err = registerManagedHTTP(); err != nil { + return + } + + err = registerManagedSSH() + }) + + return err +} diff --git a/pkg/git/libgit2/managed/options.go b/pkg/git/libgit2/managed/options.go new file mode 100644 index 00000000..4fb211fe --- /dev/null +++ b/pkg/git/libgit2/managed/options.go @@ -0,0 +1,40 @@ +package managed + +import ( + "sync" +) + +// TransportOptions represents options to be applied at transport-level +// at request time. +type TransportOptions struct { + TargetUrl string + CABundle []byte +} + +var ( + transportOpts = make(map[string]TransportOptions, 0) + m sync.RWMutex +) + +func AddTransportOptions(targetUrl string, opts TransportOptions) { + m.Lock() + transportOpts[targetUrl] = opts + m.Unlock() +} + +func RemoveTransportOptions(targetUrl string) { + m.Lock() + delete(transportOpts, targetUrl) + m.Unlock() +} + +func transportOptions(targetUrl string) (*TransportOptions, bool) { + m.RLock() + opts, found := transportOpts[targetUrl] + m.RUnlock() + + if found { + return &opts, true + } + return nil, false +} diff --git a/pkg/git/libgit2/managed/ssh.go b/pkg/git/libgit2/managed/ssh.go new file mode 100644 index 00000000..76833ac6 --- /dev/null +++ b/pkg/git/libgit2/managed/ssh.go @@ -0,0 +1,256 @@ +/* +Copyright 2022 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +This was inspired and contains part of: +https://github.com/libgit2/git2go/blob/eae00773cce87d5282a8ac7c10b5c1961ee6f9cb/ssh.go + +The MIT License + +Copyright (c) 2013 The git2go contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +package managed + +import ( + "crypto/md5" + "crypto/sha1" + "crypto/sha256" + "fmt" + "io" + "net" + "net/url" + "runtime" + "strings" + + "golang.org/x/crypto/ssh" + + git2go "github.com/libgit2/git2go/v33" +) + +// registerManagedSSH registers a Go-native implementation of +// SSH transport that doesn't rely on any lower-level libraries +// such as libssh2. +func registerManagedSSH() error { + for _, protocol := range []string{"ssh", "ssh+git", "git+ssh"} { + _, err := git2go.NewRegisteredSmartTransport(protocol, false, sshSmartSubtransportFactory) + if err != nil { + return fmt.Errorf("failed to register transport for %q: %v", protocol, err) + } + } + return nil +} + +func sshSmartSubtransportFactory(remote *git2go.Remote, transport *git2go.Transport) (git2go.SmartSubtransport, error) { + return &sshSmartSubtransport{ + transport: transport, + }, nil +} + +type sshSmartSubtransport struct { + transport *git2go.Transport + + lastAction git2go.SmartServiceAction + client *ssh.Client + session *ssh.Session + stdin io.WriteCloser + stdout io.Reader + currentStream *sshSmartSubtransportStream +} + +func (t *sshSmartSubtransport) Action(urlString string, action git2go.SmartServiceAction) (git2go.SmartSubtransportStream, error) { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + u, err := url.Parse(urlString) + if err != nil { + return nil, err + } + + // Escape \ and '. + uPath := strings.Replace(u.Path, `\`, `\\`, -1) + uPath = strings.Replace(uPath, `'`, `\'`, -1) + + // TODO: Add percentage decode similar to libgit2. + // Refer: https://github.com/libgit2/libgit2/blob/358a60e1b46000ea99ef10b4dd709e92f75ff74b/src/str.c#L455-L481 + + var cmd string + switch action { + case git2go.SmartServiceActionUploadpackLs, git2go.SmartServiceActionUploadpack: + if t.currentStream != nil { + if t.lastAction == git2go.SmartServiceActionUploadpackLs { + return t.currentStream, nil + } + t.Close() + } + cmd = fmt.Sprintf("git-upload-pack '%s'", uPath) + + case git2go.SmartServiceActionReceivepackLs, git2go.SmartServiceActionReceivepack: + if t.currentStream != nil { + if t.lastAction == git2go.SmartServiceActionReceivepackLs { + return t.currentStream, nil + } + t.Close() + } + cmd = fmt.Sprintf("git-receive-pack '%s'", uPath) + + default: + return nil, fmt.Errorf("unexpected action: %v", action) + } + + cred, err := t.transport.SmartCredentials("", git2go.CredentialTypeSSHKey|git2go.CredentialTypeSSHMemory) + if err != nil { + return nil, err + } + defer cred.Free() + + sshConfig, err := getSSHConfigFromCredential(cred) + if err != nil { + return nil, err + } + sshConfig.HostKeyCallback = func(hostname string, remote net.Addr, key ssh.PublicKey) error { + marshaledKey := key.Marshal() + cert := &git2go.Certificate{ + Kind: git2go.CertificateHostkey, + Hostkey: git2go.HostkeyCertificate{ + Kind: git2go.HostkeySHA1 | git2go.HostkeyMD5 | git2go.HostkeySHA256 | git2go.HostkeyRaw, + HashMD5: md5.Sum(marshaledKey), + HashSHA1: sha1.Sum(marshaledKey), + HashSHA256: sha256.Sum256(marshaledKey), + Hostkey: marshaledKey, + SSHPublicKey: key, + }, + } + + return t.transport.SmartCertificateCheck(cert, true, hostname) + } + + var addr string + if u.Port() != "" { + addr = fmt.Sprintf("%s:%s", u.Hostname(), u.Port()) + } else { + addr = fmt.Sprintf("%s:22", u.Hostname()) + } + + t.client, err = ssh.Dial("tcp", addr, sshConfig) + if err != nil { + return nil, err + } + + t.session, err = t.client.NewSession() + if err != nil { + return nil, err + } + + t.stdin, err = t.session.StdinPipe() + if err != nil { + return nil, err + } + + t.stdout, err = t.session.StdoutPipe() + if err != nil { + return nil, err + } + + if err := t.session.Start(cmd); err != nil { + return nil, err + } + + t.lastAction = action + t.currentStream = &sshSmartSubtransportStream{ + owner: t, + } + + return t.currentStream, nil +} + +func (t *sshSmartSubtransport) Close() error { + t.currentStream = nil + if t.client != nil { + t.stdin.Close() + t.session.Wait() + t.session.Close() + t.client = nil + } + return nil +} + +func (t *sshSmartSubtransport) Free() { +} + +type sshSmartSubtransportStream struct { + owner *sshSmartSubtransport +} + +func (stream *sshSmartSubtransportStream) Read(buf []byte) (int, error) { + return stream.owner.stdout.Read(buf) +} + +func (stream *sshSmartSubtransportStream) Write(buf []byte) (int, error) { + return stream.owner.stdin.Write(buf) +} + +func (stream *sshSmartSubtransportStream) Free() { +} + +func getSSHConfigFromCredential(cred *git2go.Credential) (*ssh.ClientConfig, error) { + username, _, privatekey, passphrase, err := cred.GetSSHKey() + if err != nil { + return nil, err + } + + var pemBytes []byte + if cred.Type() == git2go.CredentialTypeSSHMemory { + pemBytes = []byte(privatekey) + } else { + return nil, fmt.Errorf("file based SSH credential is not supported") + } + + var key ssh.Signer + if passphrase != "" { + key, err = ssh.ParsePrivateKeyWithPassphrase(pemBytes, []byte(passphrase)) + } else { + key, err = ssh.ParsePrivateKey(pemBytes) + } + + if err != nil { + return nil, err + } + + return &ssh.ClientConfig{ + User: username, + Auth: []ssh.AuthMethod{ssh.PublicKeys(key)}, + Timeout: sshConnectionTimeOut, + }, nil +} diff --git a/pkg/git/libgit2/transport.go b/pkg/git/libgit2/transport.go index 22efa054..f62ade87 100644 --- a/pkg/git/libgit2/transport.go +++ b/pkg/git/libgit2/transport.go @@ -36,6 +36,7 @@ import ( "golang.org/x/crypto/ssh/knownhosts" "github.com/fluxcd/source-controller/pkg/git" + "github.com/fluxcd/source-controller/pkg/git/libgit2/managed" ) var ( @@ -112,6 +113,18 @@ func pushTransferProgressCallback(ctx context.Context) git2go.PushTransferProgre func credentialsCallback(opts *git.AuthOptions) git2go.CredentialsCallback { return func(url string, username string, allowedTypes git2go.CredentialType) (*git2go.Credential, error) { if allowedTypes&(git2go.CredentialTypeSSHKey|git2go.CredentialTypeSSHCustom|git2go.CredentialTypeSSHMemory) != 0 { + if managed.Enabled() { + // CredentialTypeSSHMemory requires libgit2 to be built using libssh2. + // When using managed transport (handled in go instead of libgit2), + // there may be ways to remove such requirement, thefore decreasing the + // need of libz, libssh2 and OpenSSL but further investigation is required + // once Managed Transport is no longer experimental. + // + // CredentialSSHKeyFromMemory is currently required for SSH key access + // when managed transport is enabled. + return git2go.NewCredentialSSHKeyFromMemory(opts.Username, "", string(opts.Identity), opts.Password) + } + var ( signer ssh.Signer err error