gitrepo: refactor reconciler to use fluxcd/pkg/git

Signed-off-by: Sanskar Jaiswal <jaiswalsanskar078@gmail.com>
This commit is contained in:
Sanskar Jaiswal 2022-08-30 20:53:10 +05:30 committed by Paulo Gomes
parent a9a85b2b0f
commit b6d6b593c8
No known key found for this signature in database
GPG Key ID: 9995233870E99BEE
45 changed files with 93 additions and 7239 deletions

View File

@ -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
}

View File

@ -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)

View File

@ -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) {

View File

@ -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
View File

@ -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
View File

@ -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=

View File

@ -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,

View File

@ -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
}

View File

@ -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))
})
}
}

View File

@ -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 ""
}

View File

@ -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,
}
}

View File

@ -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"
)

View File

@ -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
}

View File

@ -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())
}

View File

@ -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)
}
}

View File

@ -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())
})
}
}

View File

@ -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)
}

View File

@ -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"
)

View File

@ -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
)

View File

@ -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
}

View File

@ -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()
})
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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())
})
}
}

View File

@ -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
}

View File

@ -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()
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}
})
}
}

View File

@ -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))
})
}
}

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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

View File

@ -1,18 +0,0 @@
{
"signing": {
"default": {
"expiry": "87600h"
},
"profiles": {
"web-servers": {
"usages": [
"signing",
"key encipherment",
"server auth",
"client auth"
],
"expiry": "87600h"
}
}
}
}

View File

@ -1,9 +0,0 @@
{
"CN": "example.com CA",
"hosts": [
"127.0.0.1",
"localhost",
"example.com",
"www.example.com"
]
}

View File

@ -1,5 +0,0 @@
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIOH/u9dMcpVcZ0+X9Fc78dCTj8SHuXawhLjhu/ej64WToAoGCCqGSM49
AwEHoUQDQgAEruH/kPxtX3cyYR2G7TYmxLq6AHyzo/NGXc9XjGzdJutE2SQzn37H
dvSJbH+Lvqo9ik0uiJVRVdCYD1j7gNszGA==
-----END EC PRIVATE KEY-----

View File

@ -1,9 +0,0 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIBIDCBxgIBADAZMRcwFQYDVQQDEw5leGFtcGxlLmNvbSBDQTBZMBMGByqGSM49
AgEGCCqGSM49AwEHA0IABK7h/5D8bV93MmEdhu02JsS6ugB8s6PzRl3PV4xs3Sbr
RNkkM59+x3b0iWx/i76qPYpNLoiVUVXQmA9Y+4DbMxigSzBJBgkqhkiG9w0BCQ4x
PDA6MDgGA1UdEQQxMC+CCWxvY2FsaG9zdIILZXhhbXBsZS5jb22CD3d3dy5leGFt
cGxlLmNvbYcEfwAAATAKBggqhkjOPQQDAgNJADBGAiEAkw85nyLhJssyCYsaFvRU
EErhu66xHPJug/nG50uV5OoCIQCUorrflOSxfChPeCe4xfwcPv7FpcCYbKVYtGzz
b34Wow==
-----END CERTIFICATE REQUEST-----

View File

@ -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-----

View File

@ -1,9 +0,0 @@
{
"CN": "example.com",
"hosts": [
"127.0.0.1",
"localhost",
"example.com",
"www.example.com"
]
}

View File

@ -1,5 +0,0 @@
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIKQbEXV6nljOHMmPrWVWQ+JrAE5wsbE9iMhfY7wlJgXOoAoGCCqGSM49
AwEHoUQDQgAE+53oBGlrvVUTelSGYji8GNHVhVg8jOs1PeeLuXCIZjQmctHLFEq3
fE+mGxCL93MtpYzlwIWBf0m7pEGQre6bzg==
-----END EC PRIVATE KEY-----

View File

@ -1,8 +0,0 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIBHDCBwwIBADAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbTBZMBMGByqGSM49AgEG
CCqGSM49AwEHA0IABPud6ARpa71VE3pUhmI4vBjR1YVYPIzrNT3ni7lwiGY0JnLR
yxRKt3xPphsQi/dzLaWM5cCFgX9Ju6RBkK3um86gSzBJBgkqhkiG9w0BCQ4xPDA6
MDgGA1UdEQQxMC+CCWxvY2FsaG9zdIILZXhhbXBsZS5jb22CD3d3dy5leGFtcGxl
LmNvbYcEfwAAATAKBggqhkjOPQQDAgNIADBFAiB5A6wvQ5x6g/zhiyn+wLzXsOaB
Gb/F25p/zTHHQqZbkwIhAPUgWzy/2bs6eZEi97bSlaRdmrqHwqT842t5sEwGyXNV
-----END CERTIFICATE REQUEST-----

View File

@ -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-----

View File

@ -1 +0,0 @@
test file

View File

@ -1 +0,0 @@
test file