gitrepo: refactor reconciler to use fluxcd/pkg/git
Signed-off-by: Sanskar Jaiswal <jaiswalsanskar078@gmail.com>
This commit is contained in:
parent
a9a85b2b0f
commit
b6d6b593c8
|
@ -20,6 +20,7 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
@ -41,6 +42,9 @@ import (
|
|||
"sigs.k8s.io/controller-runtime/pkg/ratelimiter"
|
||||
|
||||
"github.com/fluxcd/pkg/apis/meta"
|
||||
"github.com/fluxcd/pkg/git"
|
||||
"github.com/fluxcd/pkg/git/gogit"
|
||||
"github.com/fluxcd/pkg/git/libgit2"
|
||||
"github.com/fluxcd/pkg/runtime/conditions"
|
||||
helper "github.com/fluxcd/pkg/runtime/controller"
|
||||
"github.com/fluxcd/pkg/runtime/events"
|
||||
|
@ -54,8 +58,6 @@ import (
|
|||
sreconcile "github.com/fluxcd/source-controller/internal/reconcile"
|
||||
"github.com/fluxcd/source-controller/internal/reconcile/summarize"
|
||||
"github.com/fluxcd/source-controller/internal/util"
|
||||
"github.com/fluxcd/source-controller/pkg/git"
|
||||
"github.com/fluxcd/source-controller/pkg/git/strategy"
|
||||
)
|
||||
|
||||
// gitRepositoryReadyCondition contains the information required to summarize a
|
||||
|
@ -440,9 +442,7 @@ func (r *GitRepositoryReconciler) reconcileSource(ctx context.Context,
|
|||
conditions.Delete(obj, sourcev1.SourceVerifiedCondition)
|
||||
}
|
||||
|
||||
// Configure authentication strategy to access the source
|
||||
var authOpts *git.AuthOptions
|
||||
var err error
|
||||
var data map[string][]byte
|
||||
if obj.Spec.SecretRef != nil {
|
||||
// Attempt to retrieve secret
|
||||
name := types.NamespacedName{
|
||||
|
@ -459,12 +459,29 @@ func (r *GitRepositoryReconciler) reconcileSource(ctx context.Context,
|
|||
// Return error as the world as observed may change
|
||||
return sreconcile.ResultEmpty, e
|
||||
}
|
||||
data = secret.Data
|
||||
}
|
||||
|
||||
// Configure strategy with secret
|
||||
authOpts, err = git.AuthOptionsFromSecret(obj.Spec.URL, &secret)
|
||||
} else {
|
||||
// Set the minimal auth options for valid transport.
|
||||
authOpts, err = git.AuthOptionsWithoutSecret(obj.Spec.URL)
|
||||
u, err := url.Parse(obj.Spec.URL)
|
||||
if err != nil {
|
||||
e := serror.NewStalling(
|
||||
fmt.Errorf("failed to parse url '%s': %w", obj.Spec.URL, err),
|
||||
sourcev1.URLInvalidReason,
|
||||
)
|
||||
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
|
||||
return sreconcile.ResultEmpty, e
|
||||
}
|
||||
|
||||
// Configure authentication strategy to access the source
|
||||
authOpts, err := git.NewAuthOptions(*u, data)
|
||||
|
||||
if err != nil {
|
||||
e := serror.NewGeneric(
|
||||
fmt.Errorf("failed to configure authentication options: %w", err),
|
||||
sourcev1.AuthenticationFailedReason,
|
||||
)
|
||||
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
|
||||
return sreconcile.ResultEmpty, e
|
||||
}
|
||||
if err != nil {
|
||||
e := serror.NewGeneric(
|
||||
|
@ -725,12 +742,15 @@ func (r *GitRepositoryReconciler) reconcileInclude(ctx context.Context,
|
|||
func (r *GitRepositoryReconciler) gitCheckout(ctx context.Context,
|
||||
obj *sourcev1.GitRepository, authOpts *git.AuthOptions, dir string, optimized bool) (*git.Commit, error) {
|
||||
// Configure checkout strategy.
|
||||
checkoutOpts := git.CheckoutOptions{RecurseSubmodules: obj.Spec.RecurseSubmodules}
|
||||
cloneOpts := git.CloneOptions{
|
||||
RecurseSubmodules: obj.Spec.RecurseSubmodules,
|
||||
ShallowClone: true,
|
||||
}
|
||||
if ref := obj.Spec.Reference; ref != nil {
|
||||
checkoutOpts.Branch = ref.Branch
|
||||
checkoutOpts.Commit = ref.Commit
|
||||
checkoutOpts.Tag = ref.Tag
|
||||
checkoutOpts.SemVer = ref.SemVer
|
||||
cloneOpts.Branch = ref.Branch
|
||||
cloneOpts.Commit = ref.Commit
|
||||
cloneOpts.Tag = ref.Tag
|
||||
cloneOpts.SemVer = ref.SemVer
|
||||
}
|
||||
|
||||
// Only if the object has an existing artifact in storage, attempt to
|
||||
|
@ -738,46 +758,33 @@ func (r *GitRepositoryReconciler) gitCheckout(ctx context.Context,
|
|||
// that the artifact exists.
|
||||
if optimized && conditions.IsTrue(obj, sourcev1.ArtifactInStorageCondition) {
|
||||
if artifact := obj.GetArtifact(); artifact != nil {
|
||||
checkoutOpts.LastRevision = artifact.Revision
|
||||
cloneOpts.LastObservedCommit = artifact.Revision
|
||||
}
|
||||
}
|
||||
|
||||
gitCtx, cancel := context.WithTimeout(ctx, obj.Spec.Timeout.Duration)
|
||||
defer cancel()
|
||||
|
||||
checkoutStrategy, err := strategy.CheckoutStrategyForImplementation(gitCtx,
|
||||
git.Implementation(obj.Spec.GitImplementation), checkoutOpts)
|
||||
var gitReader git.RepositoryReader
|
||||
var err error
|
||||
|
||||
if obj.Spec.GitImplementation == libgit2.ClientName {
|
||||
gitReader, err = libgit2.NewClient(dir, authOpts)
|
||||
} else {
|
||||
gitReader, err = gogit.NewClient(dir, authOpts)
|
||||
}
|
||||
if err != nil {
|
||||
// Do not return err as recovery without changes is impossible.
|
||||
e := &serror.Stalling{
|
||||
Err: fmt.Errorf("failed to configure checkout strategy for Git implementation '%s': %w", obj.Spec.GitImplementation, err),
|
||||
Err: fmt.Errorf("failed to create Git client for implementation '%s': %w", obj.Spec.GitImplementation, err),
|
||||
Reason: sourcev1.GitOperationFailedReason,
|
||||
}
|
||||
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
|
||||
return nil, e
|
||||
}
|
||||
defer gitReader.Close()
|
||||
|
||||
// this is needed only for libgit2, due to managed transport.
|
||||
if obj.Spec.GitImplementation == sourcev1.LibGit2Implementation {
|
||||
// We set the TransportOptionsURL of this set of authentication options here by constructing
|
||||
// a unique URL that won't clash in a multi tenant environment. This unique URL 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 '%s' has invalid transport type, supported types are: http, https, ssh", obj.Spec.URL),
|
||||
Reason: sourcev1.URLInvalidReason,
|
||||
}
|
||||
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
|
||||
return nil, e
|
||||
}
|
||||
}
|
||||
|
||||
commit, err := checkoutStrategy.Checkout(gitCtx, dir, obj.Spec.URL, authOpts)
|
||||
commit, err := gitReader.Clone(gitCtx, obj.Spec.URL, cloneOpts)
|
||||
if err != nil {
|
||||
e := serror.NewGeneric(
|
||||
fmt.Errorf("failed to checkout and determine revision: %w", err),
|
||||
|
@ -786,6 +793,7 @@ func (r *GitRepositoryReconciler) gitCheckout(ctx context.Context,
|
|||
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
|
||||
return nil, e
|
||||
}
|
||||
|
||||
return commit, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -56,13 +56,13 @@ import (
|
|||
"github.com/fluxcd/pkg/ssh"
|
||||
"github.com/fluxcd/pkg/testserver"
|
||||
|
||||
"github.com/fluxcd/pkg/git"
|
||||
"github.com/fluxcd/pkg/git/libgit2/transport"
|
||||
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
|
||||
serror "github.com/fluxcd/source-controller/internal/error"
|
||||
"github.com/fluxcd/source-controller/internal/features"
|
||||
sreconcile "github.com/fluxcd/source-controller/internal/reconcile"
|
||||
"github.com/fluxcd/source-controller/internal/reconcile/summarize"
|
||||
"github.com/fluxcd/source-controller/pkg/git"
|
||||
"github.com/fluxcd/source-controller/pkg/git/libgit2/managed"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -502,7 +502,7 @@ func TestGitRepositoryReconciler_reconcileSource_authStrategy(t *testing.T) {
|
|||
EventRecorder: record.NewFakeRecorder(32),
|
||||
Storage: testStorage,
|
||||
features: features.FeatureGates(),
|
||||
Libgit2TransportInitialized: managed.Enabled,
|
||||
Libgit2TransportInitialized: transport.Enabled,
|
||||
}
|
||||
|
||||
for _, i := range testGitImplementations {
|
||||
|
@ -731,7 +731,7 @@ func TestGitRepositoryReconciler_reconcileSource_checkoutStrategy(t *testing.T)
|
|||
EventRecorder: record.NewFakeRecorder(32),
|
||||
Storage: testStorage,
|
||||
features: features.FeatureGates(),
|
||||
Libgit2TransportInitialized: managed.Enabled,
|
||||
Libgit2TransportInitialized: transport.Enabled,
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
@ -1404,7 +1404,7 @@ func TestGitRepositoryReconciler_verifyCommitSignature(t *testing.T) {
|
|||
},
|
||||
wantErr: true,
|
||||
assertConditions: []metav1.Condition{
|
||||
*conditions.FalseCondition(sourcev1.SourceVerifiedCondition, "InvalidCommitSignature", "signature verification of commit 'shasum' failed: failed to verify commit with any of the given key rings"),
|
||||
*conditions.FalseCondition(sourcev1.SourceVerifiedCondition, "InvalidCommitSignature", "signature verification of commit 'shasum' failed: unable to verify commit with any of the given key rings"),
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -1599,7 +1599,7 @@ func TestGitRepositoryReconciler_ConditionsUpdate(t *testing.T) {
|
|||
EventRecorder: record.NewFakeRecorder(32),
|
||||
Storage: testStorage,
|
||||
features: features.FeatureGates(),
|
||||
Libgit2TransportInitialized: managed.Enabled,
|
||||
Libgit2TransportInitialized: transport.Enabled,
|
||||
}
|
||||
|
||||
key := client.ObjectKeyFromObject(obj)
|
||||
|
|
|
@ -57,6 +57,7 @@ import (
|
|||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
|
||||
"github.com/fluxcd/pkg/apis/meta"
|
||||
"github.com/fluxcd/pkg/git"
|
||||
"github.com/fluxcd/pkg/oci"
|
||||
"github.com/fluxcd/pkg/runtime/conditions"
|
||||
conditionscheck "github.com/fluxcd/pkg/runtime/conditions/check"
|
||||
|
@ -66,7 +67,6 @@ import (
|
|||
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
|
||||
serror "github.com/fluxcd/source-controller/internal/error"
|
||||
sreconcile "github.com/fluxcd/source-controller/internal/reconcile"
|
||||
"github.com/fluxcd/source-controller/pkg/git"
|
||||
)
|
||||
|
||||
func TestOCIRepository_Reconcile(t *testing.T) {
|
||||
|
|
|
@ -37,6 +37,7 @@ import (
|
|||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
|
||||
dcontext "github.com/distribution/distribution/v3/context"
|
||||
"github.com/fluxcd/pkg/git/libgit2/transport"
|
||||
"github.com/fluxcd/pkg/runtime/controller"
|
||||
"github.com/fluxcd/pkg/runtime/testenv"
|
||||
"github.com/fluxcd/pkg/testserver"
|
||||
|
@ -53,7 +54,6 @@ import (
|
|||
"github.com/fluxcd/source-controller/internal/cache"
|
||||
"github.com/fluxcd/source-controller/internal/features"
|
||||
"github.com/fluxcd/source-controller/internal/helm/registry"
|
||||
"github.com/fluxcd/source-controller/pkg/git/libgit2/managed"
|
||||
// +kubebuilder:scaffold:imports
|
||||
)
|
||||
|
||||
|
@ -237,7 +237,7 @@ func TestMain(m *testing.M) {
|
|||
panic(fmt.Sprintf("Failed to create a test registry server: %v", err))
|
||||
}
|
||||
|
||||
if err = managed.InitManagedTransport(); err != nil {
|
||||
if err = transport.InitManagedTransport(); err != nil {
|
||||
panic(fmt.Sprintf("Failed to initialize libgit2 managed transport: %v", err))
|
||||
}
|
||||
|
||||
|
@ -247,7 +247,7 @@ func TestMain(m *testing.M) {
|
|||
Metrics: testMetricsH,
|
||||
Storage: testStorage,
|
||||
features: features.FeatureGates(),
|
||||
Libgit2TransportInitialized: managed.Enabled,
|
||||
Libgit2TransportInitialized: transport.Enabled,
|
||||
}).SetupWithManager(testEnv); err != nil {
|
||||
panic(fmt.Sprintf("Failed to start GitRepositoryReconciler: %v", err))
|
||||
}
|
||||
|
|
24
go.mod
24
go.mod
|
@ -12,6 +12,9 @@ replace github.com/fluxcd/source-controller/api => ./api
|
|||
// - libgit2/git2go#918.
|
||||
replace github.com/libgit2/git2go/v33 => github.com/fluxcd/git2go/v33 v33.0.9-flux
|
||||
|
||||
// Fix CVE-2022-1996 (for v2, Go Modules incompatible)
|
||||
replace github.com/emicklei/go-restful => github.com/emicklei/go-restful v2.16.0+incompatible
|
||||
|
||||
require (
|
||||
cloud.google.com/go/storage v1.27.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.3
|
||||
|
@ -22,16 +25,18 @@ require (
|
|||
// maintained by the ProtonMail team to continue to support the openpgp
|
||||
// module, after the Go team decided to no longer maintain it.
|
||||
// When in doubt (and not using openpgp), use /x/crypto.
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20220930113650-c6815a8c17ad
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20220930113650-c6815a8c17ad // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.2.3
|
||||
github.com/distribution/distribution/v3 v3.0.0-20221019080424-fb2188868d77
|
||||
github.com/docker/cli v20.10.20+incompatible
|
||||
github.com/docker/go-units v0.5.0
|
||||
github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819
|
||||
github.com/fluxcd/gitkit v0.6.0
|
||||
github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819 // indirect
|
||||
github.com/fluxcd/gitkit v0.6.0 // indirect
|
||||
github.com/fluxcd/pkg/apis/meta v0.17.0
|
||||
github.com/fluxcd/pkg/gittestserver v0.7.0
|
||||
github.com/fluxcd/pkg/gitutil v0.2.0
|
||||
github.com/fluxcd/pkg/git v0.6.1
|
||||
github.com/fluxcd/pkg/git/gogit v0.1.1-0.20220902101857-4d204a4a6fa4
|
||||
github.com/fluxcd/pkg/git/libgit2 v0.1.1-0.20220927151444-1d5a7b25a55f
|
||||
github.com/fluxcd/pkg/gitutil v0.2.0 // indirect
|
||||
github.com/fluxcd/pkg/helmtestserver v0.9.0
|
||||
github.com/fluxcd/pkg/lockedfile v0.1.0
|
||||
github.com/fluxcd/pkg/masktoken v0.2.0
|
||||
|
@ -60,7 +65,7 @@ require (
|
|||
github.com/sirupsen/logrus v1.9.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
golang.org/x/crypto v0.1.0
|
||||
golang.org/x/net v0.1.0
|
||||
golang.org/x/net v0.1.0 // indirect
|
||||
golang.org/x/sync v0.1.0
|
||||
google.golang.org/api v0.100.0
|
||||
gotest.tools v2.2.0+incompatible
|
||||
|
@ -74,11 +79,7 @@ require (
|
|||
sigs.k8s.io/yaml v1.3.0
|
||||
)
|
||||
|
||||
// Fix CVE-2022-32149
|
||||
replace golang.org/x/text => golang.org/x/text v0.4.0
|
||||
|
||||
// Fix CVE-2022-1996 (for v2, Go Modules incompatible)
|
||||
replace github.com/emicklei/go-restful => github.com/emicklei/go-restful v2.16.0+incompatible
|
||||
require github.com/fluxcd/pkg/gittestserver v0.7.0
|
||||
|
||||
require (
|
||||
bitbucket.org/creachadair/shell v0.0.7 // indirect
|
||||
|
@ -179,6 +180,7 @@ require (
|
|||
github.com/fatih/color v1.13.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.3 // indirect
|
||||
github.com/fluxcd/pkg/apis/acl v0.1.0 // indirect
|
||||
github.com/fluxcd/pkg/http/transport v0.0.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.5.4 // indirect
|
||||
github.com/fullstorydev/grpcurl v1.8.7 // indirect
|
||||
github.com/go-chi/chi v4.1.2+incompatible // indirect
|
||||
|
|
24
go.sum
24
go.sum
|
@ -461,7 +461,6 @@ github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFP
|
|||
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
|
||||
github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819 h1:RIB4cRk+lBqKK3Oy0r2gRX4ui7tuhiZq2SuTtTCi0/0=
|
||||
github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
|
||||
github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2 h1:dWB6v3RcOy03t/bUadywsbyrQwCqZeNIEX6M1OtSZOM=
|
||||
github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8=
|
||||
github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE=
|
||||
github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
|
@ -510,12 +509,20 @@ github.com/fluxcd/pkg/apis/acl v0.1.0 h1:EoAl377hDQYL3WqanWCdifauXqXbMyFuK82NnX6
|
|||
github.com/fluxcd/pkg/apis/acl v0.1.0/go.mod h1:zfEZzz169Oap034EsDhmCAGgnWlcWmIObZjYMusoXS8=
|
||||
github.com/fluxcd/pkg/apis/meta v0.17.0 h1:Y2dfo1syHZDb9Mexjr2SWdcj1FnxnRXm015hEnhl6wU=
|
||||
github.com/fluxcd/pkg/apis/meta v0.17.0/go.mod h1:GrOVzWXiu22XjLNgLLe2EBYhQPqZetes5SIADb4bmHE=
|
||||
github.com/fluxcd/pkg/git v0.6.1 h1:LC5k/5QBgDNoaDMb6ukmKNcxLih/Se09m1x5vLfUZb8=
|
||||
github.com/fluxcd/pkg/git v0.6.1/go.mod h1:O1YYuMUr5z8gHZrB3xBIMFyOdcCXG7kHUAuAqu6UkeA=
|
||||
github.com/fluxcd/pkg/git/gogit v0.1.1-0.20220902101857-4d204a4a6fa4 h1:qSo0LB4lSs+dNf7YLXsK+DRF8Dp6wdTSKHWccYHm+1Y=
|
||||
github.com/fluxcd/pkg/git/gogit v0.1.1-0.20220902101857-4d204a4a6fa4/go.mod h1:+0MYx3JTLAb62ZzBnoXU5RNnhjrD1knrQ3F/qzPh9Ds=
|
||||
github.com/fluxcd/pkg/git/libgit2 v0.1.1-0.20220927151444-1d5a7b25a55f h1:1q0xHEqxWX0weTui4QBXnwt3L365//hMoCWM0/Ipzls=
|
||||
github.com/fluxcd/pkg/git/libgit2 v0.1.1-0.20220927151444-1d5a7b25a55f/go.mod h1:6/jTPTTWZO0D3+NKWErastWxHBY0YPi0viEZzyUDoXc=
|
||||
github.com/fluxcd/pkg/gittestserver v0.7.0 h1:PRVaEjeC/ePKTusB5Bx/ExM0P6bjroPdG6K2DO7YJUM=
|
||||
github.com/fluxcd/pkg/gittestserver v0.7.0/go.mod h1:WHqqZQfdePi5M/s1ONMTB4MigktqJhzAFJOZ0KTBw9Y=
|
||||
github.com/fluxcd/pkg/gitutil v0.2.0 h1:7vvXfq+Ur1/WXEejXY/b2haJ/2Uj5Et5v4V33l+ni1Q=
|
||||
github.com/fluxcd/pkg/gitutil v0.2.0/go.mod h1:oOq6wzzTJmD/PPIM5GHj+PGtfbrL7cbQKZCDnVvyp+w=
|
||||
github.com/fluxcd/pkg/helmtestserver v0.9.0 h1:C7RM+q0C78P0xBxi/IrFqW+axMNKFsJRuO1KmVx6ClQ=
|
||||
github.com/fluxcd/pkg/helmtestserver v0.9.0/go.mod h1:A9IC8Yq+valW7CuTGmxYptncmR/5wAb8l3oiQhOrTdY=
|
||||
github.com/fluxcd/pkg/http/transport v0.0.1 h1:2iB63xfOOgkH+gdKC5qfYV1TcL546JKOE/7ZZ86hRoc=
|
||||
github.com/fluxcd/pkg/http/transport v0.0.1/go.mod h1:aDIYfECLVh3KTvM8HvNcpm2ESrVbhteJWEl0AFbcjJk=
|
||||
github.com/fluxcd/pkg/lockedfile v0.1.0 h1:YsYFAkd6wawMCcD74ikadAKXA4s2sukdxrn7w8RB5eo=
|
||||
github.com/fluxcd/pkg/lockedfile v0.1.0/go.mod h1:EJLan8t9MiOcgTs8+puDjbE6I/KAfHbdvIy9VUgIjm8=
|
||||
github.com/fluxcd/pkg/masktoken v0.2.0 h1:HoSPTk4l1fz5Fevs2vVRvZGru33blfMwWSZKsHdfG/0=
|
||||
|
@ -1556,7 +1563,6 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
|
|||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yvasiyarov/go-metrics v0.0.0-20150112132944-c25f46c4b940 h1:p7OofyZ509h8DmPLh8Hn+EIIZm/xYhdZHJ9GnXHdr6U=
|
||||
github.com/yvasiyarov/go-metrics v0.0.0-20150112132944-c25f46c4b940/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs=
|
||||
github.com/yvasiyarov/gorelic v0.0.7 h1:4DTF1WOM2ZZS/xMOkTFBOcb6XiHu/PKn3rVo6dbewQE=
|
||||
|
@ -1775,7 +1781,6 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I=
|
||||
golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
|
@ -1849,7 +1854,6 @@ golang.org/x/net v0.0.0-20220421235706-1d1ef9303861/go.mod h1:CfG3xpIq0wQ8r1q4Su
|
|||
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||
golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0=
|
||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||
|
@ -1896,7 +1900,6 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
|
|||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
@ -2014,7 +2017,6 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
@ -2025,6 +2027,15 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn
|
|||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.1.0 h1:g6Z6vPFA9dYBAF7DWcH6sCcOntplXsDKcliusYijMlw=
|
||||
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
|
@ -2038,6 +2049,7 @@ golang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA=
|
|||
golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
|
|
8
main.go
8
main.go
|
@ -33,6 +33,8 @@ import (
|
|||
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
|
||||
"github.com/fluxcd/pkg/git"
|
||||
"github.com/fluxcd/pkg/git/libgit2/transport"
|
||||
"github.com/fluxcd/pkg/runtime/client"
|
||||
helper "github.com/fluxcd/pkg/runtime/controller"
|
||||
"github.com/fluxcd/pkg/runtime/events"
|
||||
|
@ -48,8 +50,6 @@ import (
|
|||
"github.com/fluxcd/source-controller/controllers"
|
||||
"github.com/fluxcd/source-controller/internal/cache"
|
||||
"github.com/fluxcd/source-controller/internal/helm"
|
||||
"github.com/fluxcd/source-controller/pkg/git"
|
||||
"github.com/fluxcd/source-controller/pkg/git/libgit2/managed"
|
||||
// +kubebuilder:scaffold:imports
|
||||
)
|
||||
|
||||
|
@ -204,7 +204,7 @@ func main() {
|
|||
}
|
||||
storage := mustInitStorage(storagePath, storageAdvAddr, artifactRetentionTTL, artifactRetentionRecords, setupLog)
|
||||
|
||||
if err = managed.InitManagedTransport(); err != nil {
|
||||
if err = transport.InitManagedTransport(); err != nil {
|
||||
// Log the error, but don't exit so as to not block reconcilers that are healthy.
|
||||
setupLog.Error(err, "unable to initialize libgit2 managed transport")
|
||||
}
|
||||
|
@ -215,7 +215,7 @@ func main() {
|
|||
Metrics: metricsH,
|
||||
Storage: storage,
|
||||
ControllerName: controllerName,
|
||||
Libgit2TransportInitialized: managed.Enabled,
|
||||
Libgit2TransportInitialized: transport.Enabled,
|
||||
}).SetupWithManagerAndOptions(mgr, controllers.GitRepositoryReconcilerOptions{
|
||||
MaxConcurrentReconciles: concurrent,
|
||||
DependencyRequeueInterval: requeueDependency,
|
||||
|
|
118
pkg/git/git.go
118
pkg/git/git.go
|
@ -1,118 +0,0 @@
|
|||
/*
|
||||
Copyright 2020 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 git
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/go-crypto/openpgp"
|
||||
)
|
||||
|
||||
type Implementation string
|
||||
|
||||
type Hash []byte
|
||||
|
||||
// String returns the SHA1 Hash as a string.
|
||||
func (h Hash) String() string {
|
||||
return string(h)
|
||||
}
|
||||
|
||||
type Signature struct {
|
||||
Name string
|
||||
Email string
|
||||
When time.Time
|
||||
}
|
||||
|
||||
type Commit struct {
|
||||
// Hash is the SHA1 hash of the commit.
|
||||
Hash Hash
|
||||
// Reference is the original reference of the commit, for example:
|
||||
// 'refs/tags/foo'.
|
||||
Reference string
|
||||
// Author is the original author of the commit.
|
||||
Author Signature
|
||||
// Committer is the one performing the commit, might be different from
|
||||
// Author.
|
||||
Committer Signature
|
||||
// Signature is the PGP signature of the commit.
|
||||
Signature string
|
||||
// Encoded is the encoded commit, without any signature.
|
||||
Encoded []byte
|
||||
// Message is the commit message, contains arbitrary text.
|
||||
Message string
|
||||
}
|
||||
|
||||
// String returns a string representation of the Commit, composed
|
||||
// out the last part of the Reference element, and/or Hash.
|
||||
// For example: 'tag-1/a0c14dc8580a23f79bc654faa79c4f62b46c2c22',
|
||||
// for a "tag-1" tag.
|
||||
func (c *Commit) String() string {
|
||||
if short := strings.SplitAfterN(c.Reference, "/", 3); len(short) == 3 {
|
||||
return fmt.Sprintf("%s/%s", short[2], c.Hash)
|
||||
}
|
||||
return fmt.Sprintf("HEAD/%s", c.Hash)
|
||||
}
|
||||
|
||||
// Verify the Signature of the commit with the given key rings.
|
||||
// It returns the fingerprint of the key the signature was verified
|
||||
// with, or an error.
|
||||
func (c *Commit) Verify(keyRing ...string) (string, error) {
|
||||
if c.Signature == "" {
|
||||
return "", fmt.Errorf("commit does not have a PGP signature")
|
||||
}
|
||||
|
||||
for _, r := range keyRing {
|
||||
reader := strings.NewReader(r)
|
||||
keyring, err := openpgp.ReadArmoredKeyRing(reader)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read armored key ring: %w", err)
|
||||
}
|
||||
signer, err := openpgp.CheckArmoredDetachedSignature(keyring, bytes.NewBuffer(c.Encoded), bytes.NewBufferString(c.Signature), nil)
|
||||
if err == nil {
|
||||
return fmt.Sprintf("%X", signer.PrimaryKey.Fingerprint[12:20]), nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("failed to verify commit with any of the given key rings")
|
||||
}
|
||||
|
||||
// ShortMessage returns the first 50 characters of a commit subject.
|
||||
func (c *Commit) ShortMessage() string {
|
||||
subject := strings.Split(c.Message, "\n")[0]
|
||||
r := []rune(subject)
|
||||
if len(r) > 50 {
|
||||
return fmt.Sprintf("%s...", string(r[0:50]))
|
||||
}
|
||||
return subject
|
||||
}
|
||||
|
||||
type CheckoutStrategy interface {
|
||||
Checkout(ctx context.Context, path, url string, config *AuthOptions) (*Commit, error)
|
||||
}
|
||||
|
||||
// IsConcreteCommit returns if a given commit is a concrete commit. Concrete
|
||||
// commits have most of commit metadata and commit content. In contrast, a
|
||||
// partial commit may only have some metadata and no commit content.
|
||||
func IsConcreteCommit(c Commit) bool {
|
||||
if c.Hash != nil && c.Encoded != nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
|
@ -1,304 +0,0 @@
|
|||
/*
|
||||
Copyright 2021 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 git
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
const (
|
||||
encodedCommitFixture = `tree f0c522d8cc4c90b73e2bc719305a896e7e3c108a
|
||||
parent eb167bc68d0a11530923b1f24b4978535d10b879
|
||||
author Stefan Prodan <stefan.prodan@gmail.com> 1633681364 +0300
|
||||
committer Stefan Prodan <stefan.prodan@gmail.com> 1633681364 +0300
|
||||
|
||||
Update containerd and runc to fix CVEs
|
||||
|
||||
Signed-off-by: Stefan Prodan <stefan.prodan@gmail.com>
|
||||
`
|
||||
|
||||
malformedEncodedCommitFixture = `parent eb167bc68d0a11530923b1f24b4978535d10b879
|
||||
author Stefan Prodan <stefan.prodan@gmail.com> 1633681364 +0300
|
||||
committer Stefan Prodan <stefan.prodan@gmail.com> 1633681364 +0300
|
||||
|
||||
Update containerd and runc to fix CVEs
|
||||
|
||||
Signed-off-by: Stefan Prodan <stefan.prodan@gmail.com>
|
||||
`
|
||||
|
||||
signatureCommitFixture = `-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iHUEABEIAB0WIQQHgExUr4FrLdKzpNYyma6w5AhbrwUCYV//1AAKCRAyma6w5Ahb
|
||||
r7nJAQCQU4zEJu04/Q0ac/UaL6htjhq/wTDNMeUM+aWG/LcBogEAqFUea1oR2BJQ
|
||||
JCJmEtERFh39zNWSazQmxPAFhEE0kbc=
|
||||
=+Wlj
|
||||
-----END PGP SIGNATURE-----`
|
||||
|
||||
armoredKeyRingFixture = `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQSuBF9+HgMRDADKT8UBcSzpTi4JXt/ohhVW3x81AGFPrQvs6MYrcnNJfIkPTJD8
|
||||
mY5T7j1fkaN5wcf1wnxM9qTcW8BodkWNGEoEYOtVuigLSxPFqIncxK0PHvdU8ths
|
||||
TEInBrgZv9t6xIVa4QngOEUd2D/aYni7M+75z7ntgj6eU1xLZ60upRFn05862OvJ
|
||||
rZFUvzjsZXMAO3enCu2VhG/2axCY/5uI8PgWjyiKV2TH4LBJgzlb0v6SyI+fYf5K
|
||||
Bg2WzDuLKvQBi9tFSwnUbQoFFlOeiGW8G/bdkoJDWeS1oYgSD3nkmvXvrVESCrbT
|
||||
C05OtQOiDXjSpkLim81vNVPtI2XEug+9fEA+jeJakyGwwB+K8xqV3QILKCoWHKGx
|
||||
yWcMHSR6cP9tdXCk2JHZBm1PLSJ8hIgMH/YwBJLYg90u8lLAs9WtpVBKkLplzzgm
|
||||
B4Z4VxCC+xI1kt+3ZgYvYC+oUXJXrjyAzy+J1f+aWl2+S/79glWgl/xz2VibWMz6
|
||||
nZUE+wLMxOQqyOsBALsoE6z81y/7gfn4R/BziBASi1jq/r/wdboFYowmqd39DACX
|
||||
+i+V0OplP2TN/F5JajzRgkrlq5cwZHinnw+IFwj9RTfOkdGb3YwhBt/h2PP38969
|
||||
ZG+y8muNtaIqih1pXj1fz9HRtsiCABN0j+JYpvV2D2xuLL7P1O0dt5BpJ3KqNCRw
|
||||
mGgO2GLxbwvlulsLidCPxdK/M8g9Eeb/xwA5LVwvjVchHkzHuUT7durn7AT0RWiK
|
||||
BT8iDfeBB9RKienAbWyybEqRaR6/Tv+mghFIalsDiBPbfm4rsNzsq3ohfByqECiy
|
||||
yUvs2O3NDwkoaBDkA3GFyKv8/SVpcuL5OkVxAHNCIMhNzSgotQ3KLcQc0IREfFCa
|
||||
3CsBAC7CsE2bJZ9IA9sbBa3jimVhWUQVudRWiLFeYHUF/hjhqS8IHyFwprjEOLaV
|
||||
EG0kBO6ELypD/bOsmN9XZLPYyI3y9DM6Vo0KMomE+yK/By/ZMxVfex8/TZreUdhP
|
||||
VdCLL95Rc4w9io8qFb2qGtYBij2wm0RWLcM0IhXWAtjI3B17IN+6hmv+JpiZccsM
|
||||
AMNR5/RVdXIl0hzr8LROD0Xe4sTyZ+fm3mvpczoDPQNRrWpmI/9OT58itnVmZ5jM
|
||||
7djV5y/NjBk63mlqYYfkfWto97wkhg0MnTnOhzdtzSiZQRzj+vf+ilLfIlLnuRr1
|
||||
JRV9Skv6xQltcFArx4JyfZCo7JB1ZXcbdFAvIXXS11RTErO0XVrXNm2RenpW/yZA
|
||||
9f+ESQ/uUB6XNuyqVUnJDAFJFLdzx8sO3DXo7dhIlgpFqgQobUl+APpbU5LT95sm
|
||||
89UrV0Lt9vh7k6zQtKOjEUhm+dErmuBnJo8MvchAuXLagHjvb58vYBCUxVxzt1KG
|
||||
2IePwJ/oXIfawNEGad9Lmdo1FYG1u53AKWZmpYOTouu92O50FG2+7dBh0V2vO253
|
||||
aIGFRT1r14B1pkCIun7z7B/JELqOkmwmlRrUnxlADZEcQT3z/S8/4+2P7P6kXO7X
|
||||
/TAX5xBhSqUbKe3DhJSOvf05/RVL5ULc2U2JFGLAtmBOFmnD/u0qoo5UvWliI+v/
|
||||
47QnU3RlZmFuIFByb2RhbiA8c3RlZmFuLnByb2RhbkBnbWFpbC5jb20+iJAEExEI
|
||||
ADgWIQQHgExUr4FrLdKzpNYyma6w5AhbrwUCX34eAwIbAwULCQgHAgYVCgkICwIE
|
||||
FgIDAQIeAQIXgAAKCRAyma6w5Ahbrzu/AP9l2YpRaWZr6wSQuEn0gMN8DRzsWJPx
|
||||
pn0akdY7SRP3ngD9GoKgu41FAItnHAJ2KiHv/fHFyHMndNP3kPGPNW4BF+65Aw0E
|
||||
X34eAxAMAMdYFCHmVA8TZxSTMBDpKYave8RiDCMMMjk26Gl0EPN9f2Y+s5++DhiQ
|
||||
hojNH9VmJkFwZX1xppxe1y1aLa/U6fBAqMP/IdNH8270iv+A9YIxdsWLmpm99BDO
|
||||
3suRfsHcOe9T0x/CwRfDNdGM/enGMhYGTgF4VD58DRDE6WntaBhl4JJa300NG6X0
|
||||
GM4Gh59DKWDnez/Shulj8demlWmakP5imCVoY+omOEc2k3nH02U+foqaGG5WxZZ+
|
||||
GwEPswm2sBxvn8nwjy9gbQwEtzNI7lWYiz36wCj2VS56Udqt+0eNg8WzocUT0XyI
|
||||
moe1qm8YJQ6fxIzaC431DYi/mCDzgx4EV9ww33SXX3Yp2NL6PsdWJWw2QnoqSMpM
|
||||
z5otw2KlMgUHkkXEKs0apmK4Hu2b6KD7/ydoQRFUqR38Gb0IZL1tOL6PnbCRUcig
|
||||
Aypy016W/WMCjBfQ8qxIGTaj5agX2t28hbiURbxZkCkz+Z3OWkO0Rq3Y2hNAYM5s
|
||||
eTn94JIGGwADBgv/dbSZ9LrBvdMwg8pAtdlLtQdjPiT1i9w5NZuQd7OuKhOxYTEB
|
||||
NRDTgy4/DgeNThCeOkMB/UQQPtJ3Et45S2YRtnnuvfxgnlz7xlUn765/grtnRk4t
|
||||
ONjMmb6tZos1FjIJecB/6h4RsvUd2egvtlpD/Z3YKr6MpNjWg4ji7m27e9pcJfP6
|
||||
YpTDrq9GamiHy9FS2F2pZlQxriPpVhjCLVn9tFGBIsXNxxn7SP4so6rJBmyHEAlq
|
||||
iym9wl933e0FIgAw5C1vvprYu2amk+jmVBsJjjCmInW5q/kWAFnFaHBvk+v+/7tX
|
||||
hywWUI7BqseikgUlkgJ6eU7E9z1DEyuS08x/cViDoNh2ntVUhpnluDu48pdqBvvY
|
||||
a4uL/D+KI84THUAJ/vZy+q6G3BEb4hI9pFjgrdJpUKubxyZolmkCFZHjV34uOcTc
|
||||
LQr28P8xW8vQbg5DpIsivxYLqDGXt3OyiItxvLMtw/ypt6PkoeP9A4KDST4StITE
|
||||
1hrOrPtJ/VRmS2o0iHgEGBEIACAWIQQHgExUr4FrLdKzpNYyma6w5AhbrwUCX34e
|
||||
AwIbDAAKCRAyma6w5Ahbr6QWAP9/pl2R6r1nuCnXzewSbnH1OLsXf32hFQAjaQ5o
|
||||
Oomb3gD/TRf/nAdVED+k81GdLzciYdUGtI71/qI47G0nMBluLRE=
|
||||
=/4e+
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
`
|
||||
|
||||
keyRingFingerprintFixture = "3299AEB0E4085BAF"
|
||||
|
||||
malformedKeyRingFixture = `
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQSuBF9+HgMRDADKT8UBcSzpTi4JXt/ohhVW3x81AGFPrQvs6MYrcnNJfIkPTJD8
|
||||
mY5T7j1fkaN5wcf1wnxM9qTcW8BodkWNGEoEYOtVuigLSxPFqIncxK0PHvdU8ths
|
||||
TEInBrgZv9t6xIVa4QngOEUd2D/aYni7M+75z7ntgj6eU1xLZ60upRFn05862OvJ
|
||||
rZFUvzjsZXMAO3enCu2VhG/2axCY/5uI8PgWjyiKV2TH4LBJgzlb0v6SyI+fYf5K
|
||||
Bg2WzDuLKvQBi9tFSwnUbQoFFlOeiGW8G/bdkoJDWeS1oYgSD3nkmvXvrVESCrbT
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
`
|
||||
)
|
||||
|
||||
func TestCommit_String(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
commit *Commit
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "Reference and commit",
|
||||
commit: &Commit{
|
||||
Hash: []byte("commit"),
|
||||
Reference: "refs/heads/main",
|
||||
},
|
||||
want: "main/commit",
|
||||
},
|
||||
{
|
||||
name: "Reference with slash and commit",
|
||||
commit: &Commit{
|
||||
Hash: []byte("commit"),
|
||||
Reference: "refs/heads/feature/branch",
|
||||
},
|
||||
want: "feature/branch/commit",
|
||||
},
|
||||
{
|
||||
name: "No reference",
|
||||
commit: &Commit{
|
||||
Hash: []byte("commit"),
|
||||
},
|
||||
want: "HEAD/commit",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
g.Expect(tt.commit.String()).To(Equal(tt.want))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommit_Verify(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
commit *Commit
|
||||
keyRings []string
|
||||
want string
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "Valid commit signature",
|
||||
commit: &Commit{
|
||||
Encoded: []byte(encodedCommitFixture),
|
||||
Signature: signatureCommitFixture,
|
||||
},
|
||||
keyRings: []string{armoredKeyRingFixture},
|
||||
want: keyRingFingerprintFixture,
|
||||
},
|
||||
{
|
||||
name: "Malformed encoded commit",
|
||||
commit: &Commit{
|
||||
Encoded: []byte(malformedEncodedCommitFixture),
|
||||
Signature: signatureCommitFixture,
|
||||
},
|
||||
keyRings: []string{armoredKeyRingFixture},
|
||||
wantErr: "failed to verify commit with any of the given key rings",
|
||||
},
|
||||
{
|
||||
name: "Malformed key ring",
|
||||
commit: &Commit{
|
||||
Encoded: []byte(encodedCommitFixture),
|
||||
Signature: signatureCommitFixture,
|
||||
},
|
||||
keyRings: []string{malformedKeyRingFixture},
|
||||
wantErr: "failed to read armored key ring: unexpected EOF",
|
||||
},
|
||||
{
|
||||
name: "Missing signature",
|
||||
commit: &Commit{
|
||||
Encoded: []byte(encodedCommitFixture),
|
||||
},
|
||||
keyRings: []string{armoredKeyRingFixture},
|
||||
wantErr: "commit does not have a PGP signature",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
got, err := tt.commit.Verify(tt.keyRings...)
|
||||
if tt.wantErr != "" {
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
g.Expect(err.Error()).To(ContainSubstring(tt.wantErr))
|
||||
g.Expect(got).To(BeEmpty())
|
||||
return
|
||||
}
|
||||
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
g.Expect(got).To(Equal(tt.want))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommit_ShortMessage(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "short message",
|
||||
input: "a short commit message",
|
||||
want: "a short commit message",
|
||||
},
|
||||
{
|
||||
name: "long message",
|
||||
input: "hello world - a long commit message for testing long messages",
|
||||
want: "hello world - a long commit message for testing lo...",
|
||||
},
|
||||
{
|
||||
name: "multi line commit message",
|
||||
input: `title of the commit
|
||||
|
||||
detailed description
|
||||
of the commit`,
|
||||
want: "title of the commit",
|
||||
},
|
||||
{
|
||||
name: "message with unicodes",
|
||||
input: "a message with unicode characters 你好世界 🏞️ 🏕️ ⛩️ 🌌",
|
||||
want: "a message with unicode characters 你好世界 🏞️ 🏕️ ⛩️ 🌌",
|
||||
},
|
||||
{
|
||||
name: "empty commit message",
|
||||
input: "",
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
c := Commit{Message: tt.input}
|
||||
g.Expect(c.ShortMessage()).To(Equal(tt.want))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsConcreteCommit(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
commit Commit
|
||||
result bool
|
||||
}{
|
||||
{
|
||||
name: "concrete commit",
|
||||
commit: Commit{
|
||||
Hash: Hash("foo"),
|
||||
Reference: "refs/tags/main",
|
||||
Author: Signature{
|
||||
Name: "user", Email: "user@example.com", When: time.Now(),
|
||||
},
|
||||
Committer: Signature{
|
||||
Name: "user", Email: "user@example.com", When: time.Now(),
|
||||
},
|
||||
Signature: "signature",
|
||||
Encoded: []byte("commit-content"),
|
||||
Message: "commit-message",
|
||||
},
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
name: "partial commit",
|
||||
commit: Commit{Hash: Hash("foo")},
|
||||
result: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
g.Expect(IsConcreteCommit(tt.commit)).To(Equal(tt.result))
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,424 +0,0 @@
|
|||
/*
|
||||
Copyright 2020 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 gogit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
extgogit "github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/config"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
"github.com/go-git/go-git/v5/plumbing/transport"
|
||||
"github.com/go-git/go-git/v5/storage/memory"
|
||||
|
||||
"github.com/fluxcd/pkg/gitutil"
|
||||
"github.com/fluxcd/pkg/version"
|
||||
|
||||
"github.com/fluxcd/source-controller/pkg/git"
|
||||
)
|
||||
|
||||
// CheckoutStrategyForOptions returns the git.CheckoutStrategy for the given
|
||||
// git.CheckoutOptions.
|
||||
func CheckoutStrategyForOptions(_ context.Context, opts git.CheckoutOptions) git.CheckoutStrategy {
|
||||
switch {
|
||||
case opts.Commit != "":
|
||||
return &CheckoutCommit{Branch: opts.Branch, Commit: opts.Commit, RecurseSubmodules: opts.RecurseSubmodules}
|
||||
case opts.SemVer != "":
|
||||
return &CheckoutSemVer{SemVer: opts.SemVer, RecurseSubmodules: opts.RecurseSubmodules}
|
||||
case opts.Tag != "":
|
||||
return &CheckoutTag{Tag: opts.Tag, RecurseSubmodules: opts.RecurseSubmodules, LastRevision: opts.LastRevision}
|
||||
default:
|
||||
branch := opts.Branch
|
||||
if branch == "" {
|
||||
branch = git.DefaultBranch
|
||||
}
|
||||
return &CheckoutBranch{Branch: branch, RecurseSubmodules: opts.RecurseSubmodules, LastRevision: opts.LastRevision}
|
||||
}
|
||||
}
|
||||
|
||||
type CheckoutBranch struct {
|
||||
Branch string
|
||||
RecurseSubmodules bool
|
||||
LastRevision string
|
||||
}
|
||||
|
||||
func (c *CheckoutBranch) Checkout(ctx context.Context, path, url string, opts *git.AuthOptions) (*git.Commit, error) {
|
||||
authMethod, err := transportAuth(opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to construct auth method with options: %w", err)
|
||||
}
|
||||
|
||||
ref := plumbing.NewBranchReferenceName(c.Branch)
|
||||
// check if previous revision has changed before attempting to clone
|
||||
if c.LastRevision != "" {
|
||||
currentRevision, err := getLastRevision(ctx, url, ref, opts, authMethod)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if currentRevision != "" && currentRevision == c.LastRevision {
|
||||
// Construct a partial commit with the existing information.
|
||||
// Split the revision and take the last part as the hash.
|
||||
// Example revision: main/43d7eb9c49cdd49b2494efd481aea1166fc22b67
|
||||
var hash git.Hash
|
||||
ss := strings.Split(currentRevision, "/")
|
||||
if len(ss) > 1 {
|
||||
hash = git.Hash(ss[len(ss)-1])
|
||||
} else {
|
||||
hash = git.Hash(ss[0])
|
||||
}
|
||||
c := &git.Commit{
|
||||
Hash: hash,
|
||||
Reference: plumbing.NewBranchReferenceName(c.Branch).String(),
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
}
|
||||
|
||||
repo, err := extgogit.PlainCloneContext(ctx, path, false, &extgogit.CloneOptions{
|
||||
URL: url,
|
||||
Auth: authMethod,
|
||||
RemoteName: git.DefaultOrigin,
|
||||
ReferenceName: plumbing.NewBranchReferenceName(c.Branch),
|
||||
SingleBranch: true,
|
||||
NoCheckout: false,
|
||||
Depth: 1,
|
||||
RecurseSubmodules: recurseSubmodules(c.RecurseSubmodules),
|
||||
Progress: nil,
|
||||
Tags: extgogit.NoTags,
|
||||
CABundle: caBundle(opts),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to clone '%s': %w", url, gitutil.GoGitError(err))
|
||||
}
|
||||
head, err := repo.Head()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve HEAD of branch '%s': %w", c.Branch, err)
|
||||
}
|
||||
cc, err := repo.CommitObject(head.Hash())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve commit object for HEAD '%s': %w", head.Hash(), err)
|
||||
}
|
||||
return buildCommitWithRef(cc, ref)
|
||||
}
|
||||
|
||||
func getLastRevision(ctx context.Context, url string, ref plumbing.ReferenceName, opts *git.AuthOptions, authMethod transport.AuthMethod) (string, error) {
|
||||
config := &config.RemoteConfig{
|
||||
Name: git.DefaultOrigin,
|
||||
URLs: []string{url},
|
||||
}
|
||||
rem := extgogit.NewRemote(memory.NewStorage(), config)
|
||||
listOpts := &extgogit.ListOptions{
|
||||
Auth: authMethod,
|
||||
}
|
||||
if opts != nil && opts.CAFile != nil {
|
||||
listOpts.CABundle = opts.CAFile
|
||||
}
|
||||
refs, err := rem.ListContext(ctx, listOpts)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to list remote for '%s': %w", url, err)
|
||||
}
|
||||
|
||||
currentRevision := filterRefs(refs, ref)
|
||||
return currentRevision, nil
|
||||
}
|
||||
|
||||
type CheckoutTag struct {
|
||||
Tag string
|
||||
RecurseSubmodules bool
|
||||
LastRevision string
|
||||
}
|
||||
|
||||
func (c *CheckoutTag) Checkout(ctx context.Context, path, url string, opts *git.AuthOptions) (*git.Commit, error) {
|
||||
authMethod, err := transportAuth(opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to construct auth method with options: %w", err)
|
||||
}
|
||||
ref := plumbing.NewTagReferenceName(c.Tag)
|
||||
// check if previous revision has changed before attempting to clone
|
||||
if c.LastRevision != "" {
|
||||
currentRevision, err := getLastRevision(ctx, url, ref, opts, authMethod)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if currentRevision != "" && currentRevision == c.LastRevision {
|
||||
// Construct a partial commit with the existing information.
|
||||
// Split the revision and take the last part as the hash.
|
||||
// Example revision: 6.1.4/bf09377bfd5d3bcac1e895fa8ce52dc76695c060
|
||||
var hash git.Hash
|
||||
ss := strings.Split(currentRevision, "/")
|
||||
if len(ss) > 1 {
|
||||
hash = git.Hash(ss[len(ss)-1])
|
||||
} else {
|
||||
hash = git.Hash(ss[0])
|
||||
}
|
||||
c := &git.Commit{
|
||||
Hash: hash,
|
||||
Reference: ref.String(),
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
}
|
||||
repo, err := extgogit.PlainCloneContext(ctx, path, false, &extgogit.CloneOptions{
|
||||
URL: url,
|
||||
Auth: authMethod,
|
||||
RemoteName: git.DefaultOrigin,
|
||||
ReferenceName: plumbing.NewTagReferenceName(c.Tag),
|
||||
SingleBranch: true,
|
||||
NoCheckout: false,
|
||||
Depth: 1,
|
||||
RecurseSubmodules: recurseSubmodules(c.RecurseSubmodules),
|
||||
Progress: nil,
|
||||
Tags: extgogit.NoTags,
|
||||
CABundle: caBundle(opts),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to clone '%s': %w", url, gitutil.GoGitError(err))
|
||||
}
|
||||
head, err := repo.Head()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve HEAD of tag '%s': %w", c.Tag, err)
|
||||
}
|
||||
cc, err := repo.CommitObject(head.Hash())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve commit object for HEAD '%s': %w", head.Hash(), err)
|
||||
}
|
||||
return buildCommitWithRef(cc, ref)
|
||||
}
|
||||
|
||||
type CheckoutCommit struct {
|
||||
Branch string
|
||||
Commit string
|
||||
RecurseSubmodules bool
|
||||
}
|
||||
|
||||
func (c *CheckoutCommit) Checkout(ctx context.Context, path, url string, opts *git.AuthOptions) (*git.Commit, error) {
|
||||
authMethod, err := transportAuth(opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to construct auth method with options: %w", err)
|
||||
}
|
||||
cloneOpts := &extgogit.CloneOptions{
|
||||
URL: url,
|
||||
Auth: authMethod,
|
||||
RemoteName: git.DefaultOrigin,
|
||||
SingleBranch: false,
|
||||
NoCheckout: true,
|
||||
RecurseSubmodules: recurseSubmodules(c.RecurseSubmodules),
|
||||
Progress: nil,
|
||||
Tags: extgogit.NoTags,
|
||||
CABundle: caBundle(opts),
|
||||
}
|
||||
if c.Branch != "" {
|
||||
cloneOpts.SingleBranch = true
|
||||
cloneOpts.ReferenceName = plumbing.NewBranchReferenceName(c.Branch)
|
||||
}
|
||||
repo, err := extgogit.PlainCloneContext(ctx, path, false, cloneOpts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to clone '%s': %w", url, gitutil.GoGitError(err))
|
||||
}
|
||||
w, err := repo.Worktree()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open Git worktree: %w", err)
|
||||
}
|
||||
cc, err := repo.CommitObject(plumbing.NewHash(c.Commit))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve commit object for '%s': %w", c.Commit, err)
|
||||
}
|
||||
err = w.Checkout(&extgogit.CheckoutOptions{
|
||||
Hash: cc.Hash,
|
||||
Force: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to checkout commit '%s': %w", c.Commit, err)
|
||||
}
|
||||
return buildCommitWithRef(cc, cloneOpts.ReferenceName)
|
||||
}
|
||||
|
||||
type CheckoutSemVer struct {
|
||||
SemVer string
|
||||
RecurseSubmodules bool
|
||||
}
|
||||
|
||||
func (c *CheckoutSemVer) Checkout(ctx context.Context, path, url string, opts *git.AuthOptions) (*git.Commit, error) {
|
||||
verConstraint, err := semver.NewConstraint(c.SemVer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("semver parse error: %w", err)
|
||||
}
|
||||
|
||||
authMethod, err := transportAuth(opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to construct auth method with options: %w", err)
|
||||
}
|
||||
|
||||
repo, err := extgogit.PlainCloneContext(ctx, path, false, &extgogit.CloneOptions{
|
||||
URL: url,
|
||||
Auth: authMethod,
|
||||
RemoteName: git.DefaultOrigin,
|
||||
NoCheckout: false,
|
||||
Depth: 1,
|
||||
RecurseSubmodules: recurseSubmodules(c.RecurseSubmodules),
|
||||
Progress: nil,
|
||||
Tags: extgogit.AllTags,
|
||||
CABundle: caBundle(opts),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to clone '%s': %w", url, gitutil.GoGitError(err))
|
||||
}
|
||||
|
||||
repoTags, err := repo.Tags()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list tags: %w", err)
|
||||
}
|
||||
|
||||
tags := make(map[string]string)
|
||||
tagTimestamps := make(map[string]time.Time)
|
||||
if err = repoTags.ForEach(func(t *plumbing.Reference) error {
|
||||
revision := plumbing.Revision(t.Name().String())
|
||||
hash, err := repo.ResolveRevision(revision)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to resolve tag revision: %w", err)
|
||||
}
|
||||
commit, err := repo.CommitObject(*hash)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to resolve commit of a tag revision: %w", err)
|
||||
}
|
||||
tagTimestamps[t.Name().Short()] = commit.Committer.When
|
||||
|
||||
tags[t.Name().Short()] = t.Strings()[1]
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var matchedVersions semver.Collection
|
||||
for tag := range tags {
|
||||
v, err := version.ParseVersion(tag)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if !verConstraint.Check(v) {
|
||||
continue
|
||||
}
|
||||
matchedVersions = append(matchedVersions, v)
|
||||
}
|
||||
if len(matchedVersions) == 0 {
|
||||
return nil, fmt.Errorf("no match found for semver: %s", c.SemVer)
|
||||
}
|
||||
|
||||
// Sort versions
|
||||
sort.SliceStable(matchedVersions, func(i, j int) bool {
|
||||
left := matchedVersions[i]
|
||||
right := matchedVersions[j]
|
||||
|
||||
if !left.Equal(right) {
|
||||
return left.LessThan(right)
|
||||
}
|
||||
|
||||
// Having tag target timestamps at our disposal, we further try to sort
|
||||
// versions into a chronological order. This is especially important for
|
||||
// versions that differ only by build metadata, because it is not considered
|
||||
// a part of the comparable version in Semver
|
||||
return tagTimestamps[left.Original()].Before(tagTimestamps[right.Original()])
|
||||
})
|
||||
v := matchedVersions[len(matchedVersions)-1]
|
||||
t := v.Original()
|
||||
|
||||
w, err := repo.Worktree()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open Git worktree: %w", err)
|
||||
}
|
||||
|
||||
ref := plumbing.NewTagReferenceName(t)
|
||||
err = w.Checkout(&extgogit.CheckoutOptions{
|
||||
Branch: ref,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to checkout tag '%s': %w", t, err)
|
||||
}
|
||||
head, err := repo.Head()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve HEAD of tag '%s': %w", t, err)
|
||||
}
|
||||
cc, err := repo.CommitObject(head.Hash())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve commit object for HEAD '%s': %w", head.Hash(), err)
|
||||
}
|
||||
return buildCommitWithRef(cc, ref)
|
||||
}
|
||||
|
||||
func buildCommitWithRef(c *object.Commit, ref plumbing.ReferenceName) (*git.Commit, error) {
|
||||
if c == nil {
|
||||
return nil, errors.New("failed to construct commit: no object")
|
||||
}
|
||||
|
||||
// Encode commit components excluding signature into SignedData.
|
||||
encoded := &plumbing.MemoryObject{}
|
||||
if err := c.EncodeWithoutSignature(encoded); err != nil {
|
||||
return nil, fmt.Errorf("failed to encode commit '%s': %w", c.Hash, err)
|
||||
}
|
||||
reader, err := encoded.Reader()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to encode commit '%s': %w", c.Hash, err)
|
||||
}
|
||||
b, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read encoded commit '%s': %w", c.Hash, err)
|
||||
}
|
||||
return &git.Commit{
|
||||
Hash: []byte(c.Hash.String()),
|
||||
Reference: ref.String(),
|
||||
Author: buildSignature(c.Author),
|
||||
Committer: buildSignature(c.Committer),
|
||||
Signature: c.PGPSignature,
|
||||
Encoded: b,
|
||||
Message: c.Message,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildSignature(s object.Signature) git.Signature {
|
||||
return git.Signature{
|
||||
Name: s.Name,
|
||||
Email: s.Email,
|
||||
When: s.When,
|
||||
}
|
||||
}
|
||||
|
||||
func recurseSubmodules(recurse bool) extgogit.SubmoduleRescursivity {
|
||||
if recurse {
|
||||
return extgogit.DefaultSubmoduleRecursionDepth
|
||||
}
|
||||
return extgogit.NoRecurseSubmodules
|
||||
}
|
||||
|
||||
func filterRefs(refs []*plumbing.Reference, currentRef plumbing.ReferenceName) string {
|
||||
for _, ref := range refs {
|
||||
if ref.Name().String() == currentRef.String() {
|
||||
return fmt.Sprintf("%s/%s", currentRef.Short(), ref.Hash().String())
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
|
@ -1,895 +0,0 @@
|
|||
/*
|
||||
Copyright 2020 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 gogit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fluxcd/gitkit"
|
||||
"github.com/fluxcd/pkg/gittestserver"
|
||||
"github.com/fluxcd/pkg/ssh"
|
||||
"github.com/fluxcd/source-controller/pkg/git"
|
||||
"github.com/go-git/go-billy/v5/memfs"
|
||||
"github.com/go-git/go-billy/v5/osfs"
|
||||
extgogit "github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/plumbing/cache"
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
"github.com/go-git/go-git/v5/storage/filesystem"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
cryptossh "golang.org/x/crypto/ssh"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
const testRepositoryPath = "../testdata/git/repo"
|
||||
|
||||
func TestCheckoutBranch_Checkout(t *testing.T) {
|
||||
repo, path, err := initRepo(t)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
firstCommit, err := commitFile(repo, "branch", "init", time.Now())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err = createBranch(repo, "test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
secondCommit, err := commitFile(repo, "branch", "second", time.Now())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
branch string
|
||||
filesCreated map[string]string
|
||||
lastRevision string
|
||||
expectedCommit string
|
||||
expectedConcreteCommit bool
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "Default branch",
|
||||
branch: "master",
|
||||
filesCreated: map[string]string{"branch": "init"},
|
||||
expectedCommit: firstCommit.String(),
|
||||
expectedConcreteCommit: true,
|
||||
},
|
||||
{
|
||||
name: "skip clone if LastRevision hasn't changed",
|
||||
branch: "master",
|
||||
filesCreated: map[string]string{"branch": "init"},
|
||||
lastRevision: fmt.Sprintf("master/%s", firstCommit.String()),
|
||||
expectedCommit: firstCommit.String(),
|
||||
expectedConcreteCommit: false,
|
||||
},
|
||||
{
|
||||
name: "Other branch - revision has changed",
|
||||
branch: "test",
|
||||
filesCreated: map[string]string{"branch": "second"},
|
||||
lastRevision: fmt.Sprintf("master/%s", firstCommit.String()),
|
||||
expectedCommit: secondCommit.String(),
|
||||
expectedConcreteCommit: true,
|
||||
},
|
||||
{
|
||||
name: "Non existing branch",
|
||||
branch: "invalid",
|
||||
expectedErr: "couldn't find remote ref \"refs/heads/invalid\"",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
branch := CheckoutBranch{
|
||||
Branch: tt.branch,
|
||||
LastRevision: tt.lastRevision,
|
||||
}
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
cc, err := branch.Checkout(context.TODO(), tmpDir, path, nil)
|
||||
if tt.expectedErr != "" {
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
g.Expect(err.Error()).To(ContainSubstring(tt.expectedErr))
|
||||
g.Expect(cc).To(BeNil())
|
||||
return
|
||||
}
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
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))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckoutTag_Checkout(t *testing.T) {
|
||||
type testTag struct {
|
||||
name string
|
||||
annotated bool
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
tagsInRepo []testTag
|
||||
checkoutTag string
|
||||
lastRevTag string
|
||||
expectConcreteCommit bool
|
||||
expectErr string
|
||||
}{
|
||||
{
|
||||
name: "Tag",
|
||||
tagsInRepo: []testTag{{"tag-1", false}},
|
||||
checkoutTag: "tag-1",
|
||||
expectConcreteCommit: true,
|
||||
},
|
||||
{
|
||||
name: "Annotated",
|
||||
tagsInRepo: []testTag{{"annotated", true}},
|
||||
checkoutTag: "annotated",
|
||||
expectConcreteCommit: true,
|
||||
},
|
||||
{
|
||||
name: "Non existing tag",
|
||||
// Without this go-git returns error "remote repository is empty".
|
||||
tagsInRepo: []testTag{{"tag-1", false}},
|
||||
checkoutTag: "invalid",
|
||||
expectErr: "couldn't find remote ref \"refs/tags/invalid\"",
|
||||
},
|
||||
{
|
||||
name: "Skip clone - last revision unchanged",
|
||||
tagsInRepo: []testTag{{"tag-1", false}},
|
||||
checkoutTag: "tag-1",
|
||||
lastRevTag: "tag-1",
|
||||
expectConcreteCommit: false,
|
||||
},
|
||||
{
|
||||
name: "Last revision changed",
|
||||
tagsInRepo: []testTag{{"tag-1", false}, {"tag-2", false}},
|
||||
checkoutTag: "tag-2",
|
||||
lastRevTag: "tag-1",
|
||||
expectConcreteCommit: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
repo, path, err := initRepo(t)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Collect tags and their associated commit hash for later
|
||||
// reference.
|
||||
tagCommits := map[string]string{}
|
||||
|
||||
// Populate the repo with commits and tags.
|
||||
if tt.tagsInRepo != nil {
|
||||
for _, tr := range tt.tagsInRepo {
|
||||
h, err := commitFile(repo, "tag", tr.name, time.Now())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = tag(repo, h, tr.annotated, tr.name, time.Now())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tagCommits[tr.name] = h.String()
|
||||
}
|
||||
}
|
||||
|
||||
checkoutTag := 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)
|
||||
}
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
cc, err := checkoutTag.Checkout(context.TODO(), tmpDir, path, nil)
|
||||
if tt.expectErr != "" {
|
||||
g.Expect(err).ToNot(BeNil())
|
||||
g.Expect(err.Error()).To(ContainSubstring(tt.expectErr))
|
||||
g.Expect(cc).To(BeNil())
|
||||
return
|
||||
}
|
||||
|
||||
// Check successful checkout results.
|
||||
g.Expect(git.IsConcreteCommit(*cc)).To(Equal(tt.expectConcreteCommit))
|
||||
targetTagHash := tagCommits[tt.checkoutTag]
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
g.Expect(cc.String()).To(Equal(tt.checkoutTag + "/" + targetTagHash))
|
||||
|
||||
// Check file content only when there's an actual checkout.
|
||||
if tt.lastRevTag != tt.checkoutTag {
|
||||
g.Expect(filepath.Join(tmpDir, "tag")).To(BeARegularFile())
|
||||
g.Expect(os.ReadFile(filepath.Join(tmpDir, "tag"))).To(BeEquivalentTo(tt.checkoutTag))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckoutCommit_Checkout(t *testing.T) {
|
||||
repo, path, err := initRepo(t)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
firstCommit, err := commitFile(repo, "commit", "init", time.Now())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = createBranch(repo, "other-branch"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
secondCommit, err := commitFile(repo, "commit", "second", time.Now())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
commit string
|
||||
branch string
|
||||
expectCommit string
|
||||
expectFile string
|
||||
expectError string
|
||||
}{
|
||||
{
|
||||
name: "Commit",
|
||||
commit: firstCommit.String(),
|
||||
expectCommit: "HEAD/" + firstCommit.String(),
|
||||
expectFile: "init",
|
||||
},
|
||||
{
|
||||
name: "Commit in specific branch",
|
||||
commit: secondCommit.String(),
|
||||
branch: "other-branch",
|
||||
expectCommit: "other-branch/" + secondCommit.String(),
|
||||
expectFile: "second",
|
||||
},
|
||||
{
|
||||
name: "Non existing commit",
|
||||
commit: "a-random-invalid-commit",
|
||||
expectError: "failed to resolve commit object for 'a-random-invalid-commit': object not found",
|
||||
},
|
||||
{
|
||||
name: "Non existing commit in specific branch",
|
||||
commit: secondCommit.String(),
|
||||
branch: "master",
|
||||
expectError: "object not found",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
commit := CheckoutCommit{
|
||||
Commit: tt.commit,
|
||||
Branch: tt.branch,
|
||||
}
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
cc, err := commit.Checkout(context.TODO(), tmpDir, path, nil)
|
||||
if tt.expectError != "" {
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
g.Expect(err.Error()).To(ContainSubstring(tt.expectError))
|
||||
g.Expect(cc).To(BeNil())
|
||||
return
|
||||
}
|
||||
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
g.Expect(cc).ToNot(BeNil())
|
||||
g.Expect(cc.String()).To(Equal(tt.expectCommit))
|
||||
g.Expect(filepath.Join(tmpDir, "commit")).To(BeARegularFile())
|
||||
g.Expect(os.ReadFile(filepath.Join(tmpDir, "commit"))).To(BeEquivalentTo(tt.expectFile))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckoutTagSemVer_Checkout(t *testing.T) {
|
||||
now := time.Now()
|
||||
|
||||
tags := []struct {
|
||||
tag string
|
||||
annotated bool
|
||||
commitTime time.Time
|
||||
tagTime time.Time
|
||||
}{
|
||||
{
|
||||
tag: "v0.0.1",
|
||||
annotated: false,
|
||||
commitTime: now,
|
||||
},
|
||||
{
|
||||
tag: "v0.1.0+build-1",
|
||||
annotated: true,
|
||||
commitTime: now.Add(10 * time.Minute),
|
||||
tagTime: now.Add(2 * time.Hour), // This should be ignored during TS comparisons
|
||||
},
|
||||
{
|
||||
tag: "v0.1.0+build-2",
|
||||
annotated: false,
|
||||
commitTime: now.Add(30 * time.Minute),
|
||||
},
|
||||
{
|
||||
tag: "v0.1.0+build-3",
|
||||
annotated: true,
|
||||
commitTime: now.Add(1 * time.Hour),
|
||||
tagTime: now.Add(1 * time.Hour), // This should be ignored during TS comparisons
|
||||
},
|
||||
{
|
||||
tag: "0.2.0",
|
||||
annotated: true,
|
||||
commitTime: now,
|
||||
tagTime: now,
|
||||
},
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
constraint string
|
||||
expectErr error
|
||||
expectTag string
|
||||
}{
|
||||
{
|
||||
name: "Orders by SemVer",
|
||||
constraint: ">0.1.0",
|
||||
expectTag: "0.2.0",
|
||||
},
|
||||
{
|
||||
name: "Orders by SemVer and timestamp",
|
||||
constraint: "<0.2.0",
|
||||
expectTag: "v0.1.0+build-3",
|
||||
},
|
||||
{
|
||||
name: "Errors without match",
|
||||
constraint: ">=1.0.0",
|
||||
expectErr: errors.New("no match found for semver: >=1.0.0"),
|
||||
},
|
||||
}
|
||||
|
||||
repo, path, err := initRepo(t)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
refs := make(map[string]string, len(tags))
|
||||
for _, tt := range tags {
|
||||
ref, err := commitFile(repo, "tag", tt.tag, tt.commitTime)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = tag(repo, ref, tt.annotated, tt.tag, tt.tagTime)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
refs[tt.tag] = ref.String()
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
semVer := CheckoutSemVer{
|
||||
SemVer: tt.constraint,
|
||||
}
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
cc, err := semVer.Checkout(context.TODO(), tmpDir, path, nil)
|
||||
if tt.expectErr != nil {
|
||||
g.Expect(err).To(Equal(tt.expectErr))
|
||||
g.Expect(cc).To(BeNil())
|
||||
return
|
||||
}
|
||||
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
g.Expect(cc.String()).To(Equal(tt.expectTag + "/" + refs[tt.expectTag]))
|
||||
g.Expect(filepath.Join(tmpDir, "tag")).To(BeARegularFile())
|
||||
g.Expect(os.ReadFile(filepath.Join(tmpDir, "tag"))).To(BeEquivalentTo(tt.expectTag))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test_KeyTypes assures support for the different types of keys
|
||||
// for SSH Authentication supported by Flux.
|
||||
func Test_KeyTypes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
keyType ssh.KeyPairType
|
||||
authorized bool
|
||||
wantErr string
|
||||
}{
|
||||
{name: "RSA 4096", keyType: ssh.RSA_4096, authorized: true},
|
||||
{name: "ECDSA P256", keyType: ssh.ECDSA_P256, authorized: true},
|
||||
{name: "ECDSA P384", keyType: ssh.ECDSA_P384, authorized: true},
|
||||
{name: "ECDSA P521", keyType: ssh.ECDSA_P521, authorized: true},
|
||||
{name: "ED25519", keyType: ssh.ED25519, authorized: true},
|
||||
{name: "unauthorized key", keyType: ssh.RSA_4096, wantErr: "unable to authenticate, attempted methods [none publickey], no supported methods remain"},
|
||||
}
|
||||
|
||||
serverRootDir := t.TempDir()
|
||||
server := gittestserver.NewGitServer(serverRootDir)
|
||||
|
||||
// Auth needs to be called, for authentication to be enabled.
|
||||
server.Auth("", "")
|
||||
|
||||
var authorizedPublicKey string
|
||||
server.PublicKeyLookupFunc(func(content string) (*gitkit.PublicKey, error) {
|
||||
authedKey := strings.TrimSuffix(string(authorizedPublicKey), "\n")
|
||||
if authedKey == content {
|
||||
return &gitkit.PublicKey{Content: content}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("pubkey provided '%s' does not match %s", content, authedKey)
|
||||
})
|
||||
|
||||
g := NewWithT(t)
|
||||
timeout := 5 * time.Second
|
||||
|
||||
server.KeyDir(filepath.Join(server.Root(), "keys"))
|
||||
g.Expect(server.ListenSSH()).To(Succeed())
|
||||
|
||||
go func() {
|
||||
server.StartSSH()
|
||||
}()
|
||||
defer server.StopSSH()
|
||||
|
||||
repoPath := "test.git"
|
||||
err := server.InitRepo(testRepositoryPath, git.DefaultBranch, repoPath)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
sshURL := server.SSHAddress()
|
||||
repoURL := sshURL + "/" + repoPath
|
||||
|
||||
// Fetch host key.
|
||||
u, err := url.Parse(sshURL)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(u.Host).ToNot(BeEmpty())
|
||||
|
||||
knownHosts, err := ssh.ScanHostKey(u.Host, timeout, git.HostKeyAlgos, false)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
// Generate ssh keys based on key type.
|
||||
kp, err := ssh.GenerateKeyPair(tt.keyType)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Update authorized key to ensure only the new key is valid on the server.
|
||||
if tt.authorized {
|
||||
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())
|
||||
|
||||
// Prepare for checkout.
|
||||
branchCheckoutStrat := &CheckoutBranch{Branch: git.DefaultBranch}
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
|
||||
// Checkout the repo.
|
||||
commit, err := branchCheckoutStrat.Checkout(ctx, tmpDir, repoURL, authOpts)
|
||||
|
||||
if tt.wantErr == "" {
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
g.Expect(commit).ToNot(BeNil())
|
||||
|
||||
// Confirm checkout actually happened.
|
||||
d, err := os.ReadDir(tmpDir)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
g.Expect(d).To(HaveLen(2)) // .git and foo.txt
|
||||
} else {
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
g.Expect(err.Error()).Should(ContainSubstring(tt.wantErr))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test_KeyExchangeAlgos assures support for the different
|
||||
// types of SSH key exchange algorithms supported by Flux.
|
||||
func Test_KeyExchangeAlgos(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
ClientKex []string
|
||||
ServerKex []string
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "support for kex: diffie-hellman-group14-sha1",
|
||||
ClientKex: []string{"diffie-hellman-group14-sha1"},
|
||||
ServerKex: []string{"diffie-hellman-group14-sha1"},
|
||||
},
|
||||
{
|
||||
name: "support for kex: diffie-hellman-group14-sha256",
|
||||
ClientKex: []string{"diffie-hellman-group14-sha256"},
|
||||
ServerKex: []string{"diffie-hellman-group14-sha256"},
|
||||
},
|
||||
{
|
||||
name: "support for kex: curve25519-sha256",
|
||||
ClientKex: []string{"curve25519-sha256"},
|
||||
ServerKex: []string{"curve25519-sha256"},
|
||||
},
|
||||
{
|
||||
name: "support for kex: ecdh-sha2-nistp256",
|
||||
ClientKex: []string{"ecdh-sha2-nistp256"},
|
||||
ServerKex: []string{"ecdh-sha2-nistp256"},
|
||||
},
|
||||
{
|
||||
name: "support for kex: ecdh-sha2-nistp384",
|
||||
ClientKex: []string{"ecdh-sha2-nistp384"},
|
||||
ServerKex: []string{"ecdh-sha2-nistp384"},
|
||||
},
|
||||
{
|
||||
name: "support for kex: ecdh-sha2-nistp521",
|
||||
ClientKex: []string{"ecdh-sha2-nistp521"},
|
||||
ServerKex: []string{"ecdh-sha2-nistp521"},
|
||||
},
|
||||
{
|
||||
name: "support for kex: curve25519-sha256@libssh.org",
|
||||
ClientKex: []string{"curve25519-sha256@libssh.org"},
|
||||
ServerKex: []string{"curve25519-sha256@libssh.org"},
|
||||
},
|
||||
{
|
||||
name: "non-matching kex",
|
||||
ClientKex: []string{"ecdh-sha2-nistp521"},
|
||||
ServerKex: []string{"curve25519-sha256@libssh.org"},
|
||||
wantErr: "ssh: no common algorithm for key exchange; client offered: [ecdh-sha2-nistp521 ext-info-c], server offered: [curve25519-sha256@libssh.org]",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
timeout := 5 * time.Second
|
||||
|
||||
serverRootDir := t.TempDir()
|
||||
server := gittestserver.NewGitServer(serverRootDir).WithSSHConfig(&cryptossh.ServerConfig{
|
||||
Config: cryptossh.Config{
|
||||
KeyExchanges: tt.ServerKex,
|
||||
},
|
||||
})
|
||||
|
||||
// Set what Client Key Exchange Algos to send
|
||||
git.KexAlgos = tt.ClientKex
|
||||
|
||||
server.KeyDir(filepath.Join(server.Root(), "keys"))
|
||||
g.Expect(server.ListenSSH()).To(Succeed())
|
||||
|
||||
go func() {
|
||||
server.StartSSH()
|
||||
}()
|
||||
defer server.StopSSH()
|
||||
|
||||
repoPath := "test.git"
|
||||
err := server.InitRepo(testRepositoryPath, git.DefaultBranch, repoPath)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
sshURL := server.SSHAddress()
|
||||
repoURL := sshURL + "/" + repoPath
|
||||
|
||||
// Fetch host key.
|
||||
u, err := url.Parse(sshURL)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(u.Host).ToNot(BeEmpty())
|
||||
|
||||
knownHosts, err := ssh.ScanHostKey(u.Host, timeout, git.HostKeyAlgos, false)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// No authentication is required for this test, but it is
|
||||
// used here to make the Checkout logic happy.
|
||||
kp, err := ssh.GenerateKeyPair(ssh.ED25519)
|
||||
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())
|
||||
|
||||
// Prepare for checkout.
|
||||
branchCheckoutStrat := &CheckoutBranch{Branch: git.DefaultBranch}
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
|
||||
// Checkout the repo.
|
||||
_, err = branchCheckoutStrat.Checkout(ctx, tmpDir, repoURL, authOpts)
|
||||
if tt.wantErr != "" {
|
||||
g.Expect(err).Error().Should(HaveOccurred())
|
||||
g.Expect(err.Error()).Should(ContainSubstring(tt.wantErr))
|
||||
} else {
|
||||
g.Expect(err).Error().ShouldNot(HaveOccurred())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestHostKeyAlgos assures support for the different
|
||||
// types of SSH Host Key algorithms supported by Flux.
|
||||
func TestHostKeyAlgos(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
keyType ssh.KeyPairType
|
||||
ClientHostKeyAlgos []string
|
||||
hashHostNames bool
|
||||
}{
|
||||
{
|
||||
name: "support for hostkey: ssh-rsa",
|
||||
keyType: ssh.RSA_4096,
|
||||
ClientHostKeyAlgos: []string{"ssh-rsa"},
|
||||
},
|
||||
{
|
||||
name: "support for hostkey: rsa-sha2-256",
|
||||
keyType: ssh.RSA_4096,
|
||||
ClientHostKeyAlgos: []string{"rsa-sha2-256"},
|
||||
},
|
||||
{
|
||||
name: "support for hostkey: rsa-sha2-512",
|
||||
keyType: ssh.RSA_4096,
|
||||
ClientHostKeyAlgos: []string{"rsa-sha2-512"},
|
||||
},
|
||||
{
|
||||
name: "support for hostkey: ecdsa-sha2-nistp256",
|
||||
keyType: ssh.ECDSA_P256,
|
||||
ClientHostKeyAlgos: []string{"ecdsa-sha2-nistp256"},
|
||||
},
|
||||
{
|
||||
name: "support for hostkey: ecdsa-sha2-nistp384",
|
||||
keyType: ssh.ECDSA_P384,
|
||||
ClientHostKeyAlgos: []string{"ecdsa-sha2-nistp384"},
|
||||
},
|
||||
{
|
||||
name: "support for hostkey: ecdsa-sha2-nistp521",
|
||||
keyType: ssh.ECDSA_P521,
|
||||
ClientHostKeyAlgos: []string{"ecdsa-sha2-nistp521"},
|
||||
},
|
||||
{
|
||||
name: "support for hostkey: ssh-ed25519",
|
||||
keyType: ssh.ED25519,
|
||||
ClientHostKeyAlgos: []string{"ssh-ed25519"},
|
||||
},
|
||||
{
|
||||
name: "support for hostkey: ssh-rsa with hashed host names",
|
||||
keyType: ssh.RSA_4096,
|
||||
ClientHostKeyAlgos: []string{"ssh-rsa"},
|
||||
hashHostNames: true,
|
||||
},
|
||||
{
|
||||
name: "support for hostkey: rsa-sha2-256 with hashed host names",
|
||||
keyType: ssh.RSA_4096,
|
||||
ClientHostKeyAlgos: []string{"rsa-sha2-256"},
|
||||
hashHostNames: true,
|
||||
},
|
||||
{
|
||||
name: "support for hostkey: rsa-sha2-512 with hashed host names",
|
||||
keyType: ssh.RSA_4096,
|
||||
ClientHostKeyAlgos: []string{"rsa-sha2-512"},
|
||||
hashHostNames: true,
|
||||
},
|
||||
{
|
||||
name: "support for hostkey: ecdsa-sha2-nistp256 with hashed host names",
|
||||
keyType: ssh.ECDSA_P256,
|
||||
ClientHostKeyAlgos: []string{"ecdsa-sha2-nistp256"},
|
||||
hashHostNames: true,
|
||||
},
|
||||
{
|
||||
name: "support for hostkey: ecdsa-sha2-nistp384 with hashed host names",
|
||||
keyType: ssh.ECDSA_P384,
|
||||
ClientHostKeyAlgos: []string{"ecdsa-sha2-nistp384"},
|
||||
hashHostNames: true,
|
||||
},
|
||||
{
|
||||
name: "support for hostkey: ecdsa-sha2-nistp521 with hashed host names",
|
||||
keyType: ssh.ECDSA_P521,
|
||||
ClientHostKeyAlgos: []string{"ecdsa-sha2-nistp521"},
|
||||
hashHostNames: true,
|
||||
},
|
||||
{
|
||||
name: "support for hostkey: ssh-ed25519 with hashed host names",
|
||||
keyType: ssh.ED25519,
|
||||
ClientHostKeyAlgos: []string{"ssh-ed25519"},
|
||||
hashHostNames: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
timeout := 5 * time.Second
|
||||
|
||||
sshConfig := &cryptossh.ServerConfig{}
|
||||
|
||||
// Generate new keypair for the server to use for HostKeys.
|
||||
hkp, err := ssh.GenerateKeyPair(tt.keyType)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
p, err := cryptossh.ParseRawPrivateKey(hkp.PrivateKey)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Add key to server.
|
||||
signer, err := cryptossh.NewSignerFromKey(p)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
sshConfig.AddHostKey(signer)
|
||||
|
||||
serverRootDir := t.TempDir()
|
||||
server := gittestserver.NewGitServer(serverRootDir).WithSSHConfig(sshConfig)
|
||||
|
||||
// Set what HostKey Algos will be accepted from a client perspective.
|
||||
git.HostKeyAlgos = tt.ClientHostKeyAlgos
|
||||
|
||||
keyDir := filepath.Join(server.Root(), "keys")
|
||||
server.KeyDir(keyDir)
|
||||
g.Expect(server.ListenSSH()).To(Succeed())
|
||||
|
||||
go func() {
|
||||
server.StartSSH()
|
||||
}()
|
||||
defer server.StopSSH()
|
||||
|
||||
repoPath := "test.git"
|
||||
err = server.InitRepo(testRepositoryPath, git.DefaultBranch, repoPath)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
sshURL := server.SSHAddress()
|
||||
repoURL := sshURL + "/" + repoPath
|
||||
|
||||
// Fetch host key.
|
||||
u, err := url.Parse(sshURL)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(u.Host).ToNot(BeEmpty())
|
||||
|
||||
knownHosts, err := ssh.ScanHostKey(u.Host, timeout, git.HostKeyAlgos, tt.hashHostNames)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// No authentication is required for this test, but it is
|
||||
// used here to make the Checkout logic happy.
|
||||
kp, err := ssh.GenerateKeyPair(ssh.ED25519)
|
||||
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())
|
||||
|
||||
// Prepare for checkout.
|
||||
branchCheckoutStrat := &CheckoutBranch{Branch: git.DefaultBranch}
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
|
||||
// Checkout the repo.
|
||||
_, err = branchCheckoutStrat.Checkout(ctx, tmpDir, repoURL, authOpts)
|
||||
g.Expect(err).Error().ShouldNot(HaveOccurred())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func initRepo(t *testing.T) (*extgogit.Repository, string, error) {
|
||||
tmpDir := t.TempDir()
|
||||
sto := filesystem.NewStorage(osfs.New(tmpDir), cache.NewObjectLRUDefault())
|
||||
repo, err := extgogit.Init(sto, memfs.New())
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return repo, tmpDir, err
|
||||
}
|
||||
|
||||
func createBranch(repo *extgogit.Repository, branch string) error {
|
||||
wt, err := repo.Worktree()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h, err := repo.Head()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return wt.Checkout(&extgogit.CheckoutOptions{
|
||||
Hash: h.Hash(),
|
||||
Branch: plumbing.ReferenceName("refs/heads/" + branch),
|
||||
Create: true,
|
||||
})
|
||||
}
|
||||
|
||||
func commitFile(repo *extgogit.Repository, path, content string, time time.Time) (plumbing.Hash, error) {
|
||||
wt, err := repo.Worktree()
|
||||
if err != nil {
|
||||
return plumbing.Hash{}, err
|
||||
}
|
||||
f, err := wt.Filesystem.Create(path)
|
||||
if err != nil {
|
||||
return plumbing.Hash{}, err
|
||||
}
|
||||
if _, err = f.Write([]byte(content)); err != nil {
|
||||
f.Close()
|
||||
return plumbing.Hash{}, err
|
||||
}
|
||||
if err = f.Close(); err != nil {
|
||||
return plumbing.Hash{}, err
|
||||
}
|
||||
if _, err = wt.Add(path); err != nil {
|
||||
return plumbing.Hash{}, err
|
||||
}
|
||||
return wt.Commit("Adding: "+path, &extgogit.CommitOptions{
|
||||
Author: mockSignature(time),
|
||||
Committer: mockSignature(time),
|
||||
})
|
||||
}
|
||||
|
||||
func tag(repo *extgogit.Repository, commit plumbing.Hash, annotated bool, tag string, time time.Time) (*plumbing.Reference, error) {
|
||||
var opts *extgogit.CreateTagOptions
|
||||
if annotated {
|
||||
opts = &extgogit.CreateTagOptions{
|
||||
Tagger: mockSignature(time),
|
||||
Message: "Annotated tag for: " + tag,
|
||||
}
|
||||
}
|
||||
return repo.CreateTag(tag, commit, opts)
|
||||
}
|
||||
|
||||
func mockSignature(time time.Time) *object.Signature {
|
||||
return &object.Signature{
|
||||
Name: "Jane Doe",
|
||||
Email: "jane@example.com",
|
||||
When: time,
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
/*
|
||||
Copyright 2021 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 gogit
|
||||
|
||||
import "github.com/fluxcd/source-controller/pkg/git"
|
||||
|
||||
const (
|
||||
Implementation git.Implementation = "go-git"
|
||||
)
|
|
@ -1,111 +0,0 @@
|
|||
/*
|
||||
Copyright 2020 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 gogit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-git/go-git/v5/plumbing/transport"
|
||||
"github.com/go-git/go-git/v5/plumbing/transport/http"
|
||||
"github.com/go-git/go-git/v5/plumbing/transport/ssh"
|
||||
|
||||
"github.com/fluxcd/pkg/ssh/knownhosts"
|
||||
|
||||
"github.com/fluxcd/source-controller/pkg/git"
|
||||
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// transportAuth constructs the transport.AuthMethod for the git.Transport of
|
||||
// the given git.AuthOptions. It returns the result, or an error.
|
||||
func transportAuth(opts *git.AuthOptions) (transport.AuthMethod, error) {
|
||||
if opts == nil {
|
||||
return nil, nil
|
||||
}
|
||||
switch opts.Transport {
|
||||
case git.HTTPS, git.HTTP:
|
||||
// Some providers (i.e. GitLab) will reject empty credentials for
|
||||
// public repositories.
|
||||
if opts.Username != "" || opts.Password != "" {
|
||||
return &http.BasicAuth{
|
||||
Username: opts.Username,
|
||||
Password: opts.Password,
|
||||
}, nil
|
||||
}
|
||||
return nil, nil
|
||||
case git.SSH:
|
||||
if len(opts.Identity) > 0 {
|
||||
pk, err := ssh.NewPublicKeys(opts.Username, opts.Identity, opts.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(opts.KnownHosts) > 0 {
|
||||
callback, err := knownhosts.New(opts.KnownHosts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pk.HostKeyCallback = callback
|
||||
}
|
||||
customPK := &CustomPublicKeys{
|
||||
pk: pk,
|
||||
}
|
||||
return customPK, nil
|
||||
}
|
||||
case "":
|
||||
return nil, fmt.Errorf("no transport type set")
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown transport '%s'", opts.Transport)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// caBundle returns the CA bundle from the given git.AuthOptions.
|
||||
func caBundle(opts *git.AuthOptions) []byte {
|
||||
if opts == nil {
|
||||
return nil
|
||||
}
|
||||
return opts.CAFile
|
||||
}
|
||||
|
||||
// CustomPublicKeys is a wrapper around ssh.PublicKeys to help us
|
||||
// customize the ssh config. It implements ssh.AuthMethod.
|
||||
type CustomPublicKeys struct {
|
||||
pk *ssh.PublicKeys
|
||||
}
|
||||
|
||||
func (a *CustomPublicKeys) Name() string {
|
||||
return a.pk.Name()
|
||||
}
|
||||
|
||||
func (a *CustomPublicKeys) String() string {
|
||||
return a.pk.String()
|
||||
}
|
||||
|
||||
func (a *CustomPublicKeys) ClientConfig() (*gossh.ClientConfig, error) {
|
||||
config, err := a.pk.ClientConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(git.KexAlgos) > 0 {
|
||||
config.Config.KeyExchanges = git.KexAlgos
|
||||
}
|
||||
if len(git.HostKeyAlgos) > 0 {
|
||||
config.HostKeyAlgorithms = git.HostKeyAlgos
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
|
@ -1,249 +0,0 @@
|
|||
/*
|
||||
Copyright 2020 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 gogit
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/go-git/go-git/v5/plumbing/transport"
|
||||
"github.com/go-git/go-git/v5/plumbing/transport/http"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/fluxcd/source-controller/pkg/git"
|
||||
)
|
||||
|
||||
const (
|
||||
// privateKeyFixture is a randomly generated password less
|
||||
// 512bit RSA private key.
|
||||
privateKeyFixture = `-----BEGIN RSA PRIVATE KEY-----
|
||||
MIICXAIBAAKBgQCrakELAKxozvwJijQEggYlTvS1QTZx1DaBwOhW/4kRSuR21plu
|
||||
xuQeyuUiztoWeb9jgW7wjzG4j1PIJjdbsgjPIcIZ4PBY7JeEW+QRopfwuN8MHXNp
|
||||
uTLgIHbkmhoOg5qBEcjzO/lEOOPpV0EmbObgqv3+wRmLJrgfzWl/cTtRewIDAQAB
|
||||
AoGAawKFImpEN5Xn78iwWpQVZBsbV0AjzgHuGSiloxIZrorzf2DPHkHZzYNaclVx
|
||||
/o/4tBTsfg7WumH3qr541qyZJDgU7iRMABwmx0v1vm2wQiX7NJzLzH2E9vlMC3mw
|
||||
d8S99g9EqRuNH98XX8su34B9WGRPqiKvEm0RW8Hideo2/KkCQQDbs6rHcriKQyPB
|
||||
paidHZAfguu0eVbyHT2EgLgRboWE+tEAqFEW2ycqNL3VPz9fRvwexbB6rpOcPpQJ
|
||||
DEL4XB2XAkEAx7xJz8YlCQ2H38xggK8R8EUXF9Zhb0fqMJHMNmao1HCHVMtbsa8I
|
||||
jR2EGyQ4CaIqNG5tdWukXQSJrPYDRWNvvQJAZX3rP7XUYDLB2twvN12HzbbKMhX3
|
||||
v2MYnxRjc9INpi/Dyzz2MMvOnOW+aDuOh/If2AtVCmeJUx1pf4CFk3viQwJBAKyC
|
||||
t824+evjv+NQBlme3AOF6PgxtV4D4wWoJ5Uk/dTejER0j/Hbl6sqPxuiILRRV9qJ
|
||||
Ngkgu4mLjc3RfenEhJECQAx8zjWUE6kHHPGAd9DfiAIQ4bChqnyS0Nwb9+Gd4hSE
|
||||
P0Ah10mHiK/M0o3T8Eanwum0gbQHPnOwqZgsPkwXRqQ=
|
||||
-----END RSA PRIVATE KEY-----`
|
||||
|
||||
// privateKeyPassphraseFixture is a randomly generated
|
||||
// 512bit RSA private key with password foobar.
|
||||
privateKeyPassphraseFixture = `-----BEGIN RSA PRIVATE KEY-----
|
||||
Proc-Type: 4,ENCRYPTED
|
||||
DEK-Info: AES-256-CBC,0B016973B2A761D31E6B388D0F327C35
|
||||
|
||||
X9GET/qAyZkAJBl/RK+1XX75NxONgdUfZDw7PIYi/g+Efh3Z5zH5kh/dx9lxH5ZG
|
||||
HGCqPAeMO/ofGDGtDULWW6iqDUFRu5gPgEVSCnnbqoHNU325WHhXdhejVAItwObC
|
||||
IpL/zYfs2+gDHXct/n9FJ/9D/EGXZihwPqYaK8GQSfZAxz0QjLuh0wU1qpbm3y3N
|
||||
q+o9FLv3b2Ys/tCJOUsYVQOYLSrZEI77y1ii3nWgQ8lXiTJbBUKzuq4f1YWeO8Ah
|
||||
RZbdhTa57AF5lUaRtL7Nrm3HJUrK1alBbU7HHyjeW4Q4n/D3fiRDC1Mh2Bi4EOOn
|
||||
wGctSx4kHsZGhJv5qwKqqPEFPhUzph8D2tm2TABk8HJa5KJFDbGrcfvk2uODAoZr
|
||||
MbcpIxCfl8oB09bWfY6tDQjyvwSYYo2Phdwm7kT92xc=
|
||||
-----END RSA PRIVATE KEY-----`
|
||||
|
||||
// knownHostsFixture is known_hosts fixture in the expected
|
||||
// format.
|
||||
knownHostsFixture string = `github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==`
|
||||
)
|
||||
|
||||
func Test_transportAuth(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
opts *git.AuthOptions
|
||||
wantFunc func(g *WithT, t transport.AuthMethod, opts *git.AuthOptions)
|
||||
kexAlgos []string
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "Public HTTP Repositories",
|
||||
opts: &git.AuthOptions{
|
||||
Transport: git.HTTP,
|
||||
},
|
||||
wantFunc: func(g *WithT, t transport.AuthMethod, opts *git.AuthOptions) {
|
||||
g.Expect(t).To(BeNil())
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Public HTTPS Repositories",
|
||||
opts: &git.AuthOptions{
|
||||
Transport: git.HTTP,
|
||||
},
|
||||
wantFunc: func(g *WithT, t transport.AuthMethod, opts *git.AuthOptions) {
|
||||
g.Expect(t).To(BeNil())
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "HTTP basic auth",
|
||||
opts: &git.AuthOptions{
|
||||
Transport: git.HTTP,
|
||||
Username: "example",
|
||||
Password: "password",
|
||||
},
|
||||
wantFunc: func(g *WithT, t transport.AuthMethod, opts *git.AuthOptions) {
|
||||
g.Expect(t).To(Equal(&http.BasicAuth{
|
||||
Username: opts.Username,
|
||||
Password: opts.Password,
|
||||
}))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "HTTPS basic auth",
|
||||
opts: &git.AuthOptions{
|
||||
Transport: git.HTTPS,
|
||||
Username: "example",
|
||||
Password: "password",
|
||||
},
|
||||
wantFunc: func(g *WithT, t transport.AuthMethod, opts *git.AuthOptions) {
|
||||
g.Expect(t).To(Equal(&http.BasicAuth{
|
||||
Username: opts.Username,
|
||||
Password: opts.Password,
|
||||
}))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SSH private key",
|
||||
opts: &git.AuthOptions{
|
||||
Transport: git.SSH,
|
||||
Username: "example",
|
||||
Identity: []byte(privateKeyFixture),
|
||||
},
|
||||
wantFunc: func(g *WithT, t transport.AuthMethod, opts *git.AuthOptions) {
|
||||
tt, ok := t.(*CustomPublicKeys)
|
||||
g.Expect(ok).To(BeTrue())
|
||||
g.Expect(tt.pk.User).To(Equal(opts.Username))
|
||||
g.Expect(tt.pk.Signer.PublicKey().Type()).To(Equal("ssh-rsa"))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SSH private key with passphrase",
|
||||
opts: &git.AuthOptions{
|
||||
Transport: git.SSH,
|
||||
Username: "example",
|
||||
Password: "foobar",
|
||||
Identity: []byte(privateKeyPassphraseFixture),
|
||||
},
|
||||
wantFunc: func(g *WithT, t transport.AuthMethod, opts *git.AuthOptions) {
|
||||
tt, ok := t.(*CustomPublicKeys)
|
||||
g.Expect(ok).To(BeTrue())
|
||||
g.Expect(tt.pk.User).To(Equal(opts.Username))
|
||||
g.Expect(tt.pk.Signer.PublicKey().Type()).To(Equal("ssh-rsa"))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SSH with custom key exchanges",
|
||||
opts: &git.AuthOptions{
|
||||
Transport: git.SSH,
|
||||
Username: "example",
|
||||
Identity: []byte(privateKeyFixture),
|
||||
KnownHosts: []byte(knownHostsFixture),
|
||||
},
|
||||
kexAlgos: []string{"curve25519-sha256", "diffie-hellman-group-exchange-sha256"},
|
||||
wantFunc: func(g *WithT, t transport.AuthMethod, opts *git.AuthOptions) {
|
||||
tt, ok := t.(*CustomPublicKeys)
|
||||
g.Expect(ok).To(BeTrue())
|
||||
g.Expect(tt.pk.User).To(Equal(opts.Username))
|
||||
g.Expect(tt.pk.Signer.PublicKey().Type()).To(Equal("ssh-rsa"))
|
||||
config, err := tt.ClientConfig()
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
g.Expect(config.Config.KeyExchanges).To(Equal(
|
||||
[]string{"curve25519-sha256", "diffie-hellman-group-exchange-sha256"}),
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SSH private key with invalid passphrase",
|
||||
opts: &git.AuthOptions{
|
||||
Transport: git.SSH,
|
||||
Username: "example",
|
||||
Password: "",
|
||||
Identity: []byte(privateKeyPassphraseFixture),
|
||||
},
|
||||
wantErr: errors.New("x509: decryption password incorrect"),
|
||||
},
|
||||
{
|
||||
name: "SSH private key with known_hosts",
|
||||
opts: &git.AuthOptions{
|
||||
Transport: git.SSH,
|
||||
Username: "example",
|
||||
Identity: []byte(privateKeyFixture),
|
||||
KnownHosts: []byte(knownHostsFixture),
|
||||
},
|
||||
wantFunc: func(g *WithT, t transport.AuthMethod, opts *git.AuthOptions) {
|
||||
tt, ok := t.(*CustomPublicKeys)
|
||||
g.Expect(ok).To(BeTrue())
|
||||
g.Expect(tt.pk.User).To(Equal(opts.Username))
|
||||
g.Expect(tt.pk.Signer.PublicKey().Type()).To(Equal("ssh-rsa"))
|
||||
g.Expect(tt.pk.HostKeyCallback).ToNot(BeNil())
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SSH private key with invalid known_hosts",
|
||||
opts: &git.AuthOptions{
|
||||
Transport: git.SSH,
|
||||
Username: "example",
|
||||
Identity: []byte(privateKeyFixture),
|
||||
KnownHosts: []byte("invalid"),
|
||||
},
|
||||
wantErr: errors.New("knownhosts: knownhosts: missing host pattern"),
|
||||
},
|
||||
{
|
||||
name: "Empty",
|
||||
opts: &git.AuthOptions{},
|
||||
wantErr: errors.New("no transport type set"),
|
||||
},
|
||||
{
|
||||
name: "Unknown transport",
|
||||
opts: &git.AuthOptions{
|
||||
Transport: "foo",
|
||||
},
|
||||
wantErr: errors.New("unknown transport 'foo'"),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
if len(tt.kexAlgos) > 0 {
|
||||
git.KexAlgos = tt.kexAlgos
|
||||
}
|
||||
|
||||
got, err := transportAuth(tt.opts)
|
||||
if tt.wantErr != nil {
|
||||
g.Expect(err).To(Equal(tt.wantErr))
|
||||
g.Expect(got).To(BeNil())
|
||||
return
|
||||
}
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
if tt.wantFunc != nil {
|
||||
tt.wantFunc(g, got, tt.opts)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_caBundle(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
g.Expect(caBundle(&git.AuthOptions{CAFile: []byte("foo")})).To(BeEquivalentTo("foo"))
|
||||
g.Expect(caBundle(nil)).To(BeNil())
|
||||
}
|
|
@ -1,566 +0,0 @@
|
|||
/*
|
||||
Copyright 2020 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 libgit2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/go-logr/logr"
|
||||
git2go "github.com/libgit2/git2go/v33"
|
||||
|
||||
"github.com/fluxcd/pkg/gitutil"
|
||||
"github.com/fluxcd/pkg/version"
|
||||
|
||||
"github.com/fluxcd/source-controller/pkg/git"
|
||||
"github.com/fluxcd/source-controller/pkg/git/libgit2/managed"
|
||||
)
|
||||
|
||||
const defaultRemoteName = "origin"
|
||||
|
||||
// CheckoutStrategyForOptions returns the git.CheckoutStrategy for the given
|
||||
// git.CheckoutOptions.
|
||||
func CheckoutStrategyForOptions(ctx context.Context, opt git.CheckoutOptions) git.CheckoutStrategy {
|
||||
if opt.RecurseSubmodules {
|
||||
logr.FromContextOrDiscard(ctx).Info(fmt.Sprintf("git submodule recursion not supported by implementation '%s'", Implementation))
|
||||
}
|
||||
switch {
|
||||
case opt.Commit != "":
|
||||
return &CheckoutCommit{Commit: opt.Commit}
|
||||
case opt.SemVer != "":
|
||||
return &CheckoutSemVer{SemVer: opt.SemVer}
|
||||
case opt.Tag != "":
|
||||
return &CheckoutTag{
|
||||
Tag: opt.Tag,
|
||||
LastRevision: opt.LastRevision,
|
||||
}
|
||||
default:
|
||||
branch := opt.Branch
|
||||
if branch == "" {
|
||||
branch = git.DefaultBranch
|
||||
}
|
||||
return &CheckoutBranch{
|
||||
Branch: branch,
|
||||
LastRevision: opt.LastRevision,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type CheckoutBranch struct {
|
||||
Branch string
|
||||
LastRevision string
|
||||
}
|
||||
|
||||
func (c *CheckoutBranch) Checkout(ctx context.Context, path, url string, opts *git.AuthOptions) (_ *git.Commit, err error) {
|
||||
defer recoverPanic(&err)
|
||||
|
||||
err = registerManagedTransportOptions(ctx, url, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
transportOptsURL := opts.TransportOptionsURL
|
||||
remoteCallBacks := managed.RemoteCallbacks()
|
||||
defer managed.RemoveTransportOptions(transportOptsURL)
|
||||
|
||||
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", 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", 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Limit the fetch operation to the specific branch, to decrease network usage.
|
||||
err = remote.Fetch([]string{c.Branch},
|
||||
&git2go.FetchOptions{
|
||||
DownloadTags: git2go.DownloadTagsNone,
|
||||
RemoteCallbacks: remoteCallBacks,
|
||||
},
|
||||
"")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to fetch remote '%s': %w", url, gitutil.LibGit2Error(err))
|
||||
}
|
||||
|
||||
branch, err := repo.References.Lookup(fmt.Sprintf("refs/remotes/origin/%s", c.Branch))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to lookup branch '%s' for '%s': %w", c.Branch, url, gitutil.LibGit2Error(err))
|
||||
}
|
||||
defer branch.Free()
|
||||
|
||||
upstreamCommit, err := repo.LookupCommit(branch.Target())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to lookup commit '%s' for '%s': %w", c.Branch, url, gitutil.LibGit2Error(err))
|
||||
}
|
||||
defer upstreamCommit.Free()
|
||||
|
||||
// We try to lookup the branch (and create it if it doesn't exist), so that we can
|
||||
// switch the repo to the specified branch. This is done so that users of this api
|
||||
// can expect the repo to be at the desired branch, when cloned.
|
||||
localBranch, err := repo.LookupBranch(c.Branch, git2go.BranchLocal)
|
||||
if git2go.IsErrorCode(err, git2go.ErrorCodeNotFound) {
|
||||
localBranch, err = repo.CreateBranch(c.Branch, upstreamCommit, false)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to create local branch '%s': %w", c.Branch, err)
|
||||
}
|
||||
} else if err != nil {
|
||||
return nil, fmt.Errorf("unable to lookup branch '%s': %w", c.Branch, err)
|
||||
}
|
||||
defer localBranch.Free()
|
||||
|
||||
tree, err := repo.LookupTree(upstreamCommit.TreeId())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to lookup tree for branch '%s': %w", c.Branch, err)
|
||||
}
|
||||
defer tree.Free()
|
||||
|
||||
err = repo.CheckoutTree(tree, &git2go.CheckoutOpts{
|
||||
// the remote branch should take precedence if it exists at this point in time.
|
||||
Strategy: git2go.CheckoutForce,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to checkout tree for branch '%s': %w", c.Branch, err)
|
||||
}
|
||||
|
||||
// Set the current head to point to the requested branch.
|
||||
err = repo.SetHead("refs/heads/" + c.Branch)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to set HEAD to branch '%s':%w", c.Branch, err)
|
||||
}
|
||||
|
||||
// Use the current worktree's head as reference for the commit to be returned.
|
||||
head, err := repo.Head()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to resolve HEAD: %w", err)
|
||||
}
|
||||
defer head.Free()
|
||||
|
||||
cc, err := repo.LookupCommit(head.Target())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable 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 {
|
||||
Tag string
|
||||
LastRevision string
|
||||
}
|
||||
|
||||
func (c *CheckoutTag) Checkout(ctx context.Context, path, url string, opts *git.AuthOptions) (_ *git.Commit, err error) {
|
||||
defer recoverPanic(&err)
|
||||
|
||||
err = registerManagedTransportOptions(ctx, url, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
transportOptsURL := opts.TransportOptionsURL
|
||||
remoteCallBacks := managed.RemoteCallbacks()
|
||||
defer managed.RemoveTransportOptions(transportOptsURL)
|
||||
|
||||
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", 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 {
|
||||
return nil, fmt.Errorf("unable to remote ls for '%s': %w", 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
|
||||
} 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 {
|
||||
// Construct a partial commit with the existing information.
|
||||
c := &git.Commit{
|
||||
Hash: git.Hash(hash),
|
||||
Reference: "refs/tags/" + c.Tag,
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = remote.Fetch([]string{c.Tag},
|
||||
&git2go.FetchOptions{
|
||||
DownloadTags: git2go.DownloadTagsAuto,
|
||||
RemoteCallbacks: remoteCallBacks,
|
||||
},
|
||||
"")
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to fetch remote '%s': %w", url, gitutil.LibGit2Error(err))
|
||||
}
|
||||
|
||||
cc, err := checkoutDetachedDwim(repo, c.Tag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cc.Free()
|
||||
return buildCommit(cc, "refs/tags/"+c.Tag), nil
|
||||
}
|
||||
|
||||
type CheckoutCommit struct {
|
||||
Commit string
|
||||
}
|
||||
|
||||
func (c *CheckoutCommit) Checkout(ctx context.Context, path, url string, opts *git.AuthOptions) (_ *git.Commit, err error) {
|
||||
defer recoverPanic(&err)
|
||||
|
||||
err = registerManagedTransportOptions(ctx, url, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
transportOptsURL := opts.TransportOptionsURL
|
||||
defer managed.RemoveTransportOptions(transportOptsURL)
|
||||
|
||||
repo, err := git2go.Clone(transportOptsURL, path, &git2go.CloneOptions{
|
||||
FetchOptions: git2go.FetchOptions{
|
||||
DownloadTags: git2go.DownloadTagsNone,
|
||||
RemoteCallbacks: managed.RemoteCallbacks(),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to clone '%s': %w", url, gitutil.LibGit2Error(err))
|
||||
}
|
||||
defer repo.Free()
|
||||
oid, err := git2go.NewOid(c.Commit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create oid for '%s': %w", c.Commit, err)
|
||||
}
|
||||
cc, err := checkoutDetachedHEAD(repo, oid)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("git checkout error: %w", err)
|
||||
}
|
||||
return buildCommit(cc, ""), nil
|
||||
}
|
||||
|
||||
type CheckoutSemVer struct {
|
||||
SemVer string
|
||||
}
|
||||
|
||||
func (c *CheckoutSemVer) Checkout(ctx context.Context, path, url string, opts *git.AuthOptions) (_ *git.Commit, err error) {
|
||||
defer recoverPanic(&err)
|
||||
|
||||
err = registerManagedTransportOptions(ctx, url, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
transportOptsURL := opts.TransportOptionsURL
|
||||
defer managed.RemoveTransportOptions(transportOptsURL)
|
||||
|
||||
verConstraint, err := semver.NewConstraint(c.SemVer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("semver parse error: %w", err)
|
||||
}
|
||||
|
||||
repo, err := git2go.Clone(transportOptsURL, path, &git2go.CloneOptions{
|
||||
FetchOptions: git2go.FetchOptions{
|
||||
DownloadTags: git2go.DownloadTagsAll,
|
||||
RemoteCallbacks: managed.RemoteCallbacks(),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to clone '%s': %w", url, gitutil.LibGit2Error(err))
|
||||
}
|
||||
defer repo.Free()
|
||||
|
||||
tags := make(map[string]string)
|
||||
tagTimestamps := make(map[string]time.Time)
|
||||
if err := repo.Tags.Foreach(func(name string, id *git2go.Oid) error {
|
||||
cleanName := strings.TrimPrefix(name, "refs/tags/")
|
||||
// The given ID can refer to both a commit and a tag, as annotated tags contain additional metadata.
|
||||
// Due to this, first attempt to resolve it as a simple tag (commit), but fallback to attempting to
|
||||
// resolve it as an annotated tag in case this results in an error.
|
||||
if c, err := repo.LookupCommit(id); err == nil {
|
||||
defer c.Free()
|
||||
// Use the commit metadata as the decisive timestamp.
|
||||
tagTimestamps[cleanName] = c.Committer().When
|
||||
tags[cleanName] = name
|
||||
return nil
|
||||
}
|
||||
t, err := repo.LookupTag(id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not lookup '%s' as simple or annotated tag: %w", cleanName, err)
|
||||
}
|
||||
defer t.Free()
|
||||
commit, err := t.Peel(git2go.ObjectCommit)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not get commit for tag '%s': %w", t.Name(), err)
|
||||
}
|
||||
defer commit.Free()
|
||||
c, err := commit.AsCommit()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not get commit object for tag '%s': %w", t.Name(), err)
|
||||
}
|
||||
defer c.Free()
|
||||
tagTimestamps[t.Name()] = c.Committer().When
|
||||
tags[t.Name()] = name
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var matchedVersions semver.Collection
|
||||
for tag := range tags {
|
||||
v, err := version.ParseVersion(tag)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if !verConstraint.Check(v) {
|
||||
continue
|
||||
}
|
||||
matchedVersions = append(matchedVersions, v)
|
||||
}
|
||||
if len(matchedVersions) == 0 {
|
||||
return nil, fmt.Errorf("no match found for semver: %s", c.SemVer)
|
||||
}
|
||||
|
||||
// Sort versions
|
||||
sort.SliceStable(matchedVersions, func(i, j int) bool {
|
||||
left := matchedVersions[i]
|
||||
right := matchedVersions[j]
|
||||
|
||||
if !left.Equal(right) {
|
||||
return left.LessThan(right)
|
||||
}
|
||||
|
||||
// Having tag target timestamps at our disposal, we further try to sort
|
||||
// versions into a chronological order. This is especially important for
|
||||
// versions that differ only by build metadata, because it is not considered
|
||||
// a part of the comparable version in Semver
|
||||
return tagTimestamps[left.Original()].Before(tagTimestamps[right.Original()])
|
||||
})
|
||||
v := matchedVersions[len(matchedVersions)-1]
|
||||
t := v.Original()
|
||||
|
||||
cc, err := checkoutDetachedDwim(repo, t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cc.Free()
|
||||
return buildCommit(cc, "refs/tags/"+t), nil
|
||||
}
|
||||
|
||||
// checkoutDetachedDwim attempts to perform a detached HEAD checkout by first DWIMing the short name
|
||||
// to get a concrete reference, and then calling checkoutDetachedHEAD.
|
||||
func checkoutDetachedDwim(repo *git2go.Repository, name string) (*git2go.Commit, error) {
|
||||
ref, err := repo.References.Dwim(name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to find '%s': %w", name, err)
|
||||
}
|
||||
defer ref.Free()
|
||||
c, err := ref.Peel(git2go.ObjectCommit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get commit for ref '%s': %w", ref.Name(), err)
|
||||
}
|
||||
defer c.Free()
|
||||
cc, err := c.AsCommit()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get commit object for ref '%s': %w", ref.Name(), err)
|
||||
}
|
||||
defer cc.Free()
|
||||
return checkoutDetachedHEAD(repo, cc.Id())
|
||||
}
|
||||
|
||||
// checkoutDetachedHEAD attempts to perform a detached HEAD checkout for the given commit.
|
||||
func checkoutDetachedHEAD(repo *git2go.Repository, oid *git2go.Oid) (*git2go.Commit, error) {
|
||||
cc, err := repo.LookupCommit(oid)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("git commit '%s' not found: %w", oid.String(), err)
|
||||
}
|
||||
if err = repo.SetHeadDetached(cc.Id()); err != nil {
|
||||
cc.Free()
|
||||
return nil, fmt.Errorf("could not detach HEAD at '%s': %w", oid.String(), err)
|
||||
}
|
||||
if err = repo.CheckoutHead(&git2go.CheckoutOptions{
|
||||
Strategy: git2go.CheckoutForce,
|
||||
}); err != nil {
|
||||
cc.Free()
|
||||
return nil, fmt.Errorf("git checkout error: %w", err)
|
||||
}
|
||||
return cc, nil
|
||||
}
|
||||
|
||||
// headCommit returns the current HEAD of the repository, or an error.
|
||||
func headCommit(repo *git2go.Repository) (*git2go.Commit, error) {
|
||||
head, err := repo.Head()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer head.Free()
|
||||
c, err := repo.LookupCommit(head.Target())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func buildCommit(c *git2go.Commit, ref string) *git.Commit {
|
||||
sig, msg, _ := c.ExtractSignature()
|
||||
return &git.Commit{
|
||||
Hash: []byte(c.Id().String()),
|
||||
Reference: ref,
|
||||
Author: buildSignature(c.Author()),
|
||||
Committer: buildSignature(c.Committer()),
|
||||
Signature: sig,
|
||||
Encoded: []byte(msg),
|
||||
Message: c.Message(),
|
||||
}
|
||||
}
|
||||
|
||||
func buildSignature(s *git2go.Signature) git.Signature {
|
||||
return git.Signature{
|
||||
Name: s.Name,
|
||||
Email: s.Email,
|
||||
When: s.When,
|
||||
}
|
||||
}
|
||||
|
||||
// initializeRepoWithRemote initializes or opens a repository at the given path
|
||||
// and configures it with the given transport opts URL (as a placeholder for the
|
||||
// actual target url). If a remote already exists with a different URL, it overwrites
|
||||
// it with the provided transport opts URL.
|
||||
func initializeRepoWithRemote(ctx context.Context, path, url string, opts *git.AuthOptions) (*git2go.Repository, *git2go.Remote, error) {
|
||||
repo, err := git2go.InitRepository(path, false)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("unable to init repository for '%s': %w", url, gitutil.LibGit2Error(err))
|
||||
}
|
||||
|
||||
transportOptsURL := opts.TransportOptionsURL
|
||||
remote, err := repo.Remotes.Create(defaultRemoteName, transportOptsURL)
|
||||
if err != nil {
|
||||
// If the remote already exists, lookup the remote.
|
||||
if git2go.IsErrorCode(err, git2go.ErrorCodeExists) {
|
||||
remote, err = repo.Remotes.Lookup(defaultRemoteName)
|
||||
if err != nil {
|
||||
repo.Free()
|
||||
return nil, nil, fmt.Errorf("unable to create or lookup remote '%s'", defaultRemoteName)
|
||||
}
|
||||
|
||||
if remote.Url() != transportOptsURL {
|
||||
err = repo.Remotes.SetUrl("origin", transportOptsURL)
|
||||
if err != nil {
|
||||
repo.Free()
|
||||
remote.Free()
|
||||
return nil, nil, fmt.Errorf("unable to configure remote %s origin with url %s", defaultRemoteName, url)
|
||||
}
|
||||
|
||||
// refresh the remote
|
||||
remote, err = repo.Remotes.Lookup(defaultRemoteName)
|
||||
if err != nil {
|
||||
repo.Free()
|
||||
return nil, nil, fmt.Errorf("unable to create or lookup remote '%s'", defaultRemoteName)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
repo.Free()
|
||||
return nil, nil, fmt.Errorf("unable to create remote for '%s': %w", url, gitutil.LibGit2Error(err))
|
||||
}
|
||||
}
|
||||
return repo, remote, nil
|
||||
}
|
||||
|
||||
// registerManagedTransportOptions registers the given url and it's transport options.
|
||||
// Callers must make sure to call `managed.RemoveTransportOptions()` to avoid increase in
|
||||
// memory consumption.
|
||||
// We store the target URL, auth options, etc. mapped to TransporOptsURL 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 and transport level code
|
||||
// which enables us to fetch the required credentials, context, etc. at the transport level.
|
||||
func registerManagedTransportOptions(ctx context.Context, url string, authOpts *git.AuthOptions) error {
|
||||
if authOpts == nil {
|
||||
return errors.New("can't checkout using libgit2 with an empty set of auth options")
|
||||
}
|
||||
if authOpts.TransportOptionsURL == "" {
|
||||
return errors.New("can't checkout using libgit2 without a valid transport auth id")
|
||||
}
|
||||
managed.AddTransportOptions(authOpts.TransportOptionsURL, managed.TransportOptions{
|
||||
TargetURL: url,
|
||||
AuthOpts: authOpts,
|
||||
ProxyOptions: &git2go.ProxyOptions{Type: git2go.ProxyTypeAuto},
|
||||
Context: ctx,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func recoverPanic(err *error) {
|
||||
if r := recover(); r != nil {
|
||||
*err = fmt.Errorf("recovered from git2go panic: %v", r)
|
||||
}
|
||||
}
|
|
@ -1,449 +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 libgit2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fluxcd/gitkit"
|
||||
"github.com/fluxcd/pkg/gittestserver"
|
||||
"github.com/fluxcd/pkg/ssh"
|
||||
|
||||
. "github.com/onsi/gomega"
|
||||
cryptossh "golang.org/x/crypto/ssh"
|
||||
|
||||
"github.com/fluxcd/source-controller/pkg/git"
|
||||
)
|
||||
|
||||
const testRepositoryPath = "../testdata/git/repo"
|
||||
|
||||
// Test_ssh_keyTypes assures support for the different
|
||||
// types of keys for SSH Authentication supported by Flux.
|
||||
func Test_ssh_keyTypes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
keyType ssh.KeyPairType
|
||||
authorized bool
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "RSA 4096",
|
||||
keyType: ssh.RSA_4096,
|
||||
authorized: true,
|
||||
},
|
||||
{
|
||||
name: "ECDSA P256",
|
||||
keyType: ssh.ECDSA_P256,
|
||||
authorized: true,
|
||||
},
|
||||
{
|
||||
name: "ECDSA P384",
|
||||
keyType: ssh.ECDSA_P384,
|
||||
authorized: true,
|
||||
},
|
||||
{
|
||||
name: "ECDSA P521",
|
||||
keyType: ssh.ECDSA_P521,
|
||||
authorized: true,
|
||||
},
|
||||
{
|
||||
name: "ED25519",
|
||||
keyType: ssh.ED25519,
|
||||
authorized: true,
|
||||
},
|
||||
{
|
||||
name: "unauthorized key",
|
||||
keyType: ssh.RSA_4096,
|
||||
wantErr: "unable to authenticate, attempted methods [none publickey], no supported methods remain",
|
||||
},
|
||||
}
|
||||
|
||||
serverRootDir := t.TempDir()
|
||||
server := gittestserver.NewGitServer(serverRootDir)
|
||||
|
||||
// Auth needs to be called, for authentication to be enabled.
|
||||
server.Auth("", "")
|
||||
|
||||
var authorizedPublicKey string
|
||||
server.PublicKeyLookupFunc(func(content string) (*gitkit.PublicKey, error) {
|
||||
authedKey := strings.TrimSuffix(string(authorizedPublicKey), "\n")
|
||||
if authedKey == content {
|
||||
return &gitkit.PublicKey{Content: content}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("pubkey provided '%s' does not match %s", content, authedKey)
|
||||
})
|
||||
|
||||
g := NewWithT(t)
|
||||
timeout := 5 * time.Second
|
||||
|
||||
server.KeyDir(filepath.Join(server.Root(), "keys"))
|
||||
g.Expect(server.ListenSSH()).To(Succeed())
|
||||
|
||||
go func() {
|
||||
server.StartSSH()
|
||||
}()
|
||||
defer server.StopSSH()
|
||||
|
||||
repoPath := "test.git"
|
||||
err := server.InitRepo(testRepositoryPath, git.DefaultBranch, repoPath)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
sshURL := server.SSHAddress()
|
||||
repoURL := sshURL + "/" + repoPath
|
||||
|
||||
// Fetch host key.
|
||||
u, err := url.Parse(sshURL)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(u.Host).ToNot(BeEmpty())
|
||||
|
||||
knownHosts, err := ssh.ScanHostKey(u.Host, timeout, git.HostKeyAlgos, false)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
// Generate ssh keys based on key type.
|
||||
kp, err := ssh.GenerateKeyPair(tt.keyType)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Update authorized key to ensure only the new key is valid on the server.
|
||||
if tt.authorized {
|
||||
authorizedPublicKey = string(kp.PublicKey)
|
||||
}
|
||||
|
||||
authOpts := &git.AuthOptions{
|
||||
Identity: kp.PrivateKey,
|
||||
KnownHosts: knownHosts,
|
||||
}
|
||||
authOpts.TransportOptionsURL = getTransportOptionsURL(git.SSH)
|
||||
|
||||
// Prepare for checkout.
|
||||
branchCheckoutStrat := &CheckoutBranch{Branch: git.DefaultBranch}
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
|
||||
// Checkout the repo.
|
||||
commit, err := branchCheckoutStrat.Checkout(ctx, tmpDir, repoURL, authOpts)
|
||||
|
||||
if tt.wantErr == "" {
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
g.Expect(commit).ToNot(BeNil())
|
||||
|
||||
// Confirm checkout actually happened.
|
||||
d, err := os.ReadDir(tmpDir)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
g.Expect(d).To(HaveLen(2)) // .git and foo.txt
|
||||
} else {
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
g.Expect(err.Error()).Should(ContainSubstring(tt.wantErr))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test_ssh_keyExchangeAlgos assures support for the different
|
||||
// types of SSH key exchange algorithms supported by Flux.
|
||||
func Test_ssh_keyExchangeAlgos(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
ClientKex []string
|
||||
ServerKex []string
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "support for kex: diffie-hellman-group14-sha1",
|
||||
ClientKex: []string{"diffie-hellman-group14-sha1"},
|
||||
ServerKex: []string{"diffie-hellman-group14-sha1"},
|
||||
},
|
||||
{
|
||||
name: "support for kex: diffie-hellman-group14-sha256",
|
||||
ClientKex: []string{"diffie-hellman-group14-sha256"},
|
||||
ServerKex: []string{"diffie-hellman-group14-sha256"},
|
||||
},
|
||||
{
|
||||
name: "support for kex: curve25519-sha256",
|
||||
ClientKex: []string{"curve25519-sha256"},
|
||||
ServerKex: []string{"curve25519-sha256"},
|
||||
},
|
||||
{
|
||||
name: "support for kex: ecdh-sha2-nistp256",
|
||||
ClientKex: []string{"ecdh-sha2-nistp256"},
|
||||
ServerKex: []string{"ecdh-sha2-nistp256"},
|
||||
},
|
||||
{
|
||||
name: "support for kex: ecdh-sha2-nistp384",
|
||||
ClientKex: []string{"ecdh-sha2-nistp384"},
|
||||
ServerKex: []string{"ecdh-sha2-nistp384"},
|
||||
},
|
||||
{
|
||||
name: "support for kex: ecdh-sha2-nistp521",
|
||||
ClientKex: []string{"ecdh-sha2-nistp521"},
|
||||
ServerKex: []string{"ecdh-sha2-nistp521"},
|
||||
},
|
||||
{
|
||||
name: "support for kex: curve25519-sha256@libssh.org",
|
||||
ClientKex: []string{"curve25519-sha256@libssh.org"},
|
||||
ServerKex: []string{"curve25519-sha256@libssh.org"},
|
||||
},
|
||||
{
|
||||
name: "non-matching kex",
|
||||
ClientKex: []string{"ecdh-sha2-nistp521"},
|
||||
ServerKex: []string{"curve25519-sha256@libssh.org"},
|
||||
wantErr: "ssh: no common algorithm for key exchange; client offered: [ecdh-sha2-nistp521 ext-info-c], server offered: [curve25519-sha256@libssh.org]",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
timeout := 5 * time.Second
|
||||
|
||||
serverRootDir := t.TempDir()
|
||||
server := gittestserver.NewGitServer(serverRootDir).WithSSHConfig(&cryptossh.ServerConfig{
|
||||
Config: cryptossh.Config{
|
||||
KeyExchanges: tt.ServerKex,
|
||||
},
|
||||
})
|
||||
|
||||
// Set what Client Key Exchange Algos to send
|
||||
git.KexAlgos = tt.ClientKex
|
||||
|
||||
server.KeyDir(filepath.Join(server.Root(), "keys"))
|
||||
g.Expect(server.ListenSSH()).To(Succeed())
|
||||
|
||||
go func() {
|
||||
server.StartSSH()
|
||||
}()
|
||||
defer server.StopSSH()
|
||||
|
||||
repoPath := "test.git"
|
||||
|
||||
err := server.InitRepo(testRepositoryPath, git.DefaultBranch, repoPath)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
sshURL := server.SSHAddress()
|
||||
repoURL := sshURL + "/" + repoPath
|
||||
|
||||
// Fetch host key.
|
||||
u, err := url.Parse(sshURL)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(u.Host).ToNot(BeEmpty())
|
||||
|
||||
knownHosts, err := ssh.ScanHostKey(u.Host, timeout, git.HostKeyAlgos, false)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// No authentication is required for this test, but it is
|
||||
// used here to make the Checkout logic happy.
|
||||
kp, err := ssh.GenerateKeyPair(ssh.ED25519)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
authOpts := &git.AuthOptions{
|
||||
Identity: kp.PrivateKey,
|
||||
KnownHosts: knownHosts,
|
||||
}
|
||||
authOpts.TransportOptionsURL = getTransportOptionsURL(git.SSH)
|
||||
|
||||
// Prepare for checkout.
|
||||
branchCheckoutStrat := &CheckoutBranch{Branch: git.DefaultBranch}
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
|
||||
// Checkout the repo.
|
||||
_, err = branchCheckoutStrat.Checkout(ctx, tmpDir, repoURL, authOpts)
|
||||
if tt.wantErr != "" {
|
||||
g.Expect(err).Error().Should(HaveOccurred())
|
||||
g.Expect(err.Error()).Should(ContainSubstring(tt.wantErr))
|
||||
} else {
|
||||
g.Expect(err).Error().ShouldNot(HaveOccurred())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test_ssh_hostKeyAlgos assures support for the different
|
||||
// types of SSH Host Key algorithms supported by Flux.
|
||||
func Test_ssh_hostKeyAlgos(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
keyType ssh.KeyPairType
|
||||
ClientHostKeyAlgos []string
|
||||
hashHostNames bool
|
||||
}{
|
||||
{
|
||||
name: "support for hostkey: ssh-rsa",
|
||||
keyType: ssh.RSA_4096,
|
||||
ClientHostKeyAlgos: []string{"ssh-rsa"},
|
||||
},
|
||||
{
|
||||
name: "support for hostkey: rsa-sha2-256",
|
||||
keyType: ssh.RSA_4096,
|
||||
ClientHostKeyAlgos: []string{"rsa-sha2-256"},
|
||||
},
|
||||
{
|
||||
name: "support for hostkey: rsa-sha2-512",
|
||||
keyType: ssh.RSA_4096,
|
||||
ClientHostKeyAlgos: []string{"rsa-sha2-512"},
|
||||
},
|
||||
{
|
||||
name: "support for hostkey: ecdsa-sha2-nistp256",
|
||||
keyType: ssh.ECDSA_P256,
|
||||
ClientHostKeyAlgos: []string{"ecdsa-sha2-nistp256"},
|
||||
},
|
||||
{
|
||||
name: "support for hostkey: ecdsa-sha2-nistp384",
|
||||
keyType: ssh.ECDSA_P384,
|
||||
ClientHostKeyAlgos: []string{"ecdsa-sha2-nistp384"},
|
||||
},
|
||||
{
|
||||
name: "support for hostkey: ecdsa-sha2-nistp521",
|
||||
keyType: ssh.ECDSA_P521,
|
||||
ClientHostKeyAlgos: []string{"ecdsa-sha2-nistp521"},
|
||||
},
|
||||
{
|
||||
name: "support for hostkey: ssh-ed25519",
|
||||
keyType: ssh.ED25519,
|
||||
ClientHostKeyAlgos: []string{"ssh-ed25519"},
|
||||
},
|
||||
{
|
||||
name: "support for hostkey: ssh-rsa with hashed host names",
|
||||
keyType: ssh.RSA_4096,
|
||||
ClientHostKeyAlgos: []string{"ssh-rsa"},
|
||||
hashHostNames: true,
|
||||
},
|
||||
{
|
||||
name: "support for hostkey: rsa-sha2-256 with hashed host names",
|
||||
keyType: ssh.RSA_4096,
|
||||
ClientHostKeyAlgos: []string{"rsa-sha2-256"},
|
||||
hashHostNames: true,
|
||||
},
|
||||
{
|
||||
name: "support for hostkey: rsa-sha2-512 with hashed host names",
|
||||
keyType: ssh.RSA_4096,
|
||||
ClientHostKeyAlgos: []string{"rsa-sha2-512"},
|
||||
hashHostNames: true,
|
||||
},
|
||||
{
|
||||
name: "support for hostkey: ecdsa-sha2-nistp256 with hashed host names",
|
||||
keyType: ssh.ECDSA_P256,
|
||||
ClientHostKeyAlgos: []string{"ecdsa-sha2-nistp256"},
|
||||
hashHostNames: true,
|
||||
},
|
||||
{
|
||||
name: "support for hostkey: ecdsa-sha2-nistp384 with hashed host names",
|
||||
keyType: ssh.ECDSA_P384,
|
||||
ClientHostKeyAlgos: []string{"ecdsa-sha2-nistp384"},
|
||||
hashHostNames: true,
|
||||
},
|
||||
{
|
||||
name: "support for hostkey: ecdsa-sha2-nistp521 with hashed host names",
|
||||
keyType: ssh.ECDSA_P521,
|
||||
ClientHostKeyAlgos: []string{"ecdsa-sha2-nistp521"},
|
||||
hashHostNames: true,
|
||||
},
|
||||
{
|
||||
name: "support for hostkey: ssh-ed25519 with hashed host names",
|
||||
keyType: ssh.ED25519,
|
||||
ClientHostKeyAlgos: []string{"ssh-ed25519"},
|
||||
hashHostNames: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
timeout := 5 * time.Second
|
||||
|
||||
sshConfig := &cryptossh.ServerConfig{}
|
||||
|
||||
// Generate new keypair for the server to use for HostKeys.
|
||||
hkp, err := ssh.GenerateKeyPair(tt.keyType)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
p, err := cryptossh.ParseRawPrivateKey(hkp.PrivateKey)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Add key to server.
|
||||
signer, err := cryptossh.NewSignerFromKey(p)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
sshConfig.AddHostKey(signer)
|
||||
|
||||
serverRootDir := t.TempDir()
|
||||
server := gittestserver.NewGitServer(serverRootDir).WithSSHConfig(sshConfig)
|
||||
|
||||
// Set what HostKey Algos will be accepted from a client perspective.
|
||||
git.HostKeyAlgos = tt.ClientHostKeyAlgos
|
||||
|
||||
keyDir := filepath.Join(server.Root(), "keys")
|
||||
server.KeyDir(keyDir)
|
||||
g.Expect(server.ListenSSH()).To(Succeed())
|
||||
|
||||
go func() {
|
||||
server.StartSSH()
|
||||
}()
|
||||
defer server.StopSSH()
|
||||
|
||||
repoPath := "test.git"
|
||||
|
||||
err = server.InitRepo(testRepositoryPath, git.DefaultBranch, repoPath)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
sshURL := server.SSHAddress()
|
||||
repoURL := sshURL + "/" + repoPath
|
||||
|
||||
// Fetch host key.
|
||||
u, err := url.Parse(sshURL)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(u.Host).ToNot(BeEmpty())
|
||||
|
||||
knownHosts, err := ssh.ScanHostKey(u.Host, timeout, tt.ClientHostKeyAlgos, tt.hashHostNames)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// No authentication is required for this test, but it is
|
||||
// used here to make the Checkout logic happy.
|
||||
kp, err := ssh.GenerateKeyPair(ssh.ED25519)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
authOpts := &git.AuthOptions{
|
||||
Identity: kp.PrivateKey,
|
||||
KnownHosts: knownHosts,
|
||||
}
|
||||
authOpts.TransportOptionsURL = getTransportOptionsURL(git.SSH)
|
||||
|
||||
// Prepare for checkout.
|
||||
branchCheckoutStrat := &CheckoutBranch{Branch: git.DefaultBranch}
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
|
||||
// Checkout the repo.
|
||||
_, err = branchCheckoutStrat.Checkout(ctx, tmpDir, repoURL, authOpts)
|
||||
g.Expect(err).Error().ShouldNot(HaveOccurred())
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,707 +0,0 @@
|
|||
/*
|
||||
Copyright 2020 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 libgit2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fluxcd/pkg/gittestserver"
|
||||
git2go "github.com/libgit2/git2go/v33"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/fluxcd/source-controller/pkg/git"
|
||||
"github.com/fluxcd/source-controller/pkg/git/libgit2/managed"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
err := managed.InitManagedTransport()
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("failed to initialize libgit2 managed transport: %s", err))
|
||||
}
|
||||
code := m.Run()
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func TestCheckoutBranch_Checkout(t *testing.T) {
|
||||
// we use a HTTP Git server instead of a bare repo (for all tests in this
|
||||
// package), because our managed transports don't support the file protocol,
|
||||
// so we wouldn't actually be using our custom transports, if we used a bare
|
||||
// repo.
|
||||
server, err := gittestserver.NewTempGitServer()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(server.Root())
|
||||
|
||||
err = server.StartHTTP()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer server.StopHTTP()
|
||||
|
||||
repoPath := "test.git"
|
||||
err = server.InitRepo("../testdata/git/repo", git.DefaultBranch, repoPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
repo, err := git2go.OpenRepository(filepath.Join(server.Root(), repoPath))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer repo.Free()
|
||||
|
||||
defaultBranch := "master"
|
||||
|
||||
firstCommit, err := commitFile(repo, "branch", "init", time.Now())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Branch off on first commit
|
||||
if err = createBranch(repo, "test", nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create second commit on default branch
|
||||
secondCommit, err := commitFile(repo, "branch", "second", time.Now())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
repoURL := server.HTTPAddress() + "/" + repoPath
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
branch string
|
||||
filesCreated map[string]string
|
||||
lastRevision string
|
||||
expectedCommit string
|
||||
expectedConcreteCommit bool
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "Default branch",
|
||||
branch: defaultBranch,
|
||||
filesCreated: map[string]string{"branch": "second"},
|
||||
expectedCommit: secondCommit.String(),
|
||||
expectedConcreteCommit: true,
|
||||
},
|
||||
{
|
||||
name: "Other branch",
|
||||
branch: "test",
|
||||
filesCreated: map[string]string{"branch": "init"},
|
||||
expectedCommit: firstCommit.String(),
|
||||
expectedConcreteCommit: true,
|
||||
},
|
||||
{
|
||||
name: "Non existing branch",
|
||||
branch: "invalid",
|
||||
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,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
branch := CheckoutBranch{
|
||||
Branch: tt.branch,
|
||||
LastRevision: tt.lastRevision,
|
||||
}
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
authOpts := git.AuthOptions{
|
||||
TransportOptionsURL: getTransportOptionsURL(git.HTTP),
|
||||
}
|
||||
|
||||
cc, err := branch.Checkout(context.TODO(), tmpDir, repoURL, &authOpts)
|
||||
if tt.expectedErr != "" {
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
g.Expect(err.Error()).To(ContainSubstring(tt.expectedErr))
|
||||
g.Expect(cc).To(BeNil())
|
||||
return
|
||||
}
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
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))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckoutTag_Checkout(t *testing.T) {
|
||||
type testTag struct {
|
||||
name string
|
||||
annotated bool
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
tagsInRepo []testTag
|
||||
checkoutTag string
|
||||
lastRevTag string
|
||||
expectErr string
|
||||
expectConcreteCommit bool
|
||||
}{
|
||||
{
|
||||
name: "Tag",
|
||||
tagsInRepo: []testTag{{"tag-1", false}},
|
||||
checkoutTag: "tag-1",
|
||||
expectConcreteCommit: true,
|
||||
},
|
||||
{
|
||||
name: "Annotated",
|
||||
tagsInRepo: []testTag{{"annotated", true}},
|
||||
checkoutTag: "annotated",
|
||||
expectConcreteCommit: true,
|
||||
},
|
||||
{
|
||||
name: "Non existing tag",
|
||||
checkoutTag: "invalid",
|
||||
expectErr: "unable to find 'invalid': no reference found for shorthand 'invalid'",
|
||||
},
|
||||
{
|
||||
name: "Skip clone - last revision unchanged",
|
||||
tagsInRepo: []testTag{{"tag-1", false}},
|
||||
checkoutTag: "tag-1",
|
||||
lastRevTag: "tag-1",
|
||||
expectConcreteCommit: false,
|
||||
},
|
||||
{
|
||||
name: "Last revision changed",
|
||||
tagsInRepo: []testTag{{"tag-1", false}, {"tag-2", false}},
|
||||
checkoutTag: "tag-2",
|
||||
lastRevTag: "tag-1",
|
||||
expectConcreteCommit: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
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())
|
||||
defer repo.Free()
|
||||
|
||||
// Collect tags and their associated commit for later reference.
|
||||
tagCommits := map[string]*git2go.Commit{}
|
||||
|
||||
repoURL := server.HTTPAddress() + "/" + repoPath
|
||||
|
||||
// Populate the repo with commits and tags.
|
||||
if tt.tagsInRepo != nil {
|
||||
for _, tr := range tt.tagsInRepo {
|
||||
var commit *git2go.Commit
|
||||
c, err := commitFile(repo, "tag", tr.name, time.Now())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if commit, err = repo.LookupCommit(c); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = tag(repo, commit.Id(), tr.annotated, tr.name, time.Now())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tagCommits[tr.name] = commit
|
||||
}
|
||||
}
|
||||
|
||||
checkoutTag := 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()
|
||||
|
||||
authOpts := git.AuthOptions{
|
||||
TransportOptionsURL: getTransportOptionsURL(git.HTTP),
|
||||
}
|
||||
cc, err := checkoutTag.Checkout(context.TODO(), tmpDir, repoURL, &authOpts)
|
||||
if tt.expectErr != "" {
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
g.Expect(err.Error()).To(ContainSubstring(tt.expectErr))
|
||||
g.Expect(cc).To(BeNil())
|
||||
return
|
||||
}
|
||||
|
||||
// Check successful checkout results.
|
||||
targetTagCommit := tagCommits[tt.checkoutTag]
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
g.Expect(cc.String()).To(Equal(tt.checkoutTag + "/" + targetTagCommit.Id().String()))
|
||||
g.Expect(git.IsConcreteCommit(*cc)).To(Equal(tt.expectConcreteCommit))
|
||||
|
||||
// Check file content only when there's an actual checkout.
|
||||
if tt.lastRevTag != tt.checkoutTag {
|
||||
g.Expect(filepath.Join(tmpDir, "tag")).To(BeARegularFile())
|
||||
g.Expect(os.ReadFile(filepath.Join(tmpDir, "tag"))).To(BeEquivalentTo(tt.checkoutTag))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckoutCommit_Checkout(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
server, err := gittestserver.NewTempGitServer()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(server.Root())
|
||||
|
||||
err = server.StartHTTP()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer server.StopHTTP()
|
||||
|
||||
repoPath := "test.git"
|
||||
err = server.InitRepo("../testdata/git/repo", git.DefaultBranch, repoPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
repo, err := git2go.OpenRepository(filepath.Join(server.Root(), repoPath))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer repo.Free()
|
||||
|
||||
c, err := commitFile(repo, "commit", "init", time.Now())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err = commitFile(repo, "commit", "second", time.Now()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tmpDir := t.TempDir()
|
||||
authOpts := git.AuthOptions{
|
||||
TransportOptionsURL: getTransportOptionsURL(git.HTTP),
|
||||
}
|
||||
repoURL := server.HTTPAddress() + "/" + repoPath
|
||||
|
||||
commit := CheckoutCommit{
|
||||
Commit: c.String(),
|
||||
}
|
||||
|
||||
cc, err := commit.Checkout(context.TODO(), tmpDir, repoURL, &authOpts)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
g.Expect(cc).ToNot(BeNil())
|
||||
g.Expect(cc.String()).To(Equal("HEAD/" + c.String()))
|
||||
g.Expect(filepath.Join(tmpDir, "commit")).To(BeARegularFile())
|
||||
g.Expect(os.ReadFile(filepath.Join(tmpDir, "commit"))).To(BeEquivalentTo("init"))
|
||||
|
||||
commit = CheckoutCommit{
|
||||
Commit: "4dc3185c5fc94eb75048376edeb44571cece25f4",
|
||||
}
|
||||
tmpDir2 := t.TempDir()
|
||||
|
||||
cc, err = commit.Checkout(context.TODO(), tmpDir2, repoURL, &authOpts)
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
g.Expect(err.Error()).To(HavePrefix("git checkout error: git commit '4dc3185c5fc94eb75048376edeb44571cece25f4' not found:"))
|
||||
g.Expect(cc).To(BeNil())
|
||||
}
|
||||
|
||||
func TestCheckoutSemVer_Checkout(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
now := time.Now()
|
||||
|
||||
tags := []struct {
|
||||
tag string
|
||||
annotated bool
|
||||
commitTime time.Time
|
||||
tagTime time.Time
|
||||
}{
|
||||
{
|
||||
tag: "v0.0.1",
|
||||
annotated: false,
|
||||
commitTime: now,
|
||||
},
|
||||
{
|
||||
tag: "v0.1.0+build-1",
|
||||
annotated: true,
|
||||
commitTime: now.Add(10 * time.Minute),
|
||||
tagTime: now.Add(2 * time.Hour), // This should be ignored during TS comparisons
|
||||
},
|
||||
{
|
||||
tag: "v0.1.0+build-2",
|
||||
annotated: false,
|
||||
commitTime: now.Add(30 * time.Minute),
|
||||
},
|
||||
{
|
||||
tag: "v0.1.0+build-3",
|
||||
annotated: true,
|
||||
commitTime: now.Add(1 * time.Hour),
|
||||
tagTime: now.Add(1 * time.Hour), // This should be ignored during TS comparisons
|
||||
},
|
||||
{
|
||||
tag: "0.2.0",
|
||||
annotated: true,
|
||||
commitTime: now,
|
||||
tagTime: now,
|
||||
},
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
constraint string
|
||||
expectErr error
|
||||
expectTag string
|
||||
}{
|
||||
{
|
||||
name: "Orders by SemVer",
|
||||
constraint: ">0.1.0",
|
||||
expectTag: "0.2.0",
|
||||
},
|
||||
{
|
||||
name: "Orders by SemVer and timestamp",
|
||||
constraint: "<0.2.0",
|
||||
expectTag: "v0.1.0+build-3",
|
||||
},
|
||||
{
|
||||
name: "Errors without match",
|
||||
constraint: ">=1.0.0",
|
||||
expectErr: errors.New("no match found for semver: >=1.0.0"),
|
||||
},
|
||||
}
|
||||
|
||||
server, err := gittestserver.NewTempGitServer()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(server.Root())
|
||||
|
||||
err = server.StartHTTP()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer server.StopHTTP()
|
||||
|
||||
repoPath := "test.git"
|
||||
err = server.InitRepo("../testdata/git/repo", git.DefaultBranch, repoPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
repo, err := git2go.OpenRepository(filepath.Join(server.Root(), repoPath))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer repo.Free()
|
||||
repoURL := server.HTTPAddress() + "/" + repoPath
|
||||
|
||||
refs := make(map[string]string, len(tags))
|
||||
for _, tt := range tags {
|
||||
ref, err := commitFile(repo, "tag", tt.tag, tt.commitTime)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
commit, err := repo.LookupCommit(ref)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer commit.Free()
|
||||
refs[tt.tag] = commit.Id().String()
|
||||
_, err = tag(repo, ref, tt.annotated, tt.tag, tt.tagTime)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
c, err := repo.Tags.List()
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
g.Expect(c).To(HaveLen(len(tags)))
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
semVer := CheckoutSemVer{
|
||||
SemVer: tt.constraint,
|
||||
}
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
authOpts := git.AuthOptions{
|
||||
TransportOptionsURL: getTransportOptionsURL(git.HTTP),
|
||||
}
|
||||
|
||||
cc, err := semVer.Checkout(context.TODO(), tmpDir, repoURL, &authOpts)
|
||||
if tt.expectErr != nil {
|
||||
g.Expect(err).To(Equal(tt.expectErr))
|
||||
g.Expect(cc).To(BeNil())
|
||||
return
|
||||
}
|
||||
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
g.Expect(cc.String()).To(Equal(tt.expectTag + "/" + refs[tt.expectTag]))
|
||||
g.Expect(filepath.Join(tmpDir, "tag")).To(BeARegularFile())
|
||||
g.Expect(os.ReadFile(filepath.Join(tmpDir, "tag"))).To(BeEquivalentTo(tt.expectTag))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_initializeRepoWithRemote(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
tmp := t.TempDir()
|
||||
ctx := context.TODO()
|
||||
testRepoURL := "https://example.com/foo/bar"
|
||||
testRepoURL2 := "https://example.com/foo/baz"
|
||||
authOpts, err := git.AuthOptionsWithoutSecret(testRepoURL)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
authOpts.TransportOptionsURL = "https://bar123"
|
||||
authOpts2, err := git.AuthOptionsWithoutSecret(testRepoURL2)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
authOpts2.TransportOptionsURL = "https://baz789"
|
||||
|
||||
// Fresh initialization.
|
||||
repo, remote, err := initializeRepoWithRemote(ctx, tmp, testRepoURL, authOpts)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
g.Expect(repo.IsBare()).To(BeFalse())
|
||||
g.Expect(remote.Name()).To(Equal(defaultRemoteName))
|
||||
g.Expect(remote.Url()).To(Equal(authOpts.TransportOptionsURL))
|
||||
remote.Free()
|
||||
repo.Free()
|
||||
|
||||
// Reinitialize to ensure it reuses the existing origin.
|
||||
repo, remote, err = initializeRepoWithRemote(ctx, tmp, testRepoURL, authOpts)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
g.Expect(repo.IsBare()).To(BeFalse())
|
||||
g.Expect(remote.Name()).To(Equal(defaultRemoteName))
|
||||
g.Expect(remote.Url()).To(Equal(authOpts.TransportOptionsURL))
|
||||
remote.Free()
|
||||
repo.Free()
|
||||
|
||||
// Reinitialize with a different remote URL for existing origin.
|
||||
repo, remote, err = initializeRepoWithRemote(ctx, tmp, testRepoURL2, authOpts2)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
g.Expect(repo.IsBare()).To(BeFalse())
|
||||
g.Expect(remote.Name()).To(Equal(defaultRemoteName))
|
||||
g.Expect(remote.Url()).To(Equal(authOpts2.TransportOptionsURL))
|
||||
remote.Free()
|
||||
repo.Free()
|
||||
}
|
||||
|
||||
func TestCheckoutStrategyForOptions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
opts git.CheckoutOptions
|
||||
expectedStrat git.CheckoutStrategy
|
||||
}{
|
||||
{
|
||||
name: "commit works",
|
||||
opts: git.CheckoutOptions{
|
||||
Commit: "commit",
|
||||
},
|
||||
expectedStrat: &CheckoutCommit{
|
||||
Commit: "commit",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "semver works",
|
||||
opts: git.CheckoutOptions{
|
||||
SemVer: ">= 1.0.0",
|
||||
},
|
||||
expectedStrat: &CheckoutSemVer{
|
||||
SemVer: ">= 1.0.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "tag with latest revision works",
|
||||
opts: git.CheckoutOptions{
|
||||
Tag: "v0.1.0",
|
||||
LastRevision: "ar34oi2njrngjrng",
|
||||
},
|
||||
expectedStrat: &CheckoutTag{
|
||||
Tag: "v0.1.0",
|
||||
LastRevision: "ar34oi2njrngjrng",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "branch with latest revision works",
|
||||
opts: git.CheckoutOptions{
|
||||
Branch: "main",
|
||||
LastRevision: "rrgij20mkmrg",
|
||||
},
|
||||
expectedStrat: &CheckoutBranch{
|
||||
Branch: "main",
|
||||
LastRevision: "rrgij20mkmrg",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty branch falls back to default",
|
||||
opts: git.CheckoutOptions{},
|
||||
expectedStrat: &CheckoutBranch{
|
||||
Branch: git.DefaultBranch,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
strat := CheckoutStrategyForOptions(context.TODO(), tt.opts)
|
||||
g.Expect(strat).To(Equal(tt.expectedStrat))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func initBareRepo(t *testing.T) (*git2go.Repository, error) {
|
||||
tmpDir := t.TempDir()
|
||||
repo, err := git2go.InitRepository(tmpDir, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return repo, nil
|
||||
}
|
||||
|
||||
func createBranch(repo *git2go.Repository, branch string, commit *git2go.Commit) error {
|
||||
if commit == nil {
|
||||
var err error
|
||||
commit, err = headCommit(repo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer commit.Free()
|
||||
}
|
||||
_, err := repo.CreateBranch(branch, commit, false)
|
||||
return err
|
||||
}
|
||||
|
||||
func commitFile(repo *git2go.Repository, path, content string, time time.Time) (*git2go.Oid, error) {
|
||||
var parentC []*git2go.Commit
|
||||
head, err := headCommit(repo)
|
||||
if err == nil {
|
||||
defer head.Free()
|
||||
parentC = append(parentC, head)
|
||||
}
|
||||
|
||||
index, err := repo.Index()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer index.Free()
|
||||
|
||||
blobOID, err := repo.CreateBlobFromBuffer([]byte(content))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
entry := &git2go.IndexEntry{
|
||||
Mode: git2go.FilemodeBlob,
|
||||
Id: blobOID,
|
||||
Path: path,
|
||||
}
|
||||
|
||||
if err := index.Add(entry); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := index.Write(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
treeID, err := index.WriteTree()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tree, err := repo.LookupTree(treeID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tree.Free()
|
||||
|
||||
c, err := repo.CreateCommit("HEAD", mockSignature(time), mockSignature(time), "Committing "+path, tree, parentC...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func tag(repo *git2go.Repository, cId *git2go.Oid, annotated bool, tag string, time time.Time) (*git2go.Oid, error) {
|
||||
commit, err := repo.LookupCommit(cId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if annotated {
|
||||
return repo.Tags.Create(tag, commit, mockSignature(time), fmt.Sprintf("Annotated tag for %s", tag))
|
||||
}
|
||||
return repo.Tags.CreateLightweight(tag, commit, false)
|
||||
}
|
||||
|
||||
func mockSignature(time time.Time) *git2go.Signature {
|
||||
return &git2go.Signature{
|
||||
Name: "Jane Doe",
|
||||
Email: "author@example.com",
|
||||
When: time,
|
||||
}
|
||||
}
|
||||
|
||||
func getTransportOptionsURL(transport git.TransportType) string {
|
||||
letterRunes := []rune("abcdefghijklmnopqrstuvwxyz1234567890")
|
||||
b := make([]rune, 10)
|
||||
for i := range b {
|
||||
b[i] = letterRunes[rand.Intn(len(letterRunes))]
|
||||
}
|
||||
return string(transport) + "://" + string(b)
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
/*
|
||||
Copyright 2021 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 libgit2
|
||||
|
||||
import "github.com/fluxcd/source-controller/pkg/git"
|
||||
|
||||
const (
|
||||
Implementation git.Implementation = "libgit2"
|
||||
)
|
|
@ -1,27 +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
|
||||
|
||||
const (
|
||||
// URLMaxLength represents the max length for the entire URL
|
||||
// when cloning Git repositories via HTTP(S).
|
||||
URLMaxLength = 2048
|
||||
|
||||
// PathMaxLength represents the max length for the path element
|
||||
// when cloning Git repositories via SSH.
|
||||
PathMaxLength = 4096
|
||||
)
|
|
@ -1,480 +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.
|
||||
*/
|
||||
|
||||
/*
|
||||
This was inspired and contains part of:
|
||||
https://github.com/libgit2/git2go/blob/eae00773cce87d5282a8ac7c10b5c1961ee6f9cb/http.go
|
||||
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2013 The git2go contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
*/
|
||||
|
||||
package managed
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/fluxcd/pkg/runtime/logger"
|
||||
pool "github.com/fluxcd/source-controller/internal/transport"
|
||||
"github.com/fluxcd/source-controller/pkg/git"
|
||||
"github.com/go-logr/logr"
|
||||
git2go "github.com/libgit2/git2go/v33"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
)
|
||||
|
||||
var actionSuffixes = []string{
|
||||
"/info/refs?service=git-upload-pack",
|
||||
"/git-upload-pack",
|
||||
"/info/refs?service=git-receive-pack",
|
||||
"/git-receive-pack",
|
||||
}
|
||||
|
||||
// registerManagedHTTP registers a Go-native implementation of an
|
||||
// HTTP(S) transport that doesn't rely on any lower-level libraries
|
||||
// such as OpenSSL.
|
||||
func registerManagedHTTP() error {
|
||||
for _, protocol := range []string{"http", "https"} {
|
||||
_, err := git2go.NewRegisteredSmartTransport(protocol, true, httpSmartSubtransportFactory)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to register transport for %q: %v", protocol, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func httpSmartSubtransportFactory(remote *git2go.Remote, transport *git2go.Transport) (git2go.SmartSubtransport, error) {
|
||||
sst := &httpSmartSubtransport{
|
||||
transport: transport,
|
||||
httpTransport: pool.NewOrIdle(nil),
|
||||
ctx: context.Background(),
|
||||
logger: logr.Discard(),
|
||||
}
|
||||
|
||||
return sst, nil
|
||||
}
|
||||
|
||||
type httpSmartSubtransport struct {
|
||||
transport *git2go.Transport
|
||||
httpTransport *http.Transport
|
||||
|
||||
// once is used to ensure that logger and ctx is set only once,
|
||||
// on the initial (or only) Action call. Without this a mutex must
|
||||
// be applied to ensure that ctx won't be changed, as this would be
|
||||
// prone to race conditions in the stdout processing goroutine.
|
||||
once sync.Once
|
||||
// ctx defines the context to be used across long-running or
|
||||
// cancellable operations.
|
||||
// Defaults to context.Background().
|
||||
ctx context.Context
|
||||
// logger keeps a Logger instance for logging. This was preferred
|
||||
// due to the need to have a correlation ID and URL set and
|
||||
// reused across all log calls.
|
||||
// If context is not set, this defaults to logr.Discard().
|
||||
logger logr.Logger
|
||||
}
|
||||
|
||||
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)
|
||||
proxyOpts := opts.ProxyOptions
|
||||
if proxyOpts != nil {
|
||||
switch proxyOpts.Type {
|
||||
case git2go.ProxyTypeNone:
|
||||
proxyFn = nil
|
||||
case git2go.ProxyTypeAuto:
|
||||
proxyFn = http.ProxyFromEnvironment
|
||||
case git2go.ProxyTypeSpecified:
|
||||
parsedUrl, err := url.Parse(proxyOpts.Url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
proxyFn = http.ProxyURL(parsedUrl)
|
||||
}
|
||||
t.httpTransport.Proxy = proxyFn
|
||||
t.httpTransport.ProxyConnectHeader = map[string][]string{}
|
||||
} else {
|
||||
t.httpTransport.Proxy = nil
|
||||
}
|
||||
t.httpTransport.DisableCompression = false
|
||||
|
||||
t.once.Do(func() {
|
||||
if opts.Context != nil {
|
||||
t.ctx = opts.Context
|
||||
t.logger = ctrl.LoggerFrom(t.ctx,
|
||||
"transportType", "http",
|
||||
"url", opts.TargetURL)
|
||||
}
|
||||
})
|
||||
|
||||
client, req, err := createClientRequest(targetURL, action, t.httpTransport, opts.AuthOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stream := newManagedHttpStream(t, req, client)
|
||||
if req.Method == "POST" {
|
||||
stream.recvReply.Add(1)
|
||||
stream.sendRequestBackground()
|
||||
}
|
||||
|
||||
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
||||
if len(via) >= 3 {
|
||||
return fmt.Errorf("too many redirects")
|
||||
}
|
||||
|
||||
// golang will change POST to GET in case of redirects.
|
||||
if len(via) >= 0 && req.Method != via[0].Method {
|
||||
if via[0].URL.Scheme == "https" && req.URL.Scheme == "http" {
|
||||
return fmt.Errorf("downgrade from https to http is not allowed: from %q to %q", via[0].URL.String(), req.URL.String())
|
||||
}
|
||||
if via[0].URL.Host != req.URL.Host {
|
||||
return fmt.Errorf("cross hosts redirects are not allowed: from %s to %s", via[0].URL.Host, req.URL.Host)
|
||||
}
|
||||
|
||||
return http.ErrUseLastResponse
|
||||
}
|
||||
|
||||
// Some Git servers (i.e. Gitlab) only support redirection on the GET operations.
|
||||
// Therefore, on the initial GET operation we update the target URL to include the
|
||||
// new target, so the subsequent actions include the correct target URL.
|
||||
// Example of this is trying to access a Git repository without the .git suffix.
|
||||
if req.Response != nil {
|
||||
if newURL, err := req.Response.Location(); err == nil && newURL != nil {
|
||||
if strings.EqualFold(newURL.Host, req.URL.Host) && strings.EqualFold(newURL.Port(), req.URL.Port()) {
|
||||
opts, _ := getTransportOptions(transportOptionsURL)
|
||||
if opts == nil {
|
||||
opts = &TransportOptions{}
|
||||
}
|
||||
|
||||
opts.TargetURL = trimActionSuffix(newURL.String())
|
||||
AddTransportOptions(transportOptionsURL, *opts)
|
||||
|
||||
// show as info, as this should be visible regardless of the
|
||||
// chosen log-level.
|
||||
t.logger.Info("server responded with redirect",
|
||||
"newUrl", opts.TargetURL, "StatusCode", req.Response.StatusCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return stream, nil
|
||||
}
|
||||
|
||||
func trimActionSuffix(url string) string {
|
||||
newUrl := url
|
||||
for _, s := range actionSuffixes {
|
||||
newUrl = strings.TrimSuffix(newUrl, s)
|
||||
}
|
||||
|
||||
return newUrl
|
||||
}
|
||||
|
||||
func createClientRequest(targetURL string, action git2go.SmartServiceAction,
|
||||
t *http.Transport, authOpts *git.AuthOptions) (*http.Client, *http.Request, error) {
|
||||
var req *http.Request
|
||||
var err error
|
||||
|
||||
if t == nil {
|
||||
return nil, nil, fmt.Errorf("failed to create client: transport cannot be nil")
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Transport: t,
|
||||
Timeout: fullHttpClientTimeOut,
|
||||
}
|
||||
|
||||
switch action {
|
||||
case git2go.SmartServiceActionUploadpackLs:
|
||||
req, err = http.NewRequest("GET", targetURL+"/info/refs?service=git-upload-pack", nil)
|
||||
|
||||
case git2go.SmartServiceActionUploadpack:
|
||||
req, err = http.NewRequest("POST", targetURL+"/git-upload-pack", nil)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
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:
|
||||
req, err = http.NewRequest("GET", targetURL+"/info/refs?service=git-receive-pack", nil)
|
||||
|
||||
case git2go.SmartServiceActionReceivepack:
|
||||
req, err = http.NewRequest("POST", targetURL+"/git-receive-pack", nil)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
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:
|
||||
err = errors.New("unknown action")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Apply authentication and TLS settings to the HTTP transport.
|
||||
if authOpts != nil {
|
||||
if authOpts.Username != "" && authOpts.Password != "" {
|
||||
req.SetBasicAuth(authOpts.Username, authOpts.Password)
|
||||
}
|
||||
if len(authOpts.CAFile) > 0 {
|
||||
certPool := x509.NewCertPool()
|
||||
if ok := certPool.AppendCertsFromPEM(authOpts.CAFile); !ok {
|
||||
return nil, nil, fmt.Errorf("PEM CA bundle could not be appended to x509 certificate pool")
|
||||
}
|
||||
t.TLSClientConfig = &tls.Config{
|
||||
RootCAs: certPool,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (t *httpSmartSubtransport) Close() error {
|
||||
t.logger.V(logger.TraceLevel).Info("httpSmartSubtransport.Close()")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *httpSmartSubtransport) Free() {
|
||||
t.logger.V(logger.TraceLevel).Info("httpSmartSubtransport.Free()")
|
||||
|
||||
if t.httpTransport != nil {
|
||||
t.logger.V(logger.TraceLevel).Info("release http transport back to pool")
|
||||
|
||||
pool.Release(t.httpTransport)
|
||||
t.httpTransport = nil
|
||||
}
|
||||
}
|
||||
|
||||
type httpSmartSubtransportStream struct {
|
||||
owner *httpSmartSubtransport
|
||||
client *http.Client
|
||||
req *http.Request
|
||||
resp *http.Response
|
||||
reader *io.PipeReader
|
||||
writer *io.PipeWriter
|
||||
sentRequest bool
|
||||
recvReply sync.WaitGroup
|
||||
httpError error
|
||||
m sync.RWMutex
|
||||
}
|
||||
|
||||
func newManagedHttpStream(owner *httpSmartSubtransport, req *http.Request, client *http.Client) *httpSmartSubtransportStream {
|
||||
r, w := io.Pipe()
|
||||
return &httpSmartSubtransportStream{
|
||||
owner: owner,
|
||||
client: client,
|
||||
req: req,
|
||||
reader: r,
|
||||
writer: w,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *httpSmartSubtransportStream) Read(buf []byte) (int, error) {
|
||||
if !self.sentRequest {
|
||||
self.recvReply.Add(1)
|
||||
if err := self.sendRequest(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := self.writer.Close(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
self.recvReply.Wait()
|
||||
|
||||
self.m.RLock()
|
||||
err := self.httpError
|
||||
self.m.RUnlock()
|
||||
|
||||
if err != nil {
|
||||
return 0, self.httpError
|
||||
}
|
||||
return self.resp.Body.Read(buf)
|
||||
}
|
||||
|
||||
func (self *httpSmartSubtransportStream) Write(buf []byte) (int, error) {
|
||||
self.m.RLock()
|
||||
err := self.httpError
|
||||
self.m.RUnlock()
|
||||
|
||||
if err != nil {
|
||||
return 0, self.httpError
|
||||
}
|
||||
return self.writer.Write(buf)
|
||||
}
|
||||
|
||||
func (self *httpSmartSubtransportStream) Free() {
|
||||
if self.resp != nil {
|
||||
self.owner.logger.V(logger.TraceLevel).Info("httpSmartSubtransportStream.Free()")
|
||||
|
||||
if self.resp.Body != nil {
|
||||
// ensure body is fully processed and closed
|
||||
// for increased likelihood of transport reuse in HTTP/1.x.
|
||||
// it should not be a problem to do this more than once.
|
||||
if _, err := io.Copy(io.Discard, self.resp.Body); err != nil {
|
||||
self.owner.logger.V(logger.TraceLevel).Error(err, "cannot discard response body")
|
||||
}
|
||||
|
||||
if err := self.resp.Body.Close(); err != nil {
|
||||
self.owner.logger.V(logger.TraceLevel).Error(err, "cannot close response body")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (self *httpSmartSubtransportStream) sendRequestBackground() {
|
||||
go func() {
|
||||
err := self.sendRequest()
|
||||
|
||||
self.m.Lock()
|
||||
self.httpError = err
|
||||
self.m.Unlock()
|
||||
}()
|
||||
self.sentRequest = true
|
||||
}
|
||||
|
||||
func (self *httpSmartSubtransportStream) sendRequest() error {
|
||||
defer self.recvReply.Done()
|
||||
self.resp = nil
|
||||
|
||||
var resp *http.Response
|
||||
var err error
|
||||
var content []byte
|
||||
|
||||
for {
|
||||
req := &http.Request{
|
||||
Method: self.req.Method,
|
||||
URL: self.req.URL,
|
||||
Header: self.req.Header,
|
||||
}
|
||||
req = req.WithContext(self.owner.ctx)
|
||||
|
||||
if req.Method == "POST" {
|
||||
if len(content) == 0 {
|
||||
// a copy of the request body needs to be saved so
|
||||
// it can be reused in case of redirects.
|
||||
if content, err = io.ReadAll(self.reader); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
req.Body = io.NopCloser(bytes.NewReader(content))
|
||||
req.ContentLength = -1
|
||||
}
|
||||
|
||||
self.owner.logger.V(logger.TraceLevel).Info("new request", "method", req.Method, "postUrl", req.URL)
|
||||
resp, err = self.client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// GET requests will be automatically redirected.
|
||||
// POST require the new destination, and also the body content.
|
||||
if req.Method == "POST" && resp.StatusCode >= 301 && resp.StatusCode <= 308 {
|
||||
// ensure body is fully processed and closed
|
||||
// for increased likelihood of transport reuse in HTTP/1.x.
|
||||
_, _ = io.Copy(io.Discard, resp.Body) // errors can be safely ignored
|
||||
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// The next try will go against the new destination
|
||||
self.req.URL, err = resp.Location()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// for HTTP 200, the response will be cleared up by Free()
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
break
|
||||
}
|
||||
|
||||
// ensure body is fully processed and closed
|
||||
// for increased likelihood of transport reuse in HTTP/1.x.
|
||||
_, _ = io.Copy(io.Discard, resp.Body) // errors can be safely ignored
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return fmt.Errorf("unhandled HTTP error %s", resp.Status)
|
||||
}
|
||||
|
||||
self.resp = resp
|
||||
self.sentRequest = true
|
||||
return nil
|
||||
}
|
|
@ -1,292 +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 (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/fluxcd/pkg/gittestserver"
|
||||
"github.com/fluxcd/source-controller/pkg/git"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
git2go "github.com/libgit2/git2go/v33"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
err := InitManagedTransport()
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("failed to initialize libgit2 managed transport: %s", err))
|
||||
}
|
||||
code := m.Run()
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func TestHttpAction_CreateClientRequest(t *testing.T) {
|
||||
authOpts := git.AuthOptions{
|
||||
Username: "user",
|
||||
Password: "pwd",
|
||||
}
|
||||
url := "https://final-target/abc"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
assertFunc func(g *WithT, req *http.Request, client *http.Client)
|
||||
action git2go.SmartServiceAction
|
||||
authOpts git.AuthOptions
|
||||
transport *http.Transport
|
||||
wantedErr error
|
||||
}{
|
||||
{
|
||||
name: "Uploadpack: URL, method and headers are correctly set",
|
||||
action: git2go.SmartServiceActionUploadpack,
|
||||
transport: &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
ProxyConnectHeader: map[string][]string{},
|
||||
},
|
||||
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.Method).To(Equal("POST"))
|
||||
g.Expect(req.Header).To(BeEquivalentTo(map[string][]string{
|
||||
"User-Agent": {"git/2.0 (flux-libgit2)"},
|
||||
"Content-Type": {"application/x-git-upload-pack-request"},
|
||||
}))
|
||||
},
|
||||
wantedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "UploadpackLs: URL, method and headers are correctly set",
|
||||
action: git2go.SmartServiceActionUploadpackLs,
|
||||
transport: &http.Transport{},
|
||||
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.Method).To(Equal("GET"))
|
||||
g.Expect(req.Header).To(BeEquivalentTo(map[string][]string{
|
||||
"User-Agent": {"git/2.0 (flux-libgit2)"},
|
||||
}))
|
||||
},
|
||||
wantedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "Receivepack: URL, method and headers are correctly set",
|
||||
action: git2go.SmartServiceActionReceivepack,
|
||||
transport: &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
ProxyConnectHeader: map[string][]string{},
|
||||
},
|
||||
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.Method).To(Equal("POST"))
|
||||
g.Expect(req.Header).To(BeEquivalentTo(map[string][]string{
|
||||
"Content-Type": {"application/x-git-receive-pack-request"},
|
||||
"User-Agent": {"git/2.0 (flux-libgit2)"},
|
||||
}))
|
||||
},
|
||||
wantedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "ReceivepackLs: URL, method and headars are correctly set",
|
||||
action: git2go.SmartServiceActionReceivepackLs,
|
||||
transport: &http.Transport{},
|
||||
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.Method).To(Equal("GET"))
|
||||
g.Expect(req.Header).To(BeEquivalentTo(map[string][]string{
|
||||
"User-Agent": {"git/2.0 (flux-libgit2)"},
|
||||
}))
|
||||
},
|
||||
wantedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "incomplete credentials, no basic auth",
|
||||
action: git2go.SmartServiceActionReceivepackLs,
|
||||
transport: &http.Transport{},
|
||||
authOpts: git.AuthOptions{Username: "user"},
|
||||
assertFunc: func(g *WithT, req *http.Request, client *http.Client) {
|
||||
_, _, ok := req.BasicAuth()
|
||||
g.Expect(ok).To(BeFalse())
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "credentials are correctly configured",
|
||||
action: git2go.SmartServiceActionUploadpack,
|
||||
transport: &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
ProxyConnectHeader: map[string][]string{},
|
||||
},
|
||||
authOpts: authOpts,
|
||||
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.Method).To(Equal("POST"))
|
||||
|
||||
username, pwd, ok := req.BasicAuth()
|
||||
if !ok {
|
||||
t.Errorf("could not find Authentication header in request.")
|
||||
}
|
||||
g.Expect(username).To(Equal("user"))
|
||||
g.Expect(pwd).To(Equal("pwd"))
|
||||
},
|
||||
wantedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "error when no http.transport provided",
|
||||
action: git2go.SmartServiceActionUploadpack,
|
||||
transport: nil,
|
||||
wantedErr: fmt.Errorf("failed to create client: transport cannot be nil"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
client, req, err := createClientRequest(url, tt.action, tt.transport, &tt.authOpts)
|
||||
if err != nil {
|
||||
t.Log(err)
|
||||
}
|
||||
if tt.wantedErr != nil {
|
||||
g.Expect(err).To(Equal(tt.wantedErr))
|
||||
} else {
|
||||
tt.assertFunc(g, req, client)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTP_E2E(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
server, err := gittestserver.NewTempGitServer()
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
defer os.RemoveAll(server.Root())
|
||||
|
||||
user := "test-user"
|
||||
pwd := "test-pswd"
|
||||
server.Auth(user, pwd)
|
||||
server.KeyDir(filepath.Join(server.Root(), "keys"))
|
||||
|
||||
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())
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Register the auth options and target url mapped to a unique url.
|
||||
id := "http://obj-id"
|
||||
AddTransportOptions(id, TransportOptions{
|
||||
TargetURL: server.HTTPAddress() + "/" + repoPath,
|
||||
AuthOpts: &git.AuthOptions{
|
||||
Username: user,
|
||||
Password: pwd,
|
||||
},
|
||||
})
|
||||
|
||||
// We call git2go.Clone with transportOptsURL instead of the actual URL,
|
||||
// as the transport action will fetch the actual URL and the required
|
||||
// credentials using the it as an identifier.
|
||||
repo, err := git2go.Clone(id, tmpDir, &git2go.CloneOptions{
|
||||
CheckoutOptions: git2go.CheckoutOptions{
|
||||
Strategy: git2go.CheckoutForce,
|
||||
},
|
||||
})
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
repo.Free()
|
||||
}
|
||||
|
||||
func TestTrimActionSuffix(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
inURL string
|
||||
wantURL string
|
||||
}{
|
||||
{
|
||||
name: "ignore other suffixes",
|
||||
inURL: "https://gitlab/repo/podinfo.git/somethingelse",
|
||||
wantURL: "https://gitlab/repo/podinfo.git/somethingelse",
|
||||
},
|
||||
{
|
||||
name: "trim /info/refs?service=git-upload-pack",
|
||||
inURL: "https://gitlab/repo/podinfo.git/info/refs?service=git-upload-pack",
|
||||
wantURL: "https://gitlab/repo/podinfo.git",
|
||||
},
|
||||
{
|
||||
name: "trim /git-upload-pack",
|
||||
inURL: "https://gitlab/repo/podinfo.git/git-upload-pack",
|
||||
wantURL: "https://gitlab/repo/podinfo.git",
|
||||
},
|
||||
{
|
||||
name: "trim /info/refs?service=git-receive-pack",
|
||||
inURL: "https://gitlab/repo/podinfo.git/info/refs?service=git-receive-pack",
|
||||
wantURL: "https://gitlab/repo/podinfo.git",
|
||||
},
|
||||
{
|
||||
name: "trim /git-receive-pack",
|
||||
inURL: "https://gitlab/repo/podinfo.git/git-receive-pack",
|
||||
wantURL: "https://gitlab/repo/podinfo.git",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
gotURL := trimActionSuffix(tt.inURL)
|
||||
g.Expect(gotURL).To(Equal(tt.wantURL))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTP_HandleRedirect(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
repoURL string
|
||||
}{
|
||||
{name: "http to https", repoURL: "http://github.com/stefanprodan/podinfo"},
|
||||
{name: "handle gitlab redirect", repoURL: "https://gitlab.com/stefanprodan/podinfo"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
id := "http://obj-id"
|
||||
AddTransportOptions(id, TransportOptions{
|
||||
TargetURL: tt.repoURL,
|
||||
})
|
||||
|
||||
// GitHub will cause a 301 and redirect to https
|
||||
repo, err := git2go.Clone(id, tmpDir, &git2go.CloneOptions{
|
||||
CheckoutOptions: git2go.CheckoutOptions{
|
||||
Strategy: git2go.CheckoutForce,
|
||||
},
|
||||
})
|
||||
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
repo.Free()
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,75 +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 (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
once sync.Once
|
||||
|
||||
// sshConnectionTimeOut defines the timeout used for when
|
||||
// creating ssh.ClientConfig, which translates in the timeout
|
||||
// for stablishing the SSH TCP connections.
|
||||
sshConnectionTimeOut time.Duration = 30 * time.Second
|
||||
|
||||
// fullHttpClientTimeOut defines the maximum amount of
|
||||
// time a http client may take before timing out,
|
||||
// regardless of the current operation (i.e. connection,
|
||||
// handshake, put/get).
|
||||
fullHttpClientTimeOut time.Duration = 10 * time.Minute
|
||||
|
||||
enabled bool
|
||||
)
|
||||
|
||||
// Enabled defines whether the use of Managed Transport is enabled which
|
||||
// is only true if InitManagedTransport was called successfully at least
|
||||
// once.
|
||||
//
|
||||
// This is only affects git operations that uses libgit2 implementation.
|
||||
func Enabled() bool {
|
||||
return enabled
|
||||
}
|
||||
|
||||
// InitManagedTransport initialises HTTP(S) and SSH managed transport
|
||||
// for git2go, and therefore only impact git operations using the
|
||||
// libgit2 implementation.
|
||||
//
|
||||
// This must run after git2go.init takes place, hence this is not executed
|
||||
// within a init().
|
||||
// Regardless of the state in libgit2/git2go, this will replace the
|
||||
// built-in transports.
|
||||
//
|
||||
// This function will only register managed transports once, subsequent calls
|
||||
// leads to no-op.
|
||||
func InitManagedTransport() error {
|
||||
var err error
|
||||
|
||||
once.Do(func() {
|
||||
if err = registerManagedHTTP(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = registerManagedSSH(); err == nil {
|
||||
enabled = true
|
||||
}
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
|
@ -1,70 +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 (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/fluxcd/source-controller/pkg/git"
|
||||
git2go "github.com/libgit2/git2go/v33"
|
||||
)
|
||||
|
||||
// TransportOptions represents options to be applied at transport-level
|
||||
// at request time.
|
||||
type TransportOptions struct {
|
||||
TargetURL string
|
||||
AuthOpts *git.AuthOptions
|
||||
ProxyOptions *git2go.ProxyOptions
|
||||
Context context.Context
|
||||
}
|
||||
|
||||
var (
|
||||
// transportOpts maps a unique URL to a set of transport options.
|
||||
transportOpts = make(map[string]TransportOptions, 0)
|
||||
m sync.RWMutex
|
||||
)
|
||||
|
||||
// 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()
|
||||
transportOpts[transportOptsURL] = opts
|
||||
m.Unlock()
|
||||
}
|
||||
|
||||
// RemoveTransportOptions removes the registerd TransportOptions object
|
||||
// mapped to the provided id.
|
||||
func RemoveTransportOptions(transportOptsURL string) {
|
||||
m.Lock()
|
||||
delete(transportOpts, transportOptsURL)
|
||||
m.Unlock()
|
||||
}
|
||||
|
||||
func getTransportOptions(transportOptsURL string) (*TransportOptions, bool) {
|
||||
m.RLock()
|
||||
opts, found := transportOpts[transportOptsURL]
|
||||
m.RUnlock()
|
||||
|
||||
if found {
|
||||
return &opts, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
|
@ -1,94 +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 (
|
||||
"testing"
|
||||
|
||||
"github.com/fluxcd/source-controller/pkg/git"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestTransportOptions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
registerOpts bool
|
||||
url string
|
||||
opts TransportOptions
|
||||
expectOpts bool
|
||||
expectedOpts *TransportOptions
|
||||
}{
|
||||
{
|
||||
name: "return registered option",
|
||||
registerOpts: true,
|
||||
url: "https://target/?123",
|
||||
opts: TransportOptions{},
|
||||
expectOpts: true,
|
||||
expectedOpts: &TransportOptions{},
|
||||
},
|
||||
{
|
||||
name: "match registered options",
|
||||
registerOpts: true,
|
||||
url: "https://target/?876",
|
||||
opts: TransportOptions{
|
||||
TargetURL: "https://new-target/321",
|
||||
AuthOpts: &git.AuthOptions{
|
||||
CAFile: []byte{123, 213, 132},
|
||||
},
|
||||
},
|
||||
expectOpts: true,
|
||||
expectedOpts: &TransportOptions{
|
||||
TargetURL: "https://new-target/321",
|
||||
AuthOpts: &git.AuthOptions{
|
||||
CAFile: []byte{123, 213, 132},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ignore when options not registered",
|
||||
registerOpts: false,
|
||||
url: "",
|
||||
opts: TransportOptions{},
|
||||
expectOpts: false,
|
||||
expectedOpts: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
if tt.registerOpts {
|
||||
AddTransportOptions(tt.url, tt.opts)
|
||||
}
|
||||
|
||||
opts, found := getTransportOptions(tt.url)
|
||||
g.Expect(found).To(Equal(found))
|
||||
|
||||
if tt.expectOpts {
|
||||
g.Expect(tt.expectedOpts).To(Equal(opts))
|
||||
}
|
||||
|
||||
if tt.registerOpts {
|
||||
RemoveTransportOptions(tt.url)
|
||||
}
|
||||
|
||||
_, found = getTransportOptions(tt.url)
|
||||
g.Expect(found).To(BeFalse())
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,386 +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.
|
||||
*/
|
||||
|
||||
/*
|
||||
This was inspired and contains part of:
|
||||
https://github.com/libgit2/git2go/blob/eae00773cce87d5282a8ac7c10b5c1961ee6f9cb/ssh.go
|
||||
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2013 The git2go contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
*/
|
||||
|
||||
package managed
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
"golang.org/x/net/proxy"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
|
||||
"github.com/fluxcd/pkg/runtime/logger"
|
||||
"github.com/fluxcd/source-controller/pkg/git"
|
||||
"github.com/go-logr/logr"
|
||||
git2go "github.com/libgit2/git2go/v33"
|
||||
)
|
||||
|
||||
// registerManagedSSH registers a Go-native implementation of
|
||||
// SSH transport that doesn't rely on any lower-level libraries
|
||||
// such as libssh2.
|
||||
func registerManagedSSH() error {
|
||||
for _, protocol := range []string{"ssh", "ssh+git", "git+ssh"} {
|
||||
_, err := git2go.NewRegisteredSmartTransport(protocol, false, sshSmartSubtransportFactory)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to register transport for %q: %v", protocol, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func sshSmartSubtransportFactory(remote *git2go.Remote, transport *git2go.Transport) (git2go.SmartSubtransport, error) {
|
||||
var closed int32 = 0
|
||||
return &sshSmartSubtransport{
|
||||
transport: transport,
|
||||
ctx: context.Background(),
|
||||
logger: logr.Discard(),
|
||||
closedSessions: &closed,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type sshSmartSubtransport struct {
|
||||
transport *git2go.Transport
|
||||
|
||||
// once is used to ensure that logger and ctx is set only once,
|
||||
// on the initial (or only) Action call. Without this a mutex must
|
||||
// be applied to ensure that ctx won't be changed, as this would be
|
||||
// prone to race conditions in the stdout processing goroutine.
|
||||
once sync.Once
|
||||
// ctx defines the context to be used across long-running or
|
||||
// cancellable operations.
|
||||
// Defaults to context.Background().
|
||||
ctx context.Context
|
||||
// logger keeps a Logger instance for logging. This was preferred
|
||||
// due to the need to have a correlation ID and Address set and
|
||||
// reused across all log calls.
|
||||
// If context is not set, this defaults to logr.Discard().
|
||||
logger logr.Logger
|
||||
|
||||
lastAction git2go.SmartServiceAction
|
||||
stdin io.WriteCloser
|
||||
stdout io.Reader
|
||||
|
||||
closedSessions *int32
|
||||
|
||||
client *ssh.Client
|
||||
session *ssh.Session
|
||||
currentStream *sshSmartSubtransportStream
|
||||
connected bool
|
||||
}
|
||||
|
||||
func (t *sshSmartSubtransport) Action(transportOptionsURL string, action git2go.SmartServiceAction) (git2go.SmartSubtransportStream, error) {
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
|
||||
opts, found := getTransportOptions(transportOptionsURL)
|
||||
if !found {
|
||||
return nil, fmt.Errorf("could not find transport options for object: %s", transportOptionsURL)
|
||||
}
|
||||
|
||||
u, err := url.Parse(opts.TargetURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(u.Path) > PathMaxLength {
|
||||
return nil, fmt.Errorf("path exceeds the max length (%d)", PathMaxLength)
|
||||
}
|
||||
|
||||
// decode URI's path
|
||||
uPath, err := url.PathUnescape(u.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Escape \ and '.
|
||||
uPath = strings.Replace(uPath, `\`, `\\`, -1)
|
||||
uPath = strings.Replace(uPath, `'`, `\'`, -1)
|
||||
|
||||
var cmd string
|
||||
switch action {
|
||||
case git2go.SmartServiceActionUploadpackLs, git2go.SmartServiceActionUploadpack:
|
||||
if t.currentStream != nil {
|
||||
if t.lastAction == git2go.SmartServiceActionUploadpackLs {
|
||||
return t.currentStream, nil
|
||||
}
|
||||
}
|
||||
cmd = fmt.Sprintf("git-upload-pack '%s'", uPath)
|
||||
|
||||
case git2go.SmartServiceActionReceivepackLs, git2go.SmartServiceActionReceivepack:
|
||||
if t.currentStream != nil {
|
||||
if t.lastAction == git2go.SmartServiceActionReceivepackLs {
|
||||
return t.currentStream, nil
|
||||
}
|
||||
}
|
||||
cmd = fmt.Sprintf("git-receive-pack '%s'", uPath)
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected action: %v", action)
|
||||
}
|
||||
|
||||
port := "22"
|
||||
if u.Port() != "" {
|
||||
port = u.Port()
|
||||
}
|
||||
addr := net.JoinHostPort(u.Hostname(), port)
|
||||
|
||||
t.once.Do(func() {
|
||||
if opts.Context != nil {
|
||||
t.ctx = opts.Context
|
||||
t.logger = ctrl.LoggerFrom(t.ctx,
|
||||
"transportType", "ssh",
|
||||
"addr", addr)
|
||||
}
|
||||
})
|
||||
|
||||
sshConfig, err := createClientConfig(opts.AuthOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sshConfig.HostKeyCallback = func(hostname string, remote net.Addr, key ssh.PublicKey) error {
|
||||
keyHash := sha256.Sum256(key.Marshal())
|
||||
return CheckKnownHost(hostname, opts.AuthOpts.KnownHosts, keyHash[:])
|
||||
}
|
||||
|
||||
if t.connected {
|
||||
// The connection is no longer shared across actions, so ensures
|
||||
// all has been released before starting a new connection.
|
||||
_ = t.Close()
|
||||
}
|
||||
|
||||
err = t.createConn(addr, sshConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t.logger.V(logger.TraceLevel).Info("creating new ssh session")
|
||||
if t.session, err = t.client.NewSession(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if t.stdin, err = t.session.StdinPipe(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var w *io.PipeWriter
|
||||
var reader io.Reader
|
||||
t.stdout, w = io.Pipe()
|
||||
if reader, err = t.session.StdoutPipe(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If the session's stdout pipe is not serviced fast
|
||||
// enough it may cause the remote command to block.
|
||||
//
|
||||
// xref: https://github.com/golang/crypto/blob/eb4f295cb31f7fb5d52810411604a2638c9b19a2/ssh/session.go#L553-L558
|
||||
go func() error {
|
||||
defer func() {
|
||||
w.Close()
|
||||
|
||||
// In case this goroutine panics, handle recovery.
|
||||
if r := recover(); r != nil {
|
||||
t.logger.V(logger.TraceLevel).Error(errors.New(r.(string)),
|
||||
"recovered from libgit2 ssh smart subtransport panic")
|
||||
}
|
||||
}()
|
||||
var cancel context.CancelFunc
|
||||
ctx := t.ctx
|
||||
|
||||
// When context is nil, creates a new with internal SSH connection timeout.
|
||||
if ctx == nil {
|
||||
ctx, cancel = context.WithTimeout(context.Background(), sshConnectionTimeOut)
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
closedAlready := atomic.LoadInt32(t.closedSessions)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Close()
|
||||
return nil
|
||||
|
||||
default:
|
||||
if atomic.LoadInt32(t.closedSessions) > closedAlready {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := io.Copy(w, reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
t.logger.V(logger.TraceLevel).Info("run on remote", "cmd", cmd)
|
||||
if err := t.session.Start(cmd); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t.lastAction = action
|
||||
t.currentStream = &sshSmartSubtransportStream{
|
||||
owner: t,
|
||||
}
|
||||
|
||||
return t.currentStream, nil
|
||||
}
|
||||
|
||||
func (t *sshSmartSubtransport) createConn(addr string, sshConfig *ssh.ClientConfig) error {
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), sshConnectionTimeOut)
|
||||
defer cancel()
|
||||
|
||||
t.logger.V(logger.TraceLevel).Info("dial connection")
|
||||
conn, err := proxy.Dial(ctx, "tcp", addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c, chans, reqs, err := ssh.NewClientConn(conn, addr, sshConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t.connected = true
|
||||
t.client = ssh.NewClient(c, chans, reqs)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close closes the smart subtransport.
|
||||
//
|
||||
// This is called internally ahead of a new action, and also
|
||||
// upstream by the transport handler:
|
||||
// https://github.com/libgit2/git2go/blob/0e8009f00a65034d196c67b1cdd82af6f12c34d3/transport.go#L409
|
||||
//
|
||||
// Avoid returning errors, but focus on releasing anything that
|
||||
// may impair the transport to have successful actions on a new
|
||||
// SmartSubTransport (i.e. unreleased resources, staled connections).
|
||||
func (t *sshSmartSubtransport) Close() error {
|
||||
t.logger.V(logger.TraceLevel).Info("sshSmartSubtransport.Close()")
|
||||
|
||||
t.currentStream = nil
|
||||
if t.client != nil && t.stdin != nil {
|
||||
_ = t.stdin.Close()
|
||||
}
|
||||
t.stdin = nil
|
||||
|
||||
if t.session != nil {
|
||||
t.logger.V(logger.TraceLevel).Info("session.Close()")
|
||||
_ = t.session.Close()
|
||||
}
|
||||
t.session = nil
|
||||
|
||||
if t.client != nil {
|
||||
_ = t.client.Close()
|
||||
t.logger.V(logger.TraceLevel).Info("close client")
|
||||
}
|
||||
t.client = nil
|
||||
|
||||
t.connected = false
|
||||
atomic.AddInt32(t.closedSessions, 1)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *sshSmartSubtransport) Free() {
|
||||
}
|
||||
|
||||
type sshSmartSubtransportStream struct {
|
||||
owner *sshSmartSubtransport
|
||||
}
|
||||
|
||||
func (stream *sshSmartSubtransportStream) Read(buf []byte) (int, error) {
|
||||
return stream.owner.stdout.Read(buf)
|
||||
}
|
||||
|
||||
func (stream *sshSmartSubtransportStream) Write(buf []byte) (int, error) {
|
||||
return stream.owner.stdin.Write(buf)
|
||||
}
|
||||
|
||||
func (stream *sshSmartSubtransportStream) Free() {
|
||||
}
|
||||
|
||||
func createClientConfig(authOpts *git.AuthOptions) (*ssh.ClientConfig, error) {
|
||||
if authOpts == nil {
|
||||
return nil, fmt.Errorf("cannot create ssh client config from nil ssh auth options")
|
||||
}
|
||||
|
||||
var signer ssh.Signer
|
||||
var err error
|
||||
if authOpts.Password != "" {
|
||||
signer, err = ssh.ParsePrivateKeyWithPassphrase(authOpts.Identity, []byte(authOpts.Password))
|
||||
} else {
|
||||
signer, err = ssh.ParsePrivateKey(authOpts.Identity)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg := &ssh.ClientConfig{
|
||||
User: authOpts.Username,
|
||||
Auth: []ssh.AuthMethod{ssh.PublicKeys(signer)},
|
||||
Timeout: sshConnectionTimeOut,
|
||||
}
|
||||
|
||||
if len(git.KexAlgos) > 0 {
|
||||
cfg.Config.KeyExchanges = git.KexAlgos
|
||||
}
|
||||
if len(git.HostKeyAlgos) > 0 {
|
||||
cfg.HostKeyAlgorithms = git.HostKeyAlgos
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
|
@ -1,133 +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 (
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fluxcd/pkg/ssh"
|
||||
"github.com/fluxcd/source-controller/pkg/git"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/fluxcd/pkg/gittestserver"
|
||||
git2go "github.com/libgit2/git2go/v33"
|
||||
)
|
||||
|
||||
func TestSSHAction_clientConfig(t *testing.T) {
|
||||
kp, err := ssh.GenerateKeyPair(ssh.RSA_4096)
|
||||
if err != nil {
|
||||
t.Fatalf("could not generate keypair: %s", err)
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
authOpts *git.AuthOptions
|
||||
expectedUsername string
|
||||
expectedAuthLen int
|
||||
expectErr string
|
||||
}{
|
||||
{
|
||||
name: "nil SSHTransportOptions returns an error",
|
||||
authOpts: nil,
|
||||
expectErr: "cannot create ssh client config from nil ssh auth options",
|
||||
},
|
||||
{
|
||||
name: "valid SSHTransportOptions returns a valid SSHClientConfig",
|
||||
authOpts: &git.AuthOptions{
|
||||
Identity: kp.PrivateKey,
|
||||
Username: "user",
|
||||
},
|
||||
expectedUsername: "user",
|
||||
expectedAuthLen: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
cfg, err := createClientConfig(tt.authOpts)
|
||||
if tt.expectErr != "" {
|
||||
g.Expect(tt.expectErr).To(Equal(err.Error()))
|
||||
return
|
||||
}
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
g.Expect(cfg.User).To(Equal(tt.expectedUsername))
|
||||
g.Expect(len(cfg.Auth)).To(Equal(tt.expectedAuthLen))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSSH_E2E(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
server, err := gittestserver.NewTempGitServer()
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
defer os.RemoveAll(server.Root())
|
||||
|
||||
server.KeyDir(filepath.Join(server.Root(), "keys"))
|
||||
|
||||
err = server.ListenSSH()
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
go func() {
|
||||
server.StartSSH()
|
||||
}()
|
||||
defer server.StopSSH()
|
||||
|
||||
kp, err := ssh.NewEd25519Generator().Generate()
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
repoPath := "test.git"
|
||||
err = server.InitRepo("../../testdata/git/repo", git.DefaultBranch, repoPath)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
u, err := url.Parse(server.SSHAddress())
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(u.Host).ToNot(BeEmpty())
|
||||
knownhosts, err := ssh.ScanHostKey(u.Host, 5*time.Second, git.HostKeyAlgos, false)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
transportOptsURL := "ssh://git@fake-url"
|
||||
sshAddress := server.SSHAddress() + "/" + repoPath
|
||||
AddTransportOptions(transportOptsURL, TransportOptions{
|
||||
TargetURL: sshAddress,
|
||||
AuthOpts: &git.AuthOptions{
|
||||
Username: "user",
|
||||
Identity: kp.PrivateKey,
|
||||
KnownHosts: knownhosts,
|
||||
},
|
||||
})
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// 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.
|
||||
repo, err := git2go.Clone(transportOptsURL, tmpDir, &git2go.CloneOptions{
|
||||
FetchOptions: git2go.FetchOptions{
|
||||
RemoteCallbacks: RemoteCallbacks(),
|
||||
},
|
||||
CheckoutOptions: git2go.CheckoutOptions{
|
||||
Strategy: git2go.CheckoutForce,
|
||||
},
|
||||
})
|
||||
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
repo.Free()
|
||||
}
|
|
@ -1,103 +0,0 @@
|
|||
package managed
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"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 {
|
||||
// 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
|
||||
switch {
|
||||
case cert.Hostkey.Kind&git2go.HostkeySHA256 > 0:
|
||||
fingerprint = cert.Hostkey.HashSHA256[:]
|
||||
default:
|
||||
return fmt.Errorf("invalid host key kind, expected to be of kind SHA256")
|
||||
}
|
||||
|
||||
return CheckKnownHost(host, knownHosts, fingerprint)
|
||||
}
|
||||
}
|
||||
|
||||
// CheckKnownHost checks whether the host being connected to is
|
||||
// part of the known_hosts, and if so, it ensures the host
|
||||
// fingerprint matches the fingerprint of the known host with
|
||||
// the same name.
|
||||
func CheckKnownHost(host string, knownHosts []byte, fingerprint []byte) error {
|
||||
kh, err := pkgkh.ParseKnownHosts(string(knownHosts))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse known_hosts: %w", err)
|
||||
}
|
||||
|
||||
if len(kh) == 0 {
|
||||
return fmt.Errorf("hostkey verification aborted: no known_hosts found")
|
||||
}
|
||||
|
||||
// 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) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("no entries in known_hosts match host '%s' with fingerprint '%s'",
|
||||
h, base64.RawStdEncoding.EncodeToString(fingerprint))
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
|
@ -1,139 +0,0 @@
|
|||
package managed
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"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==
|
||||
github.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=
|
||||
github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl
|
||||
`
|
||||
|
||||
// To fetch latest knownhosts for source.developers.google.com run:
|
||||
// ssh-keyscan -p 2022 source.developers.google.com
|
||||
//
|
||||
// Expected hash (used in the cases) can get found with:
|
||||
// ssh-keyscan -p 2022 source.developers.google.com | ssh-keygen -l -f -
|
||||
var knownHostsFixtureWithPort = `[source.developers.google.com]:2022 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBB5Iy4/cq/gt/fPqe3uyMy4jwv1Alc94yVPxmnwNhBzJqEV5gRPiRk5u4/JJMbbu9QUVAguBABxL7sBZa5PH/xY=`
|
||||
|
||||
// This is an incorrect known hosts entry, that does not aligned with
|
||||
// the normalized format and therefore won't match.
|
||||
var knownHostsFixtureUnormalized = `source.developers.google.com:2022 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBB5Iy4/cq/gt/fPqe3uyMy4jwv1Alc94yVPxmnwNhBzJqEV5gRPiRk5u4/JJMbbu9QUVAguBABxL7sBZa5PH/xY=`
|
||||
|
||||
func TestKnownHostsCallback(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
host string
|
||||
expectedHost string
|
||||
knownHosts []byte
|
||||
hostkey git2go.HostkeyCertificate
|
||||
want error
|
||||
}{
|
||||
{
|
||||
name: "Empty",
|
||||
host: "source.developers.google.com",
|
||||
knownHosts: []byte(""),
|
||||
hostkey: git2go.HostkeyCertificate{Kind: git2go.HostkeySHA256, HashSHA256: sha256Fingerprint("AGvEpqYNMqsRNIviwyk4J4HM0lEylomDBKOWZsBn434")},
|
||||
expectedHost: "source.developers.google.com:2022",
|
||||
want: fmt.Errorf("hostkey verification aborted: no known_hosts found"),
|
||||
},
|
||||
{
|
||||
name: "Mismatch incorrect known_hosts",
|
||||
host: "source.developers.google.com",
|
||||
knownHosts: []byte(knownHostsFixtureUnormalized),
|
||||
hostkey: git2go.HostkeyCertificate{Kind: git2go.HostkeySHA256, HashSHA256: sha256Fingerprint("AGvEpqYNMqsRNIviwyk4J4HM0lEylomDBKOWZsBn434")},
|
||||
expectedHost: "source.developers.google.com:2022",
|
||||
want: fmt.Errorf("no entries in known_hosts match host '[source.developers.google.com]:2022' with fingerprint 'AGvEpqYNMqsRNIviwyk4J4HM0lEylomDBKOWZsBn434'"),
|
||||
},
|
||||
{
|
||||
name: "Match when host has port",
|
||||
host: "source.developers.google.com:2022",
|
||||
knownHosts: []byte(knownHostsFixtureWithPort),
|
||||
hostkey: git2go.HostkeyCertificate{Kind: git2go.HostkeySHA256, HashSHA256: sha256Fingerprint("AGvEpqYNMqsRNIviwyk4J4HM0lEylomDBKOWZsBn434")},
|
||||
expectedHost: "source.developers.google.com:2022",
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "Match even when host does not have port",
|
||||
host: "source.developers.google.com",
|
||||
knownHosts: []byte(knownHostsFixtureWithPort),
|
||||
hostkey: git2go.HostkeyCertificate{Kind: git2go.HostkeySHA256, HashSHA256: sha256Fingerprint("AGvEpqYNMqsRNIviwyk4J4HM0lEylomDBKOWZsBn434")},
|
||||
expectedHost: "source.developers.google.com:2022",
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "Match",
|
||||
host: "github.com",
|
||||
knownHosts: []byte(knownHostsFixture),
|
||||
hostkey: git2go.HostkeyCertificate{Kind: git2go.HostkeySHA256, HashSHA256: sha256Fingerprint("nThbg6kXUpJWGl7E1IGOCspRomTxdCARLviKw6E5SY8")},
|
||||
expectedHost: "github.com",
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "Match with port",
|
||||
host: "github.com",
|
||||
knownHosts: []byte(knownHostsFixture),
|
||||
hostkey: git2go.HostkeyCertificate{Kind: git2go.HostkeySHA256, HashSHA256: sha256Fingerprint("nThbg6kXUpJWGl7E1IGOCspRomTxdCARLviKw6E5SY8")},
|
||||
expectedHost: "github.com:22",
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
// Test case to specifically detect a regression introduced in v0.25.0
|
||||
// Ref: https://github.com/fluxcd/image-automation-controller/issues/378
|
||||
name: "Match regardless of order of known_hosts",
|
||||
host: "github.com",
|
||||
knownHosts: []byte(knownHostsFixture),
|
||||
// Use ecdsa-sha2-nistp256 instead of ssh-rsa
|
||||
hostkey: git2go.HostkeyCertificate{Kind: git2go.HostkeySHA256, HashSHA256: sha256Fingerprint("p2QAMXNIC1TJYWeIOttrVc98/R1BUFWu3/LiyKgUfQM")},
|
||||
expectedHost: "github.com:22",
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "Hostname mismatch",
|
||||
host: "github.com",
|
||||
knownHosts: []byte(knownHostsFixture),
|
||||
hostkey: git2go.HostkeyCertificate{Kind: git2go.HostkeySHA256, HashSHA256: sha256Fingerprint("nThbg6kXUpJWGl7E1IGOCspRomTxdCARLviKw6E5SY8")},
|
||||
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.HostkeySHA256, HashSHA256: sha256Fingerprint("ROQFvPThGrW4RuWLoL9tq9I9zJ42fK4XywyRtbOz/EQ")},
|
||||
expectedHost: "github.com",
|
||||
want: fmt.Errorf("no entries in known_hosts match host 'github.com' with fingerprint 'ROQFvPThGrW4RuWLoL9tq9I9zJ42fK4XywyRtbOz/EQ'"),
|
||||
},
|
||||
}
|
||||
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 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
|
||||
}
|
|
@ -1,174 +0,0 @@
|
|||
/*
|
||||
Copyright 2021 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 git
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultOrigin = "origin"
|
||||
DefaultBranch = "master"
|
||||
DefaultPublicKeyAuthUser = "git"
|
||||
)
|
||||
|
||||
// CheckoutOptions are the options used for a Git checkout.
|
||||
type CheckoutOptions struct {
|
||||
// Branch to checkout, can be combined with Branch with some
|
||||
// Implementations.
|
||||
Branch string
|
||||
|
||||
// Tag to checkout, takes precedence over Branch.
|
||||
Tag string
|
||||
|
||||
// SemVer tag expression to checkout, takes precedence over Tag.
|
||||
SemVer string `json:"semver,omitempty"`
|
||||
|
||||
// Commit SHA1 to checkout, takes precedence over Tag and SemVer,
|
||||
// can be combined with Branch with some Implementations.
|
||||
Commit string
|
||||
|
||||
// RecurseSubmodules defines if submodules should be checked out,
|
||||
// not supported by all Implementations.
|
||||
RecurseSubmodules bool
|
||||
|
||||
// LastRevision holds the last observed revision of the local repository.
|
||||
// It is used to skip clone operations when no changes were detected.
|
||||
LastRevision string
|
||||
}
|
||||
|
||||
type TransportType string
|
||||
|
||||
const (
|
||||
SSH TransportType = "ssh"
|
||||
HTTPS TransportType = "https"
|
||||
HTTP TransportType = "http"
|
||||
)
|
||||
|
||||
// AuthOptions are the authentication options for the Transport of
|
||||
// communication with a remote origin.
|
||||
type AuthOptions struct {
|
||||
Transport TransportType
|
||||
Host string
|
||||
Username string
|
||||
Password string
|
||||
Identity []byte
|
||||
KnownHosts []byte
|
||||
CAFile []byte
|
||||
// TransportOptionsURL is a unique identifier for this set of authentication
|
||||
// options. It's used by managed libgit2 transports to uniquely identify
|
||||
// which credentials to use for a particular Git operation, and avoid misuse
|
||||
// of credentials in a multi-tenant environment.
|
||||
// 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.
|
||||
// If empty, Go's default is used instead.
|
||||
var KexAlgos []string
|
||||
|
||||
// HostKeyAlgos holds the HostKey algorithms that the SSH client will advertise
|
||||
// to the server. If empty, Go's default is used instead.
|
||||
var HostKeyAlgos []string
|
||||
|
||||
// Validate the AuthOptions against the defined Transport.
|
||||
func (o AuthOptions) Validate() error {
|
||||
switch o.Transport {
|
||||
case HTTPS, HTTP:
|
||||
if o.Username == "" && o.Password != "" {
|
||||
return fmt.Errorf("invalid '%s' auth option: 'password' requires 'username' to be set", o.Transport)
|
||||
}
|
||||
case SSH:
|
||||
if o.Host == "" {
|
||||
return fmt.Errorf("invalid '%s' auth option: 'host' is required", o.Transport)
|
||||
}
|
||||
if len(o.Identity) == 0 {
|
||||
return fmt.Errorf("invalid '%s' auth option: 'identity' is required", o.Transport)
|
||||
}
|
||||
if len(o.KnownHosts) == 0 {
|
||||
return fmt.Errorf("invalid '%s' auth option: 'known_hosts' is required", o.Transport)
|
||||
}
|
||||
case "":
|
||||
return fmt.Errorf("no transport type set")
|
||||
default:
|
||||
return fmt.Errorf("unknown transport '%s'", o.Transport)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AuthOptionsFromSecret constructs an AuthOptions object from the given Secret,
|
||||
// and then validates the result. It returns the AuthOptions, or an error.
|
||||
func AuthOptionsFromSecret(URL string, secret *v1.Secret) (*AuthOptions, error) {
|
||||
if secret == nil {
|
||||
return nil, fmt.Errorf("no secret provided to construct auth strategy from")
|
||||
}
|
||||
|
||||
u, err := url.Parse(URL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse URL to determine auth strategy: %w", err)
|
||||
}
|
||||
|
||||
opts := &AuthOptions{
|
||||
Transport: TransportType(u.Scheme),
|
||||
Host: u.Host,
|
||||
Username: string(secret.Data["username"]),
|
||||
Password: string(secret.Data["password"]),
|
||||
CAFile: secret.Data["caFile"],
|
||||
Identity: secret.Data["identity"],
|
||||
KnownHosts: secret.Data["known_hosts"],
|
||||
}
|
||||
if opts.Username == "" {
|
||||
opts.Username = u.User.Username()
|
||||
}
|
||||
if opts.Username == "" {
|
||||
opts.Username = DefaultPublicKeyAuthUser
|
||||
}
|
||||
|
||||
if err = opts.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
// AuthOptionsWithoutSecret constructs a minimal AuthOptions object from the
|
||||
// given URL and then validates the result. It returns the AuthOptions, or an
|
||||
// error.
|
||||
func AuthOptionsWithoutSecret(URL string) (*AuthOptions, error) {
|
||||
u, err := url.Parse(URL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse URL to determine auth strategy: %w", err)
|
||||
}
|
||||
|
||||
opts := &AuthOptions{
|
||||
Transport: TransportType(u.Scheme),
|
||||
Host: u.Host,
|
||||
}
|
||||
|
||||
if err = opts.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return opts, nil
|
||||
}
|
|
@ -1,272 +0,0 @@
|
|||
/*
|
||||
Copyright 2021 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 git
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/gomega"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
const (
|
||||
// privateKeyFixture is a randomly generated password less
|
||||
// 512bit RSA private key.
|
||||
privateKeyFixture = `-----BEGIN RSA PRIVATE KEY-----
|
||||
MIICXAIBAAKBgQCrakELAKxozvwJijQEggYlTvS1QTZx1DaBwOhW/4kRSuR21plu
|
||||
xuQeyuUiztoWeb9jgW7wjzG4j1PIJjdbsgjPIcIZ4PBY7JeEW+QRopfwuN8MHXNp
|
||||
uTLgIHbkmhoOg5qBEcjzO/lEOOPpV0EmbObgqv3+wRmLJrgfzWl/cTtRewIDAQAB
|
||||
AoGAawKFImpEN5Xn78iwWpQVZBsbV0AjzgHuGSiloxIZrorzf2DPHkHZzYNaclVx
|
||||
/o/4tBTsfg7WumH3qr541qyZJDgU7iRMABwmx0v1vm2wQiX7NJzLzH2E9vlMC3mw
|
||||
d8S99g9EqRuNH98XX8su34B9WGRPqiKvEm0RW8Hideo2/KkCQQDbs6rHcriKQyPB
|
||||
paidHZAfguu0eVbyHT2EgLgRboWE+tEAqFEW2ycqNL3VPz9fRvwexbB6rpOcPpQJ
|
||||
DEL4XB2XAkEAx7xJz8YlCQ2H38xggK8R8EUXF9Zhb0fqMJHMNmao1HCHVMtbsa8I
|
||||
jR2EGyQ4CaIqNG5tdWukXQSJrPYDRWNvvQJAZX3rP7XUYDLB2twvN12HzbbKMhX3
|
||||
v2MYnxRjc9INpi/Dyzz2MMvOnOW+aDuOh/If2AtVCmeJUx1pf4CFk3viQwJBAKyC
|
||||
t824+evjv+NQBlme3AOF6PgxtV4D4wWoJ5Uk/dTejER0j/Hbl6sqPxuiILRRV9qJ
|
||||
Ngkgu4mLjc3RfenEhJECQAx8zjWUE6kHHPGAd9DfiAIQ4bChqnyS0Nwb9+Gd4hSE
|
||||
P0Ah10mHiK/M0o3T8Eanwum0gbQHPnOwqZgsPkwXRqQ=
|
||||
-----END RSA PRIVATE KEY-----`
|
||||
|
||||
// privateKeyPassphraseFixture is a randomly generated
|
||||
// 512bit RSA private key with password foobar.
|
||||
privateKeyPassphraseFixture = `-----BEGIN RSA PRIVATE KEY-----
|
||||
Proc-Type: 4,ENCRYPTED
|
||||
DEK-Info: AES-256-CBC,0B016973B2A761D31E6B388D0F327C35
|
||||
|
||||
X9GET/qAyZkAJBl/RK+1XX75NxONgdUfZDw7PIYi/g+Efh3Z5zH5kh/dx9lxH5ZG
|
||||
HGCqPAeMO/ofGDGtDULWW6iqDUFRu5gPgEVSCnnbqoHNU325WHhXdhejVAItwObC
|
||||
IpL/zYfs2+gDHXct/n9FJ/9D/EGXZihwPqYaK8GQSfZAxz0QjLuh0wU1qpbm3y3N
|
||||
q+o9FLv3b2Ys/tCJOUsYVQOYLSrZEI77y1ii3nWgQ8lXiTJbBUKzuq4f1YWeO8Ah
|
||||
RZbdhTa57AF5lUaRtL7Nrm3HJUrK1alBbU7HHyjeW4Q4n/D3fiRDC1Mh2Bi4EOOn
|
||||
wGctSx4kHsZGhJv5qwKqqPEFPhUzph8D2tm2TABk8HJa5KJFDbGrcfvk2uODAoZr
|
||||
MbcpIxCfl8oB09bWfY6tDQjyvwSYYo2Phdwm7kT92xc=
|
||||
-----END RSA PRIVATE KEY-----`
|
||||
|
||||
// knownHostsFixture is known_hosts fixture in the expected
|
||||
// format.
|
||||
knownHostsFixture = `github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==`
|
||||
)
|
||||
|
||||
func TestAuthOptions_Validate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
opts AuthOptions
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "HTTP transport with password requires user",
|
||||
opts: AuthOptions{
|
||||
Transport: HTTP,
|
||||
Password: "foo",
|
||||
},
|
||||
wantErr: "invalid 'http' auth option: 'password' requires 'username' to be set",
|
||||
},
|
||||
{
|
||||
name: "Valid HTTP transport",
|
||||
opts: AuthOptions{
|
||||
Transport: HTTP,
|
||||
Username: "example",
|
||||
Password: "foo",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "HTTPS transport with password requires user",
|
||||
opts: AuthOptions{
|
||||
Transport: HTTPS,
|
||||
Password: "foo",
|
||||
},
|
||||
wantErr: "invalid 'https' auth option: 'password' requires 'username' to be set",
|
||||
},
|
||||
{
|
||||
name: "Valid HTTPS transport",
|
||||
opts: AuthOptions{
|
||||
Transport: HTTPS,
|
||||
Username: "example",
|
||||
Password: "foo",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Valid HTTPS without any config",
|
||||
opts: AuthOptions{
|
||||
Transport: HTTPS,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SSH transport requires host",
|
||||
opts: AuthOptions{
|
||||
Transport: SSH,
|
||||
},
|
||||
wantErr: "invalid 'ssh' auth option: 'host' is required",
|
||||
},
|
||||
{
|
||||
name: "SSH transport requires identity",
|
||||
opts: AuthOptions{
|
||||
Transport: SSH,
|
||||
Host: "github.com:22",
|
||||
},
|
||||
wantErr: "invalid 'ssh' auth option: 'identity' is required",
|
||||
},
|
||||
{
|
||||
name: "SSH transport requires known_hosts",
|
||||
opts: AuthOptions{
|
||||
Transport: SSH,
|
||||
Host: "github.com:22",
|
||||
Identity: []byte(privateKeyFixture),
|
||||
},
|
||||
wantErr: "invalid 'ssh' auth option: 'known_hosts' is required",
|
||||
},
|
||||
{
|
||||
name: "Requires transport",
|
||||
opts: AuthOptions{},
|
||||
wantErr: "no transport type set",
|
||||
},
|
||||
{
|
||||
name: "Valid SSH transport",
|
||||
opts: AuthOptions{
|
||||
Host: "github.com:22",
|
||||
Transport: SSH,
|
||||
Identity: []byte(privateKeyPassphraseFixture),
|
||||
Password: "foobar",
|
||||
KnownHosts: []byte(knownHostsFixture),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "No transport",
|
||||
opts: AuthOptions{},
|
||||
wantErr: "no transport type set",
|
||||
},
|
||||
{
|
||||
name: "Unknown transport",
|
||||
opts: AuthOptions{
|
||||
Transport: "foo",
|
||||
},
|
||||
wantErr: "unknown transport 'foo'",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
got := tt.opts.Validate()
|
||||
if tt.wantErr != "" {
|
||||
g.Expect(got.Error()).To(ContainSubstring(tt.wantErr))
|
||||
return
|
||||
}
|
||||
g.Expect(got).ToNot(HaveOccurred())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthOptionsFromSecret(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
URL string
|
||||
secret *v1.Secret
|
||||
wantFunc func(g *WithT, opts *AuthOptions, secret *v1.Secret)
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "Sets values from Secret",
|
||||
URL: "https://git@example.com",
|
||||
secret: &v1.Secret{
|
||||
Data: map[string][]byte{
|
||||
"username": []byte("example"), // This takes precedence over the one from the URL
|
||||
"password": []byte("secret"),
|
||||
"identity": []byte(privateKeyFixture),
|
||||
"known_hosts": []byte(knownHostsFixture),
|
||||
"caFile": []byte("mock"),
|
||||
},
|
||||
},
|
||||
wantFunc: func(g *WithT, opts *AuthOptions, secret *v1.Secret) {
|
||||
g.Expect(opts.Username).To(Equal("example"))
|
||||
g.Expect(opts.Password).To(Equal("secret"))
|
||||
g.Expect(opts.Identity).To(BeEquivalentTo(privateKeyFixture))
|
||||
g.Expect(opts.KnownHosts).To(BeEquivalentTo(knownHostsFixture))
|
||||
g.Expect(opts.CAFile).To(BeEquivalentTo("mock"))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Sets default user",
|
||||
URL: "http://example.com",
|
||||
secret: &v1.Secret{},
|
||||
wantFunc: func(g *WithT, opts *AuthOptions, secret *v1.Secret) {
|
||||
g.Expect(opts.Username).To(Equal(DefaultPublicKeyAuthUser))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Sets transport from URL",
|
||||
URL: "http://git@example.com",
|
||||
secret: &v1.Secret{},
|
||||
wantFunc: func(g *WithT, opts *AuthOptions, secret *v1.Secret) {
|
||||
g.Expect(opts.Transport).To(Equal(HTTP))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Sets user from URL",
|
||||
URL: "http://example@example.com",
|
||||
secret: &v1.Secret{
|
||||
Data: map[string][]byte{
|
||||
"password": []byte("secret"),
|
||||
},
|
||||
},
|
||||
wantFunc: func(g *WithT, opts *AuthOptions, secret *v1.Secret) {
|
||||
g.Expect(opts.Username).To(Equal("example"))
|
||||
g.Expect(opts.Password).To(Equal("secret"))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Validates options",
|
||||
URL: "ssh://example.com",
|
||||
secret: &v1.Secret{
|
||||
Data: map[string][]byte{
|
||||
"identity": []byte(privateKeyFixture),
|
||||
},
|
||||
},
|
||||
wantErr: "invalid 'ssh' auth option: 'known_hosts' is required",
|
||||
},
|
||||
{
|
||||
name: "Errors without secret",
|
||||
secret: nil,
|
||||
wantErr: "no secret provided to construct auth strategy from",
|
||||
},
|
||||
{
|
||||
name: "Errors on malformed URL",
|
||||
URL: ":example",
|
||||
secret: &v1.Secret{},
|
||||
wantErr: "failed to parse URL to determine auth strategy",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
got, err := AuthOptionsFromSecret(tt.URL, tt.secret)
|
||||
if tt.wantErr != "" {
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
g.Expect(err.Error()).To(ContainSubstring(tt.wantErr))
|
||||
g.Expect(got).To(BeNil())
|
||||
return
|
||||
}
|
||||
|
||||
g.Expect(err).To(BeNil())
|
||||
if tt.wantFunc != nil {
|
||||
tt.wantFunc(g, got, tt.secret)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,383 +0,0 @@
|
|||
/*
|
||||
Copyright 2021 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 proxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/elazarl/goproxy"
|
||||
"github.com/fluxcd/pkg/gittestserver"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/fluxcd/source-controller/pkg/git"
|
||||
"github.com/fluxcd/source-controller/pkg/git/gogit"
|
||||
"github.com/fluxcd/source-controller/pkg/git/libgit2"
|
||||
"github.com/fluxcd/source-controller/pkg/git/libgit2/managed"
|
||||
"github.com/fluxcd/source-controller/pkg/git/strategy"
|
||||
)
|
||||
|
||||
// These tests are run in a different _test.go file because go-git uses the ProxyFromEnvironment function of the net/http package
|
||||
// which caches the Proxy settings, hence not including other tests in the same file ensures a clean proxy setup for the tests to run.
|
||||
func TestCheckoutStrategyForImplementation_Proxied(t *testing.T) {
|
||||
managed.InitManagedTransport()
|
||||
|
||||
type cleanupFunc func()
|
||||
|
||||
type testCase struct {
|
||||
name string
|
||||
gitImpl git.Implementation
|
||||
url string
|
||||
branch string
|
||||
setupGitProxy func(g *WithT, proxy *goproxy.ProxyHttpServer, proxiedRequests *int32) (*git.AuthOptions, cleanupFunc)
|
||||
shortTimeout bool
|
||||
wantUsedProxy bool
|
||||
wantError bool
|
||||
}
|
||||
|
||||
g := NewWithT(t)
|
||||
|
||||
// Get a free port for proxy to use.
|
||||
l, err := net.Listen("tcp", ":0")
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
proxyAddr := fmt.Sprintf("localhost:%d", l.Addr().(*net.TCPAddr).Port)
|
||||
g.Expect(l.Close()).ToNot(HaveOccurred())
|
||||
|
||||
cases := []testCase{
|
||||
{
|
||||
name: "gogit_HTTP_PROXY",
|
||||
gitImpl: gogit.Implementation,
|
||||
url: "http://example.com/bar/test-reponame",
|
||||
branch: "main",
|
||||
setupGitProxy: func(g *WithT, proxy *goproxy.ProxyHttpServer, proxiedRequests *int32) (*git.AuthOptions, cleanupFunc) {
|
||||
// Create the git server.
|
||||
gitServer, err := gittestserver.NewTempGitServer()
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
username := "test-user"
|
||||
password := "test-password"
|
||||
gitServer.Auth(username, password)
|
||||
gitServer.KeyDir(gitServer.Root())
|
||||
|
||||
g.Expect(gitServer.StartHTTP()).ToNot(HaveOccurred())
|
||||
|
||||
// Initialize a git repo.
|
||||
err = gitServer.InitRepo("../testdata/repo1", "main", "bar/test-reponame")
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
u, err := url.Parse(gitServer.HTTPAddress())
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// The request is being forwarded to the local test git server in this handler.
|
||||
var proxyHandler goproxy.FuncReqHandler = func(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) {
|
||||
userAgent := req.Header.Get("User-Agent")
|
||||
if strings.Contains(req.Host, "example.com") && strings.Contains(userAgent, "git") {
|
||||
atomic.AddInt32(proxiedRequests, 1)
|
||||
req.Host = u.Host
|
||||
req.URL.Host = req.Host
|
||||
return req, nil
|
||||
}
|
||||
// Reject if it isnt our request.
|
||||
return req, goproxy.NewResponse(req, goproxy.ContentTypeText, http.StatusForbidden, "")
|
||||
}
|
||||
proxy.OnRequest().Do(proxyHandler)
|
||||
|
||||
return &git.AuthOptions{
|
||||
Transport: git.HTTP,
|
||||
Username: username,
|
||||
Password: password,
|
||||
}, func() {
|
||||
os.RemoveAll(gitServer.Root())
|
||||
gitServer.StopHTTP()
|
||||
}
|
||||
},
|
||||
shortTimeout: false,
|
||||
wantUsedProxy: true,
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "gogit_HTTPS_PROXY",
|
||||
gitImpl: gogit.Implementation,
|
||||
url: "https://github.com/git-fixtures/basic",
|
||||
branch: "master",
|
||||
setupGitProxy: func(g *WithT, proxy *goproxy.ProxyHttpServer, proxiedRequests *int32) (*git.AuthOptions, cleanupFunc) {
|
||||
var proxyHandler goproxy.FuncHttpsHandler = func(host string, ctx *goproxy.ProxyCtx) (*goproxy.ConnectAction, string) {
|
||||
// We don't check for user agent as this handler is only going to process CONNECT requests, and because Go's net/http
|
||||
// is the one making such a request on behalf of go-git, adding a check for the go net/http user agent (Go-http-client)
|
||||
// would only allow false positives from any request originating from Go's net/http.
|
||||
if strings.Contains(host, "github.com") {
|
||||
atomic.AddInt32(proxiedRequests, 1)
|
||||
return goproxy.OkConnect, host
|
||||
}
|
||||
// Reject if it isnt our request.
|
||||
return goproxy.RejectConnect, host
|
||||
}
|
||||
proxy.OnRequest().HandleConnect(proxyHandler)
|
||||
|
||||
// go-git does not allow to use an HTTPS proxy and a custom root CA at the same time.
|
||||
// See https://github.com/fluxcd/source-controller/pull/524#issuecomment-1006673163.
|
||||
return nil, func() {}
|
||||
},
|
||||
shortTimeout: false,
|
||||
wantUsedProxy: true,
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "gogit_NO_PROXY",
|
||||
gitImpl: gogit.Implementation,
|
||||
url: "https://192.0.2.1/bar/test-reponame",
|
||||
branch: "main",
|
||||
setupGitProxy: func(g *WithT, proxy *goproxy.ProxyHttpServer, proxiedRequests *int32) (*git.AuthOptions, cleanupFunc) {
|
||||
var proxyHandler goproxy.FuncHttpsHandler = func(host string, ctx *goproxy.ProxyCtx) (*goproxy.ConnectAction, string) {
|
||||
// We shouldn't hit the proxy so we just want to check for any interaction, then reject.
|
||||
atomic.AddInt32(proxiedRequests, 1)
|
||||
return goproxy.RejectConnect, host
|
||||
}
|
||||
proxy.OnRequest().HandleConnect(proxyHandler)
|
||||
|
||||
return nil, func() {}
|
||||
},
|
||||
shortTimeout: true,
|
||||
wantUsedProxy: false,
|
||||
wantError: true,
|
||||
},
|
||||
{
|
||||
name: "libgit2_HTTPS_PROXY",
|
||||
gitImpl: libgit2.Implementation,
|
||||
url: "https://example.com/bar/test-reponame",
|
||||
branch: "main",
|
||||
setupGitProxy: func(g *WithT, proxy *goproxy.ProxyHttpServer, proxiedRequests *int32) (*git.AuthOptions, cleanupFunc) {
|
||||
// Create the git server.
|
||||
gitServer, err := gittestserver.NewTempGitServer()
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
username := "test-user"
|
||||
password := "test-password"
|
||||
gitServer.Auth(username, password)
|
||||
gitServer.KeyDir(gitServer.Root())
|
||||
|
||||
// Start the HTTPS server.
|
||||
examplePublicKey, err := os.ReadFile("../testdata/certs/server.pem")
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
examplePrivateKey, err := os.ReadFile("../testdata/certs/server-key.pem")
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
exampleCA, err := os.ReadFile("../testdata/certs/ca.pem")
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
err = gitServer.StartHTTPS(examplePublicKey, examplePrivateKey, exampleCA, "example.com")
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Initialize a git repo.
|
||||
repoPath := "bar/test-reponame"
|
||||
err = gitServer.InitRepo("../testdata/repo1", "main", repoPath)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
u, err := url.Parse(gitServer.HTTPAddress())
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// The request is being forwarded to the local test git server in this handler.
|
||||
// The certificate used here is valid for both example.com and localhost.
|
||||
var proxyHandler goproxy.FuncHttpsHandler = func(host string, ctx *goproxy.ProxyCtx) (*goproxy.ConnectAction, string) {
|
||||
defer managed.RemoveTransportOptions("https://example.com/bar/test-reponame")
|
||||
// Check if the host matches with the git server address and the user-agent is the expected git client.
|
||||
userAgent := ctx.Req.Header.Get("User-Agent")
|
||||
if strings.Contains(host, "example.com") && strings.Contains(userAgent, "libgit2") {
|
||||
atomic.AddInt32(proxiedRequests, 1)
|
||||
return goproxy.OkConnect, u.Host
|
||||
}
|
||||
// Reject if it isn't our request.
|
||||
return goproxy.RejectConnect, host
|
||||
}
|
||||
proxy.OnRequest().HandleConnect(proxyHandler)
|
||||
|
||||
return &git.AuthOptions{
|
||||
Transport: git.HTTPS,
|
||||
Username: username,
|
||||
Password: password,
|
||||
CAFile: exampleCA,
|
||||
TransportOptionsURL: "https://proxy-test",
|
||||
}, func() {
|
||||
os.RemoveAll(gitServer.Root())
|
||||
gitServer.StopHTTP()
|
||||
}
|
||||
},
|
||||
shortTimeout: false,
|
||||
wantUsedProxy: true,
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "libgit2_HTTP_PROXY",
|
||||
gitImpl: libgit2.Implementation,
|
||||
url: "http://example.com/bar/test-reponame",
|
||||
branch: "main",
|
||||
setupGitProxy: func(g *WithT, proxy *goproxy.ProxyHttpServer, proxiedRequests *int32) (*git.AuthOptions, cleanupFunc) {
|
||||
// Create the git server.
|
||||
gitServer, err := gittestserver.NewTempGitServer()
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
err = gitServer.StartHTTP()
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Initialize a git repo.
|
||||
repoPath := "bar/test-reponame"
|
||||
err = gitServer.InitRepo("../testdata/repo1", "main", repoPath)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
u, err := url.Parse(gitServer.HTTPAddress())
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// The request is being forwarded to the local test git server in this handler.
|
||||
// The certificate used here is valid for both example.com and localhost.
|
||||
var proxyHandler goproxy.FuncReqHandler = func(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) {
|
||||
userAgent := req.Header.Get("User-Agent")
|
||||
if strings.Contains(req.Host, "example.com") && strings.Contains(userAgent, "git") {
|
||||
atomic.AddInt32(proxiedRequests, 1)
|
||||
req.Host = u.Host
|
||||
req.URL.Host = req.Host
|
||||
return req, nil
|
||||
}
|
||||
// Reject if it isnt our request.
|
||||
return req, goproxy.NewResponse(req, goproxy.ContentTypeText, http.StatusForbidden, "")
|
||||
}
|
||||
proxy.OnRequest().Do(proxyHandler)
|
||||
|
||||
return &git.AuthOptions{
|
||||
Transport: git.HTTP,
|
||||
TransportOptionsURL: "http://proxy-test",
|
||||
}, func() {
|
||||
os.RemoveAll(gitServer.Root())
|
||||
gitServer.StopHTTP()
|
||||
}
|
||||
},
|
||||
shortTimeout: false,
|
||||
wantUsedProxy: true,
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "gogit_HTTPS_PROXY",
|
||||
gitImpl: gogit.Implementation,
|
||||
url: "https://github.com/git-fixtures/basic",
|
||||
branch: "master",
|
||||
setupGitProxy: func(g *WithT, proxy *goproxy.ProxyHttpServer, proxiedRequests *int32) (*git.AuthOptions, cleanupFunc) {
|
||||
var proxyHandler goproxy.FuncHttpsHandler = func(host string, ctx *goproxy.ProxyCtx) (*goproxy.ConnectAction, string) {
|
||||
// We don't check for user agent as this handler is only going to process CONNECT requests, and because Go's net/http
|
||||
// is the one making such a request on behalf of go-git, adding a check for the go net/http user agent (Go-http-client)
|
||||
// would only allow false positives from any request originating from Go's net/http.
|
||||
if strings.Contains(host, "github.com") {
|
||||
atomic.AddInt32(proxiedRequests, 1)
|
||||
return goproxy.OkConnect, host
|
||||
}
|
||||
// Reject if it isnt our request.
|
||||
return goproxy.RejectConnect, host
|
||||
}
|
||||
proxy.OnRequest().HandleConnect(proxyHandler)
|
||||
|
||||
// go-git does not allow to use an HTTPS proxy and a custom root CA at the same time.
|
||||
// See https://github.com/fluxcd/source-controller/pull/524#issuecomment-1006673163.
|
||||
return nil, func() {}
|
||||
},
|
||||
shortTimeout: false,
|
||||
wantUsedProxy: true,
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "gogit_NO_PROXY",
|
||||
gitImpl: gogit.Implementation,
|
||||
url: "https://192.0.2.1/bar/test-reponame",
|
||||
branch: "main",
|
||||
setupGitProxy: func(g *WithT, proxy *goproxy.ProxyHttpServer, proxiedRequests *int32) (*git.AuthOptions, cleanupFunc) {
|
||||
var proxyHandler goproxy.FuncHttpsHandler = func(host string, ctx *goproxy.ProxyCtx) (*goproxy.ConnectAction, string) {
|
||||
// We shouldn't hit the proxy so we just want to check for any interaction, then reject.
|
||||
atomic.AddInt32(proxiedRequests, 1)
|
||||
return goproxy.RejectConnect, host
|
||||
}
|
||||
proxy.OnRequest().HandleConnect(proxyHandler)
|
||||
|
||||
return nil, func() {}
|
||||
},
|
||||
shortTimeout: true,
|
||||
wantUsedProxy: false,
|
||||
wantError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range cases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
// Run a proxy server.
|
||||
proxy := goproxy.NewProxyHttpServer()
|
||||
proxy.Verbose = true
|
||||
|
||||
proxiedRequests := int32(0)
|
||||
authOpts, cleanup := tt.setupGitProxy(g, proxy, &proxiedRequests)
|
||||
defer cleanup()
|
||||
|
||||
proxyServer := http.Server{
|
||||
Addr: proxyAddr,
|
||||
Handler: proxy,
|
||||
}
|
||||
l, err := net.Listen("tcp", proxyServer.Addr)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
go proxyServer.Serve(l)
|
||||
defer proxyServer.Close()
|
||||
|
||||
// Set the proxy env vars for both HTTP and HTTPS because go-git caches them.
|
||||
os.Setenv("HTTPS_PROXY", fmt.Sprintf("http://smth:else@%s", proxyAddr))
|
||||
defer os.Unsetenv("HTTPS_PROXY")
|
||||
|
||||
os.Setenv("HTTP_PROXY", fmt.Sprintf("http://smth:else@%s", proxyAddr))
|
||||
defer os.Unsetenv("HTTP_PROXY")
|
||||
|
||||
os.Setenv("NO_PROXY", "*.0.2.1")
|
||||
defer os.Unsetenv("NO_PROXY")
|
||||
|
||||
// Checkout the repo.
|
||||
checkoutStrategy, err := strategy.CheckoutStrategyForImplementation(context.TODO(), tt.gitImpl, git.CheckoutOptions{
|
||||
Branch: tt.branch,
|
||||
})
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// for the NO_PROXY test we dont want to wait the 30s for it to timeout/fail, so shorten the timeout
|
||||
checkoutCtx := context.TODO()
|
||||
if tt.shortTimeout {
|
||||
var cancel context.CancelFunc
|
||||
checkoutCtx, cancel = context.WithTimeout(context.TODO(), 1*time.Second)
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
_, err = checkoutStrategy.Checkout(checkoutCtx, tmpDir, tt.url, authOpts)
|
||||
if tt.wantError {
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
} else {
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
}
|
||||
|
||||
g.Expect(atomic.LoadInt32(&proxiedRequests) > 0).To(Equal(tt.wantUsedProxy))
|
||||
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
/*
|
||||
Copyright 2020 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 strategy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/fluxcd/source-controller/pkg/git"
|
||||
"github.com/fluxcd/source-controller/pkg/git/gogit"
|
||||
"github.com/fluxcd/source-controller/pkg/git/libgit2"
|
||||
)
|
||||
|
||||
// CheckoutStrategyForImplementation returns the CheckoutStrategy for the given
|
||||
// git.Implementation and git.CheckoutOptions.
|
||||
func CheckoutStrategyForImplementation(ctx context.Context, impl git.Implementation, opts git.CheckoutOptions) (git.CheckoutStrategy, error) {
|
||||
switch impl {
|
||||
case gogit.Implementation:
|
||||
return gogit.CheckoutStrategyForOptions(ctx, opts), nil
|
||||
case libgit2.Implementation:
|
||||
return libgit2.CheckoutStrategyForOptions(ctx, opts), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported Git implementation '%s'", impl)
|
||||
}
|
||||
}
|
|
@ -1,513 +0,0 @@
|
|||
/*
|
||||
Copyright 2021 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 strategy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fluxcd/pkg/gittestserver"
|
||||
"github.com/fluxcd/pkg/ssh"
|
||||
extgogit "github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/config"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/fluxcd/source-controller/pkg/git"
|
||||
"github.com/fluxcd/source-controller/pkg/git/gogit"
|
||||
"github.com/fluxcd/source-controller/pkg/git/libgit2"
|
||||
"github.com/fluxcd/source-controller/pkg/git/libgit2/managed"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
err := managed.InitManagedTransport()
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("failed to initialize libgit2 managed transport: %s", err))
|
||||
}
|
||||
code := m.Run()
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func TestCheckoutStrategyForImplementation_Auth(t *testing.T) {
|
||||
gitImpls := []git.Implementation{gogit.Implementation, libgit2.Implementation}
|
||||
|
||||
type testCase struct {
|
||||
name string
|
||||
transport git.TransportType
|
||||
repoURLFunc func(g *WithT, srv *gittestserver.GitServer, repoPath string) string
|
||||
authOptsFunc func(g *WithT, u *url.URL, user string, pswd string, ca []byte) *git.AuthOptions
|
||||
wantFunc func(g *WithT, cs git.CheckoutStrategy, dir string, repoURL string, authOpts *git.AuthOptions)
|
||||
}
|
||||
|
||||
cases := []testCase{
|
||||
{
|
||||
name: "HTTP clone",
|
||||
transport: git.HTTP,
|
||||
repoURLFunc: func(g *WithT, srv *gittestserver.GitServer, repoPath string) string {
|
||||
return srv.HTTPAddressWithCredentials() + "/" + repoPath
|
||||
},
|
||||
authOptsFunc: func(g *WithT, u *url.URL, user string, pswd string, ca []byte) *git.AuthOptions {
|
||||
return &git.AuthOptions{
|
||||
Transport: git.HTTP,
|
||||
Username: user,
|
||||
Password: pswd,
|
||||
TransportOptionsURL: getTransportOptionsURL(git.HTTP),
|
||||
}
|
||||
},
|
||||
wantFunc: func(g *WithT, cs git.CheckoutStrategy, dir string, repoURL string, authOpts *git.AuthOptions) {
|
||||
_, err := cs.Checkout(context.TODO(), dir, repoURL, authOpts)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "HTTPS clone",
|
||||
transport: git.HTTPS,
|
||||
repoURLFunc: func(g *WithT, srv *gittestserver.GitServer, repoPath string) string {
|
||||
return srv.HTTPAddress() + "/" + repoPath
|
||||
},
|
||||
authOptsFunc: func(g *WithT, u *url.URL, user, pswd string, ca []byte) *git.AuthOptions {
|
||||
return &git.AuthOptions{
|
||||
Transport: git.HTTPS,
|
||||
Username: user,
|
||||
Password: pswd,
|
||||
CAFile: ca,
|
||||
TransportOptionsURL: getTransportOptionsURL(git.HTTPS),
|
||||
}
|
||||
},
|
||||
wantFunc: func(g *WithT, cs git.CheckoutStrategy, dir, repoURL string, authOpts *git.AuthOptions) {
|
||||
_, err := cs.Checkout(context.TODO(), dir, repoURL, authOpts)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SSH clone",
|
||||
transport: git.SSH,
|
||||
repoURLFunc: func(g *WithT, srv *gittestserver.GitServer, repoPath string) string {
|
||||
return getSSHRepoURL(srv.SSHAddress(), repoPath)
|
||||
},
|
||||
authOptsFunc: func(g *WithT, u *url.URL, user, pswd string, ca []byte) *git.AuthOptions {
|
||||
knownhosts, err := ssh.ScanHostKey(u.Host, 5*time.Second, git.HostKeyAlgos, false)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
keygen := ssh.NewRSAGenerator(2048)
|
||||
pair, err := keygen.Generate()
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
return &git.AuthOptions{
|
||||
Host: u.Host, // Without this libgit2 returns error "user cancelled hostkey check".
|
||||
Transport: git.SSH,
|
||||
Username: "git", // Without this libgit2 returns error "username does not match previous request".
|
||||
Identity: pair.PrivateKey,
|
||||
KnownHosts: knownhosts,
|
||||
TransportOptionsURL: getTransportOptionsURL(git.SSH),
|
||||
}
|
||||
},
|
||||
wantFunc: func(g *WithT, cs git.CheckoutStrategy, dir, repoURL string, authOpts *git.AuthOptions) {
|
||||
_, err := cs.Checkout(context.TODO(), dir, repoURL, authOpts)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
testFunc := func(tt testCase, impl git.Implementation) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
var examplePublicKey, examplePrivateKey, exampleCA []byte
|
||||
|
||||
gitServer, err := gittestserver.NewTempGitServer()
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
defer os.RemoveAll(gitServer.Root())
|
||||
|
||||
username := "test-user"
|
||||
password := "test-password"
|
||||
gitServer.Auth(username, password)
|
||||
gitServer.KeyDir(gitServer.Root())
|
||||
|
||||
// Start the HTTP/HTTPS server.
|
||||
if tt.transport == git.HTTPS {
|
||||
var err error
|
||||
examplePublicKey, err = os.ReadFile("testdata/certs/server.pem")
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
examplePrivateKey, err = os.ReadFile("testdata/certs/server-key.pem")
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
exampleCA, err = os.ReadFile("testdata/certs/ca.pem")
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
err = gitServer.StartHTTPS(examplePublicKey, examplePrivateKey, exampleCA, "example.com")
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
} else {
|
||||
g.Expect(gitServer.StartHTTP()).ToNot(HaveOccurred())
|
||||
}
|
||||
|
||||
defer gitServer.StopHTTP()
|
||||
|
||||
// Start the SSH server.
|
||||
if tt.transport == git.SSH {
|
||||
g.Expect(gitServer.ListenSSH()).ToNot(HaveOccurred())
|
||||
go func() {
|
||||
gitServer.StartSSH()
|
||||
}()
|
||||
defer func() {
|
||||
g.Expect(gitServer.StopSSH()).To(Succeed())
|
||||
}()
|
||||
}
|
||||
|
||||
// Initialize a git repo.
|
||||
branch := "main"
|
||||
repoPath := "bar/test-reponame"
|
||||
err = gitServer.InitRepo("testdata/repo1", branch, repoPath)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
repoURL := tt.repoURLFunc(g, gitServer, repoPath)
|
||||
u, err := url.Parse(repoURL)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
authOpts := tt.authOptsFunc(g, u, username, password, exampleCA)
|
||||
|
||||
// Get the checkout strategy.
|
||||
checkoutOpts := git.CheckoutOptions{
|
||||
Branch: branch,
|
||||
}
|
||||
checkoutStrategy, err := CheckoutStrategyForImplementation(context.TODO(), impl, checkoutOpts)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
tt.wantFunc(g, checkoutStrategy, tmpDir, repoURL, authOpts)
|
||||
}
|
||||
}
|
||||
|
||||
// Run the test cases against the git implementations.
|
||||
for _, gitImpl := range gitImpls {
|
||||
for _, tt := range cases {
|
||||
t.Run(fmt.Sprintf("%s_%s", gitImpl, tt.name), testFunc(tt, gitImpl))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getSSHRepoURL(sshAddress, repoPath string) string {
|
||||
// This is expected to use 127.0.0.1, but host key
|
||||
// checking usually wants a hostname, so use
|
||||
// "localhost".
|
||||
sshURL := strings.Replace(sshAddress, "127.0.0.1", "localhost", 1)
|
||||
return sshURL + "/" + repoPath
|
||||
}
|
||||
|
||||
func TestCheckoutStrategyForImplementation_SemVerCheckout(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
gitImpls := []git.Implementation{gogit.Implementation, libgit2.Implementation}
|
||||
|
||||
// Setup git server and repo.
|
||||
gitServer, err := gittestserver.NewTempGitServer()
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
defer os.RemoveAll(gitServer.Root())
|
||||
username := "test-user"
|
||||
password := "test-password"
|
||||
gitServer.Auth(username, password)
|
||||
gitServer.KeyDir(gitServer.Root())
|
||||
g.Expect(gitServer.StartHTTP()).ToNot(HaveOccurred())
|
||||
defer gitServer.StopHTTP()
|
||||
|
||||
repoPath := "bar/test-reponame"
|
||||
err = gitServer.InitRepo("testdata/repo1", "main", repoPath)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
repoURL := gitServer.HTTPAddressWithCredentials() + "/" + repoPath
|
||||
|
||||
authOpts := &git.AuthOptions{
|
||||
Transport: git.HTTP,
|
||||
Username: username,
|
||||
Password: password,
|
||||
TransportOptionsURL: getTransportOptionsURL(git.HTTP),
|
||||
}
|
||||
|
||||
// Create test tags in the repo.
|
||||
now := time.Now()
|
||||
tags := []struct {
|
||||
tag string
|
||||
annotated bool
|
||||
commitTime time.Time
|
||||
tagTime time.Time
|
||||
}{
|
||||
{
|
||||
tag: "v0.0.1",
|
||||
annotated: false,
|
||||
commitTime: now,
|
||||
},
|
||||
{
|
||||
tag: "v0.1.0+build-1",
|
||||
annotated: true,
|
||||
commitTime: now.Add(10 * time.Minute),
|
||||
tagTime: now.Add(2 * time.Hour), // This should be ignored during TS comparisons
|
||||
},
|
||||
{
|
||||
tag: "v0.1.0+build-2",
|
||||
annotated: false,
|
||||
commitTime: now.Add(30 * time.Minute),
|
||||
},
|
||||
{
|
||||
tag: "v0.1.0+build-3",
|
||||
annotated: true,
|
||||
commitTime: now.Add(1 * time.Hour),
|
||||
tagTime: now.Add(1 * time.Hour), // This should be ignored during TS comparisons
|
||||
},
|
||||
{
|
||||
tag: "0.2.0",
|
||||
annotated: true,
|
||||
commitTime: now,
|
||||
tagTime: now,
|
||||
},
|
||||
}
|
||||
|
||||
// Clone the repo locally.
|
||||
cloneDir := t.TempDir()
|
||||
repo, err := extgogit.PlainClone(cloneDir, false, &extgogit.CloneOptions{
|
||||
URL: repoURL,
|
||||
})
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Create commits and tags.
|
||||
// Keep a record of all the tags and commit refs.
|
||||
refs := make(map[string]string, len(tags))
|
||||
for _, tt := range tags {
|
||||
ref, err := commitFile(repo, "tag", tt.tag, tt.commitTime)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
_, err = tag(repo, ref, tt.annotated, tt.tag, tt.tagTime)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
refs[tt.tag] = ref.String()
|
||||
}
|
||||
|
||||
// Push everything.
|
||||
err = repo.Push(&extgogit.PushOptions{
|
||||
RefSpecs: []config.RefSpec{"refs/*:refs/*"},
|
||||
})
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Test cases.
|
||||
type testCase struct {
|
||||
name string
|
||||
constraint string
|
||||
expectErr error
|
||||
expectTag string
|
||||
}
|
||||
tests := []testCase{
|
||||
{
|
||||
name: "Orders by SemVer",
|
||||
constraint: ">0.1.0",
|
||||
expectTag: "0.2.0",
|
||||
},
|
||||
{
|
||||
name: "Orders by SemVer and timestamp",
|
||||
constraint: "<0.2.0",
|
||||
expectTag: "v0.1.0+build-3",
|
||||
},
|
||||
{
|
||||
name: "Errors without match",
|
||||
constraint: ">=1.0.0",
|
||||
expectErr: errors.New("no match found for semver: >=1.0.0"),
|
||||
},
|
||||
}
|
||||
testFunc := func(tt testCase, impl git.Implementation) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
// Get the checkout strategy.
|
||||
checkoutOpts := git.CheckoutOptions{
|
||||
SemVer: tt.constraint,
|
||||
}
|
||||
checkoutStrategy, err := CheckoutStrategyForImplementation(context.TODO(), impl, checkoutOpts)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Checkout and verify.
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
cc, err := checkoutStrategy.Checkout(context.TODO(), tmpDir, repoURL, authOpts)
|
||||
if tt.expectErr != nil {
|
||||
g.Expect(err).To(Equal(tt.expectErr))
|
||||
g.Expect(cc).To(BeNil())
|
||||
return
|
||||
}
|
||||
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
g.Expect(cc.String()).To(Equal(tt.expectTag + "/" + refs[tt.expectTag]))
|
||||
g.Expect(filepath.Join(tmpDir, "tag")).To(BeARegularFile())
|
||||
g.Expect(os.ReadFile(filepath.Join(tmpDir, "tag"))).To(BeEquivalentTo(tt.expectTag))
|
||||
}
|
||||
}
|
||||
|
||||
// Run the test cases against the git implementations.
|
||||
for _, gitImpl := range gitImpls {
|
||||
for _, tt := range tests {
|
||||
t.Run(fmt.Sprintf("%s_%s", gitImpl, tt.name), testFunc(tt, gitImpl))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckoutStrategyForImplementation_WithCtxTimeout(t *testing.T) {
|
||||
gitImpls := []git.Implementation{gogit.Implementation, libgit2.Implementation}
|
||||
|
||||
type testCase struct {
|
||||
name string
|
||||
timeout time.Duration
|
||||
wantErr bool
|
||||
}
|
||||
|
||||
cases := []testCase{
|
||||
{
|
||||
name: "fails with short timeout",
|
||||
timeout: 100 * time.Millisecond,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "succeeds with sufficient timeout",
|
||||
timeout: 5 * time.Second,
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
// Keeping it low to keep the test run time low.
|
||||
serverDelay := 500 * time.Millisecond
|
||||
|
||||
testFunc := func(tt testCase, impl git.Implementation) func(t *testing.T) {
|
||||
return func(*testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
gitServer, err := gittestserver.NewTempGitServer()
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
defer os.RemoveAll(gitServer.Root())
|
||||
username := "test-user"
|
||||
password := "test-password"
|
||||
gitServer.Auth(username, password)
|
||||
gitServer.KeyDir(gitServer.Root())
|
||||
|
||||
middleware := func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(serverDelay)
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
gitServer.AddHTTPMiddlewares(middleware)
|
||||
|
||||
g.Expect(gitServer.StartHTTP()).ToNot(HaveOccurred())
|
||||
defer gitServer.StopHTTP()
|
||||
|
||||
branch := "main"
|
||||
repoPath := "bar/test-reponame"
|
||||
err = gitServer.InitRepo("testdata/repo1", branch, repoPath)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
repoURL := gitServer.HTTPAddressWithCredentials() + "/" + repoPath
|
||||
|
||||
authOpts := &git.AuthOptions{
|
||||
Transport: git.HTTP,
|
||||
Username: username,
|
||||
Password: password,
|
||||
TransportOptionsURL: getTransportOptionsURL(git.HTTP),
|
||||
}
|
||||
|
||||
checkoutOpts := git.CheckoutOptions{
|
||||
Branch: branch,
|
||||
}
|
||||
checkoutStrategy, err := CheckoutStrategyForImplementation(context.TODO(), impl, checkoutOpts)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
checkoutCtx, cancel := context.WithTimeout(context.TODO(), tt.timeout)
|
||||
defer cancel()
|
||||
|
||||
_, gotErr := checkoutStrategy.Checkout(checkoutCtx, tmpDir, repoURL, authOpts)
|
||||
if tt.wantErr {
|
||||
g.Expect(gotErr).To(HaveOccurred())
|
||||
} else {
|
||||
g.Expect(gotErr).ToNot(HaveOccurred())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run the test cases against the git implementations.
|
||||
for _, gitImpl := range gitImpls {
|
||||
for _, tt := range cases {
|
||||
t.Run(fmt.Sprintf("%s_%s", gitImpl, tt.name), testFunc(tt, gitImpl))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func commitFile(repo *extgogit.Repository, path, content string, time time.Time) (plumbing.Hash, error) {
|
||||
wt, err := repo.Worktree()
|
||||
if err != nil {
|
||||
return plumbing.Hash{}, err
|
||||
}
|
||||
f, err := wt.Filesystem.Create(path)
|
||||
if err != nil {
|
||||
return plumbing.Hash{}, err
|
||||
}
|
||||
if _, err := f.Write([]byte(content)); err != nil {
|
||||
if ferr := f.Close(); ferr != nil {
|
||||
return plumbing.Hash{}, ferr
|
||||
}
|
||||
return plumbing.Hash{}, err
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
return plumbing.Hash{}, err
|
||||
}
|
||||
if _, err := wt.Add(path); err != nil {
|
||||
return plumbing.Hash{}, err
|
||||
}
|
||||
return wt.Commit("Adding: "+path, &extgogit.CommitOptions{
|
||||
Author: mockSignature(time),
|
||||
Committer: mockSignature(time),
|
||||
})
|
||||
}
|
||||
|
||||
func tag(repo *extgogit.Repository, commit plumbing.Hash, annotated bool, tag string, time time.Time) (*plumbing.Reference, error) {
|
||||
var opts *extgogit.CreateTagOptions
|
||||
if annotated {
|
||||
opts = &extgogit.CreateTagOptions{
|
||||
Tagger: mockSignature(time),
|
||||
Message: "Annotated tag for: " + tag,
|
||||
}
|
||||
}
|
||||
return repo.CreateTag(tag, commit, opts)
|
||||
}
|
||||
|
||||
func mockSignature(time time.Time) *object.Signature {
|
||||
return &object.Signature{
|
||||
Name: "Jane Doe",
|
||||
Email: "jane@example.com",
|
||||
When: time,
|
||||
}
|
||||
}
|
||||
|
||||
func getTransportOptionsURL(transport git.TransportType) string {
|
||||
letterRunes := []rune("abcdefghijklmnopqrstuvwxyz1234567890")
|
||||
b := make([]rune, 10)
|
||||
for i := range b {
|
||||
b[i] = letterRunes[rand.Intn(len(letterRunes))]
|
||||
}
|
||||
return string(transport) + "://" + string(b)
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
# Copyright 2021 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.
|
||||
|
||||
all: server-key.pem
|
||||
|
||||
ca-key.pem: ca-csr.json
|
||||
cfssl gencert -initca ca-csr.json | cfssljson -bare ca –
|
||||
ca.pem: ca-key.pem
|
||||
ca.csr: ca-key.pem
|
||||
|
||||
server-key.pem: server-csr.json ca-config.json ca-key.pem
|
||||
cfssl gencert \
|
||||
-ca=ca.pem \
|
||||
-ca-key=ca-key.pem \
|
||||
-config=ca-config.json \
|
||||
-profile=web-servers \
|
||||
server-csr.json | cfssljson -bare server
|
||||
sever.pem: server-key.pem
|
||||
server.csr: server-key.pem
|
|
@ -1,18 +0,0 @@
|
|||
{
|
||||
"signing": {
|
||||
"default": {
|
||||
"expiry": "87600h"
|
||||
},
|
||||
"profiles": {
|
||||
"web-servers": {
|
||||
"usages": [
|
||||
"signing",
|
||||
"key encipherment",
|
||||
"server auth",
|
||||
"client auth"
|
||||
],
|
||||
"expiry": "87600h"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"CN": "example.com CA",
|
||||
"hosts": [
|
||||
"127.0.0.1",
|
||||
"localhost",
|
||||
"example.com",
|
||||
"www.example.com"
|
||||
]
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIOH/u9dMcpVcZ0+X9Fc78dCTj8SHuXawhLjhu/ej64WToAoGCCqGSM49
|
||||
AwEHoUQDQgAEruH/kPxtX3cyYR2G7TYmxLq6AHyzo/NGXc9XjGzdJutE2SQzn37H
|
||||
dvSJbH+Lvqo9ik0uiJVRVdCYD1j7gNszGA==
|
||||
-----END EC PRIVATE KEY-----
|
|
@ -1,9 +0,0 @@
|
|||
-----BEGIN CERTIFICATE REQUEST-----
|
||||
MIIBIDCBxgIBADAZMRcwFQYDVQQDEw5leGFtcGxlLmNvbSBDQTBZMBMGByqGSM49
|
||||
AgEGCCqGSM49AwEHA0IABK7h/5D8bV93MmEdhu02JsS6ugB8s6PzRl3PV4xs3Sbr
|
||||
RNkkM59+x3b0iWx/i76qPYpNLoiVUVXQmA9Y+4DbMxigSzBJBgkqhkiG9w0BCQ4x
|
||||
PDA6MDgGA1UdEQQxMC+CCWxvY2FsaG9zdIILZXhhbXBsZS5jb22CD3d3dy5leGFt
|
||||
cGxlLmNvbYcEfwAAATAKBggqhkjOPQQDAgNJADBGAiEAkw85nyLhJssyCYsaFvRU
|
||||
EErhu66xHPJug/nG50uV5OoCIQCUorrflOSxfChPeCe4xfwcPv7FpcCYbKVYtGzz
|
||||
b34Wow==
|
||||
-----END CERTIFICATE REQUEST-----
|
|
@ -1,11 +0,0 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIBhzCCAS2gAwIBAgIUdsAtiX3gN0uk7ddxASWYE/tdv0wwCgYIKoZIzj0EAwIw
|
||||
GTEXMBUGA1UEAxMOZXhhbXBsZS5jb20gQ0EwHhcNMjAwNDE3MDgxODAwWhcNMjUw
|
||||
NDE2MDgxODAwWjAZMRcwFQYDVQQDEw5leGFtcGxlLmNvbSBDQTBZMBMGByqGSM49
|
||||
AgEGCCqGSM49AwEHA0IABK7h/5D8bV93MmEdhu02JsS6ugB8s6PzRl3PV4xs3Sbr
|
||||
RNkkM59+x3b0iWx/i76qPYpNLoiVUVXQmA9Y+4DbMxijUzBRMA4GA1UdDwEB/wQE
|
||||
AwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQGyUiU1QEZiMAqjsnIYTwZ
|
||||
4yp5wzAPBgNVHREECDAGhwR/AAABMAoGCCqGSM49BAMCA0gAMEUCIQDzdtvKdE8O
|
||||
1+WRTZ9MuSiFYcrEz7Zne7VXouDEKqKEigIgM4WlbDeuNCKbqhqj+xZV0pa3rweb
|
||||
OD8EjjCMY69RMO0=
|
||||
-----END CERTIFICATE-----
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"CN": "example.com",
|
||||
"hosts": [
|
||||
"127.0.0.1",
|
||||
"localhost",
|
||||
"example.com",
|
||||
"www.example.com"
|
||||
]
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIKQbEXV6nljOHMmPrWVWQ+JrAE5wsbE9iMhfY7wlJgXOoAoGCCqGSM49
|
||||
AwEHoUQDQgAE+53oBGlrvVUTelSGYji8GNHVhVg8jOs1PeeLuXCIZjQmctHLFEq3
|
||||
fE+mGxCL93MtpYzlwIWBf0m7pEGQre6bzg==
|
||||
-----END EC PRIVATE KEY-----
|
|
@ -1,8 +0,0 @@
|
|||
-----BEGIN CERTIFICATE REQUEST-----
|
||||
MIIBHDCBwwIBADAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbTBZMBMGByqGSM49AgEG
|
||||
CCqGSM49AwEHA0IABPud6ARpa71VE3pUhmI4vBjR1YVYPIzrNT3ni7lwiGY0JnLR
|
||||
yxRKt3xPphsQi/dzLaWM5cCFgX9Ju6RBkK3um86gSzBJBgkqhkiG9w0BCQ4xPDA6
|
||||
MDgGA1UdEQQxMC+CCWxvY2FsaG9zdIILZXhhbXBsZS5jb22CD3d3dy5leGFtcGxl
|
||||
LmNvbYcEfwAAATAKBggqhkjOPQQDAgNIADBFAiB5A6wvQ5x6g/zhiyn+wLzXsOaB
|
||||
Gb/F25p/zTHHQqZbkwIhAPUgWzy/2bs6eZEi97bSlaRdmrqHwqT842t5sEwGyXNV
|
||||
-----END CERTIFICATE REQUEST-----
|
|
@ -1,13 +0,0 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIB7TCCAZKgAwIBAgIUB+17B8PU05wVTzRHLeG+S+ybZK4wCgYIKoZIzj0EAwIw
|
||||
GTEXMBUGA1UEAxMOZXhhbXBsZS5jb20gQ0EwHhcNMjAwNDE3MDgxODAwWhcNMzAw
|
||||
NDE1MDgxODAwWjAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbTBZMBMGByqGSM49AgEG
|
||||
CCqGSM49AwEHA0IABPud6ARpa71VE3pUhmI4vBjR1YVYPIzrNT3ni7lwiGY0JnLR
|
||||
yxRKt3xPphsQi/dzLaWM5cCFgX9Ju6RBkK3um86jgbowgbcwDgYDVR0PAQH/BAQD
|
||||
AgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAA
|
||||
MB0GA1UdDgQWBBTM8HS5EIlVMBYv/300jN8PEArUgDAfBgNVHSMEGDAWgBQGyUiU
|
||||
1QEZiMAqjsnIYTwZ4yp5wzA4BgNVHREEMTAvgglsb2NhbGhvc3SCC2V4YW1wbGUu
|
||||
Y29tgg93d3cuZXhhbXBsZS5jb22HBH8AAAEwCgYIKoZIzj0EAwIDSQAwRgIhAOgB
|
||||
5W82FEgiTTOmsNRekkK5jUPbj4D4eHtb2/BI7ph4AiEA2AxHASIFBdv5b7Qf5prb
|
||||
bdNmUCzAvVuCAKuMjg2OPrE=
|
||||
-----END CERTIFICATE-----
|
|
@ -1 +0,0 @@
|
|||
test file
|
|
@ -1 +0,0 @@
|
|||
test file
|
Loading…
Reference in New Issue