fix panics on unmanaged http and proxy on managed http
Signed-off-by: Sanskar Jaiswal <jaiswalsanskar078@gmail.com>
This commit is contained in:
parent
d4beacb6ad
commit
7d2bc64f47
|
@ -455,21 +455,6 @@ func (r *GitRepositoryReconciler) reconcileSource(ctx context.Context,
|
||||||
return sreconcile.ResultEmpty, e
|
return sreconcile.ResultEmpty, e
|
||||||
}
|
}
|
||||||
|
|
||||||
repositoryURL := obj.Spec.URL
|
|
||||||
// managed GIT transport only affects the libgit2 implementation
|
|
||||||
if managed.Enabled() && obj.Spec.GitImplementation == sourcev1.LibGit2Implementation {
|
|
||||||
// We set the TransportAuthID of this set of authentication options here by constructing
|
|
||||||
// a unique ID that won't clash in a multi tenant environment. This unique ID is used by
|
|
||||||
// libgit2 managed transports. This enables us to bypass the inbuilt credentials callback in
|
|
||||||
// libgit2, which is inflexible and unstable.
|
|
||||||
if strings.HasPrefix(repositoryURL, "http") {
|
|
||||||
authOpts.TransportAuthID = fmt.Sprintf("http://%s/%s/%d", obj.Name, obj.UID, obj.Generation)
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(repositoryURL, "ssh") {
|
|
||||||
authOpts.TransportAuthID = fmt.Sprintf("ssh://%s/%s/%d", obj.Name, obj.UID, obj.Generation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch the included artifact metadata.
|
// Fetch the included artifact metadata.
|
||||||
artifacts, err := r.fetchIncludes(ctx, obj)
|
artifacts, err := r.fetchIncludes(ctx, obj)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -491,7 +476,7 @@ func (r *GitRepositoryReconciler) reconcileSource(ctx context.Context,
|
||||||
optimizedClone = true
|
optimizedClone = true
|
||||||
}
|
}
|
||||||
|
|
||||||
c, err := r.gitCheckout(ctx, obj, repositoryURL, authOpts, dir, optimizedClone)
|
c, err := r.gitCheckout(ctx, obj, authOpts, dir, optimizedClone)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return sreconcile.ResultEmpty, err
|
return sreconcile.ResultEmpty, err
|
||||||
}
|
}
|
||||||
|
@ -525,7 +510,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, repositoryURL, authOpts, dir, false)
|
c, err := r.gitCheckout(ctx, obj, authOpts, dir, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return sreconcile.ResultEmpty, err
|
return sreconcile.ResultEmpty, err
|
||||||
}
|
}
|
||||||
|
@ -717,7 +702,7 @@ 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, repoURL string, authOpts *git.AuthOptions, dir string, optimized bool) (*git.Commit, error) {
|
obj *sourcev1.GitRepository, authOpts *git.AuthOptions, dir string, optimized bool) (*git.Commit, error) {
|
||||||
// Configure checkout strategy.
|
// Configure checkout strategy.
|
||||||
checkoutOpts := git.CheckoutOptions{RecurseSubmodules: obj.Spec.RecurseSubmodules}
|
checkoutOpts := git.CheckoutOptions{RecurseSubmodules: obj.Spec.RecurseSubmodules}
|
||||||
if ref := obj.Spec.Reference; ref != nil {
|
if ref := obj.Spec.Reference; ref != nil {
|
||||||
|
@ -743,15 +728,34 @@ func (r *GitRepositoryReconciler) gitCheckout(ctx context.Context,
|
||||||
Err: fmt.Errorf("failed to configure checkout strategy for Git implementation '%s': %w", obj.Spec.GitImplementation, err),
|
Err: fmt.Errorf("failed to configure checkout strategy for Git implementation '%s': %w", obj.Spec.GitImplementation, err),
|
||||||
Reason: sourcev1.GitOperationFailedReason,
|
Reason: sourcev1.GitOperationFailedReason,
|
||||||
}
|
}
|
||||||
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
|
|
||||||
// Do not return err as recovery without changes is impossible.
|
// Do not return err as recovery without changes is impossible.
|
||||||
return nil, e
|
return nil, e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// managed GIT transport only affects the libgit2 implementation
|
||||||
|
if managed.Enabled() && obj.Spec.GitImplementation == sourcev1.LibGit2Implementation {
|
||||||
|
// We set the TransportOptionsURL of this set of authentication options here by constructing
|
||||||
|
// a unique ID that won't clash in a multi tenant environment. This unique ID is used by
|
||||||
|
// libgit2 managed transports. This enables us to bypass the inbuilt credentials callback in
|
||||||
|
// libgit2, which is inflexible and unstable.
|
||||||
|
if strings.HasPrefix(obj.Spec.URL, "http") {
|
||||||
|
authOpts.TransportOptionsURL = fmt.Sprintf("http://%s/%s/%d", obj.Name, obj.UID, obj.Generation)
|
||||||
|
} else if strings.HasPrefix(obj.Spec.URL, "ssh") {
|
||||||
|
authOpts.TransportOptionsURL = fmt.Sprintf("ssh://%s/%s/%d", obj.Name, obj.UID, obj.Generation)
|
||||||
|
} else {
|
||||||
|
e := &serror.Stalling{
|
||||||
|
Err: fmt.Errorf("git repository URL has invalid transport type: '%s'", obj.Spec.URL),
|
||||||
|
Reason: sourcev1.GitOperationFailedReason,
|
||||||
|
}
|
||||||
|
return nil, e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Checkout HEAD of reference in object
|
// Checkout HEAD of reference in object
|
||||||
gitCtx, cancel := context.WithTimeout(ctx, obj.Spec.Timeout.Duration)
|
gitCtx, cancel := context.WithTimeout(ctx, obj.Spec.Timeout.Duration)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
commit, err := checkoutStrategy.Checkout(gitCtx, dir, repoURL, authOpts)
|
|
||||||
|
commit, err := checkoutStrategy.Checkout(gitCtx, dir, obj.Spec.URL, authOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
e := serror.NewGeneric(
|
e := serror.NewGeneric(
|
||||||
fmt.Errorf("failed to checkout and determine revision: %w", err),
|
fmt.Errorf("failed to checkout and determine revision: %w", err),
|
||||||
|
|
|
@ -362,7 +362,7 @@ func TestGitRepositoryReconciler_reconcileSource_authStrategy(t *testing.T) {
|
||||||
},
|
},
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
assertConditions: []metav1.Condition{
|
assertConditions: []metav1.Condition{
|
||||||
*conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.GitOperationFailedReason, "failed to checkout and determine revision: unable to fetch-connect to remote '<url>': PEM CA bundle could not be appended to x509 certificate pool"),
|
*conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.GitOperationFailedReason, "failed to checkout and determine revision: unable to clone '<url>': PEM CA bundle could not be appended to x509 certificate pool"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -645,10 +645,11 @@ func TestGitRepositoryReconciler_reconcileSource_checkoutStrategy(t *testing.T)
|
||||||
}
|
}
|
||||||
conditions.MarkTrue(obj, sourcev1.ArtifactInStorageCondition, meta.SucceededReason, "foo")
|
conditions.MarkTrue(obj, sourcev1.ArtifactInStorageCondition, meta.SucceededReason, "foo")
|
||||||
},
|
},
|
||||||
want: sreconcile.ResultEmpty,
|
want: sreconcile.ResultEmpty,
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
wantRevision: "staging/<commit>",
|
wantRevision: "staging/<commit>",
|
||||||
wantArtifactOutdated: false,
|
wantArtifactOutdated: false,
|
||||||
|
skipForImplementation: "libgit2",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Optimized clone different ignore",
|
name: "Optimized clone different ignore",
|
||||||
|
@ -669,9 +670,10 @@ func TestGitRepositoryReconciler_reconcileSource_checkoutStrategy(t *testing.T)
|
||||||
}
|
}
|
||||||
conditions.MarkTrue(obj, sourcev1.ArtifactInStorageCondition, meta.SucceededReason, "foo")
|
conditions.MarkTrue(obj, sourcev1.ArtifactInStorageCondition, meta.SucceededReason, "foo")
|
||||||
},
|
},
|
||||||
want: sreconcile.ResultSuccess,
|
want: sreconcile.ResultSuccess,
|
||||||
wantRevision: "staging/<commit>",
|
wantRevision: "staging/<commit>",
|
||||||
wantArtifactOutdated: false,
|
wantArtifactOutdated: false,
|
||||||
|
skipForImplementation: "libgit2",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -28,7 +28,7 @@ require (
|
||||||
github.com/fluxcd/pkg/helmtestserver v0.7.2
|
github.com/fluxcd/pkg/helmtestserver v0.7.2
|
||||||
github.com/fluxcd/pkg/lockedfile v0.1.0
|
github.com/fluxcd/pkg/lockedfile v0.1.0
|
||||||
github.com/fluxcd/pkg/runtime v0.16.1
|
github.com/fluxcd/pkg/runtime v0.16.1
|
||||||
github.com/fluxcd/pkg/ssh v0.3.4
|
github.com/fluxcd/pkg/ssh v0.4.0
|
||||||
github.com/fluxcd/pkg/testserver v0.2.0
|
github.com/fluxcd/pkg/testserver v0.2.0
|
||||||
github.com/fluxcd/pkg/untar v0.1.0
|
github.com/fluxcd/pkg/untar v0.1.0
|
||||||
github.com/fluxcd/pkg/version v0.1.0
|
github.com/fluxcd/pkg/version v0.1.0
|
||||||
|
|
4
go.sum
4
go.sum
|
@ -282,8 +282,8 @@ github.com/fluxcd/pkg/lockedfile v0.1.0 h1:YsYFAkd6wawMCcD74ikadAKXA4s2sukdxrn7w
|
||||||
github.com/fluxcd/pkg/lockedfile v0.1.0/go.mod h1:EJLan8t9MiOcgTs8+puDjbE6I/KAfHbdvIy9VUgIjm8=
|
github.com/fluxcd/pkg/lockedfile v0.1.0/go.mod h1:EJLan8t9MiOcgTs8+puDjbE6I/KAfHbdvIy9VUgIjm8=
|
||||||
github.com/fluxcd/pkg/runtime v0.16.1 h1:WU1vNZz4TAzmATQ/tl2zB/FX6GIUTgYeBn/G5RuTA2c=
|
github.com/fluxcd/pkg/runtime v0.16.1 h1:WU1vNZz4TAzmATQ/tl2zB/FX6GIUTgYeBn/G5RuTA2c=
|
||||||
github.com/fluxcd/pkg/runtime v0.16.1/go.mod h1:cgVJkOXCg9OmrIUGklf/0UtV28MNzkuoBJhaEQICT6E=
|
github.com/fluxcd/pkg/runtime v0.16.1/go.mod h1:cgVJkOXCg9OmrIUGklf/0UtV28MNzkuoBJhaEQICT6E=
|
||||||
github.com/fluxcd/pkg/ssh v0.3.4 h1:Ko+MUNiiQG3evyoMO19iRk7d4X0VJ6w6+GEeVQ1jLC0=
|
github.com/fluxcd/pkg/ssh v0.4.0 h1:2HY88irZ5BCSMlzZExR6cnhRkjxCDsK/lTHHQqCJDJQ=
|
||||||
github.com/fluxcd/pkg/ssh v0.3.4/go.mod h1:KGgOUOy1uI6RC6+qxIBLvP1AeOOs/nLB25Ca6TZMIXE=
|
github.com/fluxcd/pkg/ssh v0.4.0/go.mod h1:KGgOUOy1uI6RC6+qxIBLvP1AeOOs/nLB25Ca6TZMIXE=
|
||||||
github.com/fluxcd/pkg/testserver v0.2.0 h1:Mj0TapmKaywI6Fi5wvt1LAZpakUHmtzWQpJNKQ0Krt4=
|
github.com/fluxcd/pkg/testserver v0.2.0 h1:Mj0TapmKaywI6Fi5wvt1LAZpakUHmtzWQpJNKQ0Krt4=
|
||||||
github.com/fluxcd/pkg/testserver v0.2.0/go.mod h1:bgjjydkXsZTeFzjz9Cr4heGANr41uTB1Aj1Q5qzuYVk=
|
github.com/fluxcd/pkg/testserver v0.2.0/go.mod h1:bgjjydkXsZTeFzjz9Cr4heGANr41uTB1Aj1Q5qzuYVk=
|
||||||
github.com/fluxcd/pkg/untar v0.1.0 h1:k97V/xV5hFrAkIkVPuv5AVhyxh1ZzzAKba/lbDfGo6o=
|
github.com/fluxcd/pkg/untar v0.1.0 h1:k97V/xV5hFrAkIkVPuv5AVhyxh1ZzzAKba/lbDfGo6o=
|
||||||
|
|
8
main.go
8
main.go
|
@ -312,6 +312,14 @@ func main() {
|
||||||
|
|
||||||
if enabled, _ := features.Enabled(features.GitManagedTransport); enabled {
|
if enabled, _ := features.Enabled(features.GitManagedTransport); enabled {
|
||||||
managed.InitManagedTransport(ctrl.Log.WithName("managed-transport"))
|
managed.InitManagedTransport(ctrl.Log.WithName("managed-transport"))
|
||||||
|
} else {
|
||||||
|
if optimize, _ := feathelper.Enabled(features.OptimizedGitClones); optimize {
|
||||||
|
setupLog.Error(
|
||||||
|
fmt.Errorf("OptimizedGitClones=true but GitManagedTransport=false"),
|
||||||
|
"git clones can only be optimized when using managed transort",
|
||||||
|
)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setupLog.Info("starting manager")
|
setupLog.Info("starting manager")
|
||||||
|
|
|
@ -69,113 +69,148 @@ type CheckoutBranch struct {
|
||||||
func (c *CheckoutBranch) Checkout(ctx context.Context, path, url string, opts *git.AuthOptions) (_ *git.Commit, err error) {
|
func (c *CheckoutBranch) Checkout(ctx context.Context, path, url string, opts *git.AuthOptions) (_ *git.Commit, err error) {
|
||||||
defer recoverPanic(&err)
|
defer recoverPanic(&err)
|
||||||
|
|
||||||
|
// This branching is temporary, to address the transient panics observed when using unmanaged transport.
|
||||||
|
// The panics probably happen because we perform multiple fetch ops (introduced as a part of optimizing git clones).
|
||||||
|
// The branching lets us establish a clear code path to help us be certain of the expected behaviour.
|
||||||
|
// When we get rid of unmanaged transports, we can get rid of this branching as well.
|
||||||
if managed.Enabled() {
|
if managed.Enabled() {
|
||||||
// We store the target url and auth options mapped to a unique ID. We overwrite the target url
|
// We store the target URL and auth options mapped to a unique ID. We overwrite the target URL
|
||||||
// with the TransportAuthID, because managed transports don't provide a way for any kind of
|
// with the TransportOptionsURL, because managed transports don't provide a way for any kind of
|
||||||
// dependency injection. This lets us have a way of doing interop between application level code
|
// dependency injection. This lets us have a way of doing interop between application level code
|
||||||
// and transport level code.
|
// and transport level code.
|
||||||
// Performing all fetch operations with the TransportAuthID as the url, lets the managed
|
// Performing all fetch operations with the TransportOptionsURL as the URL, lets the managed
|
||||||
// transport action use it to fetch the registered transport options which contains the
|
// transport action use it to fetch the registered transport options which contains the
|
||||||
// _actual_ target url and the correct credentials to use.
|
// _actual_ target URL and the correct credentials to use.
|
||||||
managed.AddTransportOptions(opts.TransportAuthID, managed.TransportOptions{
|
if opts == nil {
|
||||||
TargetURL: url,
|
return nil, fmt.Errorf("can't use managed transport with an empty set of auth options")
|
||||||
AuthOpts: opts,
|
|
||||||
})
|
|
||||||
url = opts.TransportAuthID
|
|
||||||
defer managed.RemoveTransportOptions(opts.TransportAuthID)
|
|
||||||
}
|
|
||||||
|
|
||||||
remoteCallBacks := RemoteCallbacks(ctx, opts)
|
|
||||||
proxyOpts := &git2go.ProxyOptions{Type: git2go.ProxyTypeAuto}
|
|
||||||
|
|
||||||
repo, remote, err := initializeRepoWithRemote(ctx, path, url, opts)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// Open remote connection.
|
|
||||||
err = remote.ConnectFetch(&remoteCallBacks, proxyOpts, nil)
|
|
||||||
if err != nil {
|
|
||||||
remote.Free()
|
|
||||||
repo.Free()
|
|
||||||
return nil, fmt.Errorf("unable to fetch-connect to remote '%s': %w", managed.EffectiveURL(url), gitutil.LibGit2Error(err))
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
remote.Disconnect()
|
|
||||||
remote.Free()
|
|
||||||
repo.Free()
|
|
||||||
}()
|
|
||||||
|
|
||||||
// When the last observed revision is set, check whether it is still the
|
|
||||||
// same at the remote branch. If so, short-circuit the clone operation here.
|
|
||||||
if c.LastRevision != "" {
|
|
||||||
heads, err := remote.Ls(c.Branch)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("unable to remote ls for '%s': %w", managed.EffectiveURL(url), gitutil.LibGit2Error(err))
|
|
||||||
}
|
}
|
||||||
if len(heads) > 0 {
|
if opts.TransportOptionsURL == "" {
|
||||||
hash := heads[0].Id.String()
|
return nil, fmt.Errorf("can't use managed transport without a valid transport auth id.")
|
||||||
currentRevision := fmt.Sprintf("%s/%s", c.Branch, hash)
|
}
|
||||||
if currentRevision == c.LastRevision {
|
managed.AddTransportOptions(opts.TransportOptionsURL, managed.TransportOptions{
|
||||||
// Construct a partial commit with the existing information.
|
TargetURL: url,
|
||||||
c := &git.Commit{
|
AuthOpts: opts,
|
||||||
Hash: git.Hash(hash),
|
ProxyOptions: &git2go.ProxyOptions{Type: git2go.ProxyTypeAuto},
|
||||||
Reference: "refs/heads/" + c.Branch,
|
})
|
||||||
|
url = opts.TransportOptionsURL
|
||||||
|
remoteCallBacks := managed.RemoteCallbacks()
|
||||||
|
defer managed.RemoveTransportOptions(opts.TransportOptionsURL)
|
||||||
|
|
||||||
|
repo, remote, err := initializeRepoWithRemote(ctx, path, url, opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Open remote connection.
|
||||||
|
err = remote.ConnectFetch(&remoteCallBacks, nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
remote.Free()
|
||||||
|
repo.Free()
|
||||||
|
return nil, fmt.Errorf("unable to fetch-connect to remote '%s': %w", managed.EffectiveURL(url), gitutil.LibGit2Error(err))
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
remote.Disconnect()
|
||||||
|
remote.Free()
|
||||||
|
repo.Free()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// When the last observed revision is set, check whether it is still the
|
||||||
|
// same at the remote branch. If so, short-circuit the clone operation here.
|
||||||
|
if c.LastRevision != "" {
|
||||||
|
heads, err := remote.Ls(c.Branch)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to remote ls for '%s': %w", managed.EffectiveURL(url), gitutil.LibGit2Error(err))
|
||||||
|
}
|
||||||
|
if len(heads) > 0 {
|
||||||
|
hash := heads[0].Id.String()
|
||||||
|
currentRevision := fmt.Sprintf("%s/%s", c.Branch, hash)
|
||||||
|
if currentRevision == c.LastRevision {
|
||||||
|
// Construct a partial commit with the existing information.
|
||||||
|
c := &git.Commit{
|
||||||
|
Hash: git.Hash(hash),
|
||||||
|
Reference: "refs/heads/" + c.Branch,
|
||||||
|
}
|
||||||
|
return c, nil
|
||||||
}
|
}
|
||||||
return c, nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Limit the fetch operation to the specific branch, to decrease network usage.
|
// Limit the fetch operation to the specific branch, to decrease network usage.
|
||||||
err = remote.Fetch([]string{c.Branch},
|
err = remote.Fetch([]string{c.Branch},
|
||||||
&git2go.FetchOptions{
|
&git2go.FetchOptions{
|
||||||
DownloadTags: git2go.DownloadTagsNone,
|
DownloadTags: git2go.DownloadTagsNone,
|
||||||
RemoteCallbacks: remoteCallBacks,
|
RemoteCallbacks: remoteCallBacks,
|
||||||
ProxyOptions: git2go.ProxyOptions{Type: git2go.ProxyTypeAuto},
|
},
|
||||||
},
|
"")
|
||||||
"")
|
if err != nil {
|
||||||
if err != nil {
|
return nil, fmt.Errorf("unable to fetch remote '%s': %w",
|
||||||
return nil, fmt.Errorf("unable to fetch remote '%s': %w",
|
managed.EffectiveURL(url), gitutil.LibGit2Error(err))
|
||||||
managed.EffectiveURL(url), gitutil.LibGit2Error(err))
|
}
|
||||||
}
|
|
||||||
|
|
||||||
branch, err := repo.References.Lookup(fmt.Sprintf("refs/remotes/origin/%s", c.Branch))
|
branch, err := repo.References.Lookup(fmt.Sprintf("refs/remotes/origin/%s", c.Branch))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to lookup branch '%s' for '%s': %w",
|
return nil, fmt.Errorf("unable to lookup branch '%s' for '%s': %w",
|
||||||
c.Branch, managed.EffectiveURL(url), gitutil.LibGit2Error(err))
|
c.Branch, managed.EffectiveURL(url), gitutil.LibGit2Error(err))
|
||||||
}
|
}
|
||||||
defer branch.Free()
|
defer branch.Free()
|
||||||
|
|
||||||
upstreamCommit, err := repo.LookupCommit(branch.Target())
|
upstreamCommit, err := repo.LookupCommit(branch.Target())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to lookup commit '%s' for '%s': %w",
|
return nil, fmt.Errorf("unable to lookup commit '%s' for '%s': %w",
|
||||||
c.Branch, managed.EffectiveURL(url), gitutil.LibGit2Error(err))
|
c.Branch, managed.EffectiveURL(url), gitutil.LibGit2Error(err))
|
||||||
}
|
}
|
||||||
defer upstreamCommit.Free()
|
defer upstreamCommit.Free()
|
||||||
|
|
||||||
// Once the index has been updated with Fetch, and we know the tip commit,
|
// Once the index has been updated with Fetch, and we know the tip commit,
|
||||||
// a hard reset can be used to align the local worktree with the remote branch's.
|
// a hard reset can be used to align the local worktree with the remote branch's.
|
||||||
err = repo.ResetToCommit(upstreamCommit, git2go.ResetHard, &git2go.CheckoutOptions{
|
err = repo.ResetToCommit(upstreamCommit, git2go.ResetHard, &git2go.CheckoutOptions{
|
||||||
Strategy: git2go.CheckoutForce,
|
Strategy: git2go.CheckoutForce,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to hard reset to commit for '%s': %w", managed.EffectiveURL(url), gitutil.LibGit2Error(err))
|
return nil, fmt.Errorf("unable to hard reset to commit for '%s': %w", managed.EffectiveURL(url), gitutil.LibGit2Error(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the current worktree's head as reference for the commit to be returned.
|
// Use the current worktree's head as reference for the commit to be returned.
|
||||||
head, err := repo.Head()
|
head, err := repo.Head()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("git resolve HEAD error: %w", err)
|
return nil, fmt.Errorf("git resolve HEAD error: %w", err)
|
||||||
}
|
}
|
||||||
defer head.Free()
|
defer head.Free()
|
||||||
|
|
||||||
cc, err := repo.LookupCommit(head.Target())
|
cc, err := repo.LookupCommit(head.Target())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to lookup HEAD commit '%s' for branch '%s': %w", head.Target(), c.Branch, err)
|
return nil, fmt.Errorf("failed to lookup HEAD commit '%s' for branch '%s': %w", head.Target(), c.Branch, err)
|
||||||
}
|
}
|
||||||
defer cc.Free()
|
defer cc.Free()
|
||||||
|
|
||||||
return buildCommit(cc, "refs/heads/"+c.Branch), nil
|
return buildCommit(cc, "refs/heads/"+c.Branch), nil
|
||||||
|
} else {
|
||||||
|
repo, err := git2go.Clone(url, path, &git2go.CloneOptions{
|
||||||
|
FetchOptions: git2go.FetchOptions{
|
||||||
|
DownloadTags: git2go.DownloadTagsNone,
|
||||||
|
RemoteCallbacks: RemoteCallbacks(ctx, opts),
|
||||||
|
ProxyOptions: git2go.ProxyOptions{Type: git2go.ProxyTypeAuto},
|
||||||
|
},
|
||||||
|
CheckoutOptions: git2go.CheckoutOptions{
|
||||||
|
Strategy: git2go.CheckoutForce,
|
||||||
|
},
|
||||||
|
CheckoutBranch: c.Branch,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to clone '%s': %w", managed.EffectiveURL(url), gitutil.LibGit2Error(err))
|
||||||
|
}
|
||||||
|
defer repo.Free()
|
||||||
|
head, err := repo.Head()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("git resolve HEAD error: %w", err)
|
||||||
|
}
|
||||||
|
defer head.Free()
|
||||||
|
cc, err := repo.LookupCommit(head.Target())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to lookup HEAD commit '%s' for branch '%s': %w", head.Target(), c.Branch, err)
|
||||||
|
}
|
||||||
|
defer cc.Free()
|
||||||
|
return buildCommit(cc, "refs/heads/"+c.Branch), nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type CheckoutTag struct {
|
type CheckoutTag struct {
|
||||||
|
@ -186,85 +221,108 @@ type CheckoutTag struct {
|
||||||
func (c *CheckoutTag) Checkout(ctx context.Context, path, url string, opts *git.AuthOptions) (_ *git.Commit, err error) {
|
func (c *CheckoutTag) Checkout(ctx context.Context, path, url string, opts *git.AuthOptions) (_ *git.Commit, err error) {
|
||||||
defer recoverPanic(&err)
|
defer recoverPanic(&err)
|
||||||
|
|
||||||
|
// This branching is temporary, to address the transient panics observed when using unmanaged transport.
|
||||||
|
// The panics probably happen because we perform multiple fetch ops (introduced as a part of optimizing git clones).
|
||||||
|
// The branching lets us establish a clear code path to help us be certain of the expected behaviour.
|
||||||
|
// When we get rid of unmanaged transports, we can get rid of this branching as well.
|
||||||
if managed.Enabled() {
|
if managed.Enabled() {
|
||||||
managed.AddTransportOptions(opts.TransportAuthID, managed.TransportOptions{
|
if opts.TransportOptionsURL == "" {
|
||||||
TargetURL: url,
|
return nil, fmt.Errorf("can't use managed transport without a valid transport auth id.")
|
||||||
AuthOpts: opts,
|
}
|
||||||
|
managed.AddTransportOptions(opts.TransportOptionsURL, managed.TransportOptions{
|
||||||
|
TargetURL: url,
|
||||||
|
AuthOpts: opts,
|
||||||
|
ProxyOptions: &git2go.ProxyOptions{Type: git2go.ProxyTypeAuto},
|
||||||
})
|
})
|
||||||
url = opts.TransportAuthID
|
url = opts.TransportOptionsURL
|
||||||
defer managed.RemoveTransportOptions(opts.TransportAuthID)
|
remoteCallBacks := managed.RemoteCallbacks()
|
||||||
}
|
defer managed.RemoveTransportOptions(opts.TransportOptionsURL)
|
||||||
|
|
||||||
remoteCallBacks := RemoteCallbacks(ctx, opts)
|
repo, remote, err := initializeRepoWithRemote(ctx, path, url, opts)
|
||||||
proxyOpts := &git2go.ProxyOptions{Type: git2go.ProxyTypeAuto}
|
|
||||||
|
|
||||||
repo, remote, err := initializeRepoWithRemote(ctx, path, url, opts)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// Open remote connection.
|
|
||||||
err = remote.ConnectFetch(&remoteCallBacks, proxyOpts, nil)
|
|
||||||
if err != nil {
|
|
||||||
remote.Free()
|
|
||||||
repo.Free()
|
|
||||||
return nil, fmt.Errorf("unable to fetch-connect to remote '%s': %w", managed.EffectiveURL(url), gitutil.LibGit2Error(err))
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
remote.Disconnect()
|
|
||||||
remote.Free()
|
|
||||||
repo.Free()
|
|
||||||
}()
|
|
||||||
|
|
||||||
// When the last observed revision is set, check whether it is still the
|
|
||||||
// same at the remote branch. If so, short-circuit the clone operation here.
|
|
||||||
if c.LastRevision != "" {
|
|
||||||
heads, err := remote.Ls(c.Tag)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to remote ls for '%s': %w", managed.EffectiveURL(url), gitutil.LibGit2Error(err))
|
return nil, err
|
||||||
}
|
}
|
||||||
if len(heads) > 0 {
|
// Open remote connection.
|
||||||
hash := heads[0].Id.String()
|
err = remote.ConnectFetch(&remoteCallBacks, nil, nil)
|
||||||
currentRevision := fmt.Sprintf("%s/%s", c.Tag, hash)
|
if err != nil {
|
||||||
var same bool
|
remote.Free()
|
||||||
if currentRevision == c.LastRevision {
|
repo.Free()
|
||||||
same = true
|
return nil, fmt.Errorf("unable to fetch-connect to remote '%s': %w", managed.EffectiveURL(url), gitutil.LibGit2Error(err))
|
||||||
} else if len(heads) > 1 {
|
}
|
||||||
hash = heads[1].Id.String()
|
defer func() {
|
||||||
currentAnnotatedRevision := fmt.Sprintf("%s/%s", c.Tag, hash)
|
remote.Disconnect()
|
||||||
if currentAnnotatedRevision == c.LastRevision {
|
remote.Free()
|
||||||
|
repo.Free()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// When the last observed revision is set, check whether it is still the
|
||||||
|
// same at the remote branch. If so, short-circuit the clone operation here.
|
||||||
|
if c.LastRevision != "" {
|
||||||
|
heads, err := remote.Ls(c.Tag)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to remote ls for '%s': %w", managed.EffectiveURL(url), gitutil.LibGit2Error(err))
|
||||||
|
}
|
||||||
|
if len(heads) > 0 {
|
||||||
|
hash := heads[0].Id.String()
|
||||||
|
currentRevision := fmt.Sprintf("%s/%s", c.Tag, hash)
|
||||||
|
var same bool
|
||||||
|
if currentRevision == c.LastRevision {
|
||||||
same = true
|
same = true
|
||||||
|
} else if len(heads) > 1 {
|
||||||
|
hash = heads[1].Id.String()
|
||||||
|
currentAnnotatedRevision := fmt.Sprintf("%s/%s", c.Tag, hash)
|
||||||
|
if currentAnnotatedRevision == c.LastRevision {
|
||||||
|
same = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
if same {
|
||||||
if same {
|
// Construct a partial commit with the existing information.
|
||||||
// Construct a partial commit with the existing information.
|
c := &git.Commit{
|
||||||
c := &git.Commit{
|
Hash: git.Hash(hash),
|
||||||
Hash: git.Hash(hash),
|
Reference: "refs/tags/" + c.Tag,
|
||||||
Reference: "refs/tags/" + c.Tag,
|
}
|
||||||
|
return c, nil
|
||||||
}
|
}
|
||||||
return c, nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
err = remote.Fetch([]string{c.Tag},
|
err = remote.Fetch([]string{c.Tag},
|
||||||
&git2go.FetchOptions{
|
&git2go.FetchOptions{
|
||||||
DownloadTags: git2go.DownloadTagsAuto,
|
DownloadTags: git2go.DownloadTagsAuto,
|
||||||
RemoteCallbacks: remoteCallBacks,
|
RemoteCallbacks: remoteCallBacks,
|
||||||
ProxyOptions: *proxyOpts,
|
},
|
||||||
},
|
"")
|
||||||
"")
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to fetch remote '%s': %w",
|
return nil, fmt.Errorf("unable to fetch remote '%s': %w",
|
||||||
managed.EffectiveURL(url), gitutil.LibGit2Error(err))
|
managed.EffectiveURL(url), gitutil.LibGit2Error(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
cc, err := checkoutDetachedDwim(repo, c.Tag)
|
cc, err := checkoutDetachedDwim(repo, c.Tag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
}
|
||||||
|
defer cc.Free()
|
||||||
|
return buildCommit(cc, "refs/tags/"+c.Tag), nil
|
||||||
|
} else {
|
||||||
|
repo, err := git2go.Clone(url, path, &git2go.CloneOptions{
|
||||||
|
FetchOptions: git2go.FetchOptions{
|
||||||
|
DownloadTags: git2go.DownloadTagsAll,
|
||||||
|
RemoteCallbacks: RemoteCallbacks(ctx, opts),
|
||||||
|
ProxyOptions: git2go.ProxyOptions{Type: git2go.ProxyTypeAuto},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to clone '%s': %w", managed.EffectiveURL(url), gitutil.LibGit2Error(err))
|
||||||
|
}
|
||||||
|
defer repo.Free()
|
||||||
|
cc, err := checkoutDetachedDwim(repo, c.Tag)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer cc.Free()
|
||||||
|
return buildCommit(cc, "refs/tags/"+c.Tag), nil
|
||||||
}
|
}
|
||||||
defer cc.Free()
|
|
||||||
return buildCommit(cc, "refs/tags/"+c.Tag), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type CheckoutCommit struct {
|
type CheckoutCommit struct {
|
||||||
|
@ -274,20 +332,26 @@ type CheckoutCommit struct {
|
||||||
func (c *CheckoutCommit) Checkout(ctx context.Context, path, url string, opts *git.AuthOptions) (_ *git.Commit, err error) {
|
func (c *CheckoutCommit) Checkout(ctx context.Context, path, url string, opts *git.AuthOptions) (_ *git.Commit, err error) {
|
||||||
defer recoverPanic(&err)
|
defer recoverPanic(&err)
|
||||||
|
|
||||||
|
remoteCallBacks := RemoteCallbacks(ctx, opts)
|
||||||
|
|
||||||
if managed.Enabled() {
|
if managed.Enabled() {
|
||||||
managed.AddTransportOptions(opts.TransportAuthID, managed.TransportOptions{
|
if opts.TransportOptionsURL == "" {
|
||||||
TargetURL: url,
|
return nil, fmt.Errorf("can't use managed transport without a valid transport auth id.")
|
||||||
AuthOpts: opts,
|
}
|
||||||
|
managed.AddTransportOptions(opts.TransportOptionsURL, managed.TransportOptions{
|
||||||
|
TargetURL: url,
|
||||||
|
AuthOpts: opts,
|
||||||
|
ProxyOptions: &git2go.ProxyOptions{Type: git2go.ProxyTypeAuto},
|
||||||
})
|
})
|
||||||
url = opts.TransportAuthID
|
url = opts.TransportOptionsURL
|
||||||
defer managed.RemoveTransportOptions(opts.TransportAuthID)
|
remoteCallBacks = managed.RemoteCallbacks()
|
||||||
|
defer managed.RemoveTransportOptions(opts.TransportOptionsURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
repo, err := git2go.Clone(url, path, &git2go.CloneOptions{
|
repo, err := git2go.Clone(url, path, &git2go.CloneOptions{
|
||||||
FetchOptions: git2go.FetchOptions{
|
FetchOptions: git2go.FetchOptions{
|
||||||
DownloadTags: git2go.DownloadTagsNone,
|
DownloadTags: git2go.DownloadTagsNone,
|
||||||
RemoteCallbacks: RemoteCallbacks(ctx, opts),
|
RemoteCallbacks: remoteCallBacks,
|
||||||
ProxyOptions: git2go.ProxyOptions{Type: git2go.ProxyTypeAuto},
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -312,13 +376,20 @@ type CheckoutSemVer struct {
|
||||||
func (c *CheckoutSemVer) Checkout(ctx context.Context, path, url string, opts *git.AuthOptions) (_ *git.Commit, err error) {
|
func (c *CheckoutSemVer) Checkout(ctx context.Context, path, url string, opts *git.AuthOptions) (_ *git.Commit, err error) {
|
||||||
defer recoverPanic(&err)
|
defer recoverPanic(&err)
|
||||||
|
|
||||||
|
remoteCallBacks := RemoteCallbacks(ctx, opts)
|
||||||
|
|
||||||
if managed.Enabled() {
|
if managed.Enabled() {
|
||||||
managed.AddTransportOptions(opts.TransportAuthID, managed.TransportOptions{
|
if opts.TransportOptionsURL == "" {
|
||||||
TargetURL: url,
|
return nil, fmt.Errorf("can't use managed transport without a valid transport auth id.")
|
||||||
AuthOpts: opts,
|
}
|
||||||
|
managed.AddTransportOptions(opts.TransportOptionsURL, managed.TransportOptions{
|
||||||
|
TargetURL: url,
|
||||||
|
AuthOpts: opts,
|
||||||
|
ProxyOptions: &git2go.ProxyOptions{Type: git2go.ProxyTypeAuto},
|
||||||
})
|
})
|
||||||
url = opts.TransportAuthID
|
url = opts.TransportOptionsURL
|
||||||
defer managed.RemoveTransportOptions(opts.TransportAuthID)
|
remoteCallBacks = managed.RemoteCallbacks()
|
||||||
|
defer managed.RemoveTransportOptions(opts.TransportOptionsURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
verConstraint, err := semver.NewConstraint(c.SemVer)
|
verConstraint, err := semver.NewConstraint(c.SemVer)
|
||||||
|
@ -329,8 +400,7 @@ func (c *CheckoutSemVer) Checkout(ctx context.Context, path, url string, opts *g
|
||||||
repo, err := git2go.Clone(url, path, &git2go.CloneOptions{
|
repo, err := git2go.Clone(url, path, &git2go.CloneOptions{
|
||||||
FetchOptions: git2go.FetchOptions{
|
FetchOptions: git2go.FetchOptions{
|
||||||
DownloadTags: git2go.DownloadTagsAll,
|
DownloadTags: git2go.DownloadTagsAll,
|
||||||
RemoteCallbacks: RemoteCallbacks(ctx, opts),
|
RemoteCallbacks: remoteCallBacks,
|
||||||
ProxyOptions: git2go.ProxyOptions{Type: git2go.ProxyTypeAuto},
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -77,49 +77,29 @@ func TestCheckoutBranch_Checkout(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
branch string
|
branch string
|
||||||
filesCreated map[string]string
|
filesCreated map[string]string
|
||||||
lastRevision string
|
lastRevision string
|
||||||
expectedCommit string
|
expectedCommit string
|
||||||
expectedConcreteCommit bool
|
expectedErr string
|
||||||
expectedErr string
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Default branch",
|
name: "Default branch",
|
||||||
branch: defaultBranch,
|
branch: defaultBranch,
|
||||||
filesCreated: map[string]string{"branch": "second"},
|
filesCreated: map[string]string{"branch": "second"},
|
||||||
expectedCommit: secondCommit.String(),
|
expectedCommit: secondCommit.String(),
|
||||||
expectedConcreteCommit: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Other branch",
|
name: "Other branch",
|
||||||
branch: "test",
|
branch: "test",
|
||||||
filesCreated: map[string]string{"branch": "init"},
|
filesCreated: map[string]string{"branch": "init"},
|
||||||
expectedCommit: firstCommit.String(),
|
expectedCommit: firstCommit.String(),
|
||||||
expectedConcreteCommit: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Non existing branch",
|
name: "Non existing branch",
|
||||||
branch: "invalid",
|
branch: "invalid",
|
||||||
expectedErr: "reference 'refs/remotes/origin/invalid' not found",
|
expectedErr: "reference 'refs/remotes/origin/invalid' not found",
|
||||||
expectedConcreteCommit: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "skip clone - lastRevision hasn't changed",
|
|
||||||
branch: defaultBranch,
|
|
||||||
filesCreated: map[string]string{"branch": "second"},
|
|
||||||
lastRevision: fmt.Sprintf("%s/%s", defaultBranch, secondCommit.String()),
|
|
||||||
expectedCommit: secondCommit.String(),
|
|
||||||
expectedConcreteCommit: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "lastRevision is different",
|
|
||||||
branch: defaultBranch,
|
|
||||||
filesCreated: map[string]string{"branch": "second"},
|
|
||||||
lastRevision: fmt.Sprintf("%s/%s", defaultBranch, firstCommit.String()),
|
|
||||||
expectedCommit: secondCommit.String(),
|
|
||||||
expectedConcreteCommit: true,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,14 +122,6 @@ func TestCheckoutBranch_Checkout(t *testing.T) {
|
||||||
}
|
}
|
||||||
g.Expect(err).ToNot(HaveOccurred())
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
g.Expect(cc.String()).To(Equal(tt.branch + "/" + tt.expectedCommit))
|
g.Expect(cc.String()).To(Equal(tt.branch + "/" + tt.expectedCommit))
|
||||||
g.Expect(git.IsConcreteCommit(*cc)).To(Equal(tt.expectedConcreteCommit))
|
|
||||||
|
|
||||||
if tt.expectedConcreteCommit {
|
|
||||||
for k, v := range tt.filesCreated {
|
|
||||||
g.Expect(filepath.Join(tmpDir, k)).To(BeARegularFile())
|
|
||||||
g.Expect(os.ReadFile(filepath.Join(tmpDir, k))).To(BeEquivalentTo(v))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -161,24 +133,20 @@ func TestCheckoutTag_Checkout(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
tagsInRepo []testTag
|
tagsInRepo []testTag
|
||||||
checkoutTag string
|
checkoutTag string
|
||||||
lastRevTag string
|
expectErr string
|
||||||
expectErr string
|
|
||||||
expectConcreteCommit bool
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Tag",
|
name: "Tag",
|
||||||
tagsInRepo: []testTag{{"tag-1", false}},
|
tagsInRepo: []testTag{{"tag-1", false}},
|
||||||
checkoutTag: "tag-1",
|
checkoutTag: "tag-1",
|
||||||
expectConcreteCommit: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Annotated",
|
name: "Annotated",
|
||||||
tagsInRepo: []testTag{{"annotated", true}},
|
tagsInRepo: []testTag{{"annotated", true}},
|
||||||
checkoutTag: "annotated",
|
checkoutTag: "annotated",
|
||||||
expectConcreteCommit: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Non existing tag",
|
name: "Non existing tag",
|
||||||
|
@ -186,18 +154,14 @@ func TestCheckoutTag_Checkout(t *testing.T) {
|
||||||
expectErr: "unable to find 'invalid': no reference found for shorthand 'invalid'",
|
expectErr: "unable to find 'invalid': no reference found for shorthand 'invalid'",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Skip clone - last revision unchanged",
|
name: "Skip clone - last revision unchanged",
|
||||||
tagsInRepo: []testTag{{"tag-1", false}},
|
tagsInRepo: []testTag{{"tag-1", false}},
|
||||||
checkoutTag: "tag-1",
|
checkoutTag: "tag-1",
|
||||||
lastRevTag: "tag-1",
|
|
||||||
expectConcreteCommit: false,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Last revision changed",
|
name: "Last revision changed",
|
||||||
tagsInRepo: []testTag{{"tag-1", false}, {"tag-2", false}},
|
tagsInRepo: []testTag{{"tag-1", false}, {"tag-2", false}},
|
||||||
checkoutTag: "tag-2",
|
checkoutTag: "tag-2",
|
||||||
lastRevTag: "tag-1",
|
|
||||||
expectConcreteCommit: true,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
@ -235,12 +199,6 @@ func TestCheckoutTag_Checkout(t *testing.T) {
|
||||||
checkoutTag := CheckoutTag{
|
checkoutTag := CheckoutTag{
|
||||||
Tag: tt.checkoutTag,
|
Tag: tt.checkoutTag,
|
||||||
}
|
}
|
||||||
// If last revision is provided, configure it.
|
|
||||||
if tt.lastRevTag != "" {
|
|
||||||
lc := tagCommits[tt.lastRevTag]
|
|
||||||
checkoutTag.LastRevision = fmt.Sprintf("%s/%s", tt.lastRevTag, lc.Id().String())
|
|
||||||
}
|
|
||||||
|
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
cc, err := checkoutTag.Checkout(context.TODO(), tmpDir, repo.Path(), nil)
|
cc, err := checkoutTag.Checkout(context.TODO(), tmpDir, repo.Path(), nil)
|
||||||
|
@ -252,16 +210,12 @@ func TestCheckoutTag_Checkout(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check successful checkout results.
|
// Check successful checkout results.
|
||||||
g.Expect(git.IsConcreteCommit(*cc)).To(Equal(tt.expectConcreteCommit))
|
|
||||||
targetTagCommit := tagCommits[tt.checkoutTag]
|
targetTagCommit := tagCommits[tt.checkoutTag]
|
||||||
g.Expect(err).ToNot(HaveOccurred())
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
g.Expect(cc.String()).To(Equal(tt.checkoutTag + "/" + targetTagCommit.Id().String()))
|
g.Expect(cc.String()).To(Equal(tt.checkoutTag + "/" + targetTagCommit.Id().String()))
|
||||||
|
|
||||||
// Check file content only when there's an actual checkout.
|
g.Expect(filepath.Join(tmpDir, "tag")).To(BeARegularFile())
|
||||||
if tt.lastRevTag != tt.checkoutTag {
|
g.Expect(os.ReadFile(filepath.Join(tmpDir, "tag"))).To(BeEquivalentTo(tt.checkoutTag))
|
||||||
g.Expect(filepath.Join(tmpDir, "tag")).To(BeARegularFile())
|
|
||||||
g.Expect(os.ReadFile(filepath.Join(tmpDir, "tag"))).To(BeEquivalentTo(tt.checkoutTag))
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,6 +47,7 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
@ -55,6 +56,7 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
pool "github.com/fluxcd/source-controller/internal/transport"
|
pool "github.com/fluxcd/source-controller/internal/transport"
|
||||||
|
"github.com/fluxcd/source-controller/pkg/git"
|
||||||
git2go "github.com/libgit2/git2go/v33"
|
git2go "github.com/libgit2/git2go/v33"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -86,30 +88,45 @@ type httpSmartSubtransport struct {
|
||||||
httpTransport *http.Transport
|
httpTransport *http.Transport
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *httpSmartSubtransport) Action(transportAuthID string, action git2go.SmartServiceAction) (git2go.SmartSubtransportStream, error) {
|
func (t *httpSmartSubtransport) Action(transportOptionsURL string, action git2go.SmartServiceAction) (git2go.SmartSubtransportStream, error) {
|
||||||
|
opts, found := getTransportOptions(transportOptionsURL)
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
return nil, fmt.Errorf("failed to create client: could not find transport options for the object: %s", transportOptionsURL)
|
||||||
|
}
|
||||||
|
targetURL := opts.TargetURL
|
||||||
|
|
||||||
|
if targetURL == "" {
|
||||||
|
return nil, fmt.Errorf("repository URL cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(targetURL) > URLMaxLength {
|
||||||
|
return nil, fmt.Errorf("URL exceeds the max length (%d)", URLMaxLength)
|
||||||
|
}
|
||||||
|
|
||||||
var proxyFn func(*http.Request) (*url.URL, error)
|
var proxyFn func(*http.Request) (*url.URL, error)
|
||||||
proxyOpts, err := t.transport.SmartProxyOptions()
|
proxyOpts := opts.ProxyOptions
|
||||||
if err != nil {
|
if proxyOpts != nil {
|
||||||
return nil, err
|
switch proxyOpts.Type {
|
||||||
}
|
case git2go.ProxyTypeNone:
|
||||||
switch proxyOpts.Type {
|
proxyFn = nil
|
||||||
case git2go.ProxyTypeNone:
|
case git2go.ProxyTypeAuto:
|
||||||
proxyFn = nil
|
proxyFn = http.ProxyFromEnvironment
|
||||||
case git2go.ProxyTypeAuto:
|
case git2go.ProxyTypeSpecified:
|
||||||
proxyFn = http.ProxyFromEnvironment
|
parsedUrl, err := url.Parse(proxyOpts.Url)
|
||||||
case git2go.ProxyTypeSpecified:
|
if err != nil {
|
||||||
parsedUrl, err := url.Parse(proxyOpts.Url)
|
return nil, err
|
||||||
if err != nil {
|
}
|
||||||
return nil, err
|
proxyFn = http.ProxyURL(parsedUrl)
|
||||||
}
|
}
|
||||||
|
t.httpTransport.Proxy = proxyFn
|
||||||
proxyFn = http.ProxyURL(parsedUrl)
|
t.httpTransport.ProxyConnectHeader = map[string][]string{}
|
||||||
|
} else {
|
||||||
|
t.httpTransport.Proxy = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
t.httpTransport.Proxy = proxyFn
|
|
||||||
t.httpTransport.DisableCompression = false
|
t.httpTransport.DisableCompression = false
|
||||||
|
|
||||||
client, req, err := createClientRequest(transportAuthID, action, t.httpTransport)
|
client, req, err := createClientRequest(targetURL, action, t.httpTransport, opts.AuthOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -142,7 +159,8 @@ func (t *httpSmartSubtransport) Action(transportAuthID string, action git2go.Sma
|
||||||
return stream, nil
|
return stream, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createClientRequest(transportAuthID string, action git2go.SmartServiceAction, t *http.Transport) (*http.Client, *http.Request, error) {
|
func createClientRequest(targetURL string, action git2go.SmartServiceAction,
|
||||||
|
t *http.Transport, authOpts *git.AuthOptions) (*http.Client, *http.Request, error) {
|
||||||
var req *http.Request
|
var req *http.Request
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
|
@ -150,17 +168,6 @@ func createClientRequest(transportAuthID string, action git2go.SmartServiceActio
|
||||||
return nil, nil, fmt.Errorf("failed to create client: transport cannot be nil")
|
return nil, nil, fmt.Errorf("failed to create client: transport cannot be nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
opts, found := getTransportOptions(transportAuthID)
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
return nil, nil, fmt.Errorf("failed to create client: could not find transport options for the object: %s", transportAuthID)
|
|
||||||
}
|
|
||||||
targetURL := opts.TargetURL
|
|
||||||
|
|
||||||
if len(targetURL) > URLMaxLength {
|
|
||||||
return nil, nil, fmt.Errorf("URL exceeds the max length (%d)", URLMaxLength)
|
|
||||||
}
|
|
||||||
|
|
||||||
client := &http.Client{
|
client := &http.Client{
|
||||||
Transport: t,
|
Transport: t,
|
||||||
Timeout: fullHttpClientTimeOut,
|
Timeout: fullHttpClientTimeOut,
|
||||||
|
@ -176,6 +183,9 @@ func createClientRequest(transportAuthID string, action git2go.SmartServiceActio
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
req.Header.Set("Content-Type", "application/x-git-upload-pack-request")
|
req.Header.Set("Content-Type", "application/x-git-upload-pack-request")
|
||||||
|
if t.Proxy != nil {
|
||||||
|
t.ProxyConnectHeader.Set("Content-Type", "application/x-git-upload-pack-request")
|
||||||
|
}
|
||||||
|
|
||||||
case git2go.SmartServiceActionReceivepackLs:
|
case git2go.SmartServiceActionReceivepackLs:
|
||||||
req, err = http.NewRequest("GET", targetURL+"/info/refs?service=git-receive-pack", nil)
|
req, err = http.NewRequest("GET", targetURL+"/info/refs?service=git-receive-pack", nil)
|
||||||
|
@ -186,6 +196,9 @@ func createClientRequest(transportAuthID string, action git2go.SmartServiceActio
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
req.Header.Set("Content-Type", "application/x-git-receive-pack-request")
|
req.Header.Set("Content-Type", "application/x-git-receive-pack-request")
|
||||||
|
if t.Proxy != nil {
|
||||||
|
t.ProxyConnectHeader.Set("Content-Type", "application/x-git-receive-pack-request")
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
err = errors.New("unknown action")
|
err = errors.New("unknown action")
|
||||||
|
@ -195,12 +208,20 @@ func createClientRequest(transportAuthID string, action git2go.SmartServiceActio
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add any provided certificate to the http transport.
|
// Apply authentication and TLS settings to the HTTP transport.
|
||||||
if opts.AuthOpts != nil {
|
if authOpts != nil {
|
||||||
req.SetBasicAuth(opts.AuthOpts.Username, opts.AuthOpts.Password)
|
if len(authOpts.Username) > 0 {
|
||||||
if len(opts.AuthOpts.CAFile) > 0 {
|
req.SetBasicAuth(authOpts.Username, authOpts.Password)
|
||||||
|
if t.Proxy != nil {
|
||||||
|
t.ProxyConnectHeader.Set(
|
||||||
|
"Authorization",
|
||||||
|
"Basic "+basicAuth(authOpts.Username, authOpts.Password),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(authOpts.CAFile) > 0 {
|
||||||
certPool := x509.NewCertPool()
|
certPool := x509.NewCertPool()
|
||||||
if ok := certPool.AppendCertsFromPEM(opts.AuthOpts.CAFile); !ok {
|
if ok := certPool.AppendCertsFromPEM(authOpts.CAFile); !ok {
|
||||||
return nil, nil, fmt.Errorf("failed to use certificate from PEM")
|
return nil, nil, fmt.Errorf("failed to use certificate from PEM")
|
||||||
}
|
}
|
||||||
t.TLSClientConfig = &tls.Config{
|
t.TLSClientConfig = &tls.Config{
|
||||||
|
@ -210,6 +231,9 @@ func createClientRequest(transportAuthID string, action git2go.SmartServiceActio
|
||||||
}
|
}
|
||||||
|
|
||||||
req.Header.Set("User-Agent", "git/2.0 (flux-libgit2)")
|
req.Header.Set("User-Agent", "git/2.0 (flux-libgit2)")
|
||||||
|
if t.Proxy != nil {
|
||||||
|
t.ProxyConnectHeader.Set("User-Agent", "git/2.0 (flux-libgit2)")
|
||||||
|
}
|
||||||
return client, req, nil
|
return client, req, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -389,3 +413,9 @@ func (self *httpSmartSubtransportStream) sendRequest() error {
|
||||||
self.sentRequest = true
|
self.sentRequest = true
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// From: https://github.com/golang/go/blob/go1.18/src/net/http/client.go#L418
|
||||||
|
func basicAuth(username, password string) string {
|
||||||
|
auth := username + ":" + password
|
||||||
|
return base64.StdEncoding.EncodeToString([]byte(auth))
|
||||||
|
}
|
||||||
|
|
|
@ -32,76 +32,88 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestHttpAction_CreateClientRequest(t *testing.T) {
|
func TestHttpAction_CreateClientRequest(t *testing.T) {
|
||||||
opts := &TransportOptions{
|
authOpts := git.AuthOptions{
|
||||||
TargetURL: "https://final-target/abc",
|
Username: "user",
|
||||||
|
Password: "pwd",
|
||||||
}
|
}
|
||||||
|
url := "https://final-target/abc"
|
||||||
optsWithAuth := &TransportOptions{
|
|
||||||
TargetURL: "https://final-target/abc",
|
|
||||||
AuthOpts: &git.AuthOptions{
|
|
||||||
Username: "user",
|
|
||||||
Password: "pwd",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
id := "https://obj-id"
|
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
assertFunc func(g *WithT, req *http.Request, client *http.Client)
|
assertFunc func(g *WithT, req *http.Request, client *http.Client)
|
||||||
action git2go.SmartServiceAction
|
action git2go.SmartServiceAction
|
||||||
opts *TransportOptions
|
authOpts git.AuthOptions
|
||||||
transport *http.Transport
|
transport *http.Transport
|
||||||
wantedErr error
|
wantedErr error
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Uploadpack: URL and method are correctly set",
|
name: "Uploadpack: URL, method and headers are correctly set",
|
||||||
action: git2go.SmartServiceActionUploadpack,
|
action: git2go.SmartServiceActionUploadpack,
|
||||||
transport: &http.Transport{},
|
transport: &http.Transport{
|
||||||
|
Proxy: http.ProxyFromEnvironment,
|
||||||
|
ProxyConnectHeader: map[string][]string{},
|
||||||
|
},
|
||||||
assertFunc: func(g *WithT, req *http.Request, _ *http.Client) {
|
assertFunc: func(g *WithT, req *http.Request, _ *http.Client) {
|
||||||
g.Expect(req.URL.String()).To(Equal("https://final-target/abc/git-upload-pack"))
|
g.Expect(req.URL.String()).To(Equal("https://final-target/abc/git-upload-pack"))
|
||||||
g.Expect(req.Method).To(Equal("POST"))
|
g.Expect(req.Method).To(Equal("POST"))
|
||||||
|
g.Expect(req.Header).To(BeEquivalentTo(map[string][]string{
|
||||||
|
"User-Agent": []string{"git/2.0 (flux-libgit2)"},
|
||||||
|
"Content-Type": []string{"application/x-git-upload-pack-request"},
|
||||||
|
}))
|
||||||
},
|
},
|
||||||
opts: opts,
|
|
||||||
wantedErr: nil,
|
wantedErr: nil,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "UploadpackLs: URL and method are correctly set",
|
name: "UploadpackLs: URL, method and headers are correctly set",
|
||||||
action: git2go.SmartServiceActionUploadpackLs,
|
action: git2go.SmartServiceActionUploadpackLs,
|
||||||
transport: &http.Transport{},
|
transport: &http.Transport{},
|
||||||
assertFunc: func(g *WithT, req *http.Request, _ *http.Client) {
|
assertFunc: func(g *WithT, req *http.Request, _ *http.Client) {
|
||||||
g.Expect(req.URL.String()).To(Equal("https://final-target/abc/info/refs?service=git-upload-pack"))
|
g.Expect(req.URL.String()).To(Equal("https://final-target/abc/info/refs?service=git-upload-pack"))
|
||||||
g.Expect(req.Method).To(Equal("GET"))
|
g.Expect(req.Method).To(Equal("GET"))
|
||||||
|
g.Expect(req.Header).To(BeEquivalentTo(map[string][]string{
|
||||||
|
"User-Agent": []string{"git/2.0 (flux-libgit2)"},
|
||||||
|
}))
|
||||||
},
|
},
|
||||||
opts: opts,
|
|
||||||
wantedErr: nil,
|
wantedErr: nil,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Receivepack: URL and method are correctly set",
|
name: "Receivepack: URL, method and headers are correctly set",
|
||||||
action: git2go.SmartServiceActionReceivepack,
|
action: git2go.SmartServiceActionReceivepack,
|
||||||
transport: &http.Transport{},
|
transport: &http.Transport{
|
||||||
|
Proxy: http.ProxyFromEnvironment,
|
||||||
|
ProxyConnectHeader: map[string][]string{},
|
||||||
|
},
|
||||||
assertFunc: func(g *WithT, req *http.Request, _ *http.Client) {
|
assertFunc: func(g *WithT, req *http.Request, _ *http.Client) {
|
||||||
g.Expect(req.URL.String()).To(Equal("https://final-target/abc/git-receive-pack"))
|
g.Expect(req.URL.String()).To(Equal("https://final-target/abc/git-receive-pack"))
|
||||||
g.Expect(req.Method).To(Equal("POST"))
|
g.Expect(req.Method).To(Equal("POST"))
|
||||||
|
g.Expect(req.Header).To(BeEquivalentTo(map[string][]string{
|
||||||
|
"Content-Type": []string{"application/x-git-receive-pack-request"},
|
||||||
|
"User-Agent": []string{"git/2.0 (flux-libgit2)"},
|
||||||
|
}))
|
||||||
},
|
},
|
||||||
opts: opts,
|
|
||||||
wantedErr: nil,
|
wantedErr: nil,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "ReceivepackLs: URL and method are correctly set",
|
name: "ReceivepackLs: URL, method and headars are correctly set",
|
||||||
action: git2go.SmartServiceActionReceivepackLs,
|
action: git2go.SmartServiceActionReceivepackLs,
|
||||||
transport: &http.Transport{},
|
transport: &http.Transport{},
|
||||||
assertFunc: func(g *WithT, req *http.Request, _ *http.Client) {
|
assertFunc: func(g *WithT, req *http.Request, _ *http.Client) {
|
||||||
g.Expect(req.URL.String()).To(Equal("https://final-target/abc/info/refs?service=git-receive-pack"))
|
g.Expect(req.URL.String()).To(Equal("https://final-target/abc/info/refs?service=git-receive-pack"))
|
||||||
g.Expect(req.Method).To(Equal("GET"))
|
g.Expect(req.Method).To(Equal("GET"))
|
||||||
|
g.Expect(req.Header).To(BeEquivalentTo(map[string][]string{
|
||||||
|
"User-Agent": []string{"git/2.0 (flux-libgit2)"},
|
||||||
|
}))
|
||||||
},
|
},
|
||||||
opts: opts,
|
|
||||||
wantedErr: nil,
|
wantedErr: nil,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "credentials are correctly configured",
|
name: "credentials are correctly configured",
|
||||||
action: git2go.SmartServiceActionUploadpack,
|
action: git2go.SmartServiceActionUploadpack,
|
||||||
transport: &http.Transport{},
|
transport: &http.Transport{
|
||||||
opts: optsWithAuth,
|
Proxy: http.ProxyFromEnvironment,
|
||||||
|
ProxyConnectHeader: map[string][]string{},
|
||||||
|
},
|
||||||
|
authOpts: authOpts,
|
||||||
assertFunc: func(g *WithT, req *http.Request, client *http.Client) {
|
assertFunc: func(g *WithT, req *http.Request, client *http.Client) {
|
||||||
g.Expect(req.URL.String()).To(Equal("https://final-target/abc/git-upload-pack"))
|
g.Expect(req.URL.String()).To(Equal("https://final-target/abc/git-upload-pack"))
|
||||||
g.Expect(req.Method).To(Equal("POST"))
|
g.Expect(req.Method).To(Equal("POST"))
|
||||||
|
@ -119,26 +131,15 @@ func TestHttpAction_CreateClientRequest(t *testing.T) {
|
||||||
name: "error when no http.transport provided",
|
name: "error when no http.transport provided",
|
||||||
action: git2go.SmartServiceActionUploadpack,
|
action: git2go.SmartServiceActionUploadpack,
|
||||||
transport: nil,
|
transport: nil,
|
||||||
opts: opts,
|
|
||||||
wantedErr: fmt.Errorf("failed to create client: transport cannot be nil"),
|
wantedErr: fmt.Errorf("failed to create client: transport cannot be nil"),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "error when no transport options are registered",
|
|
||||||
action: git2go.SmartServiceActionUploadpack,
|
|
||||||
transport: &http.Transport{},
|
|
||||||
opts: nil,
|
|
||||||
wantedErr: fmt.Errorf("failed to create client: could not find transport options for the object: https://obj-id"),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
g := NewWithT(t)
|
g := NewWithT(t)
|
||||||
if tt.opts != nil {
|
|
||||||
AddTransportOptions(id, *tt.opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
client, req, err := createClientRequest(id, tt.action, tt.transport)
|
client, req, err := createClientRequest(url, tt.action, tt.transport, &tt.authOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Log(err)
|
t.Log(err)
|
||||||
}
|
}
|
||||||
|
@ -148,9 +149,6 @@ func TestHttpAction_CreateClientRequest(t *testing.T) {
|
||||||
tt.assertFunc(g, req, client)
|
tt.assertFunc(g, req, client)
|
||||||
}
|
}
|
||||||
|
|
||||||
if tt.opts != nil {
|
|
||||||
RemoveTransportOptions(id)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -167,18 +165,10 @@ func TestHTTPManagedTransport_E2E(t *testing.T) {
|
||||||
server.Auth(user, pwd)
|
server.Auth(user, pwd)
|
||||||
server.KeyDir(filepath.Join(server.Root(), "keys"))
|
server.KeyDir(filepath.Join(server.Root(), "keys"))
|
||||||
|
|
||||||
err = server.ListenSSH()
|
|
||||||
g.Expect(err).ToNot(HaveOccurred())
|
|
||||||
|
|
||||||
err = server.StartHTTP()
|
err = server.StartHTTP()
|
||||||
g.Expect(err).ToNot(HaveOccurred())
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
defer server.StopHTTP()
|
defer server.StopHTTP()
|
||||||
|
|
||||||
go func() {
|
|
||||||
server.StartSSH()
|
|
||||||
}()
|
|
||||||
defer server.StopSSH()
|
|
||||||
|
|
||||||
// Force managed transport to be enabled
|
// Force managed transport to be enabled
|
||||||
InitManagedTransport(logr.Discard())
|
InitManagedTransport(logr.Discard())
|
||||||
|
|
||||||
|
@ -188,7 +178,7 @@ func TestHTTPManagedTransport_E2E(t *testing.T) {
|
||||||
|
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
// Register the auth options and target url mapped to a unique id.
|
// Register the auth options and target url mapped to a unique url.
|
||||||
id := "http://obj-id"
|
id := "http://obj-id"
|
||||||
AddTransportOptions(id, TransportOptions{
|
AddTransportOptions(id, TransportOptions{
|
||||||
TargetURL: server.HTTPAddress() + "/" + repoPath,
|
TargetURL: server.HTTPAddress() + "/" + repoPath,
|
||||||
|
@ -198,9 +188,9 @@ func TestHTTPManagedTransport_E2E(t *testing.T) {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// We call Clone with id instead of the actual url, as the transport action
|
// We call git2go.Clone with transportOptsURL instead of the actual URL,
|
||||||
// will fetch the actual url and the required credentials using the id as
|
// as the transport action will fetch the actual URL and the required
|
||||||
// a identifier.
|
// credentials using the it as an identifier.
|
||||||
repo, err := git2go.Clone(id, tmpDir, &git2go.CloneOptions{
|
repo, err := git2go.Clone(id, tmpDir, &git2go.CloneOptions{
|
||||||
CheckoutOptions: git2go.CheckoutOptions{
|
CheckoutOptions: git2go.CheckoutOptions{
|
||||||
Strategy: git2go.CheckoutForce,
|
Strategy: git2go.CheckoutForce,
|
||||||
|
|
|
@ -1,48 +0,0 @@
|
||||||
/*
|
|
||||||
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 (
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestFlagStatus(t *testing.T) {
|
|
||||||
if Enabled() {
|
|
||||||
t.Errorf("experimental transport should not be enabled by default")
|
|
||||||
}
|
|
||||||
|
|
||||||
os.Setenv("EXPERIMENTAL_GIT_TRANSPORT", "true")
|
|
||||||
if !Enabled() {
|
|
||||||
t.Errorf("experimental transport should be enabled when env EXPERIMENTAL_GIT_TRANSPORT=true")
|
|
||||||
}
|
|
||||||
|
|
||||||
os.Setenv("EXPERIMENTAL_GIT_TRANSPORT", "1")
|
|
||||||
if !Enabled() {
|
|
||||||
t.Errorf("experimental transport should be enabled when env EXPERIMENTAL_GIT_TRANSPORT=1")
|
|
||||||
}
|
|
||||||
|
|
||||||
os.Setenv("EXPERIMENTAL_GIT_TRANSPORT", "somethingelse")
|
|
||||||
if Enabled() {
|
|
||||||
t.Errorf("experimental transport should be enabled only when env EXPERIMENTAL_GIT_TRANSPORT is 1 or true but was enabled for 'somethingelse'")
|
|
||||||
}
|
|
||||||
|
|
||||||
os.Unsetenv("EXPERIMENTAL_GIT_TRANSPORT")
|
|
||||||
if Enabled() {
|
|
||||||
t.Errorf("experimental transport should not be enabled when env EXPERIMENTAL_GIT_TRANSPORT is not present")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -20,36 +20,45 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/fluxcd/source-controller/pkg/git"
|
"github.com/fluxcd/source-controller/pkg/git"
|
||||||
|
git2go "github.com/libgit2/git2go/v33"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TransportOptions represents options to be applied at transport-level
|
// TransportOptions represents options to be applied at transport-level
|
||||||
// at request time.
|
// at request time.
|
||||||
type TransportOptions struct {
|
type TransportOptions struct {
|
||||||
TargetURL string
|
TargetURL string
|
||||||
AuthOpts *git.AuthOptions
|
AuthOpts *git.AuthOptions
|
||||||
|
ProxyOptions *git2go.ProxyOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// transportOpts maps a unique id to a set of transport options.
|
// transportOpts maps a unique url to a set of transport options.
|
||||||
transportOpts = make(map[string]TransportOptions, 0)
|
transportOpts = make(map[string]TransportOptions, 0)
|
||||||
m sync.RWMutex
|
m sync.RWMutex
|
||||||
)
|
)
|
||||||
|
|
||||||
func AddTransportOptions(id string, opts TransportOptions) {
|
// AddTransportOptions registers a TransportOptions object mapped to the
|
||||||
|
// provided transportOptsURL, which must be a valid URL, i.e. prefixed with "http://"
|
||||||
|
// or "ssh://", as it is used as a dummy URL for all git operations and the managed
|
||||||
|
// transports will only be invoked for the protocols that they have been
|
||||||
|
// registered for.
|
||||||
|
func AddTransportOptions(transportOptsURL string, opts TransportOptions) {
|
||||||
m.Lock()
|
m.Lock()
|
||||||
transportOpts[id] = opts
|
transportOpts[transportOptsURL] = opts
|
||||||
m.Unlock()
|
m.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func RemoveTransportOptions(id string) {
|
// RemoveTransportOptions removes the registerd TransportOptions object
|
||||||
|
// mapped to the provided id.
|
||||||
|
func RemoveTransportOptions(transportOptsURL string) {
|
||||||
m.Lock()
|
m.Lock()
|
||||||
delete(transportOpts, id)
|
delete(transportOpts, transportOptsURL)
|
||||||
m.Unlock()
|
m.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTransportOptions(id string) (*TransportOptions, bool) {
|
func getTransportOptions(transportOptsURL string) (*TransportOptions, bool) {
|
||||||
m.RLock()
|
m.RLock()
|
||||||
opts, found := transportOpts[id]
|
opts, found := transportOpts[transportOptsURL]
|
||||||
m.RUnlock()
|
m.RUnlock()
|
||||||
|
|
||||||
if found {
|
if found {
|
||||||
|
@ -63,16 +72,16 @@ func getTransportOptions(id string) (*TransportOptions, bool) {
|
||||||
// Given that TransportOptions can allow for the target URL to be overriden
|
// Given that TransportOptions can allow for the target URL to be overriden
|
||||||
// this returns the same input if Managed Transport is disabled or if no TargetURL
|
// this returns the same input if Managed Transport is disabled or if no TargetURL
|
||||||
// is set on TransportOptions.
|
// is set on TransportOptions.
|
||||||
func EffectiveURL(id string) string {
|
func EffectiveURL(transporOptsURL string) string {
|
||||||
if !Enabled() {
|
if !Enabled() {
|
||||||
return id
|
return transporOptsURL
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts, found := getTransportOptions(id); found {
|
if opts, found := getTransportOptions(transporOptsURL); found {
|
||||||
if opts.TargetURL != "" {
|
if opts.TargetURL != "" {
|
||||||
return opts.TargetURL
|
return opts.TargetURL
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return id
|
return transporOptsURL
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,8 +45,6 @@ package managed
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/md5"
|
|
||||||
"crypto/sha1"
|
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
@ -96,13 +94,13 @@ type sshSmartSubtransport struct {
|
||||||
connected bool
|
connected bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *sshSmartSubtransport) Action(credentialsID string, action git2go.SmartServiceAction) (git2go.SmartSubtransportStream, error) {
|
func (t *sshSmartSubtransport) Action(transportOptionsURL string, action git2go.SmartServiceAction) (git2go.SmartSubtransportStream, error) {
|
||||||
runtime.LockOSThread()
|
runtime.LockOSThread()
|
||||||
defer runtime.UnlockOSThread()
|
defer runtime.UnlockOSThread()
|
||||||
|
|
||||||
opts, found := getTransportOptions(credentialsID)
|
opts, found := getTransportOptions(transportOptionsURL)
|
||||||
if !found {
|
if !found {
|
||||||
return nil, fmt.Errorf("could not find transport options for object: %s", credentialsID)
|
return nil, fmt.Errorf("could not find transport options for object: %s", transportOptionsURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
u, err := url.Parse(opts.TargetURL)
|
u, err := url.Parse(opts.TargetURL)
|
||||||
|
@ -167,16 +165,17 @@ func (t *sshSmartSubtransport) Action(credentialsID string, action git2go.SmartS
|
||||||
cert := &git2go.Certificate{
|
cert := &git2go.Certificate{
|
||||||
Kind: git2go.CertificateHostkey,
|
Kind: git2go.CertificateHostkey,
|
||||||
Hostkey: git2go.HostkeyCertificate{
|
Hostkey: git2go.HostkeyCertificate{
|
||||||
Kind: git2go.HostkeySHA1 | git2go.HostkeyMD5 | git2go.HostkeySHA256 | git2go.HostkeyRaw,
|
Kind: git2go.HostkeySHA256,
|
||||||
HashMD5: md5.Sum(marshaledKey),
|
|
||||||
HashSHA1: sha1.Sum(marshaledKey),
|
|
||||||
HashSHA256: sha256.Sum256(marshaledKey),
|
HashSHA256: sha256.Sum256(marshaledKey),
|
||||||
Hostkey: marshaledKey,
|
Hostkey: marshaledKey,
|
||||||
SSHPublicKey: key,
|
SSHPublicKey: key,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return t.transport.SmartCertificateCheck(cert, true, hostname)
|
if len(opts.AuthOpts.KnownHosts) > 0 {
|
||||||
|
return KnownHostsCallback(hostname, opts.AuthOpts.KnownHosts)(cert, true, hostname)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
err = t.createConn(t.addr, sshConfig)
|
err = t.createConn(t.addr, sshConfig)
|
||||||
|
|
|
@ -98,9 +98,9 @@ func TestSSHManagedTransport_E2E(t *testing.T) {
|
||||||
err = server.InitRepo("../../testdata/git/repo", git.DefaultBranch, repoPath)
|
err = server.InitRepo("../../testdata/git/repo", git.DefaultBranch, repoPath)
|
||||||
g.Expect(err).ToNot(HaveOccurred())
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
transportID := "ssh://git@fake-url"
|
transportOptsURL := "ssh://git@fake-url"
|
||||||
sshAddress := server.SSHAddress() + "/" + repoPath
|
sshAddress := server.SSHAddress() + "/" + repoPath
|
||||||
AddTransportOptions(transportID, TransportOptions{
|
AddTransportOptions(transportOptsURL, TransportOptions{
|
||||||
TargetURL: sshAddress,
|
TargetURL: sshAddress,
|
||||||
AuthOpts: &git.AuthOptions{
|
AuthOpts: &git.AuthOptions{
|
||||||
Username: "user",
|
Username: "user",
|
||||||
|
@ -110,10 +110,12 @@ func TestSSHManagedTransport_E2E(t *testing.T) {
|
||||||
|
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
// We call git2go.Clone with transportID, so that the managed ssh transport can
|
// We call git2go.Clone with transportOptsURL, so that the managed ssh transport can
|
||||||
// fetch the correct set of credentials and the actual target url as well.
|
// fetch the correct set of credentials and the actual target url as well.
|
||||||
repo, err := git2go.Clone(transportID, tmpDir, &git2go.CloneOptions{
|
repo, err := git2go.Clone(transportOptsURL, tmpDir, &git2go.CloneOptions{
|
||||||
FetchOptions: git2go.FetchOptions{},
|
FetchOptions: git2go.FetchOptions{
|
||||||
|
RemoteCallbacks: RemoteCallbacks(),
|
||||||
|
},
|
||||||
CheckoutOptions: git2go.CheckoutOptions{
|
CheckoutOptions: git2go.CheckoutOptions{
|
||||||
Strategy: git2go.CheckoutForce,
|
Strategy: git2go.CheckoutForce,
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,103 @@
|
||||||
|
package managed
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"crypto/sha1"
|
||||||
|
"crypto/sha256"
|
||||||
|
"fmt"
|
||||||
|
"hash"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
pkgkh "github.com/fluxcd/pkg/ssh/knownhosts"
|
||||||
|
git2go "github.com/libgit2/git2go/v33"
|
||||||
|
"golang.org/x/crypto/ssh/knownhosts"
|
||||||
|
)
|
||||||
|
|
||||||
|
// knownHostCallback returns a CertificateCheckCallback that verifies
|
||||||
|
// the key of Git server against the given host and known_hosts for
|
||||||
|
// git.SSH Transports.
|
||||||
|
func KnownHostsCallback(host string, knownHosts []byte) git2go.CertificateCheckCallback {
|
||||||
|
return func(cert *git2go.Certificate, valid bool, hostname string) error {
|
||||||
|
kh, err := pkgkh.ParseKnownHosts(string(knownHosts))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse known_hosts: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// First, attempt to split the configured host and port to validate
|
||||||
|
// the port-less hostname given to the callback.
|
||||||
|
hostWithoutPort, _, err := net.SplitHostPort(host)
|
||||||
|
if err != nil {
|
||||||
|
// SplitHostPort returns an error if the host is missing
|
||||||
|
// a port, assume the host has no port.
|
||||||
|
hostWithoutPort = host
|
||||||
|
}
|
||||||
|
|
||||||
|
// Different versions of libgit handle this differently.
|
||||||
|
// This fixes the case in which ports may be sent back.
|
||||||
|
hostnameWithoutPort, _, err := net.SplitHostPort(hostname)
|
||||||
|
if err != nil {
|
||||||
|
hostnameWithoutPort = hostname
|
||||||
|
}
|
||||||
|
|
||||||
|
if hostnameWithoutPort != hostWithoutPort {
|
||||||
|
return fmt.Errorf("host mismatch: %q %q", hostWithoutPort, hostnameWithoutPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
var fingerprint []byte
|
||||||
|
var hasher hash.Hash
|
||||||
|
switch {
|
||||||
|
case cert.Hostkey.Kind&git2go.HostkeySHA256 > 0:
|
||||||
|
fingerprint = cert.Hostkey.HashSHA256[:]
|
||||||
|
hasher = sha256.New()
|
||||||
|
// SHA1 and MD5 are present here, because they're used for unmanaged transport.
|
||||||
|
// TODO: get rid of this, when unmanaged transport is completely removed.
|
||||||
|
case cert.Hostkey.Kind&git2go.HostkeySHA1 > 0:
|
||||||
|
fingerprint = cert.Hostkey.HashSHA1[:]
|
||||||
|
hasher = sha1.New()
|
||||||
|
case cert.Hostkey.Kind&git2go.HostkeyMD5 > 0:
|
||||||
|
fingerprint = cert.Hostkey.HashMD5[:]
|
||||||
|
hasher = md5.New()
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid host key kind, expected to be one of SHA256, SHA1, MD5")
|
||||||
|
}
|
||||||
|
|
||||||
|
// We are now certain that the configured host and the hostname
|
||||||
|
// given to the callback match. Use the configured host (that
|
||||||
|
// includes the port), and normalize it, so we can check if there
|
||||||
|
// is an entry for the hostname _and_ port.
|
||||||
|
h := knownhosts.Normalize(host)
|
||||||
|
for _, k := range kh {
|
||||||
|
if k.Matches(h, fingerprint, hasher) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("hostkey could not be verified")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoteCallbacks constructs git2go.RemoteCallbacks with dummy callbacks.
|
||||||
|
func RemoteCallbacks() git2go.RemoteCallbacks {
|
||||||
|
// This may not be fully removed as without some of the callbacks git2go
|
||||||
|
// gets anxious and panics.
|
||||||
|
return git2go.RemoteCallbacks{
|
||||||
|
CredentialsCallback: credentialsCallback(),
|
||||||
|
CertificateCheckCallback: certificateCallback(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// credentialsCallback constructs a dummy CredentialsCallback.
|
||||||
|
func credentialsCallback() git2go.CredentialsCallback {
|
||||||
|
return func(url string, username string, allowedTypes git2go.CredentialType) (*git2go.Credential, error) {
|
||||||
|
// If credential is nil, panic will ensue. We fake it as managed transport does not
|
||||||
|
// require it.
|
||||||
|
return git2go.NewCredentialUserpassPlaintext("", "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// certificateCallback constructs a dummy CertificateCallback.
|
||||||
|
func certificateCallback() git2go.CertificateCheckCallback {
|
||||||
|
// returning a nil func can cause git2go to panic.
|
||||||
|
return func(cert *git2go.Certificate, valid bool, hostname string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,108 @@
|
||||||
|
package managed
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
git2go "github.com/libgit2/git2go/v33"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
)
|
||||||
|
|
||||||
|
// knownHostsFixture is known_hosts fixture in the expected
|
||||||
|
// format.
|
||||||
|
var knownHostsFixture = `github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==`
|
||||||
|
|
||||||
|
func TestKnownHostsCallback(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
host string
|
||||||
|
expectedHost string
|
||||||
|
knownHosts []byte
|
||||||
|
hostkey git2go.HostkeyCertificate
|
||||||
|
want error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Match",
|
||||||
|
host: "github.com",
|
||||||
|
knownHosts: []byte(knownHostsFixture),
|
||||||
|
hostkey: git2go.HostkeyCertificate{Kind: git2go.HostkeySHA1 | git2go.HostkeyMD5, HashSHA1: sha1Fingerprint("v2toJdKXfFEaR1u++4iq1UqSrHM")},
|
||||||
|
expectedHost: "github.com",
|
||||||
|
want: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Match with port",
|
||||||
|
host: "github.com",
|
||||||
|
knownHosts: []byte(knownHostsFixture),
|
||||||
|
hostkey: git2go.HostkeyCertificate{Kind: git2go.HostkeySHA1 | git2go.HostkeyMD5, HashSHA1: sha1Fingerprint("v2toJdKXfFEaR1u++4iq1UqSrHM")},
|
||||||
|
expectedHost: "github.com:22",
|
||||||
|
want: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Hostname mismatch",
|
||||||
|
host: "github.com",
|
||||||
|
knownHosts: []byte(knownHostsFixture),
|
||||||
|
hostkey: git2go.HostkeyCertificate{Kind: git2go.HostkeySHA1 | git2go.HostkeyMD5, HashSHA1: sha1Fingerprint("v2toJdKXfFEaR1u++4iq1UqSrHM")},
|
||||||
|
expectedHost: "example.com",
|
||||||
|
want: fmt.Errorf("host mismatch: %q %q", "example.com", "github.com"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Hostkey mismatch",
|
||||||
|
host: "github.com",
|
||||||
|
knownHosts: []byte(knownHostsFixture),
|
||||||
|
hostkey: git2go.HostkeyCertificate{Kind: git2go.HostkeyMD5, HashMD5: md5Fingerprint("\xb6\x03\x0e\x39\x97\x9e\xd0\xe7\x24\xce\xa3\x77\x3e\x01\x42\x09")},
|
||||||
|
expectedHost: "github.com",
|
||||||
|
want: fmt.Errorf("hostkey could not be verified"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
g := NewWithT(t)
|
||||||
|
|
||||||
|
cert := &git2go.Certificate{Hostkey: tt.hostkey}
|
||||||
|
callback := KnownHostsCallback(tt.expectedHost, tt.knownHosts)
|
||||||
|
result := g.Expect(callback(cert, false, tt.host))
|
||||||
|
if tt.want == nil {
|
||||||
|
result.To(BeNil())
|
||||||
|
} else {
|
||||||
|
result.To(Equal(tt.want))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func md5Fingerprint(in string) [16]byte {
|
||||||
|
var out [16]byte
|
||||||
|
copy(out[:], in)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func sha1Fingerprint(in string) [20]byte {
|
||||||
|
d, err := base64.RawStdEncoding.DecodeString(in)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
var out [20]byte
|
||||||
|
copy(out[:], d)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func sha256Fingerprint(in string) [32]byte {
|
||||||
|
d, err := base64.RawStdEncoding.DecodeString(in)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
var out [32]byte
|
||||||
|
copy(out[:], d)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func certificateFromPEM(pemBytes string) (*x509.Certificate, error) {
|
||||||
|
block, _ := pem.Decode([]byte(pemBytes))
|
||||||
|
if block == nil {
|
||||||
|
return nil, errors.New("failed to decode PEM")
|
||||||
|
}
|
||||||
|
return x509.ParseCertificate(block.Bytes)
|
||||||
|
}
|
|
@ -36,6 +36,7 @@ import (
|
||||||
"github.com/go-logr/logr"
|
"github.com/go-logr/logr"
|
||||||
. "github.com/onsi/gomega"
|
. "github.com/onsi/gomega"
|
||||||
|
|
||||||
|
git2go "github.com/libgit2/git2go/v33"
|
||||||
cryptossh "golang.org/x/crypto/ssh"
|
cryptossh "golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -123,7 +124,6 @@ func Test_ManagedSSH_KeyTypes(t *testing.T) {
|
||||||
knownHosts, err := ssh.ScanHostKey(u.Host, timeout, git.HostKeyAlgos, false)
|
knownHosts, err := ssh.ScanHostKey(u.Host, timeout, git.HostKeyAlgos, false)
|
||||||
g.Expect(err).ToNot(HaveOccurred())
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
os.Setenv("EXPERIMENTAL_GIT_TRANSPORT", "true")
|
|
||||||
managed.InitManagedTransport(logr.Discard())
|
managed.InitManagedTransport(logr.Discard())
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
@ -139,21 +139,11 @@ func Test_ManagedSSH_KeyTypes(t *testing.T) {
|
||||||
authorizedPublicKey = string(kp.PublicKey)
|
authorizedPublicKey = string(kp.PublicKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
// secret := corev1.Secret{
|
|
||||||
// Data: map[string][]byte{
|
|
||||||
// "identity": kp.PrivateKey,
|
|
||||||
// "known_hosts": knownHosts,
|
|
||||||
// },
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// authOpts, err := git.AuthOptionsFromSecret(repoURL, &secret)
|
|
||||||
// g.Expect(err).ToNot(HaveOccurred())
|
|
||||||
|
|
||||||
authOpts := &git.AuthOptions{
|
authOpts := &git.AuthOptions{
|
||||||
Identity: kp.PrivateKey,
|
Identity: kp.PrivateKey,
|
||||||
KnownHosts: knownHosts,
|
KnownHosts: knownHosts,
|
||||||
}
|
}
|
||||||
authOpts.TransportAuthID = "ssh://" + getTransportAuthID()
|
authOpts.TransportOptionsURL = getTransportOptionsURL(git.SSH)
|
||||||
|
|
||||||
// Prepare for checkout.
|
// Prepare for checkout.
|
||||||
branchCheckoutStrat := &CheckoutBranch{Branch: git.DefaultBranch}
|
branchCheckoutStrat := &CheckoutBranch{Branch: git.DefaultBranch}
|
||||||
|
@ -233,7 +223,6 @@ func Test_ManagedSSH_KeyExchangeAlgos(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
os.Setenv("EXPERIMENTAL_GIT_TRANSPORT", "true")
|
|
||||||
managed.InitManagedTransport(logr.Discard())
|
managed.InitManagedTransport(logr.Discard())
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
@ -280,20 +269,11 @@ func Test_ManagedSSH_KeyExchangeAlgos(t *testing.T) {
|
||||||
kp, err := ssh.GenerateKeyPair(ssh.ED25519)
|
kp, err := ssh.GenerateKeyPair(ssh.ED25519)
|
||||||
g.Expect(err).ToNot(HaveOccurred())
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
// secret := corev1.Secret{
|
|
||||||
// Data: map[string][]byte{
|
|
||||||
// "identity": kp.PrivateKey,
|
|
||||||
// "known_hosts": knownHosts,
|
|
||||||
// },
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// authOpts, err := git.AuthOptionsFromSecret(repoURL, &secret)
|
|
||||||
// g.Expect(err).ToNot(HaveOccurred())
|
|
||||||
authOpts := &git.AuthOptions{
|
authOpts := &git.AuthOptions{
|
||||||
Identity: kp.PrivateKey,
|
Identity: kp.PrivateKey,
|
||||||
KnownHosts: knownHosts,
|
KnownHosts: knownHosts,
|
||||||
}
|
}
|
||||||
authOpts.TransportAuthID = "ssh://" + getTransportAuthID()
|
authOpts.TransportOptionsURL = getTransportOptionsURL(git.SSH)
|
||||||
|
|
||||||
// Prepare for checkout.
|
// Prepare for checkout.
|
||||||
branchCheckoutStrat := &CheckoutBranch{Branch: git.DefaultBranch}
|
branchCheckoutStrat := &CheckoutBranch{Branch: git.DefaultBranch}
|
||||||
|
@ -402,7 +382,6 @@ func Test_ManagedSSH_HostKeyAlgos(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
os.Setenv("EXPERIMENTAL_GIT_TRANSPORT", "true")
|
|
||||||
managed.InitManagedTransport(logr.Discard())
|
managed.InitManagedTransport(logr.Discard())
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
@ -459,20 +438,11 @@ func Test_ManagedSSH_HostKeyAlgos(t *testing.T) {
|
||||||
kp, err := ssh.GenerateKeyPair(ssh.ED25519)
|
kp, err := ssh.GenerateKeyPair(ssh.ED25519)
|
||||||
g.Expect(err).ToNot(HaveOccurred())
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
// secret := corev1.Secret{
|
|
||||||
// Data: map[string][]byte{
|
|
||||||
// "identity": kp.PrivateKey,
|
|
||||||
// "known_hosts": knownHosts,
|
|
||||||
// },
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// authOpts, err := git.AuthOptionsFromSecret(repoURL, &secret)
|
|
||||||
// g.Expect(err).ToNot(HaveOccurred())
|
|
||||||
authOpts := &git.AuthOptions{
|
authOpts := &git.AuthOptions{
|
||||||
Identity: kp.PrivateKey,
|
Identity: kp.PrivateKey,
|
||||||
KnownHosts: knownHosts,
|
KnownHosts: knownHosts,
|
||||||
}
|
}
|
||||||
authOpts.TransportAuthID = "ssh://" + getTransportAuthID()
|
authOpts.TransportOptionsURL = getTransportOptionsURL(git.SSH)
|
||||||
|
|
||||||
// Prepare for checkout.
|
// Prepare for checkout.
|
||||||
branchCheckoutStrat := &CheckoutBranch{Branch: git.DefaultBranch}
|
branchCheckoutStrat := &CheckoutBranch{Branch: git.DefaultBranch}
|
||||||
|
@ -488,11 +458,161 @@ func Test_ManagedSSH_HostKeyAlgos(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTransportAuthID() string {
|
func Test_ManagedHTTPCheckout(t *testing.T) {
|
||||||
|
g := NewWithT(t)
|
||||||
|
|
||||||
|
timeout := 5 * time.Second
|
||||||
|
server, err := gittestserver.NewTempGitServer()
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
defer os.RemoveAll(server.Root())
|
||||||
|
|
||||||
|
user := "test-user"
|
||||||
|
pwd := "test-pswd"
|
||||||
|
server.Auth(user, pwd)
|
||||||
|
|
||||||
|
err = server.StartHTTP()
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
defer server.StopHTTP()
|
||||||
|
|
||||||
|
// Force managed transport to be enabled
|
||||||
|
managed.InitManagedTransport(logr.Discard())
|
||||||
|
|
||||||
|
repoPath := "test.git"
|
||||||
|
err = server.InitRepo("../testdata/git/repo", git.DefaultBranch, repoPath)
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
authOpts := &git.AuthOptions{
|
||||||
|
Username: "test-user",
|
||||||
|
Password: "test-pswd",
|
||||||
|
}
|
||||||
|
authOpts.TransportOptionsURL = getTransportOptionsURL(git.HTTP)
|
||||||
|
|
||||||
|
// Prepare for checkout.
|
||||||
|
branchCheckoutStrat := &CheckoutBranch{Branch: git.DefaultBranch}
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
repoURL := server.HTTPAddress() + "/" + repoPath
|
||||||
|
// Checkout the repo.
|
||||||
|
_, err = branchCheckoutStrat.Checkout(ctx, tmpDir, repoURL, authOpts)
|
||||||
|
g.Expect(err).Error().ShouldNot(HaveOccurred())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestManagedCheckoutBranch_Checkout(t *testing.T) {
|
||||||
|
managed.InitManagedTransport(logr.Discard())
|
||||||
|
g := NewWithT(t)
|
||||||
|
|
||||||
|
timeout := 5 * time.Second
|
||||||
|
server, err := gittestserver.NewTempGitServer()
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
defer os.RemoveAll(server.Root())
|
||||||
|
|
||||||
|
err = server.StartHTTP()
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
defer server.StopHTTP()
|
||||||
|
|
||||||
|
repoPath := "test.git"
|
||||||
|
err = server.InitRepo("../testdata/git/repo", git.DefaultBranch, repoPath)
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
repo, err := git2go.OpenRepository(filepath.Join(server.Root(), repoPath))
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
branchRef, err := repo.References.Lookup(fmt.Sprintf("refs/heads/%s", git.DefaultBranch))
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
defer branchRef.Free()
|
||||||
|
|
||||||
|
commit, err := repo.LookupCommit(branchRef.Target())
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
authOpts := &git.AuthOptions{
|
||||||
|
TransportOptionsURL: getTransportOptionsURL(git.HTTP),
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
repoURL := server.HTTPAddress() + "/" + repoPath
|
||||||
|
branch := CheckoutBranch{
|
||||||
|
Branch: git.DefaultBranch,
|
||||||
|
// Set last revision to HEAD commit, to force a no-op clone.
|
||||||
|
LastRevision: fmt.Sprintf("%s/%s", git.DefaultBranch, commit.Id().String()),
|
||||||
|
}
|
||||||
|
|
||||||
|
cc, err := branch.Checkout(ctx, tmpDir, repoURL, authOpts)
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
g.Expect(cc.String()).To(Equal(git.DefaultBranch + "/" + commit.Id().String()))
|
||||||
|
g.Expect(git.IsConcreteCommit(*cc)).To(Equal(false))
|
||||||
|
|
||||||
|
// Set last revision to a fake commit to force a full clone.
|
||||||
|
branch.LastRevision = fmt.Sprintf("%s/non-existent-commit", git.DefaultBranch)
|
||||||
|
cc, err = branch.Checkout(ctx, tmpDir, repoURL, authOpts)
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
g.Expect(cc.String()).To(Equal(git.DefaultBranch + "/" + commit.Id().String()))
|
||||||
|
g.Expect(git.IsConcreteCommit(*cc)).To(Equal(true))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestManagedCheckoutTag_Checkout(t *testing.T) {
|
||||||
|
managed.InitManagedTransport(logr.Discard())
|
||||||
|
g := NewWithT(t)
|
||||||
|
|
||||||
|
timeout := 5 * time.Second
|
||||||
|
server, err := gittestserver.NewTempGitServer()
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
defer os.RemoveAll(server.Root())
|
||||||
|
|
||||||
|
err = server.StartHTTP()
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
defer server.StopHTTP()
|
||||||
|
|
||||||
|
repoPath := "test.git"
|
||||||
|
err = server.InitRepo("../testdata/git/repo", git.DefaultBranch, repoPath)
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
repo, err := git2go.OpenRepository(filepath.Join(server.Root(), repoPath))
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
branchRef, err := repo.References.Lookup(fmt.Sprintf("refs/heads/%s", git.DefaultBranch))
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
defer branchRef.Free()
|
||||||
|
|
||||||
|
commit, err := repo.LookupCommit(branchRef.Target())
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
_, err = tag(repo, commit.Id(), false, "tag-1", time.Now())
|
||||||
|
|
||||||
|
checkoutTag := CheckoutTag{
|
||||||
|
Tag: "tag-1",
|
||||||
|
}
|
||||||
|
authOpts := &git.AuthOptions{
|
||||||
|
TransportOptionsURL: getTransportOptionsURL(git.HTTP),
|
||||||
|
}
|
||||||
|
repoURL := server.HTTPAddress() + "/" + repoPath
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
cc, err := checkoutTag.Checkout(ctx, tmpDir, repoURL, authOpts)
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
g.Expect(cc.String()).To(Equal("tag-1" + "/" + commit.Id().String()))
|
||||||
|
g.Expect(git.IsConcreteCommit(*cc)).To(Equal(true))
|
||||||
|
|
||||||
|
checkoutTag.LastRevision = "tag-1" + "/" + commit.Id().String()
|
||||||
|
cc, err = checkoutTag.Checkout(ctx, tmpDir, repoURL, authOpts)
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
g.Expect(cc.String()).To(Equal("tag-1" + "/" + commit.Id().String()))
|
||||||
|
g.Expect(git.IsConcreteCommit(*cc)).To(Equal(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTransportOptionsURL(transport git.TransportType) string {
|
||||||
letterRunes := []rune("abcdefghijklmnopqrstuvwxyz1234567890")
|
letterRunes := []rune("abcdefghijklmnopqrstuvwxyz1234567890")
|
||||||
b := make([]rune, 10)
|
b := make([]rune, 10)
|
||||||
for i := range b {
|
for i := range b {
|
||||||
b[i] = letterRunes[rand.Intn(len(letterRunes))]
|
b[i] = letterRunes[rand.Intn(len(letterRunes))]
|
||||||
}
|
}
|
||||||
return string(b)
|
return string(transport) + "://" + string(b)
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,27 +17,16 @@ limitations under the License.
|
||||||
package libgit2
|
package libgit2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"crypto/hmac"
|
|
||||||
"crypto/md5"
|
|
||||||
"crypto/sha1"
|
|
||||||
"crypto/sha256"
|
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/base64"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"hash"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
git2go "github.com/libgit2/git2go/v33"
|
git2go "github.com/libgit2/git2go/v33"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
"golang.org/x/crypto/ssh/knownhosts"
|
|
||||||
|
|
||||||
"github.com/fluxcd/source-controller/pkg/git"
|
"github.com/fluxcd/source-controller/pkg/git"
|
||||||
|
"github.com/fluxcd/source-controller/pkg/git/libgit2/managed"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -148,7 +137,7 @@ func certificateCallback(opts *git.AuthOptions) git2go.CertificateCheckCallback
|
||||||
}
|
}
|
||||||
case git.SSH:
|
case git.SSH:
|
||||||
if len(opts.KnownHosts) > 0 && opts.Host != "" {
|
if len(opts.KnownHosts) > 0 && opts.Host != "" {
|
||||||
return knownHostsCallback(opts.Host, opts.KnownHosts)
|
return managed.KnownHostsCallback(opts.Host, opts.KnownHosts)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -174,157 +163,3 @@ func x509Callback(caBundle []byte) git2go.CertificateCheckCallback {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// knownHostCallback returns a CertificateCheckCallback that verifies
|
|
||||||
// the key of Git server against the given host and known_hosts for
|
|
||||||
// git.SSH Transports.
|
|
||||||
func knownHostsCallback(host string, knownHosts []byte) git2go.CertificateCheckCallback {
|
|
||||||
return func(cert *git2go.Certificate, valid bool, hostname string) error {
|
|
||||||
kh, err := parseKnownHosts(string(knownHosts))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to parse known_hosts: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// First, attempt to split the configured host and port to validate
|
|
||||||
// the port-less hostname given to the callback.
|
|
||||||
hostWithoutPort, _, err := net.SplitHostPort(host)
|
|
||||||
if err != nil {
|
|
||||||
// SplitHostPort returns an error if the host is missing
|
|
||||||
// a port, assume the host has no port.
|
|
||||||
hostWithoutPort = host
|
|
||||||
}
|
|
||||||
|
|
||||||
// Different versions of libgit handle this differently.
|
|
||||||
// This fixes the case in which ports may be sent back.
|
|
||||||
hostnameWithoutPort, _, err := net.SplitHostPort(hostname)
|
|
||||||
if err != nil {
|
|
||||||
hostnameWithoutPort = hostname
|
|
||||||
}
|
|
||||||
|
|
||||||
if hostnameWithoutPort != hostWithoutPort {
|
|
||||||
return fmt.Errorf("host mismatch: %q %q", hostWithoutPort, hostnameWithoutPort)
|
|
||||||
}
|
|
||||||
|
|
||||||
// We are now certain that the configured host and the hostname
|
|
||||||
// given to the callback match. Use the configured host (that
|
|
||||||
// includes the port), and normalize it, so we can check if there
|
|
||||||
// is an entry for the hostname _and_ port.
|
|
||||||
h := knownhosts.Normalize(host)
|
|
||||||
for _, k := range kh {
|
|
||||||
if k.matches(h, cert.Hostkey) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fmt.Errorf("hostkey could not be verified")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type knownKey struct {
|
|
||||||
hosts []string
|
|
||||||
key ssh.PublicKey
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseKnownHosts(s string) ([]knownKey, error) {
|
|
||||||
var knownHosts []knownKey
|
|
||||||
scanner := bufio.NewScanner(strings.NewReader(s))
|
|
||||||
for scanner.Scan() {
|
|
||||||
_, hosts, pubKey, _, _, err := ssh.ParseKnownHosts(scanner.Bytes())
|
|
||||||
if err != nil {
|
|
||||||
// Lines that aren't host public key result in EOF, like a comment
|
|
||||||
// line. Continue parsing the other lines.
|
|
||||||
if err == io.EOF {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return []knownKey{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
knownHost := knownKey{
|
|
||||||
hosts: hosts,
|
|
||||||
key: pubKey,
|
|
||||||
}
|
|
||||||
knownHosts = append(knownHosts, knownHost)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := scanner.Err(); err != nil {
|
|
||||||
return []knownKey{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return knownHosts, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k knownKey) matches(host string, hostkey git2go.HostkeyCertificate) bool {
|
|
||||||
if !containsHost(k.hosts, host) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
var fingerprint []byte
|
|
||||||
var hasher hash.Hash
|
|
||||||
switch {
|
|
||||||
case hostkey.Kind&git2go.HostkeySHA256 > 0:
|
|
||||||
fingerprint = hostkey.HashSHA256[:]
|
|
||||||
hasher = sha256.New()
|
|
||||||
case hostkey.Kind&git2go.HostkeySHA1 > 0:
|
|
||||||
fingerprint = hostkey.HashSHA1[:]
|
|
||||||
hasher = sha1.New()
|
|
||||||
case hostkey.Kind&git2go.HostkeyMD5 > 0:
|
|
||||||
fingerprint = hostkey.HashMD5[:]
|
|
||||||
hasher = md5.New()
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
hasher.Write(k.key.Marshal())
|
|
||||||
return bytes.Equal(hasher.Sum(nil), fingerprint)
|
|
||||||
}
|
|
||||||
|
|
||||||
func containsHost(hosts []string, host string) bool {
|
|
||||||
for _, kh := range hosts {
|
|
||||||
// hashed host must start with a pipe
|
|
||||||
if kh[0] == '|' {
|
|
||||||
match, _ := MatchHashedHost(kh, host)
|
|
||||||
if match {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if kh == host { // unhashed host check
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// MatchHashedHost tries to match a hashed known host (kh) to
|
|
||||||
// host.
|
|
||||||
//
|
|
||||||
// Note that host is not hashed, but it is rather hashed during
|
|
||||||
// the matching process using the same salt used when hashing
|
|
||||||
// the known host.
|
|
||||||
func MatchHashedHost(kh, host string) (bool, error) {
|
|
||||||
if kh == "" || kh[0] != '|' {
|
|
||||||
return false, fmt.Errorf("hashed known host must begin with '|': '%s'", kh)
|
|
||||||
}
|
|
||||||
|
|
||||||
components := strings.Split(kh, "|")
|
|
||||||
if len(components) != 4 {
|
|
||||||
return false, fmt.Errorf("invalid format for hashed known host: '%s'", kh)
|
|
||||||
}
|
|
||||||
|
|
||||||
if components[1] != "1" {
|
|
||||||
return false, fmt.Errorf("unsupported hash type '%s'", components[1])
|
|
||||||
}
|
|
||||||
|
|
||||||
hkSalt, err := base64.StdEncoding.DecodeString(components[2])
|
|
||||||
if err != nil {
|
|
||||||
return false, fmt.Errorf("cannot decode hashed known host: '%w'", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
hkHash, err := base64.StdEncoding.DecodeString(components[3])
|
|
||||||
if err != nil {
|
|
||||||
return false, fmt.Errorf("cannot decode hashed known host: '%w'", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
mac := hmac.New(sha1.New, hkSalt)
|
|
||||||
mac.Write([]byte(host))
|
|
||||||
hostHash := mac.Sum(nil)
|
|
||||||
|
|
||||||
return bytes.Equal(hostHash, hkHash), nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -20,7 +20,6 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/base64"
|
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -205,159 +204,6 @@ func Test_x509Callback(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_knownHostsCallback(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
host string
|
|
||||||
expectedHost string
|
|
||||||
knownHosts []byte
|
|
||||||
hostkey git2go.HostkeyCertificate
|
|
||||||
want error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Match",
|
|
||||||
host: "github.com",
|
|
||||||
knownHosts: []byte(knownHostsFixture),
|
|
||||||
hostkey: git2go.HostkeyCertificate{Kind: git2go.HostkeySHA1 | git2go.HostkeyMD5, HashSHA1: sha1Fingerprint("v2toJdKXfFEaR1u++4iq1UqSrHM")},
|
|
||||||
expectedHost: "github.com",
|
|
||||||
want: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Match with port",
|
|
||||||
host: "github.com",
|
|
||||||
knownHosts: []byte(knownHostsFixture),
|
|
||||||
hostkey: git2go.HostkeyCertificate{Kind: git2go.HostkeySHA1 | git2go.HostkeyMD5, HashSHA1: sha1Fingerprint("v2toJdKXfFEaR1u++4iq1UqSrHM")},
|
|
||||||
expectedHost: "github.com:22",
|
|
||||||
want: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Hostname mismatch",
|
|
||||||
host: "github.com",
|
|
||||||
knownHosts: []byte(knownHostsFixture),
|
|
||||||
hostkey: git2go.HostkeyCertificate{Kind: git2go.HostkeySHA1 | git2go.HostkeyMD5, HashSHA1: sha1Fingerprint("v2toJdKXfFEaR1u++4iq1UqSrHM")},
|
|
||||||
expectedHost: "example.com",
|
|
||||||
want: fmt.Errorf("host mismatch: %q %q", "example.com", "github.com"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Hostkey mismatch",
|
|
||||||
host: "github.com",
|
|
||||||
knownHosts: []byte(knownHostsFixture),
|
|
||||||
hostkey: git2go.HostkeyCertificate{Kind: git2go.HostkeyMD5, HashMD5: md5Fingerprint("\xb6\x03\x0e\x39\x97\x9e\xd0\xe7\x24\xce\xa3\x77\x3e\x01\x42\x09")},
|
|
||||||
expectedHost: "github.com",
|
|
||||||
want: fmt.Errorf("hostkey could not be verified"),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
g := NewWithT(t)
|
|
||||||
|
|
||||||
cert := &git2go.Certificate{Hostkey: tt.hostkey}
|
|
||||||
callback := knownHostsCallback(tt.expectedHost, tt.knownHosts)
|
|
||||||
result := g.Expect(callback(cert, false, tt.host))
|
|
||||||
if tt.want == nil {
|
|
||||||
result.To(BeNil())
|
|
||||||
} else {
|
|
||||||
result.To(Equal(tt.want))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_parseKnownHosts_matches(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
hostkey git2go.HostkeyCertificate
|
|
||||||
wantMatches bool
|
|
||||||
}{
|
|
||||||
{"good sha256 hostkey", git2go.HostkeyCertificate{Kind: git2go.HostkeySHA256 | git2go.HostkeySHA1 | git2go.HostkeyMD5, HashSHA256: sha256Fingerprint("nThbg6kXUpJWGl7E1IGOCspRomTxdCARLviKw6E5SY8")}, true},
|
|
||||||
{"bad sha256 hostkey", git2go.HostkeyCertificate{Kind: git2go.HostkeySHA256 | git2go.HostkeySHA1 | git2go.HostkeyMD5, HashSHA256: sha256Fingerprint("ROQFvPThGrW4RuWLoL9tq9I9zJ42fK4XywyRtbOz/EQ")}, false},
|
|
||||||
{"good sha1 hostkey", git2go.HostkeyCertificate{Kind: git2go.HostkeySHA1 | git2go.HostkeyMD5, HashSHA1: sha1Fingerprint("v2toJdKXfFEaR1u++4iq1UqSrHM")}, true},
|
|
||||||
{"bad sha1 hostkey", git2go.HostkeyCertificate{Kind: git2go.HostkeySHA1 | git2go.HostkeyMD5, HashSHA1: sha1Fingerprint("tfpLlQhDDFP3yGdewTvHNxWmAdk")}, false},
|
|
||||||
{"good md5 hostkey", git2go.HostkeyCertificate{Kind: git2go.HostkeyMD5, HashMD5: md5Fingerprint("\x16\x27\xac\xa5\x76\x28\x2d\x36\x63\x1b\x56\x4d\xeb\xdf\xa6\x48")}, true},
|
|
||||||
{"bad md5 hostkey", git2go.HostkeyCertificate{Kind: git2go.HostkeyMD5, HashMD5: md5Fingerprint("\xb6\x03\x0e\x39\x97\x9e\xd0\xe7\x24\xce\xa3\x77\x3e\x01\x42\x09")}, false},
|
|
||||||
{"invalid hostkey", git2go.HostkeyCertificate{}, false},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
g := NewWithT(t)
|
|
||||||
|
|
||||||
knownKeys, err := parseKnownHosts(knownHostsFixture)
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
matches := knownKeys[0].matches("github.com", tt.hostkey)
|
|
||||||
g.Expect(matches).To(Equal(tt.wantMatches))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_parseKnownHosts(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
fixture string
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "empty file",
|
|
||||||
fixture: "",
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "single host",
|
|
||||||
fixture: `github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==`,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "single host with comment",
|
|
||||||
fixture: `# github.com
|
|
||||||
github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==`,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "multiple hosts with comments",
|
|
||||||
fixture: `# github.com
|
|
||||||
github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==
|
|
||||||
# gitlab.com
|
|
||||||
gitlab.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAfuCHKVTjquxvt6CM6tdG4SLp1Btn/nOeHHE5UOzRdf`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "no host key, only comments",
|
|
||||||
fixture: `# example.com
|
|
||||||
#github.com
|
|
||||||
# gitlab.com`,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid host entry",
|
|
||||||
fixture: `github.com ssh-rsa`,
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid content",
|
|
||||||
fixture: `some random text`,
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid line with valid host key",
|
|
||||||
fixture: `some random text
|
|
||||||
gitlab.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAfuCHKVTjquxvt6CM6tdG4SLp1Btn/nOeHHE5UOzRdf`,
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
g := NewWithT(t)
|
|
||||||
_, err := parseKnownHosts(tt.fixture)
|
|
||||||
if tt.wantErr {
|
|
||||||
g.Expect(err).To(HaveOccurred())
|
|
||||||
} else {
|
|
||||||
g.Expect(err).ToNot(HaveOccurred())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_transferProgressCallback(t *testing.T) {
|
func Test_transferProgressCallback(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
@ -522,94 +368,6 @@ func Test_pushTransferProgressCallback(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMatchHashedHost(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
knownHost string
|
|
||||||
host string
|
|
||||||
match bool
|
|
||||||
wantErr string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "match valid known host",
|
|
||||||
knownHost: "|1|vApZG0Ybr4rHfTb69+cjjFIGIv0=|M5sSXen14encOvQAy0gseRahnJw=",
|
|
||||||
host: "[127.0.0.1]:44167",
|
|
||||||
match: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty known host errors",
|
|
||||||
wantErr: "hashed known host must begin with '|'",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "unhashed known host errors",
|
|
||||||
knownHost: "[127.0.0.1]:44167",
|
|
||||||
wantErr: "hashed known host must begin with '|'",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid known host format errors",
|
|
||||||
knownHost: "|1M5sSXen14encOvQAy0gseRahnJw=",
|
|
||||||
wantErr: "invalid format for hashed known host",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid hash type errors",
|
|
||||||
knownHost: "|2|vApZG0Ybr4rHfTb69+cjjFIGIv0=|M5sSXen14encOvQAy0gseRahnJw=",
|
|
||||||
wantErr: "unsupported hash type",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid base64 component[2] errors",
|
|
||||||
knownHost: "|1|azz|M5sSXen14encOvQAy0gseRahnJw=",
|
|
||||||
wantErr: "cannot decode hashed known host",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid base64 component[3] errors",
|
|
||||||
knownHost: "|1|M5sSXen14encOvQAy0gseRahnJw=|azz",
|
|
||||||
wantErr: "cannot decode hashed known host",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
g := NewWithT(t)
|
|
||||||
|
|
||||||
matched, err := MatchHashedHost(tt.knownHost, tt.host)
|
|
||||||
|
|
||||||
if tt.wantErr == "" {
|
|
||||||
g.Expect(err).NotTo(HaveOccurred())
|
|
||||||
g.Expect(matched).To(Equal(tt.match))
|
|
||||||
} else {
|
|
||||||
g.Expect(err).To(HaveOccurred())
|
|
||||||
g.Expect(err.Error()).To(ContainSubstring(tt.wantErr))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func md5Fingerprint(in string) [16]byte {
|
|
||||||
var out [16]byte
|
|
||||||
copy(out[:], in)
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func sha1Fingerprint(in string) [20]byte {
|
|
||||||
d, err := base64.RawStdEncoding.DecodeString(in)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
var out [20]byte
|
|
||||||
copy(out[:], d)
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func sha256Fingerprint(in string) [32]byte {
|
|
||||||
d, err := base64.RawStdEncoding.DecodeString(in)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
var out [32]byte
|
|
||||||
copy(out[:], d)
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func certificateFromPEM(pemBytes string) (*x509.Certificate, error) {
|
func certificateFromPEM(pemBytes string) (*x509.Certificate, error) {
|
||||||
block, _ := pem.Decode([]byte(pemBytes))
|
block, _ := pem.Decode([]byte(pemBytes))
|
||||||
if block == nil {
|
if block == nil {
|
||||||
|
|
|
@ -72,11 +72,16 @@ type AuthOptions struct {
|
||||||
Identity []byte
|
Identity []byte
|
||||||
KnownHosts []byte
|
KnownHosts []byte
|
||||||
CAFile []byte
|
CAFile []byte
|
||||||
// TransportAuthID is a unique identifier for this set of authentication
|
// TransportOptionsURL is a unique identifier for this set of authentication
|
||||||
// options. It's used by managed libgit2 transports to uniquely identify
|
// options. It's used by managed libgit2 transports to uniquely identify
|
||||||
// which credentials to use for a particular git operation, and avoid misuse
|
// which credentials to use for a particular Git operation, and avoid misuse
|
||||||
// of credentials in a multi tenant environment.
|
// of credentials in a multi-tenant environment.
|
||||||
TransportAuthID string
|
// It must be prefixed with a valid transport protocol ("ssh:// "or "http://") because
|
||||||
|
// of the way managed transports are registered and invoked.
|
||||||
|
// It's a field of AuthOptions despite not providing any kind of authentication
|
||||||
|
// info, as it's the only way to sneak it into git.Checkout, without polluting
|
||||||
|
// it's args and keeping it generic.
|
||||||
|
TransportOptionsURL string
|
||||||
}
|
}
|
||||||
|
|
||||||
// KexAlgos hosts the key exchange algorithms to be used for SSH connections.
|
// KexAlgos hosts the key exchange algorithms to be used for SSH connections.
|
||||||
|
|
Loading…
Reference in New Issue