Merge branch 'main' into bucket-provider-interface
This commit is contained in:
commit
fb6024ed3d
30
CHANGELOG.md
30
CHANGELOG.md
|
@ -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
|
||||
|
|
102
CONTRIBUTING.md
102
CONTRIBUTING.md
|
@ -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.
|
|
@ -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
|
||||
```
|
|
@ -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"`
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -6,4 +6,4 @@ resources:
|
|||
images:
|
||||
- name: fluxcd/source-controller
|
||||
newName: fluxcd/source-controller
|
||||
newTag: v0.16.1
|
||||
newTag: v0.17.1
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
5
go.mod
|
@ -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
5
go.sum
|
@ -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=
|
||||
|
|
101
pkg/git/git.go
101
pkg/git/git.go
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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"
|
||||
)
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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"
|
||||
)
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"signing": {
|
||||
"default": {
|
||||
"expiry": "87600h"
|
||||
},
|
||||
"profiles": {
|
||||
"web-servers": {
|
||||
"usages": [
|
||||
"signing",
|
||||
"key encipherment",
|
||||
"server auth",
|
||||
"client auth"
|
||||
],
|
||||
"expiry": "87600h"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"CN": "example.com CA",
|
||||
"hosts": [
|
||||
"127.0.0.1",
|
||||
"localhost",
|
||||
"example.com",
|
||||
"www.example.com"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIOH/u9dMcpVcZ0+X9Fc78dCTj8SHuXawhLjhu/ej64WToAoGCCqGSM49
|
||||
AwEHoUQDQgAEruH/kPxtX3cyYR2G7TYmxLq6AHyzo/NGXc9XjGzdJutE2SQzn37H
|
||||
dvSJbH+Lvqo9ik0uiJVRVdCYD1j7gNszGA==
|
||||
-----END EC PRIVATE KEY-----
|
|
@ -0,0 +1,9 @@
|
|||
-----BEGIN CERTIFICATE REQUEST-----
|
||||
MIIBIDCBxgIBADAZMRcwFQYDVQQDEw5leGFtcGxlLmNvbSBDQTBZMBMGByqGSM49
|
||||
AgEGCCqGSM49AwEHA0IABK7h/5D8bV93MmEdhu02JsS6ugB8s6PzRl3PV4xs3Sbr
|
||||
RNkkM59+x3b0iWx/i76qPYpNLoiVUVXQmA9Y+4DbMxigSzBJBgkqhkiG9w0BCQ4x
|
||||
PDA6MDgGA1UdEQQxMC+CCWxvY2FsaG9zdIILZXhhbXBsZS5jb22CD3d3dy5leGFt
|
||||
cGxlLmNvbYcEfwAAATAKBggqhkjOPQQDAgNJADBGAiEAkw85nyLhJssyCYsaFvRU
|
||||
EErhu66xHPJug/nG50uV5OoCIQCUorrflOSxfChPeCe4xfwcPv7FpcCYbKVYtGzz
|
||||
b34Wow==
|
||||
-----END CERTIFICATE REQUEST-----
|
|
@ -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-----
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"CN": "example.com",
|
||||
"hosts": [
|
||||
"127.0.0.1",
|
||||
"localhost",
|
||||
"example.com",
|
||||
"www.example.com"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIKQbEXV6nljOHMmPrWVWQ+JrAE5wsbE9iMhfY7wlJgXOoAoGCCqGSM49
|
||||
AwEHoUQDQgAE+53oBGlrvVUTelSGYji8GNHVhVg8jOs1PeeLuXCIZjQmctHLFEq3
|
||||
fE+mGxCL93MtpYzlwIWBf0m7pEGQre6bzg==
|
||||
-----END EC PRIVATE KEY-----
|
|
@ -0,0 +1,8 @@
|
|||
-----BEGIN CERTIFICATE REQUEST-----
|
||||
MIIBHDCBwwIBADAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbTBZMBMGByqGSM49AgEG
|
||||
CCqGSM49AwEHA0IABPud6ARpa71VE3pUhmI4vBjR1YVYPIzrNT3ni7lwiGY0JnLR
|
||||
yxRKt3xPphsQi/dzLaWM5cCFgX9Ju6RBkK3um86gSzBJBgkqhkiG9w0BCQ4xPDA6
|
||||
MDgGA1UdEQQxMC+CCWxvY2FsaG9zdIILZXhhbXBsZS5jb22CD3d3dy5leGFtcGxl
|
||||
LmNvbYcEfwAAATAKBggqhkjOPQQDAgNIADBFAiB5A6wvQ5x6g/zhiyn+wLzXsOaB
|
||||
Gb/F25p/zTHHQqZbkwIhAPUgWzy/2bs6eZEi97bSlaRdmrqHwqT842t5sEwGyXNV
|
||||
-----END CERTIFICATE REQUEST-----
|
|
@ -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-----
|
|
@ -0,0 +1 @@
|
|||
test file
|
Loading…
Reference in New Issue