Merge branch 'main' into bucket-provider-interface

This commit is contained in:
Joe Alagoa 2021-11-02 14:55:53 -05:00 committed by GitHub
commit fb6024ed3d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 2603 additions and 950 deletions

View File

@ -2,6 +2,36 @@
All notable changes to this project are documented in this file.
## 0.17.1
**Release date:** 2021-10-30
Fixes:
* Fix pointer error during public key import
[#479](https://github.com/fluxcd/source-controller/pull/479)
## 0.17.0
**Release date:** 2021-10-28
For this prerelease we focused on further improving the Git implementations, partly
to increase stability and test coverage, partly to ensure they are prepared to be
moved out into a separate module. With this work, it is now possible to define just
a Git commit as a reference, which will result in an `Artifact` with a `Revision`
format of `HEAD/<commit SHA>`.
For the `go-git` implementation, defining the branch and a commit reference will
result in a more efficient shallow clone, and using this information when it is
available to you is therefore encouraged.
Improvements:
* git: refactor authentication, checkout and verification
[#462](https://github.com/fluxcd/source-controller/pull/462)
Fixes:
* libgit2: handle EOF in parseKnownHosts()
[#475](https://github.com/fluxcd/source-controller/pull/475)
## 0.16.1
**Release date:** 2021-10-22

View File

@ -1,102 +0,0 @@
# Contributing
Source Controller is [Apache 2.0 licensed](LICENSE) and accepts contributions
via GitHub pull requests. This document outlines some of the conventions on
to make it easier to get your contribution accepted.
We gratefully welcome improvements to issues and documentation as well as to
code.
## Certificate of Origin
By contributing to this project you agree to the Developer Certificate of
Origin (DCO). This document was created by the Linux Kernel community and is a
simple statement that you, as a contributor, have the legal right to make the
contribution. No action from you is required, but it's a good idea to see the
[DCO](DCO) file for details before you start contributing code to Source
Controller.
## Communications
The project uses Slack: To join the conversation, simply join the
[CNCF](https://slack.cncf.io/) Slack workspace and use the
[#flux](https://cloud-native.slack.com/messages/flux/) channel.
The developers use a mailing list to discuss development as well.
Simply subscribe to [flux-dev on cncf.io](https://lists.cncf.io/g/cncf-flux-dev)
to join the conversation (this will also add an invitation to your
Google calendar for our [Flux
meeting](https://docs.google.com/document/d/1l_M0om0qUEN_NNiGgpqJ2tvsF2iioHkaARDeh6b70B0/edit#)).
## Installing required dependencies
The dependency [libgit2](https://libgit2.org/) needs to be installed to be able
to run source-controller or its test-suite locally (not in a container).
In case this dependency is not present on your system (at the expected
version), the first invocation of a `make` target that requires the
dependency will attempt to compile it locally to `hack/libgit2`. For this build
to succeed; CMake, Docker, OpenSSL 1.1 and LibSSH2 must be present on the system.
Triggering a manual build of the dependency is possible as well by running
`make libgit2`. To enforce the build, for example if your system dependencies
match but are not linked in a compatible way, append `LIBGIT2_FORCE=1` to the
`make` command.
### macOS
```console
$ # Ensure libgit2 dependencies are available
$ brew install cmake openssl@1.1 libssh2 pkg-config
$ LIBGIT2_FORCE=1 make libgit2
```
### Linux
```console
$ # Ensure libgit2 dependencies are available
$ pacman -S cmake openssl libssh2
$ LIBGIT2_FORCE=1 make libgit2
```
**Note:** Example shown is for Arch Linux, but likewise procedure can be
followed using any other package manager, e.g. `apt`.
## How to run the test suite
You can run the unit tests by simply doing
```bash
make test
```
## Acceptance policy
These things will make a PR more likely to be accepted:
- a well-described requirement
- tests for new code
- tests for old code!
- new code and tests follow the conventions in old code and tests
- a good commit message (see below)
- all code must abide [Go Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments)
- names should abide [What's in a name](https://talks.golang.org/2014/names.slide#1)
- code must build on both Linux and Darwin, via plain `go build`
- code should have appropriate test coverage and tests should be written
to work with `go test`
In general, we will merge a PR once one maintainer has endorsed it.
For substantial changes, more people may become involved, and you might
get asked to resubmit the PR or divide the changes into more than one PR.
### Format of the Commit Message
For Source Controller we prefer the following rules for good commit messages:
- Limit the subject to 50 characters and write as the continuation
of the sentence "If applied, this commit will ..."
- Explain what and why in the body, if more than a trivial change;
wrap it at 72 characters.
The [following article](https://chris.beams.io/posts/git-commit/#seven-rules)
has some more helpful advice on documenting your work.

47
DEVELOPMENT.md Normal file
View File

@ -0,0 +1,47 @@
# Development
> **Note:** Please take a look at <https://fluxcd.io/docs/contributing/flux/>
> to find out about how to contribute to Flux and how to interact with the
> Flux Development team.
## Installing required dependencies
The dependency [libgit2](https://libgit2.org/) needs to be installed to be able
to run source-controller or its test-suite locally (not in a container).
In case this dependency is not present on your system (at the expected
version), the first invocation of a `make` target that requires the
dependency will attempt to compile it locally to `hack/libgit2`. For this build
to succeed; CMake, Docker, OpenSSL 1.1 and LibSSH2 must be present on the system.
Triggering a manual build of the dependency is possible as well by running
`make libgit2`. To enforce the build, for example if your system dependencies
match but are not linked in a compatible way, append `LIBGIT2_FORCE=1` to the
`make` command.
### macOS
```console
$ # Ensure libgit2 dependencies are available
$ brew install cmake openssl@1.1 libssh2 pkg-config
$ LIBGIT2_FORCE=1 make libgit2
```
### Linux
```console
$ # Ensure libgit2 dependencies are available
$ pacman -S cmake openssl libssh2
$ LIBGIT2_FORCE=1 make libgit2
```
**Note:** Example shown is for Arch Linux, but likewise procedure can be
followed using any other package manager, e.g. `apt`.
## How to run the test suite
You can run the unit tests by simply doing
```bash
make test
```

View File

@ -120,7 +120,6 @@ type GitRepositoryInclude struct {
// GitRepositoryRef defines the Git ref used for pull and checkout operations.
type GitRepositoryRef struct {
// The Git branch to checkout, defaults to master.
// +kubebuilder:default:=master
// +optional
Branch string `json:"branch,omitempty"`

View File

@ -91,7 +91,6 @@ spec:
description: The Git reference to checkout and monitor for changes, defaults to master branch.
properties:
branch:
default: master
description: The Git branch to checkout, defaults to master.
type: string
commit:

View File

@ -6,4 +6,4 @@ resources:
images:
- name: fluxcd/source-controller
newName: fluxcd/source-controller
newTag: v0.16.1
newTag: v0.17.1

View File

@ -229,45 +229,35 @@ func (r *GitRepositoryReconciler) reconcile(ctx context.Context, repository sour
}
defer os.RemoveAll(tmpGit)
// determine auth method
auth := &git.Auth{}
// Configure auth options using secret
var authOpts *git.AuthOptions
if repository.Spec.SecretRef != nil {
authStrategy, err := strategy.AuthSecretStrategyForURL(
repository.Spec.URL,
git.CheckoutOptions{
GitImplementation: repository.Spec.GitImplementation,
RecurseSubmodules: repository.Spec.RecurseSubmodules,
})
if err != nil {
return sourcev1.GitRepositoryNotReady(repository, sourcev1.AuthenticationFailedReason, err.Error()), err
}
name := types.NamespacedName{
Namespace: repository.GetNamespace(),
Name: repository.Spec.SecretRef.Name,
}
var secret corev1.Secret
err = r.Client.Get(ctx, name, &secret)
secret := &corev1.Secret{}
err = r.Client.Get(ctx, name, secret)
if err != nil {
err = fmt.Errorf("auth secret error: %w", err)
return sourcev1.GitRepositoryNotReady(repository, sourcev1.AuthenticationFailedReason, err.Error()), err
}
auth, err = authStrategy.Method(secret)
authOpts, err = git.AuthOptionsFromSecret(repository.Spec.URL, secret)
if err != nil {
err = fmt.Errorf("auth error: %w", err)
return sourcev1.GitRepositoryNotReady(repository, sourcev1.AuthenticationFailedReason, err.Error()), err
}
}
checkoutStrategy, err := strategy.CheckoutStrategyForRef(
repository.Spec.Reference,
git.CheckoutOptions{
GitImplementation: repository.Spec.GitImplementation,
RecurseSubmodules: repository.Spec.RecurseSubmodules,
},
)
checkoutOpts := git.CheckoutOptions{RecurseSubmodules: repository.Spec.RecurseSubmodules}
if ref := repository.Spec.Reference; ref != nil {
checkoutOpts.Branch = ref.Branch
checkoutOpts.Commit = ref.Commit
checkoutOpts.Tag = ref.Tag
checkoutOpts.SemVer = ref.SemVer
}
checkoutStrategy, err := strategy.CheckoutStrategyForImplementation(ctx,
git.Implementation(repository.Spec.GitImplementation), checkoutOpts)
if err != nil {
return sourcev1.GitRepositoryNotReady(repository, sourcev1.GitOperationFailedReason, err.Error()), err
}
@ -275,12 +265,11 @@ func (r *GitRepositoryReconciler) reconcile(ctx context.Context, repository sour
gitCtx, cancel := context.WithTimeout(ctx, repository.Spec.Timeout.Duration)
defer cancel()
commit, revision, err := checkoutStrategy.Checkout(gitCtx, tmpGit, repository.Spec.URL, auth)
commit, err := checkoutStrategy.Checkout(gitCtx, tmpGit, repository.Spec.URL, authOpts)
if err != nil {
return sourcev1.GitRepositoryNotReady(repository, sourcev1.GitOperationFailedReason, err.Error()), err
}
artifact := r.Storage.NewArtifactFor(repository.Kind, repository.GetObjectMeta(), revision, fmt.Sprintf("%s.tar.gz", commit.Hash()))
artifact := r.Storage.NewArtifactFor(repository.Kind, repository.GetObjectMeta(), commit.String(), fmt.Sprintf("%s.tar.gz", commit.Hash.String()))
// copy all included repository into the artifact
includedArtifacts := []*sourcev1.Artifact{}
@ -309,14 +298,17 @@ func (r *GitRepositoryReconciler) reconcile(ctx context.Context, repository sour
Namespace: repository.Namespace,
Name: repository.Spec.Verification.SecretRef.Name,
}
var secret corev1.Secret
if err := r.Client.Get(ctx, publicKeySecret, &secret); err != nil {
secret := &corev1.Secret{}
if err := r.Client.Get(ctx, publicKeySecret, secret); err != nil {
err = fmt.Errorf("PGP public keys secret error: %w", err)
return sourcev1.GitRepositoryNotReady(repository, sourcev1.VerificationFailedReason, err.Error()), err
}
err := commit.Verify(secret)
if err != nil {
var keyRings []string
for _, v := range secret.Data {
keyRings = append(keyRings, string(v))
}
if _, err = commit.Verify(keyRings...); err != nil {
return sourcev1.GitRepositoryNotReady(repository, sourcev1.VerificationFailedReason, err.Error()), err
}
}

View File

@ -23,11 +23,9 @@ import (
"net/http"
"net/url"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"time"
@ -251,7 +249,7 @@ var _ = Describe("GitRepositoryReconciler", func() {
reference: &sourcev1.GitRepositoryRef{SemVer: "1.2.3.4"},
waitForReason: sourcev1.GitOperationFailedReason,
expectStatus: metav1.ConditionFalse,
expectMessage: "semver parse range error: improper constraint: 1.2.3.4",
expectMessage: "semver parse error: improper constraint: 1.2.3.4",
}),
Entry("semver no match", refTestCase{
reference: &sourcev1.GitRepositoryRef{SemVer: "1.0.0"},
@ -265,7 +263,7 @@ var _ = Describe("GitRepositoryReconciler", func() {
},
waitForReason: sourcev1.GitOperationSucceedReason,
expectStatus: metav1.ConditionTrue,
expectRevision: "master",
expectRevision: "HEAD",
}),
Entry("commit in branch", refTestCase{
reference: &sourcev1.GitRepositoryRef{
@ -284,7 +282,7 @@ var _ = Describe("GitRepositoryReconciler", func() {
},
waitForReason: sourcev1.GitOperationFailedReason,
expectStatus: metav1.ConditionFalse,
expectMessage: "git commit 'invalid' not found: object not found",
expectMessage: "failed to resolve commit object for 'invalid': object not found",
}),
)
@ -385,7 +383,7 @@ var _ = Describe("GitRepositoryReconciler", func() {
reference: &sourcev1.GitRepositoryRef{Branch: "main"},
waitForReason: sourcev1.GitOperationFailedReason,
expectStatus: metav1.ConditionFalse,
expectMessage: "error: user rejected certificate",
expectMessage: "unable to clone: user rejected certificate",
gitImplementation: sourcev1.LibGit2Implementation,
}),
Entry("self signed libgit2 with CA", refTestCase{

View File

@ -529,7 +529,7 @@ func (r *HelmChartReconciler) reconcileFromTarballArtifact(ctx context.Context,
v, err := semver.NewVersion(helmChart.Metadata.Version)
if err != nil {
err = fmt.Errorf("semver error: %w", err)
err = fmt.Errorf("semver parse error: %w", err)
return sourcev1.HelmChartNotReady(chart, sourcev1.StorageOperationFailedReason, err.Error()), err
}
@ -539,7 +539,7 @@ func (r *HelmChartReconciler) reconcileFromTarballArtifact(ctx context.Context,
splitRev := strings.Split(artifact.Revision, "/")
v, err := v.SetMetadata(splitRev[len(splitRev)-1])
if err != nil {
err = fmt.Errorf("semver error: %w", err)
err = fmt.Errorf("semver parse error: %w", err)
return sourcev1.HelmChartNotReady(chart, sourcev1.StorageOperationFailedReason, err.Error()), err
}

View File

@ -273,6 +273,21 @@ spec:
commit: 363a6a8fe6a7f13e05d34c163b0ef02a777da20a
```
Checkout a specific commit:
```yaml
apiVersion: source.toolkit.fluxcd.io/v1beta1
kind: GitRepository
metadata:
name: podinfo
namespace: default
spec:
interval: 1m
url: https://github.com/stefanprodan/podinfo
ref:
commit: 363a6a8fe6a7f13e05d34c163b0ef02a777da20a
```
Pull a specific tag:
```yaml

5
go.mod
View File

@ -8,9 +8,10 @@ require (
cloud.google.com/go v0.93.3 // indirect
cloud.google.com/go/storage v1.16.0
github.com/Masterminds/semver/v3 v3.1.1
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7
github.com/cyphar/filepath-securejoin v0.2.2
github.com/fluxcd/pkg/apis/meta v0.10.0
github.com/fluxcd/pkg/gittestserver v0.3.0
github.com/fluxcd/pkg/gittestserver v0.4.1
github.com/fluxcd/pkg/gitutil v0.1.0
github.com/fluxcd/pkg/helmtestserver v0.2.0
github.com/fluxcd/pkg/lockedfile v0.1.0
@ -18,7 +19,7 @@ require (
github.com/fluxcd/pkg/ssh v0.1.0
github.com/fluxcd/pkg/untar v0.1.0
github.com/fluxcd/pkg/version v0.1.0
github.com/fluxcd/source-controller/api v0.16.1
github.com/fluxcd/source-controller/api v0.17.1
github.com/go-git/go-billy/v5 v5.3.1
github.com/go-git/go-git/v5 v5.4.2
github.com/go-logr/logr v0.4.0

5
go.sum
View File

@ -266,8 +266,8 @@ github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fluxcd/pkg/apis/meta v0.10.0 h1:N7wVGHC1cyPdT87hrDC7UwCwRwnZdQM46PBSLjG2rlE=
github.com/fluxcd/pkg/apis/meta v0.10.0/go.mod h1:CW9X9ijMTpNe7BwnokiUOrLl/h13miwVr/3abEQLbKE=
github.com/fluxcd/pkg/gittestserver v0.3.0 h1:6aa30mybecBwBWaJ2IEk7pQzefWnjWjxkTSrHMHawvg=
github.com/fluxcd/pkg/gittestserver v0.3.0/go.mod h1:8j36Z6B0BuKNZZ6exAWoyDEpyQoFcjz1IX3WBT7PZNg=
github.com/fluxcd/pkg/gittestserver v0.4.1 h1:knghRrVEEPnpO0VJYjoz0H2YMc4fnKAVt5hDGsB1IHc=
github.com/fluxcd/pkg/gittestserver v0.4.1/go.mod h1:hUPx21fe/6oox336Wih/XF1fnmzLmptNMOvATbTZXNY=
github.com/fluxcd/pkg/gitutil v0.1.0 h1:VO3kJY/CKOCO4ysDNqfdpTg04icAKBOSb3lbR5uE/IE=
github.com/fluxcd/pkg/gitutil v0.1.0/go.mod h1:Ybz50Ck5gkcnvF0TagaMwtlRy3X3wXuiri1HVsK5id4=
github.com/fluxcd/pkg/helmtestserver v0.2.0 h1:cE7YHDmrWI0hr9QpaaeQ0vQ16Z0IiqZKiINDpqdY610=
@ -986,7 +986,6 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=

View File

@ -17,43 +17,82 @@ limitations under the License.
package git
import (
"bytes"
"context"
"fmt"
"strings"
"time"
"github.com/go-git/go-git/v5/plumbing/transport"
git2go "github.com/libgit2/git2go/v31"
corev1 "k8s.io/api/core/v1"
"github.com/ProtonMail/go-crypto/openpgp"
)
const (
DefaultOrigin = "origin"
DefaultBranch = "master"
DefaultPublicKeyAuthUser = "git"
CAFile = "caFile"
)
type Implementation string
type Commit interface {
Verify(secret corev1.Secret) error
Hash() 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")
}
type CheckoutStrategy interface {
Checkout(ctx context.Context, path, url string, auth *Auth) (Commit, string, error)
}
type CheckoutOptions struct {
GitImplementation string
RecurseSubmodules bool
}
// TODO(hidde): candidate for refactoring, so that we do not directly
// depend on implementation specifics here.
type Auth struct {
AuthMethod transport.AuthMethod
CABundle []byte
CredCallback git2go.CredentialsCallback
CertCallback git2go.CertificateCheckCallback
}
type AuthSecretStrategy interface {
Method(secret corev1.Secret) (*Auth, error)
Checkout(ctx context.Context, path, url string, config *AuthOptions) (*Commit, error)
}

220
pkg/git/git_test.go Normal file
View File

@ -0,0 +1,220 @@
/*
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"
)
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))
})
}
}

View File

@ -18,177 +18,200 @@ package gogit
import (
"context"
"errors"
"fmt"
"io"
"sort"
"time"
"github.com/Masterminds/semver/v3"
extgogit "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/fluxcd/pkg/gitutil"
"github.com/fluxcd/pkg/version"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta1"
"github.com/fluxcd/source-controller/pkg/git"
)
func CheckoutStrategyForRef(ref *sourcev1.GitRepositoryRef, opt git.CheckoutOptions) git.CheckoutStrategy {
// CheckoutStrategyForOptions returns the git.CheckoutStrategy for the given
// git.CheckoutOptions.
func CheckoutStrategyForOptions(_ context.Context, opts git.CheckoutOptions) git.CheckoutStrategy {
switch {
case ref == nil:
return &CheckoutBranch{branch: git.DefaultBranch}
case ref.SemVer != "":
return &CheckoutSemVer{semVer: ref.SemVer, recurseSubmodules: opt.RecurseSubmodules}
case ref.Tag != "":
return &CheckoutTag{tag: ref.Tag, recurseSubmodules: opt.RecurseSubmodules}
case ref.Commit != "":
strategy := &CheckoutCommit{branch: ref.Branch, commit: ref.Commit, recurseSubmodules: opt.RecurseSubmodules}
if strategy.branch == "" {
strategy.branch = git.DefaultBranch
}
return strategy
case ref.Branch != "":
return &CheckoutBranch{branch: ref.Branch, recurseSubmodules: opt.RecurseSubmodules}
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}
default:
return &CheckoutBranch{branch: git.DefaultBranch}
branch := opts.Branch
if branch == "" {
branch = git.DefaultBranch
}
return &CheckoutBranch{Branch: branch, RecurseSubmodules: opts.RecurseSubmodules}
}
}
type CheckoutBranch struct {
branch string
recurseSubmodules bool
Branch string
RecurseSubmodules bool
}
func (c *CheckoutBranch) Checkout(ctx context.Context, path, url string, auth *git.Auth) (git.Commit, string, error) {
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)
repo, err := extgogit.PlainCloneContext(ctx, path, false, &extgogit.CloneOptions{
URL: url,
Auth: auth.AuthMethod,
Auth: authMethod,
RemoteName: git.DefaultOrigin,
ReferenceName: plumbing.NewBranchReferenceName(c.branch),
ReferenceName: plumbing.NewBranchReferenceName(c.Branch),
SingleBranch: true,
NoCheckout: false,
Depth: 1,
RecurseSubmodules: recurseSubmodules(c.recurseSubmodules),
RecurseSubmodules: recurseSubmodules(c.RecurseSubmodules),
Progress: nil,
Tags: extgogit.NoTags,
CABundle: auth.CABundle,
CABundle: caBundle(opts),
})
if err != nil {
return nil, "", fmt.Errorf("unable to clone '%s', error: %w", url, gitutil.GoGitError(err))
return nil, fmt.Errorf("unable to clone '%s': %w", url, gitutil.GoGitError(err))
}
head, err := repo.Head()
if err != nil {
return nil, "", fmt.Errorf("git resolve HEAD error: %w", err)
return nil, fmt.Errorf("failed to resolve HEAD of branch '%s': %w", c.Branch, err)
}
commit, err := repo.CommitObject(head.Hash())
cc, err := repo.CommitObject(head.Hash())
if err != nil {
return nil, "", fmt.Errorf("git commit '%s' not found: %w", head.Hash(), err)
return nil, fmt.Errorf("failed to resolve commit object for HEAD '%s': %w", head.Hash(), err)
}
return &Commit{commit}, fmt.Sprintf("%s/%s", c.branch, head.Hash().String()), nil
return buildCommitWithRef(cc, ref)
}
type CheckoutTag struct {
tag string
recurseSubmodules bool
Tag string
RecurseSubmodules bool
}
func (c *CheckoutTag) Checkout(ctx context.Context, path, url string, auth *git.Auth) (git.Commit, string, error) {
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)
repo, err := extgogit.PlainCloneContext(ctx, path, false, &extgogit.CloneOptions{
URL: url,
Auth: auth.AuthMethod,
Auth: authMethod,
RemoteName: git.DefaultOrigin,
ReferenceName: plumbing.NewTagReferenceName(c.tag),
ReferenceName: plumbing.NewTagReferenceName(c.Tag),
SingleBranch: true,
NoCheckout: false,
Depth: 1,
RecurseSubmodules: recurseSubmodules(c.recurseSubmodules),
RecurseSubmodules: recurseSubmodules(c.RecurseSubmodules),
Progress: nil,
Tags: extgogit.NoTags,
CABundle: auth.CABundle,
CABundle: caBundle(opts),
})
if err != nil {
return nil, "", fmt.Errorf("unable to clone '%s', error: %w", url, err)
return nil, fmt.Errorf("unable to clone '%s': %w", url, gitutil.GoGitError(err))
}
head, err := repo.Head()
if err != nil {
return nil, "", fmt.Errorf("git resolve HEAD error: %w", err)
return nil, fmt.Errorf("failed to resolve HEAD of tag '%s': %w", c.Tag, err)
}
commit, err := repo.CommitObject(head.Hash())
cc, err := repo.CommitObject(head.Hash())
if err != nil {
return nil, "", fmt.Errorf("git commit '%s' not found: %w", head.Hash(), err)
return nil, fmt.Errorf("failed to resolve commit object for HEAD '%s': %w", head.Hash(), err)
}
return &Commit{commit}, fmt.Sprintf("%s/%s", c.tag, head.Hash().String()), nil
return buildCommitWithRef(cc, ref)
}
type CheckoutCommit struct {
branch string
commit string
recurseSubmodules bool
Branch string
Commit string
RecurseSubmodules bool
}
func (c *CheckoutCommit) Checkout(ctx context.Context, path, url string, auth *git.Auth) (git.Commit, string, error) {
repo, err := extgogit.PlainCloneContext(ctx, path, false, &extgogit.CloneOptions{
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: auth.AuthMethod,
Auth: authMethod,
RemoteName: git.DefaultOrigin,
ReferenceName: plumbing.NewBranchReferenceName(c.branch),
SingleBranch: true,
NoCheckout: false,
RecurseSubmodules: recurseSubmodules(c.recurseSubmodules),
SingleBranch: false,
NoCheckout: true,
RecurseSubmodules: recurseSubmodules(c.RecurseSubmodules),
Progress: nil,
Tags: extgogit.NoTags,
CABundle: auth.CABundle,
})
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', error: %w", url, err)
return nil, fmt.Errorf("unable to clone '%s': %w", url, gitutil.GoGitError(err))
}
w, err := repo.Worktree()
if err != nil {
return nil, "", fmt.Errorf("git worktree error: %w", err)
return nil, fmt.Errorf("failed to open Git worktree: %w", err)
}
commit, err := repo.CommitObject(plumbing.NewHash(c.commit))
cc, err := repo.CommitObject(plumbing.NewHash(c.Commit))
if err != nil {
return nil, "", fmt.Errorf("git commit '%s' not found: %w", c.commit, err)
return nil, fmt.Errorf("failed to resolve commit object for '%s': %w", c.Commit, err)
}
err = w.Checkout(&extgogit.CheckoutOptions{
Hash: commit.Hash,
Hash: cc.Hash,
Force: true,
})
if err != nil {
return nil, "", fmt.Errorf("git checkout error: %w", err)
return nil, fmt.Errorf("failed to checkout commit '%s': %w", c.Commit, err)
}
return &Commit{commit}, fmt.Sprintf("%s/%s", c.branch, commit.Hash.String()), nil
return buildCommitWithRef(cc, cloneOpts.ReferenceName)
}
type CheckoutSemVer struct {
semVer string
recurseSubmodules bool
SemVer string
RecurseSubmodules bool
}
func (c *CheckoutSemVer) Checkout(ctx context.Context, path, url string, auth *git.Auth) (git.Commit, string, error) {
verConstraint, err := semver.NewConstraint(c.semVer)
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 range error: %w", err)
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: auth.AuthMethod,
Auth: authMethod,
RemoteName: git.DefaultOrigin,
NoCheckout: false,
Depth: 1,
RecurseSubmodules: recurseSubmodules(c.recurseSubmodules),
RecurseSubmodules: recurseSubmodules(c.RecurseSubmodules),
Progress: nil,
Tags: extgogit.AllTags,
CABundle: auth.CABundle,
CABundle: caBundle(opts),
})
if err != nil {
return nil, "", fmt.Errorf("unable to clone '%s', error: %w", url, err)
return nil, fmt.Errorf("unable to clone '%s': %w", url, gitutil.GoGitError(err))
}
repoTags, err := repo.Tags()
if err != nil {
return nil, "", fmt.Errorf("git list tags error: %w", err)
return nil, fmt.Errorf("failed to list tags: %w", err)
}
tags := make(map[string]string)
@ -208,7 +231,7 @@ func (c *CheckoutSemVer) Checkout(ctx context.Context, path, url string, auth *g
tags[t.Name().Short()] = t.Strings()[1]
return nil
}); err != nil {
return nil, "", err
return nil, err
}
var matchedVersions semver.Collection
@ -223,7 +246,7 @@ func (c *CheckoutSemVer) Checkout(ctx context.Context, path, url string, auth *g
matchedVersions = append(matchedVersions, v)
}
if len(matchedVersions) == 0 {
return nil, "", fmt.Errorf("no match found for semver: %s", c.semVer)
return nil, fmt.Errorf("no match found for semver: %s", c.SemVer)
}
// Sort versions
@ -246,27 +269,61 @@ func (c *CheckoutSemVer) Checkout(ctx context.Context, path, url string, auth *g
w, err := repo.Worktree()
if err != nil {
return nil, "", fmt.Errorf("git worktree error: %w", err)
return nil, fmt.Errorf("failed to open Git worktree: %w", err)
}
ref := plumbing.NewTagReferenceName(t)
err = w.Checkout(&extgogit.CheckoutOptions{
Branch: plumbing.NewTagReferenceName(t),
Branch: ref,
})
if err != nil {
return nil, "", fmt.Errorf("git checkout error: %w", err)
return nil, fmt.Errorf("failed to checkout tag '%s': %w", t, err)
}
head, err := repo.Head()
if err != nil {
return nil, "", fmt.Errorf("git resolve HEAD error: %w", err)
return nil, fmt.Errorf("failed to resolve HEAD of tag '%s': %w", t, err)
}
commit, err := repo.CommitObject(head.Hash())
cc, err := repo.CommitObject(head.Hash())
if err != nil {
return nil, "", fmt.Errorf("git commit '%s' not found: %w", head.Hash(), err)
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")
}
return &Commit{commit}, fmt.Sprintf("%s/%s", t, head.Hash().String()), nil
// 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,
}, 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 {

View File

@ -18,37 +18,420 @@ package gogit
import (
"context"
"errors"
"os"
"path/filepath"
"testing"
"time"
"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"
)
func TestCheckoutTagSemVer_Checkout(t *testing.T) {
auth := &git.Auth{}
tag := CheckoutTag{
tag: "v1.7.0",
}
tmpDir, _ := os.MkdirTemp("", "test")
defer os.RemoveAll(tmpDir)
cTag, _, err := tag.Checkout(context.TODO(), tmpDir, "https://github.com/projectcontour/contour", auth)
func TestCheckoutBranch_Checkout(t *testing.T) {
repo, path, err := initRepo()
if err != nil {
t.Error(err)
t.Fatal(err)
}
defer os.RemoveAll(path)
semVer := CheckoutSemVer{
semVer: ">=1.0.0 <=1.7.0",
}
tmpDir2, _ := os.MkdirTemp("", "test")
defer os.RemoveAll(tmpDir2)
cSemVer, _, err := semVer.Checkout(context.TODO(), tmpDir2, "https://github.com/projectcontour/contour", auth)
firstCommit, err := commitFile(repo, "branch", "init", time.Now())
if err != nil {
t.Error(err)
t.Fatal(err)
}
if cTag.Hash() != cSemVer.Hash() {
t.Errorf("expected semver hash %s, got %s", cTag.Hash(), cSemVer.Hash())
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
expectedCommit string
expectedErr string
}{
{
name: "Default branch",
branch: "master",
expectedCommit: firstCommit.String(),
},
{
name: "Other branch",
branch: "test",
expectedCommit: secondCommit.String(),
},
{
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,
}
tmpDir, _ := os.MkdirTemp("", "test")
defer os.RemoveAll(tmpDir)
cc, err := branch.Checkout(context.TODO(), tmpDir, path, nil)
if tt.expectedErr != "" {
g.Expect(err.Error()).To(ContainSubstring(tt.expectedErr))
g.Expect(cc).To(BeNil())
return
}
g.Expect(err).To(BeNil())
g.Expect(cc.String()).To(Equal(tt.branch + "/" + tt.expectedCommit))
})
}
}
func TestCheckoutTag_Checkout(t *testing.T) {
tests := []struct {
name string
tag string
annotated bool
checkoutTag string
expectTag string
expectErr string
}{
{
name: "Tag",
tag: "tag-1",
checkoutTag: "tag-1",
expectTag: "tag-1",
},
{
name: "Annotated",
tag: "annotated",
annotated: true,
checkoutTag: "annotated",
expectTag: "annotated",
},
{
name: "Non existing tag",
tag: "tag-1",
checkoutTag: "invalid",
expectErr: "couldn't find remote ref \"refs/tags/invalid\"",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
repo, path, err := initRepo()
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(path)
var h plumbing.Hash
if tt.tag != "" {
h, err = commitFile(repo, "tag", tt.tag, time.Now())
if err != nil {
t.Fatal(err)
}
_, err = tag(repo, h, !tt.annotated, tt.tag, time.Now())
if err != nil {
t.Fatal(err)
}
}
tag := CheckoutTag{
Tag: tt.checkoutTag,
}
tmpDir, _ := os.MkdirTemp("", "test")
defer os.RemoveAll(tmpDir)
cc, err := tag.Checkout(context.TODO(), tmpDir, path, nil)
if tt.expectErr != "" {
g.Expect(err.Error()).To(ContainSubstring(tt.expectErr))
g.Expect(cc).To(BeNil())
return
}
g.Expect(err).To(BeNil())
g.Expect(cc.String()).To(Equal(tt.expectTag + "/" + h.String()))
g.Expect(filepath.Join(tmpDir, "tag")).To(BeARegularFile())
g.Expect(os.ReadFile(filepath.Join(tmpDir, "tag"))).To(BeEquivalentTo(tt.tag))
})
}
}
func TestCheckoutCommit_Checkout(t *testing.T) {
repo, path, err := initRepo()
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(path)
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, err := os.MkdirTemp("", "git2go")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)
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()
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(path)
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, _ := os.MkdirTemp("", "test")
defer os.RemoveAll(tmpDir)
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))
})
}
}
func initRepo() (*extgogit.Repository, string, error) {
tmpDir, err := os.MkdirTemp("", "gogit")
if err != nil {
os.RemoveAll(tmpDir)
return nil, "", err
}
sto := filesystem.NewStorage(osfs.New(tmpDir), cache.NewObjectLRUDefault())
repo, err := extgogit.Init(sto, memfs.New())
if err != nil {
os.RemoveAll(tmpDir)
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,51 +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/object"
corev1 "k8s.io/api/core/v1"
)
type Commit struct {
commit *object.Commit
}
func (c *Commit) Hash() string {
return c.commit.Hash.String()
}
// Verify returns an error if the PGP signature can't be verified
func (c *Commit) Verify(secret corev1.Secret) error {
if c.commit.PGPSignature == "" {
return fmt.Errorf("no PGP signature found for commit: %s", c.commit.Hash)
}
var verified bool
for _, bytes := range secret.Data {
if _, err := c.commit.Verify(string(bytes)); err == nil {
verified = true
break
}
}
if !verified {
return fmt.Errorf("PGP signature '%s' of '%s' can't be verified", c.commit.PGPSignature, c.commit.Author)
}
return nil
}

23
pkg/git/gogit/gogit.go Normal file
View File

@ -0,0 +1,23 @@
/*
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

@ -18,87 +18,55 @@ package gogit
import (
"fmt"
"net/url"
"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"
corev1 "k8s.io/api/core/v1"
"github.com/fluxcd/pkg/ssh/knownhosts"
"github.com/fluxcd/source-controller/pkg/git"
)
func AuthSecretStrategyForURL(URL string) (git.AuthSecretStrategy, error) {
u, err := url.Parse(URL)
if err != nil {
return nil, fmt.Errorf("failed to parse URL to determine auth strategy: %w", err)
// 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 {
case u.Scheme == "http", u.Scheme == "https":
return &BasicAuth{}, nil
case u.Scheme == "ssh":
return &PublicKeyAuth{user: u.User.Username()}, nil
switch opts.Transport {
case git.HTTPS, git.HTTP:
return &http.BasicAuth{
Username: opts.Username,
Password: opts.Password,
}, 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
}
return pk, nil
}
case "":
return nil, fmt.Errorf("no transport type set")
default:
return nil, fmt.Errorf("no auth secret strategy for scheme %s", u.Scheme)
return nil, fmt.Errorf("unknown transport '%s'", opts.Transport)
}
return nil, nil
}
type BasicAuth struct{}
func (s *BasicAuth) Method(secret corev1.Secret) (*git.Auth, error) {
auth := &git.Auth{}
basicAuth := &http.BasicAuth{}
if caBundle, ok := secret.Data[git.CAFile]; ok {
auth.CABundle = caBundle
// caBundle returns the CA bundle from the given git.AuthOptions.
func caBundle(opts *git.AuthOptions) []byte {
if opts == nil {
return nil
}
if username, ok := secret.Data["username"]; ok {
basicAuth.Username = string(username)
}
if password, ok := secret.Data["password"]; ok {
basicAuth.Password = string(password)
}
if (basicAuth.Username == "" && basicAuth.Password != "") || (basicAuth.Username != "" && basicAuth.Password == "") {
return nil, fmt.Errorf("invalid '%s' secret data: required fields 'username' and 'password'", secret.Name)
}
if basicAuth.Username != "" && basicAuth.Password != "" {
auth.AuthMethod = basicAuth
}
return auth, nil
}
type PublicKeyAuth struct {
user string
}
func (s *PublicKeyAuth) Method(secret corev1.Secret) (*git.Auth, error) {
if _, ok := secret.Data[git.CAFile]; ok {
return nil, fmt.Errorf("found caFile key in secret '%s' but go-git SSH transport does not support custom certificates", secret.Name)
}
identity := secret.Data["identity"]
knownHosts := secret.Data["known_hosts"]
if len(identity) == 0 || len(knownHosts) == 0 {
return nil, fmt.Errorf("invalid '%s' secret data: required fields 'identity' and 'known_hosts'", secret.Name)
}
user := s.user
if user == "" {
user = git.DefaultPublicKeyAuthUser
}
password := secret.Data["password"]
pk, err := ssh.NewPublicKeys(user, identity, string(password))
if err != nil {
return nil, err
}
callback, err := knownhosts.New(knownHosts)
if err != nil {
return nil, err
}
pk.HostKeyCallback = callback
return &git.Auth{AuthMethod: pk}, nil
return opts.CAFile
}

View File

@ -17,19 +17,21 @@ limitations under the License.
package gogit
import (
"reflect"
"errors"
"testing"
"github.com/go-git/go-git/v5/plumbing/transport"
"github.com/go-git/go-git/v5/plumbing/transport/http"
corev1 "k8s.io/api/core/v1"
"github.com/go-git/go-git/v5/plumbing/transport/ssh"
. "github.com/onsi/gomega"
"github.com/fluxcd/source-controller/pkg/git"
)
const (
// secretKeyFixture is a randomly generated password less
// privateKeyFixture is a randomly generated password less
// 512bit RSA private key.
secretKeyFixture string = `-----BEGIN RSA PRIVATE KEY-----
privateKeyFixture = `-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQCrakELAKxozvwJijQEggYlTvS1QTZx1DaBwOhW/4kRSuR21plu
xuQeyuUiztoWeb9jgW7wjzG4j1PIJjdbsgjPIcIZ4PBY7JeEW+QRopfwuN8MHXNp
uTLgIHbkmhoOg5qBEcjzO/lEOOPpV0EmbObgqv3+wRmLJrgfzWl/cTtRewIDAQAB
@ -45,9 +47,9 @@ Ngkgu4mLjc3RfenEhJECQAx8zjWUE6kHHPGAd9DfiAIQ4bChqnyS0Nwb9+Gd4hSE
P0Ah10mHiK/M0o3T8Eanwum0gbQHPnOwqZgsPkwXRqQ=
-----END RSA PRIVATE KEY-----`
// secretKeyFixture is a randomly generated
// privateKeyPassphraseFixture is a randomly generated
// 512bit RSA private key with password foobar.
secretPassphraseFixture = `-----BEGIN RSA PRIVATE KEY-----
privateKeyPassphraseFixture = `-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-256-CBC,0B016973B2A761D31E6B388D0F327C35
@ -60,138 +62,145 @@ wGctSx4kHsZGhJv5qwKqqPEFPhUzph8D2tm2TABk8HJa5KJFDbGrcfvk2uODAoZr
MbcpIxCfl8oB09bWfY6tDQjyvwSYYo2Phdwm7kT92xc=
-----END RSA PRIVATE KEY-----`
// generated with sshkey-gen with password `password`. Fails test
secretEDCSAFicture = `-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABCUNUDYpS
GJ0GjHSoOJvNzrAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIAUwMlCdqwINTCFe
0QTLK2w04AMyMDkH4keEHnTDB9KAAAAAoLv9vPS65ie3CQ9XYDXhX4TQUKg15kYmbt/Lqu
Eg5i6G2aJOIeq/ZwBOjySG328zucwptzScx1bgwIHfkPmUSBBoATcilGtglVFDmBuYSrky
r2bP9MJYmUIx3RkMZI0RcYIwuH/fMNPnyBbGMCwEEZP3xYXst8oNyGz47s9k6Woqy64bgh
Q0YEW1Vyqn/Tt8nBJrbtyY1iLnQjOZ167bYxc=
-----END OPENSSH 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==`
)
var (
basicAuthSecretFixture = corev1.Secret{
Data: map[string][]byte{
"username": []byte("git"),
"password": []byte("password"),
},
}
privateKeySecretFixture = corev1.Secret{
Data: map[string][]byte{
"identity": []byte(secretKeyFixture),
"known_hosts": []byte(knownHostsFixture),
},
}
privateKeySecretWithPassphraseFixture = corev1.Secret{
Data: map[string][]byte{
"identity": []byte(secretPassphraseFixture),
"known_hosts": []byte(knownHostsFixture),
"password": []byte("foobar"),
},
}
failingPrivateKey = corev1.Secret{
Data: map[string][]byte{
"identity": []byte(secretEDCSAFicture),
"known_hosts": []byte(knownHostsFixture),
"password": []byte("password"),
},
}
)
func TestAuthSecretStrategyForURL(t *testing.T) {
func Test_transportAuth(t *testing.T) {
tests := []struct {
name string
url string
want git.AuthSecretStrategy
wantErr bool
name string
opts *git.AuthOptions
wantFunc func(g *WithT, t transport.AuthMethod, opts *git.AuthOptions)
wantErr error
}{
{"HTTP", "http://git.example.com/org/repo.git", &BasicAuth{}, false},
{"HTTPS", "https://git.example.com/org/repo.git", &BasicAuth{}, false},
{"SSH", "ssh://git.example.com:2222/org/repo.git", &PublicKeyAuth{}, false},
{"SSH with username", "ssh://example@git.example.com:2222/org/repo.git", &PublicKeyAuth{user: "example"}, false},
{"unsupported", "protocol://example.com", nil, true},
{
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.(*ssh.PublicKeys)
g.Expect(ok).To(BeTrue())
g.Expect(tt.User).To(Equal(opts.Username))
g.Expect(tt.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.(*ssh.PublicKeys)
g.Expect(ok).To(BeTrue())
g.Expect(tt.User).To(Equal(opts.Username))
g.Expect(tt.Signer.PublicKey().Type()).To(Equal("ssh-rsa"))
},
},
{
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.(*ssh.PublicKeys)
g.Expect(ok).To(BeTrue())
g.Expect(tt.User).To(Equal(opts.Username))
g.Expect(tt.Signer.PublicKey().Type()).To(Equal("ssh-rsa"))
g.Expect(tt.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) {
got, err := AuthSecretStrategyForURL(tt.url)
if (err != nil) != tt.wantErr {
t.Errorf("AuthSecretStrategyForURL() error = %v, wantErr %v", err, tt.wantErr)
g := NewWithT(t)
got, err := transportAuth(tt.opts)
if tt.wantErr != nil {
g.Expect(err).To(Equal(tt.wantErr))
g.Expect(got).To(BeNil())
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("AuthSecretStrategyForURL() got = %v, want %v", got, tt.want)
g.Expect(err).ToNot(HaveOccurred())
if tt.wantFunc != nil {
tt.wantFunc(g, got, tt.opts)
}
})
}
}
func TestBasicAuthStrategy_Method(t *testing.T) {
tests := []struct {
name string
secret corev1.Secret
modify func(secret *corev1.Secret)
want *git.Auth
wantErr bool
}{
{"username and password", basicAuthSecretFixture, nil, &git.Auth{AuthMethod: &http.BasicAuth{Username: "git", Password: "password"}}, false},
{"without username", basicAuthSecretFixture, func(s *corev1.Secret) { delete(s.Data, "username") }, nil, true},
{"without password", basicAuthSecretFixture, func(s *corev1.Secret) { delete(s.Data, "password") }, nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
secret := tt.secret.DeepCopy()
if tt.modify != nil {
tt.modify(secret)
}
s := &BasicAuth{}
got, err := s.Method(*secret)
if (err != nil) != tt.wantErr {
t.Errorf("Method() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Method() got = %v, want %v", got, tt.want)
}
})
}
}
func Test_caBundle(t *testing.T) {
g := NewWithT(t)
func TestPublicKeyStrategy_Method(t *testing.T) {
tests := []struct {
name string
secret corev1.Secret
modify func(secret *corev1.Secret)
wantErr bool
}{
{"private key and known_hosts", privateKeySecretFixture, nil, false},
{"private key with passphrase and known_hosts", privateKeySecretWithPassphraseFixture, nil, false},
{"edcsa private key with passphrase and known_hosts", failingPrivateKey, nil, false},
{"missing private key", privateKeySecretFixture, func(s *corev1.Secret) { delete(s.Data, "identity") }, true},
{"invalid private key", privateKeySecretFixture, func(s *corev1.Secret) { s.Data["identity"] = []byte(`-----BEGIN RSA PRIVATE KEY-----`) }, true},
{"missing known_hosts", privateKeySecretFixture, func(s *corev1.Secret) { delete(s.Data, "known_hosts") }, true},
{"invalid known_hosts", privateKeySecretFixture, func(s *corev1.Secret) { s.Data["known_hosts"] = []byte(`invalid`) }, true},
{"missing password", privateKeySecretWithPassphraseFixture, func(s *corev1.Secret) { delete(s.Data, "password") }, true},
{"wrong password", privateKeySecretWithPassphraseFixture, func(s *corev1.Secret) { s.Data["password"] = []byte("pass") }, true},
{"empty", corev1.Secret{}, nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
secret := tt.secret.DeepCopy()
if tt.modify != nil {
tt.modify(secret)
}
s := &PublicKeyAuth{}
_, err := s.Method(*secret)
if (err != nil) != tt.wantErr {
t.Errorf("Method() error = %v, wantErr %v", err, tt.wantErr)
return
}
})
}
g.Expect(caBundle(&git.AuthOptions{CAFile: []byte("foo")})).To(BeEquivalentTo("foo"))
g.Expect(caBundle(nil)).To(BeNil())
}

View File

@ -24,142 +24,135 @@ import (
"time"
"github.com/Masterminds/semver/v3"
"github.com/go-logr/logr"
git2go "github.com/libgit2/git2go/v31"
"github.com/fluxcd/pkg/gitutil"
"github.com/fluxcd/pkg/version"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta1"
"github.com/fluxcd/source-controller/pkg/git"
)
func CheckoutStrategyForRef(ref *sourcev1.GitRepositoryRef, opt git.CheckoutOptions) git.CheckoutStrategy {
// 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("git submodule recursion not supported by '%s'", Implementation)
}
switch {
case ref == nil:
return &CheckoutBranch{branch: git.DefaultBranch}
case ref.SemVer != "":
return &CheckoutSemVer{semVer: ref.SemVer}
case ref.Tag != "":
return &CheckoutTag{tag: ref.Tag}
case ref.Commit != "":
strategy := &CheckoutCommit{branch: ref.Branch, commit: ref.Commit}
if strategy.branch == "" {
strategy.branch = git.DefaultBranch
}
return strategy
case ref.Branch != "":
return &CheckoutBranch{branch: ref.Branch}
case opt.Commit != "":
return &CheckoutCommit{Commit: opt.Commit}
case opt.SemVer != "":
return &CheckoutSemVer{SemVer: opt.SemVer}
case opt.Tag != "":
return &CheckoutTag{Tag: opt.Tag}
default:
return &CheckoutBranch{branch: git.DefaultBranch}
branch := opt.Branch
if branch == "" {
branch = git.DefaultBranch
}
return &CheckoutBranch{Branch: branch}
}
}
type CheckoutBranch struct {
branch string
Branch string
}
func (c *CheckoutBranch) Checkout(ctx context.Context, path, url string, auth *git.Auth) (git.Commit, string, error) {
func (c *CheckoutBranch) Checkout(ctx context.Context, path, url string, opts *git.AuthOptions) (*git.Commit, error) {
repo, err := git2go.Clone(url, path, &git2go.CloneOptions{
FetchOptions: &git2go.FetchOptions{
DownloadTags: git2go.DownloadTagsNone,
RemoteCallbacks: git2go.RemoteCallbacks{
CredentialsCallback: auth.CredCallback,
CertificateCheckCallback: auth.CertCallback,
},
DownloadTags: git2go.DownloadTagsNone,
RemoteCallbacks: RemoteCallbacks(opts),
},
CheckoutBranch: c.branch,
CheckoutBranch: c.Branch,
})
if err != nil {
return nil, "", fmt.Errorf("unable to clone '%s', error: %w", url, gitutil.LibGit2Error(err))
return nil, fmt.Errorf("unable to clone: %w", gitutil.LibGit2Error(err))
}
defer repo.Free()
head, err := repo.Head()
if err != nil {
return nil, "", fmt.Errorf("git resolve HEAD error: %w", err)
return nil, fmt.Errorf("git resolve HEAD error: %w", err)
}
defer head.Free()
commit, err := repo.LookupCommit(head.Target())
cc, err := repo.LookupCommit(head.Target())
if err != nil {
return nil, "", fmt.Errorf("git commit '%s' not found: %w", head.Target(), err)
return nil, fmt.Errorf("could not find commit '%s' in branch '%s': %w", head.Target(), c.Branch, err)
}
return &Commit{commit}, fmt.Sprintf("%s/%s", c.branch, head.Target().String()), nil
defer cc.Free()
return buildCommit(cc, "refs/heads/"+c.Branch), nil
}
type CheckoutTag struct {
tag string
Tag string
}
func (c *CheckoutTag) Checkout(ctx context.Context, path, url string, auth *git.Auth) (git.Commit, string, error) {
func (c *CheckoutTag) Checkout(ctx context.Context, path, url string, opts *git.AuthOptions) (*git.Commit, error) {
repo, err := git2go.Clone(url, path, &git2go.CloneOptions{
FetchOptions: &git2go.FetchOptions{
DownloadTags: git2go.DownloadTagsAll,
RemoteCallbacks: git2go.RemoteCallbacks{
CredentialsCallback: auth.CredCallback,
CertificateCheckCallback: auth.CertCallback,
},
DownloadTags: git2go.DownloadTagsAll,
RemoteCallbacks: RemoteCallbacks(opts),
},
})
if err != nil {
return nil, "", fmt.Errorf("unable to clone '%s', error: %w", url, gitutil.LibGit2Error(err))
return nil, fmt.Errorf("unable to clone '%s': %w", url, gitutil.LibGit2Error(err))
}
commit, err := checkoutDetachedDwim(repo, c.tag)
defer repo.Free()
cc, err := checkoutDetachedDwim(repo, c.Tag)
if err != nil {
return nil, "", err
return nil, err
}
return &Commit{commit}, fmt.Sprintf("%s/%s", c.tag, commit.Id().String()), nil
defer cc.Free()
return buildCommit(cc, "refs/tags/"+c.Tag), nil
}
type CheckoutCommit struct {
branch string
commit string
Commit string
}
func (c *CheckoutCommit) Checkout(ctx context.Context, path, url string, auth *git.Auth) (git.Commit, string, error) {
func (c *CheckoutCommit) Checkout(ctx context.Context, path, url string, opts *git.AuthOptions) (*git.Commit, error) {
repo, err := git2go.Clone(url, path, &git2go.CloneOptions{
FetchOptions: &git2go.FetchOptions{
DownloadTags: git2go.DownloadTagsNone,
RemoteCallbacks: git2go.RemoteCallbacks{
CredentialsCallback: auth.CredCallback,
CertificateCheckCallback: auth.CertCallback,
},
DownloadTags: git2go.DownloadTagsNone,
RemoteCallbacks: RemoteCallbacks(opts),
},
})
if err != nil {
return nil, "", fmt.Errorf("unable to clone '%s', error: %w", url, gitutil.LibGit2Error(err))
return nil, fmt.Errorf("unable to clone '%s': %w", url, gitutil.LibGit2Error(err))
}
oid, err := git2go.NewOid(c.commit)
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)
return nil, fmt.Errorf("could not create oid for '%s': %w", c.Commit, err)
}
commit, err := checkoutDetachedHEAD(repo, oid)
cc, err := checkoutDetachedHEAD(repo, oid)
if err != nil {
return nil, "", fmt.Errorf("git checkout error: %w", err)
return nil, fmt.Errorf("git checkout error: %w", err)
}
return &Commit{commit}, fmt.Sprintf("%s/%s", c.branch, commit.Id().String()), nil
return buildCommit(cc, ""), nil
}
type CheckoutSemVer struct {
semVer string
SemVer string
}
func (c *CheckoutSemVer) Checkout(ctx context.Context, path, url string, auth *git.Auth) (git.Commit, string, error) {
verConstraint, err := semver.NewConstraint(c.semVer)
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 range error: %w", err)
return nil, fmt.Errorf("semver parse error: %w", err)
}
repo, err := git2go.Clone(url, path, &git2go.CloneOptions{
FetchOptions: &git2go.FetchOptions{
DownloadTags: git2go.DownloadTagsAll,
RemoteCallbacks: git2go.RemoteCallbacks{
CredentialsCallback: auth.CredCallback,
CertificateCheckCallback: auth.CertCallback,
},
DownloadTags: git2go.DownloadTagsAll,
RemoteCallbacks: RemoteCallbacks(opts),
},
})
if err != nil {
return nil, "", fmt.Errorf("unable to clone '%s', error: %w", url, gitutil.LibGit2Error(err))
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)
@ -194,7 +187,7 @@ func (c *CheckoutSemVer) Checkout(ctx context.Context, path, url string, auth *g
tags[t.Name()] = name
return nil
}); err != nil {
return nil, "", err
return nil, err
}
var matchedVersions semver.Collection
@ -209,7 +202,7 @@ func (c *CheckoutSemVer) Checkout(ctx context.Context, path, url string, auth *g
matchedVersions = append(matchedVersions, v)
}
if len(matchedVersions) == 0 {
return nil, "", fmt.Errorf("no match found for semver: %s", c.semVer)
return nil, fmt.Errorf("no match found for semver: %s", c.SemVer)
}
// Sort versions
@ -230,8 +223,12 @@ func (c *CheckoutSemVer) Checkout(ctx context.Context, path, url string, auth *g
v := matchedVersions[len(matchedVersions)-1]
t := v.Original()
commit, err := checkoutDetachedDwim(repo, t)
return &Commit{commit}, fmt.Sprintf("%s/%s", t, commit.Id().String()), nil
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
@ -247,31 +244,31 @@ func checkoutDetachedDwim(repo *git2go.Repository, name string) (*git2go.Commit,
return nil, fmt.Errorf("could not get commit for ref '%s': %w", ref.Name(), err)
}
defer c.Free()
commit, err := c.AsCommit()
cc, err := c.AsCommit()
if err != nil {
return nil, fmt.Errorf("could not get commit object for ref '%s': %w", ref.Name(), err)
}
defer commit.Free()
return checkoutDetachedHEAD(repo, commit.Id())
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) {
commit, err := repo.LookupCommit(oid)
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(commit.Id()); err != nil {
commit.Free()
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 {
commit.Free()
cc.Free()
return nil, fmt.Errorf("git checkout error: %w", err)
}
return commit, nil
return cc, nil
}
// headCommit returns the current HEAD of the repository, or an error.
@ -281,11 +278,30 @@ func headCommit(repo *git2go.Repository) (*git2go.Commit, error) {
return nil, err
}
defer head.Free()
commit, err := repo.LookupCommit(head.Target())
c, err := repo.LookupCommit(head.Target())
if err != nil {
return nil, err
}
return commit, nil
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,
}
}

View File

@ -27,8 +27,6 @@ import (
git2go "github.com/libgit2/git2go/v31"
. "github.com/onsi/gomega"
"github.com/fluxcd/source-controller/pkg/git"
)
func TestCheckoutBranch_Checkout(t *testing.T) {
@ -79,19 +77,20 @@ func TestCheckoutBranch_Checkout(t *testing.T) {
g := NewWithT(t)
branch := CheckoutBranch{
branch: tt.branch,
Branch: tt.branch,
}
tmpDir, _ := os.MkdirTemp("", "test")
defer os.RemoveAll(tmpDir)
_, ref, err := branch.Checkout(context.TODO(), tmpDir, repo.Path(), &git.Auth{})
cc, err := branch.Checkout(context.TODO(), tmpDir, repo.Path(), nil)
if tt.expectedErr != "" {
g.Expect(err).To(HaveOccurred())
g.Expect(err.Error()).To(ContainSubstring(tt.expectedErr))
g.Expect(ref).To(BeEmpty())
g.Expect(cc).To(BeNil())
return
}
g.Expect(ref).To(Equal(tt.branch + "/" + tt.expectedCommit))
g.Expect(err).To(BeNil())
g.Expect(err).ToNot(HaveOccurred())
g.Expect(cc.String()).To(Equal(tt.branch + "/" + tt.expectedCommit))
})
}
}
@ -149,22 +148,23 @@ func TestCheckoutTag_Checkout(t *testing.T) {
}
tag := CheckoutTag{
tag: tt.checkoutTag,
Tag: tt.checkoutTag,
}
tmpDir, _ := os.MkdirTemp("", "test")
defer os.RemoveAll(tmpDir)
_, ref, err := tag.Checkout(context.TODO(), tmpDir, repo.Path(), &git.Auth{})
cc, err := tag.Checkout(context.TODO(), tmpDir, repo.Path(), nil)
if tt.expectErr != "" {
g.Expect(err.Error()).To(Equal(tt.expectErr))
g.Expect(ref).To(BeEmpty())
g.Expect(err).To(HaveOccurred())
g.Expect(err.Error()).To(ContainSubstring(tt.expectErr))
g.Expect(cc).To(BeNil())
return
}
if tt.expectTag != "" {
g.Expect(ref).To(Equal(tt.expectTag + "/" + commit.Id().String()))
g.Expect(filepath.Join(tmpDir, "tag")).To(BeARegularFile())
g.Expect(os.ReadFile(filepath.Join(tmpDir, "tag"))).To(BeEquivalentTo(tt.tag))
}
g.Expect(err).ToNot(HaveOccurred())
g.Expect(cc.String()).To(Equal(tt.expectTag + "/" + commit.Id().String()))
g.Expect(filepath.Join(tmpDir, "tag")).To(BeARegularFile())
g.Expect(os.ReadFile(filepath.Join(tmpDir, "tag"))).To(BeEquivalentTo(tt.tag))
})
}
}
@ -188,27 +188,28 @@ func TestCheckoutCommit_Checkout(t *testing.T) {
}
commit := CheckoutCommit{
commit: c.String(),
branch: "main",
Commit: c.String(),
}
tmpDir, _ := os.MkdirTemp("", "git2go")
defer os.RemoveAll(tmpDir)
_, ref, err := commit.Checkout(context.TODO(), tmpDir, repo.Path(), &git.Auth{})
g.Expect(err).To(BeNil())
g.Expect(ref).To(Equal("main/" + c.String()))
cc, err := commit.Checkout(context.TODO(), tmpDir, repo.Path(), nil)
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",
Commit: "4dc3185c5fc94eb75048376edeb44571cece25f4",
}
tmpDir2, _ := os.MkdirTemp("", "git2go")
defer os.RemoveAll(tmpDir)
_, ref, err = commit.Checkout(context.TODO(), tmpDir2, repo.Path(), &git.Auth{})
cc, err = commit.Checkout(context.TODO(), tmpDir2, repo.Path(), nil)
g.Expect(err).To(HaveOccurred())
g.Expect(err.Error()).To(HavePrefix("git checkout error: git commit '4dc3185c5fc94eb75048376edeb44571cece25f4' not found:"))
g.Expect(ref).To(BeEmpty())
g.Expect(cc).To(BeNil())
}
func TestCheckoutTagSemVer_Checkout(t *testing.T) {
@ -307,19 +308,20 @@ func TestCheckoutTagSemVer_Checkout(t *testing.T) {
g := NewWithT(t)
semVer := CheckoutSemVer{
semVer: tt.constraint,
SemVer: tt.constraint,
}
tmpDir, _ := os.MkdirTemp("", "test")
defer os.RemoveAll(tmpDir)
_, ref, err := semVer.Checkout(context.TODO(), tmpDir, repo.Path(), &git.Auth{})
cc, err := semVer.Checkout(context.TODO(), tmpDir, repo.Path(), nil)
if tt.expectErr != nil {
g.Expect(err).To(Equal(tt.expectErr))
g.Expect(ref).To(BeEmpty())
g.Expect(cc).To(BeNil())
return
}
g.Expect(err).ToNot(HaveOccurred())
g.Expect(ref).To(Equal(tt.expectTag + "/" + refs[tt.expectTag]))
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))
})
@ -395,11 +397,11 @@ func commitFile(repo *git2go.Repository, path, content string, time time.Time) (
}
defer tree.Free()
commit, err := repo.CreateCommit("HEAD", signature(time), signature(time), "Committing "+path, tree, parentC...)
c, err := repo.CreateCommit("HEAD", mockSignature(time), mockSignature(time), "Committing "+path, tree, parentC...)
if err != nil {
return nil, err
}
return commit, nil
return c, nil
}
func tag(repo *git2go.Repository, cId *git2go.Oid, annotated bool, tag string, time time.Time) (*git2go.Oid, error) {
@ -408,12 +410,12 @@ func tag(repo *git2go.Repository, cId *git2go.Oid, annotated bool, tag string, t
return nil, err
}
if annotated {
return repo.Tags.Create(tag, commit, signature(time), fmt.Sprintf("Annotated tag for %s", tag))
return repo.Tags.Create(tag, commit, mockSignature(time), fmt.Sprintf("Annotated tag for %s", tag))
}
return repo.Tags.CreateLightweight(tag, commit, false)
}
func signature(time time.Time) *git2go.Signature {
func mockSignature(time time.Time) *git2go.Signature {
return &git2go.Signature{
Name: "Jane Doe",
Email: "author@example.com",

View File

@ -1,65 +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 (
"bytes"
"fmt"
"strings"
"golang.org/x/crypto/openpgp"
git2go "github.com/libgit2/git2go/v31"
corev1 "k8s.io/api/core/v1"
)
type Commit struct {
commit *git2go.Commit
}
func (c *Commit) Hash() string {
return c.commit.Id().String()
}
// Verify returns an error if the PGP signature can't be verified
func (c *Commit) Verify(secret corev1.Secret) error {
signature, signedData, err := c.commit.ExtractSignature()
if err != nil {
return err
}
var verified bool
for _, b := range secret.Data {
keyRingReader := strings.NewReader(string(b))
keyring, err := openpgp.ReadArmoredKeyRing(keyRingReader)
if err != nil {
return err
}
_, err = openpgp.CheckArmoredDetachedSignature(keyring, strings.NewReader(signedData), bytes.NewBufferString(signature))
if err == nil {
verified = true
break
}
}
if !verified {
return fmt.Errorf("PGP signature '%s' of '%s' can't be verified", signature, c.commit.Committer().Email)
}
return nil
}

View File

@ -0,0 +1,23 @@
/*
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

@ -25,138 +25,122 @@ import (
"crypto/x509"
"fmt"
"hash"
"io"
"net"
"net/url"
"strings"
"time"
git2go "github.com/libgit2/git2go/v31"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/knownhosts"
corev1 "k8s.io/api/core/v1"
"github.com/fluxcd/source-controller/pkg/git"
)
func AuthSecretStrategyForURL(URL string) (git.AuthSecretStrategy, error) {
u, err := url.Parse(URL)
if err != nil {
return nil, fmt.Errorf("failed to parse URL to determine auth strategy: %w", err)
}
var (
now = time.Now
)
switch {
case u.Scheme == "http", u.Scheme == "https":
return &BasicAuth{}, nil
case u.Scheme == "ssh":
return &PublicKeyAuth{user: u.User.Username(), host: u.Host}, nil
default:
return nil, fmt.Errorf("no auth secret strategy for scheme %s", u.Scheme)
// RemoteCallbacks constructs RemoteCallbacks with credentialsCallback and
// certificateCallback, and the given options if the given opts is not nil.
func RemoteCallbacks(opts *git.AuthOptions) git2go.RemoteCallbacks {
if opts != nil {
return git2go.RemoteCallbacks{
CredentialsCallback: credentialsCallback(opts),
CertificateCheckCallback: certificateCallback(opts),
}
}
return git2go.RemoteCallbacks{}
}
type BasicAuth struct{}
func (s *BasicAuth) Method(secret corev1.Secret) (*git.Auth, error) {
var credCallback git2go.CredentialsCallback
var username string
if d, ok := secret.Data["username"]; ok {
username = string(d)
}
var password string
if d, ok := secret.Data["password"]; ok {
password = string(d)
}
if username != "" && password != "" {
credCallback = func(url string, usernameFromURL string, allowedTypes git2go.CredentialType) (*git2go.Credential, error) {
cred, err := git2go.NewCredentialUserpassPlaintext(username, password)
// credentialsCallback constructs CredentialsCallbacks with the given options
// for git.Transport, and returns the result.
func credentialsCallback(opts *git.AuthOptions) git2go.CredentialsCallback {
return func(url string, username string, allowedTypes git2go.CredentialType) (*git2go.Credential, error) {
if allowedTypes&(git2go.CredentialTypeSSHKey|git2go.CredentialTypeSSHCustom|git2go.CredentialTypeSSHMemory) != 0 {
var (
signer ssh.Signer
err error
)
if opts.Password != "" {
signer, err = ssh.ParsePrivateKeyWithPassphrase(opts.Identity, []byte(opts.Password))
} else {
signer, err = ssh.ParsePrivateKey(opts.Identity)
}
if err != nil {
return nil, err
}
return cred, nil
return git2go.NewCredentialSSHKeyFromSigner(opts.Username, signer)
}
}
var certCallback git2go.CertificateCheckCallback
if caFile, ok := secret.Data[git.CAFile]; ok {
certCallback = func(cert *git2go.Certificate, valid bool, hostname string) git2go.ErrorCode {
roots := x509.NewCertPool()
ok := roots.AppendCertsFromPEM(caFile)
if !ok {
return git2go.ErrorCodeCertificate
}
opts := x509.VerifyOptions{
Roots: roots,
DNSName: hostname,
}
_, err := cert.X509.Verify(opts)
if err != nil {
return git2go.ErrorCodeCertificate
}
return git2go.ErrorCodeOK
if (allowedTypes & git2go.CredentialTypeUserpassPlaintext) != 0 {
return git2go.NewCredentialUserpassPlaintext(opts.Username, opts.Password)
}
if (allowedTypes & git2go.CredentialTypeUsername) != 0 {
return git2go.NewCredentialUsername(opts.Username)
}
return nil, fmt.Errorf("unknown credential type %+v", allowedTypes)
}
return &git.Auth{CredCallback: credCallback, CertCallback: certCallback}, nil
}
type PublicKeyAuth struct {
user string
host string
// certificateCallback constructs CertificateCallback with the given options
// for git.Transport if the given opts is not nil, and returns the result.
func certificateCallback(opts *git.AuthOptions) git2go.CertificateCheckCallback {
switch opts.Transport {
case git.HTTPS:
if len(opts.CAFile) > 0 {
return x509Callback(opts.CAFile)
}
case git.SSH:
if len(opts.KnownHosts) > 0 && opts.Host != "" {
return knownHostsCallback(opts.Host, opts.KnownHosts)
}
}
return nil
}
func (s *PublicKeyAuth) Method(secret corev1.Secret) (*git.Auth, error) {
if _, ok := secret.Data[git.CAFile]; ok {
return nil, fmt.Errorf("found %s key in secret '%s' but libgit2 SSH transport does not support custom certificates", git.CAFile, secret.Name)
}
identity := secret.Data["identity"]
knownHosts := secret.Data["known_hosts"]
if len(identity) == 0 || len(knownHosts) == 0 {
return nil, fmt.Errorf("invalid '%s' secret data: required fields 'identity' and 'known_hosts'", secret.Name)
}
// x509Callback returns a CertificateCheckCallback that verifies the
// certificate against the given caBundle for git.HTTPS Transports.
func x509Callback(caBundle []byte) git2go.CertificateCheckCallback {
return func(cert *git2go.Certificate, valid bool, hostname string) git2go.ErrorCode {
roots := x509.NewCertPool()
if ok := roots.AppendCertsFromPEM(caBundle); !ok {
return git2go.ErrorCodeCertificate
}
kk, err := parseKnownHosts(string(knownHosts))
if err != nil {
return nil, err
opts := x509.VerifyOptions{
Roots: roots,
DNSName: hostname,
CurrentTime: now(),
}
if _, err := cert.X509.Verify(opts); err != nil {
return git2go.ErrorCodeCertificate
}
return git2go.ErrorCodeOK
}
}
// Need to validate private key as it is not
// done by git2go when loading the key
password, ok := secret.Data["password"]
if ok {
_, err = ssh.ParsePrivateKeyWithPassphrase(identity, password)
} else {
_, err = ssh.ParsePrivateKey(identity)
}
if err != nil {
return nil, err
}
user := s.user
if user == "" {
user = git.DefaultPublicKeyAuthUser
}
credCallback := func(url string, usernameFromURL string, allowedTypes git2go.CredentialType) (*git2go.Credential, error) {
cred, err := git2go.NewCredentialSSHKeyFromMemory(user, "", string(identity), string(password))
// 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) git2go.ErrorCode {
kh, err := parseKnownHosts(string(knownHosts))
if err != nil {
return nil, err
return git2go.ErrorCodeCertificate
}
return cred, nil
}
certCallback := func(cert *git2go.Certificate, valid bool, hostname string) git2go.ErrorCode {
// First, attempt to split the configured host and port to validate
// the port-less hostname given to the callback.
host, _, err := net.SplitHostPort(s.host)
h, _, err := net.SplitHostPort(host)
if err != nil {
// SplitHostPort returns an error if the host is missing
// a port, assume the host has no port.
host = s.host
h = host
}
// Check if the configured host matches the hostname given to
// the callback.
if host != hostname {
if h != hostname {
return git2go.ErrorCodeUser
}
@ -164,16 +148,14 @@ func (s *PublicKeyAuth) Method(secret corev1.Secret) (*git.Auth, error) {
// 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.
host = knownhosts.Normalize(s.host)
for _, k := range kk {
if k.matches(host, cert.Hostkey) {
h = knownhosts.Normalize(host)
for _, k := range kh {
if k.matches(h, cert.Hostkey) {
return git2go.ErrorCodeOK
}
}
return git2go.ErrorCodeCertificate
}
return &git.Auth{CredCallback: credCallback, CertCallback: certCallback}, nil
}
type knownKey struct {
@ -187,6 +169,11 @@ func parseKnownHosts(s string) ([]knownKey, error) {
for scanner.Scan() {
_, hosts, pubKey, _, _, err := ssh.ParseKnownHosts(scanner.Bytes())
if err != nil {
// Lines that aren't host public key result in EOF, like a comment
// line. Continue parsing the other lines.
if err == io.EOF {
continue
}
return []knownKey{}, err
}
@ -234,6 +221,5 @@ func containsHost(hosts []string, host string) bool {
return true
}
}
return false
}

View File

@ -17,163 +17,241 @@ limitations under the License.
package libgit2
import (
"bytes"
"crypto/x509"
"encoding/base64"
"reflect"
"encoding/pem"
"errors"
"testing"
"time"
git2go "github.com/libgit2/git2go/v31"
corev1 "k8s.io/api/core/v1"
"github.com/fluxcd/source-controller/pkg/git"
. "github.com/onsi/gomega"
)
const (
// secretKeyFixture is a randomly generated password less
// 512bit RSA private key.
secretKeyFixture string = `-----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-----`
geoTrustRootFixture = `-----BEGIN CERTIFICATE-----
MIIDVDCCAjygAwIBAgIDAjRWMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT
MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i
YWwgQ0EwHhcNMDIwNTIxMDQwMDAwWhcNMjIwNTIxMDQwMDAwWjBCMQswCQYDVQQG
EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEbMBkGA1UEAxMSR2VvVHJ1c3Qg
R2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2swYYzD9
9BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9mOSm9BXiLnTjoBbdq
fnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIuT8rxh0PBFpVXLVDv
iS2Aelet8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6cJmTM386DGXHKTubU
1XupGc1V3sjs0l44U+VcT4wt/lAjNvxm5suOpDkZALeVAjmRCw7+OC7RHQWa9k0+
bw8HHa8sHo9gOeL6NlMTOdReJivbPagUvTLrGAMoUgRx5aszPeE4uwc2hGKceeoW
MPRfwCvocWvk+QIDAQABo1MwUTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTA
ephojYn7qwVkDBF9qn1luMrMTjAfBgNVHSMEGDAWgBTAephojYn7qwVkDBF9qn1l
uMrMTjANBgkqhkiG9w0BAQUFAAOCAQEANeMpauUvXVSOKVCUn5kaFOSPeCpilKIn
Z57QzxpeR+nBsqTP3UEaBU6bS+5Kb1VSsyShNwrrZHYqLizz/Tt1kL/6cdjHPTfS
tQWVYrmm3ok9Nns4d0iXrKYgjy6myQzCsplFAMfOEVEiIuCl6rYVSAlk6l5PdPcF
PseKUgzbFbS9bZvlxrFUaKnjaZC2mqUPuLk/IH2uSrW4nOQdtqvmlKXBx4Ot2/Un
hw4EbNX/3aBd7YdStysVAq45pmp06drE57xNNB6pXE0zX5IJL4hmXXeXxx12E6nV
5fEWCRE11azbJHFwLJhWC9kXtNHjUStedejV0NxPNO3CBWaAocvmMw==
-----END CERTIFICATE-----`
// secretKeyFixture is a randomly generated
// 512bit RSA private key with password foobar.
secretPassphraseFixture = `-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-256-CBC,0B016973B2A761D31E6B388D0F327C35
giag2IntermediateFixture = `-----BEGIN CERTIFICATE-----
MIIEBDCCAuygAwIBAgIDAjppMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT
MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i
YWwgQ0EwHhcNMTMwNDA1MTUxNTU1WhcNMTUwNDA0MTUxNTU1WjBJMQswCQYDVQQG
EwJVUzETMBEGA1UEChMKR29vZ2xlIEluYzElMCMGA1UEAxMcR29vZ2xlIEludGVy
bmV0IEF1dGhvcml0eSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
AJwqBHdc2FCROgajguDYUEi8iT/xGXAaiEZ+4I/F8YnOIe5a/mENtzJEiaB0C1NP
VaTOgmKV7utZX8bhBYASxF6UP7xbSDj0U/ck5vuR6RXEz/RTDfRK/J9U3n2+oGtv
h8DQUB8oMANA2ghzUWx//zo8pzcGjr1LEQTrfSTe5vn8MXH7lNVg8y5Kr0LSy+rE
ahqyzFPdFUuLH8gZYR/Nnag+YyuENWllhMgZxUYi+FOVvuOAShDGKuy6lyARxzmZ
EASg8GF6lSWMTlJ14rbtCMoU/M4iarNOz0YDl5cDfsCx3nuvRTPPuj5xt970JSXC
DTWJnZ37DhF5iR43xa+OcmkCAwEAAaOB+zCB+DAfBgNVHSMEGDAWgBTAephojYn7
qwVkDBF9qn1luMrMTjAdBgNVHQ4EFgQUSt0GFhu89mi1dvWBtrtiGrpagS8wEgYD
VR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAQYwOgYDVR0fBDMwMTAvoC2g
K4YpaHR0cDovL2NybC5nZW90cnVzdC5jb20vY3Jscy9ndGdsb2JhbC5jcmwwPQYI
KwYBBQUHAQEEMTAvMC0GCCsGAQUFBzABhiFodHRwOi8vZ3RnbG9iYWwtb2NzcC5n
ZW90cnVzdC5jb20wFwYDVR0gBBAwDjAMBgorBgEEAdZ5AgUBMA0GCSqGSIb3DQEB
BQUAA4IBAQA21waAESetKhSbOHezI6B1WLuxfoNCunLaHtiONgaX4PCVOzf9G0JY
/iLIa704XtE7JW4S615ndkZAkNoUyHgN7ZVm2o6Gb4ChulYylYbc3GrKBIxbf/a/
zG+FA1jDaFETzf3I93k9mTXwVqO94FntT0QJo544evZG0R0SnU++0ED8Vf4GXjza
HFa9llF7b1cq26KqltyMdMKVvvBulRP/F/A8rLIQjcxz++iPAsbw+zOzlTvjwsto
WHPbqCRiOwY1nQ2pM714A5AuTHhdUDqB1O6gyHA43LL5Z/qHQF1hwFGPa4NrzQU6
yuGnBXj8ytqU0CwIPX4WecigUCAkVDNx
-----END CERTIFICATE-----`
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-----`
googleLeafFixture = `-----BEGIN CERTIFICATE-----
MIIEdjCCA16gAwIBAgIIcR5k4dkoe04wDQYJKoZIhvcNAQEFBQAwSTELMAkGA1UE
BhMCVVMxEzARBgNVBAoTCkdvb2dsZSBJbmMxJTAjBgNVBAMTHEdvb2dsZSBJbnRl
cm5ldCBBdXRob3JpdHkgRzIwHhcNMTQwMzEyMDkzODMwWhcNMTQwNjEwMDAwMDAw
WjBoMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwN
TW91bnRhaW4gVmlldzETMBEGA1UECgwKR29vZ2xlIEluYzEXMBUGA1UEAwwOd3d3
Lmdvb2dsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4zYCe
m0oUBhwE0EwBr65eBOcgcQO2PaSIAB2dEP/c1EMX2tOy0ov8rk83ePhJ+MWdT1z6
jge9X4zQQI8ZyA9qIiwrKBZOi8DNUvrqNZC7fJAVRrb9aX/99uYOJCypIbpmWG1q
fhbHjJewhwf8xYPj71eU4rLG80a+DapWmphtfq3h52lDQIBzLVf1yYbyrTaELaz4
NXF7HXb5YkId/gxIsSzM0aFUVu2o8sJcLYAsJqwfFKBKOMxUcn545nlspf0mTcWZ
0APlbwsKznNs4/xCDwIxxWjjqgHrYAFl6y07i1gzbAOqdNEyR24p+3JWI8WZBlBI
dk2KGj0W1fIfsvyxAgMBAAGjggFBMIIBPTAdBgNVHSUEFjAUBggrBgEFBQcDAQYI
KwYBBQUHAwIwGQYDVR0RBBIwEIIOd3d3Lmdvb2dsZS5jb20waAYIKwYBBQUHAQEE
XDBaMCsGCCsGAQUFBzAChh9odHRwOi8vcGtpLmdvb2dsZS5jb20vR0lBRzIuY3J0
MCsGCCsGAQUFBzABhh9odHRwOi8vY2xpZW50czEuZ29vZ2xlLmNvbS9vY3NwMB0G
A1UdDgQWBBTXD5Bx6iqT+dmEhbFL4OUoHyZn8zAMBgNVHRMBAf8EAjAAMB8GA1Ud
IwQYMBaAFErdBhYbvPZotXb1gba7Yhq6WoEvMBcGA1UdIAQQMA4wDAYKKwYBBAHW
eQIFATAwBgNVHR8EKTAnMCWgI6Ahhh9odHRwOi8vcGtpLmdvb2dsZS5jb20vR0lB
RzIuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQCR3RJtHzgDh33b/MI1ugiki+nl8Ikj
5larbJRE/rcA5oite+QJyAr6SU1gJJ/rRrK3ItVEHr9L621BCM7GSdoNMjB9MMcf
tJAW0kYGJ+wqKm53wG/JaOADTnnq2Mt/j6F2uvjgN/ouns1nRHufIvd370N0LeH+
orKqTuAPzXK7imQk6+OycYABbqCtC/9qmwRd8wwn7sF97DtYfK8WuNHtFalCAwyi
8LxJJYJCLWoMhZ+V8GZm+FOex5qkQAjnZrtNlbQJ8ro4r+rpKXtmMFFhfa+7L+PA
Kom08eUK8skxAzfDDijZPh10VtJ66uBoiDPdT+uCBehcBIcmSTrKjFGX
-----END CERTIFICATE-----`
// googleLeafWithInvalidHashFixture is the same as googleLeafFixture, but the signature
// algorithm in the certificate contains a nonsense OID.
googleLeafWithInvalidHashFixture = `-----BEGIN CERTIFICATE-----
MIIEdjCCA16gAwIBAgIIcR5k4dkoe04wDQYJKoZIhvcNAWAFBQAwSTELMAkGA1UE
BhMCVVMxEzARBgNVBAoTCkdvb2dsZSBJbmMxJTAjBgNVBAMTHEdvb2dsZSBJbnRl
cm5ldCBBdXRob3JpdHkgRzIwHhcNMTQwMzEyMDkzODMwWhcNMTQwNjEwMDAwMDAw
WjBoMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwN
TW91bnRhaW4gVmlldzETMBEGA1UECgwKR29vZ2xlIEluYzEXMBUGA1UEAwwOd3d3
Lmdvb2dsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4zYCe
m0oUBhwE0EwBr65eBOcgcQO2PaSIAB2dEP/c1EMX2tOy0ov8rk83ePhJ+MWdT1z6
jge9X4zQQI8ZyA9qIiwrKBZOi8DNUvrqNZC7fJAVRrb9aX/99uYOJCypIbpmWG1q
fhbHjJewhwf8xYPj71eU4rLG80a+DapWmphtfq3h52lDQIBzLVf1yYbyrTaELaz4
NXF7HXb5YkId/gxIsSzM0aFUVu2o8sJcLYAsJqwfFKBKOMxUcn545nlspf0mTcWZ
0APlbwsKznNs4/xCDwIxxWjjqgHrYAFl6y07i1gzbAOqdNEyR24p+3JWI8WZBlBI
dk2KGj0W1fIfsvyxAgMBAAGjggFBMIIBPTAdBgNVHSUEFjAUBggrBgEFBQcDAQYI
KwYBBQUHAwIwGQYDVR0RBBIwEIIOd3d3Lmdvb2dsZS5jb20waAYIKwYBBQUHAQEE
XDBaMCsGCCsGAQUFBzAChh9odHRwOi8vcGtpLmdvb2dsZS5jb20vR0lBRzIuY3J0
MCsGCCsGAQUFBzABhh9odHRwOi8vY2xpZW50czEuZ29vZ2xlLmNvbS9vY3NwMB0G
A1UdDgQWBBTXD5Bx6iqT+dmEhbFL4OUoHyZn8zAMBgNVHRMBAf8EAjAAMB8GA1Ud
IwQYMBaAFErdBhYbvPZotXb1gba7Yhq6WoEvMBcGA1UdIAQQMA4wDAYKKwYBBAHW
eQIFATAwBgNVHR8EKTAnMCWgI6Ahhh9odHRwOi8vcGtpLmdvb2dsZS5jb20vR0lB
RzIuY3JsMA0GCSqGSIb3DQFgBQUAA4IBAQCR3RJtHzgDh33b/MI1ugiki+nl8Ikj
5larbJRE/rcA5oite+QJyAr6SU1gJJ/rRrK3ItVEHr9L621BCM7GSdoNMjB9MMcf
tJAW0kYGJ+wqKm53wG/JaOADTnnq2Mt/j6F2uvjgN/ouns1nRHufIvd370N0LeH+
orKqTuAPzXK7imQk6+OycYABbqCtC/9qmwRd8wwn7sF97DtYfK8WuNHtFalCAwyi
8LxJJYJCLWoMhZ+V8GZm+FOex5qkQAjnZrtNlbQJ8ro4r+rpKXtmMFFhfa+7L+PA
Kom08eUK8skxAzfDDijZPh10VtJ66uBoiDPdT+uCBehcBIcmSTrKjFGX
-----END CERTIFICATE-----`
// knownHostsFixture is known_hosts fixture in the expected
// format.
knownHostsFixture string = `github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==`
)
var (
basicAuthSecretFixture = corev1.Secret{
Data: map[string][]byte{
"username": []byte("git"),
"password": []byte("password"),
},
}
privateKeySecretFixture = corev1.Secret{
Data: map[string][]byte{
"identity": []byte(secretKeyFixture),
"known_hosts": []byte(knownHostsFixture),
},
}
privateKeySecretWithPassphraseFixture = corev1.Secret{
Data: map[string][]byte{
"identity": []byte(secretPassphraseFixture),
"known_hosts": []byte(knownHostsFixture),
"password": []byte("foobar"),
},
}
)
func Test_x509Callback(t *testing.T) {
now = func() time.Time { return time.Unix(1395785200, 0) }
func TestAuthSecretStrategyForURL(t *testing.T) {
tests := []struct {
name string
url string
want git.AuthSecretStrategy
wantErr bool
name string
certificate string
host string
caBundle []byte
want git2go.ErrorCode
}{
{"HTTP", "http://git.example.com/org/repo.git", &BasicAuth{}, false},
{"HTTPS", "https://git.example.com/org/repo.git", &BasicAuth{}, false},
{"SSH", "ssh://git.example.com:2222/org/repo.git", &PublicKeyAuth{host: "git.example.com:2222"}, false},
{"SSH with username", "ssh://example@git.example.com:2222/org/repo.git", &PublicKeyAuth{user: "example", host: "git.example.com:2222"}, false},
{"unsupported", "protocol://example.com", nil, true},
{
name: "Valid certificate authority bundle",
certificate: googleLeafFixture,
host: "www.google.com",
caBundle: []byte(giag2IntermediateFixture + "\n" + geoTrustRootFixture),
want: git2go.ErrorCodeOK,
},
{
name: "Invalid certificate",
certificate: googleLeafWithInvalidHashFixture,
host: "www.google.com",
caBundle: []byte(giag2IntermediateFixture + "\n" + geoTrustRootFixture),
want: git2go.ErrorCodeCertificate,
},
{
name: "Invalid certificate authority bundle",
certificate: googleLeafFixture,
host: "www.google.com",
caBundle: bytes.Trim([]byte(giag2IntermediateFixture+"\n"+geoTrustRootFixture), "-"),
want: git2go.ErrorCodeCertificate,
},
{
name: "Missing intermediate in bundle",
certificate: googleLeafFixture,
host: "www.google.com",
caBundle: []byte(geoTrustRootFixture),
want: git2go.ErrorCodeCertificate,
},
{
name: "Invalid host",
certificate: googleLeafFixture,
host: "www.google.co",
caBundle: []byte(giag2IntermediateFixture + "\n" + geoTrustRootFixture),
want: git2go.ErrorCodeCertificate,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := AuthSecretStrategyForURL(tt.url)
if (err != nil) != tt.wantErr {
t.Errorf("AuthSecretStrategyForURL() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("AuthSecretStrategyForURL() got = %v, want %v", got, tt.want)
g := NewWithT(t)
cert := &git2go.Certificate{}
if tt.certificate != "" {
x509Cert, err := certificateFromPEM(tt.certificate)
g.Expect(err).ToNot(HaveOccurred())
cert.X509 = x509Cert
}
callback := x509Callback(tt.caBundle)
g.Expect(callback(cert, false, tt.host)).To(Equal(tt.want))
})
}
}
func TestBasicAuthStrategy_Method(t *testing.T) {
func Test_knownHostsCallback(t *testing.T) {
tests := []struct {
name string
secret corev1.Secret
modify func(secret *corev1.Secret)
wantErr bool
name string
host string
expectedHost string
knownHosts []byte
hostkey git2go.HostkeyCertificate
want git2go.ErrorCode
}{
{"with username and password", basicAuthSecretFixture, nil, false},
{
name: "Match",
host: "github.com",
knownHosts: []byte(knownHostsFixture),
hostkey: git2go.HostkeyCertificate{Kind: git2go.HostkeySHA1 | git2go.HostkeyMD5, HashSHA1: sha1Fingerprint("v2toJdKXfFEaR1u++4iq1UqSrHM")},
expectedHost: "github.com",
want: git2go.ErrorCodeOK,
},
{
name: "Match with port",
host: "github.com",
knownHosts: []byte(knownHostsFixture),
hostkey: git2go.HostkeyCertificate{Kind: git2go.HostkeySHA1 | git2go.HostkeyMD5, HashSHA1: sha1Fingerprint("v2toJdKXfFEaR1u++4iq1UqSrHM")},
expectedHost: "github.com:22",
want: git2go.ErrorCodeOK,
},
{
name: "Hostname mismatch",
host: "github.com",
knownHosts: []byte(knownHostsFixture),
hostkey: git2go.HostkeyCertificate{Kind: git2go.HostkeySHA1 | git2go.HostkeyMD5, HashSHA1: sha1Fingerprint("v2toJdKXfFEaR1u++4iq1UqSrHM")},
expectedHost: "example.com",
want: git2go.ErrorCodeUser,
},
{
name: "Hostkey mismatch",
host: "github.com",
knownHosts: []byte(knownHostsFixture),
hostkey: git2go.HostkeyCertificate{Kind: git2go.HostkeyMD5, HashMD5: md5Fingerprint("\xb6\x03\x0e\x39\x97\x9e\xd0\xe7\x24\xce\xa3\x77\x3e\x01\x42\x09")},
expectedHost: "github.com",
want: git2go.ErrorCodeCertificate,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
secret := tt.secret.DeepCopy()
if tt.modify != nil {
tt.modify(secret)
}
s := &BasicAuth{}
_, err := s.Method(*secret)
if (err != nil) != tt.wantErr {
t.Errorf("Method() error = %v, wantErr %v", err, tt.wantErr)
return
}
g := NewWithT(t)
cert := &git2go.Certificate{Hostkey: tt.hostkey}
callback := knownHostsCallback(tt.expectedHost, tt.knownHosts)
g.Expect(callback(cert, false, tt.host)).To(Equal(tt.want))
})
}
}
func TestPublicKeyStrategy_Method(t *testing.T) {
tests := []struct {
name string
secret corev1.Secret
modify func(secret *corev1.Secret)
wantErr bool
}{
{"private key and known_hosts", privateKeySecretFixture, nil, false},
{"private key with passphrase and known_hosts", privateKeySecretWithPassphraseFixture, nil, false},
{"missing private key", privateKeySecretFixture, func(s *corev1.Secret) { delete(s.Data, "identity") }, true},
{"invalid private key", privateKeySecretFixture, func(s *corev1.Secret) { s.Data["identity"] = []byte(`-----BEGIN RSA PRIVATE KEY-----`) }, true},
{"missing known_hosts", privateKeySecretFixture, func(s *corev1.Secret) { delete(s.Data, "known_hosts") }, true},
{"invalid known_hosts", privateKeySecretFixture, func(s *corev1.Secret) { s.Data["known_hosts"] = []byte(`invalid`) }, true},
{"missing password", privateKeySecretWithPassphraseFixture, func(s *corev1.Secret) { delete(s.Data, "password") }, true},
{"invalid password", privateKeySecretWithPassphraseFixture, func(s *corev1.Secret) { s.Data["password"] = []byte("foo") }, true},
{"empty", corev1.Secret{}, nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
secret := tt.secret.DeepCopy()
if tt.modify != nil {
tt.modify(secret)
}
s := &PublicKeyAuth{}
_, err := s.Method(*secret)
if (err != nil) != tt.wantErr {
t.Errorf("Method() error = %v, wantErr %v", err, tt.wantErr)
return
}
})
}
}
func TestKnownKeyHash(t *testing.T) {
func Test_parseKnownHosts_matches(t *testing.T) {
tests := []struct {
name string
hostkey git2go.HostkeyCertificate
@ -189,16 +267,80 @@ func TestKnownKeyHash(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
knownKeys, err := parseKnownHosts(knownHostsFixture)
if err != nil {
t.Error(err)
return
}
matches := knownKeys[0].matches("github.com", tt.hostkey)
if matches != tt.wantMatches {
t.Errorf("Method() matches = %v, wantMatches %v", matches, tt.wantMatches)
return
g.Expect(matches).To(Equal(tt.wantMatches))
})
}
}
func Test_parseKnownHosts(t *testing.T) {
tests := []struct {
name string
fixture string
wantErr bool
}{
{
name: "empty file",
fixture: "",
wantErr: false,
},
{
name: "single host",
fixture: `github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==`,
wantErr: false,
},
{
name: "single host with comment",
fixture: `# github.com
github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==`,
wantErr: false,
},
{
name: "multiple hosts with comments",
fixture: `# github.com
github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==
# gitlab.com
gitlab.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAfuCHKVTjquxvt6CM6tdG4SLp1Btn/nOeHHE5UOzRdf`,
},
{
name: "no host key, only comments",
fixture: `# example.com
#github.com
# gitlab.com`,
wantErr: false,
},
{
name: "invalid host entry",
fixture: `github.com ssh-rsa`,
wantErr: true,
},
{
name: "invalid content",
fixture: `some random text`,
wantErr: true,
},
{
name: "invalid line with valid host key",
fixture: `some random text
gitlab.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAfuCHKVTjquxvt6CM6tdG4SLp1Btn/nOeHHE5UOzRdf`,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
_, err := parseKnownHosts(tt.fixture)
if tt.wantErr {
g.Expect(err).To(HaveOccurred())
} else {
g.Expect(err).ToNot(HaveOccurred())
}
})
}
@ -206,7 +348,7 @@ func TestKnownKeyHash(t *testing.T) {
func md5Fingerprint(in string) [16]byte {
var out [16]byte
copy(out[:], []byte(in))
copy(out[:], in)
return out
}
@ -229,3 +371,11 @@ func sha256Fingerprint(in string) [32]byte {
copy(out[:], d)
return out
}
func certificateFromPEM(pemBytes string) (*x509.Certificate, error) {
block, _ := pem.Decode([]byte(pemBytes))
if block == nil {
return nil, errors.New("failed to decode PEM")
}
return x509.ParseCertificate(block.Bytes)
}

131
pkg/git/options.go Normal file
View File

@ -0,0 +1,131 @@
/*
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
}
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
}
// 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
}

272
pkg/git/options_test.go Normal file
View File

@ -0,0 +1,272 @@
/*
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

@ -17,32 +17,23 @@ limitations under the License.
package strategy
import (
"context"
"fmt"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta1"
"github.com/fluxcd/source-controller/pkg/git"
"github.com/fluxcd/source-controller/pkg/git/gogit"
"github.com/fluxcd/source-controller/pkg/git/libgit2"
)
func CheckoutStrategyForRef(ref *sourcev1.GitRepositoryRef, opt git.CheckoutOptions) (git.CheckoutStrategy, error) {
switch opt.GitImplementation {
case sourcev1.GoGitImplementation:
return gogit.CheckoutStrategyForRef(ref, opt), nil
case sourcev1.LibGit2Implementation:
return libgit2.CheckoutStrategyForRef(ref, opt), nil
// 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("invalid Git implementation %s", opt.GitImplementation)
}
}
func AuthSecretStrategyForURL(url string, opt git.CheckoutOptions) (git.AuthSecretStrategy, error) {
switch opt.GitImplementation {
case sourcev1.GoGitImplementation:
return gogit.AuthSecretStrategyForURL(url)
case sourcev1.LibGit2Implementation:
return libgit2.AuthSecretStrategyForURL(url)
default:
return nil, fmt.Errorf("invalid Git implementation %s", opt.GitImplementation)
return nil, fmt.Errorf("unsupported Git implementation '%s'", impl)
}
}

View File

@ -0,0 +1,403 @@
/*
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"
"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"
)
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,
}
},
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,
}
},
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)
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,
}
},
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, err := os.MkdirTemp("", "test-checkout")
g.Expect(err).ToNot(HaveOccurred())
defer os.RemoveAll(tmpDir)
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(string(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,
}
// 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, err := os.MkdirTemp("", "test-clone")
g.Expect(err).ToNot(HaveOccurred())
defer os.RemoveAll(cloneDir)
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, err := os.MkdirTemp("", "test-checkout")
g.Expect(err).ToNot(HaveOccurred())
defer os.RemoveAll(tmpDir)
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(string(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,
}
}

View File

@ -0,0 +1,30 @@
# 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

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

View File

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

View File

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

View File

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

11
pkg/git/strategy/testdata/certs/ca.pem vendored Normal file
View File

@ -0,0 +1,11 @@
-----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

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

View File

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

View File

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

View File

@ -0,0 +1,13 @@
-----BEGIN CERTIFICATE-----
MIIB7TCCAZKgAwIBAgIUB+17B8PU05wVTzRHLeG+S+ybZK4wCgYIKoZIzj0EAwIw
GTEXMBUGA1UEAxMOZXhhbXBsZS5jb20gQ0EwHhcNMjAwNDE3MDgxODAwWhcNMzAw
NDE1MDgxODAwWjAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbTBZMBMGByqGSM49AgEG
CCqGSM49AwEHA0IABPud6ARpa71VE3pUhmI4vBjR1YVYPIzrNT3ni7lwiGY0JnLR
yxRKt3xPphsQi/dzLaWM5cCFgX9Ju6RBkK3um86jgbowgbcwDgYDVR0PAQH/BAQD
AgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAA
MB0GA1UdDgQWBBTM8HS5EIlVMBYv/300jN8PEArUgDAfBgNVHSMEGDAWgBQGyUiU
1QEZiMAqjsnIYTwZ4yp5wzA4BgNVHREEMTAvgglsb2NhbGhvc3SCC2V4YW1wbGUu
Y29tgg93d3cuZXhhbXBsZS5jb22HBH8AAAEwCgYIKoZIzj0EAwIDSQAwRgIhAOgB
5W82FEgiTTOmsNRekkK5jUPbj4D4eHtb2/BI7ph4AiEA2AxHASIFBdv5b7Qf5prb
bdNmUCzAvVuCAKuMjg2OPrE=
-----END CERTIFICATE-----

View File

@ -0,0 +1 @@
test file