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.
|
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
|
## 0.16.1
|
||||||
|
|
||||||
**Release date:** 2021-10-22
|
**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.
|
// GitRepositoryRef defines the Git ref used for pull and checkout operations.
|
||||||
type GitRepositoryRef struct {
|
type GitRepositoryRef struct {
|
||||||
// The Git branch to checkout, defaults to master.
|
// The Git branch to checkout, defaults to master.
|
||||||
// +kubebuilder:default:=master
|
|
||||||
// +optional
|
// +optional
|
||||||
Branch string `json:"branch,omitempty"`
|
Branch string `json:"branch,omitempty"`
|
||||||
|
|
||||||
|
|
|
@ -91,7 +91,6 @@ spec:
|
||||||
description: The Git reference to checkout and monitor for changes, defaults to master branch.
|
description: The Git reference to checkout and monitor for changes, defaults to master branch.
|
||||||
properties:
|
properties:
|
||||||
branch:
|
branch:
|
||||||
default: master
|
|
||||||
description: The Git branch to checkout, defaults to master.
|
description: The Git branch to checkout, defaults to master.
|
||||||
type: string
|
type: string
|
||||||
commit:
|
commit:
|
||||||
|
|
|
@ -6,4 +6,4 @@ resources:
|
||||||
images:
|
images:
|
||||||
- name: fluxcd/source-controller
|
- name: fluxcd/source-controller
|
||||||
newName: 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)
|
defer os.RemoveAll(tmpGit)
|
||||||
|
|
||||||
// determine auth method
|
// Configure auth options using secret
|
||||||
auth := &git.Auth{}
|
var authOpts *git.AuthOptions
|
||||||
if repository.Spec.SecretRef != nil {
|
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{
|
name := types.NamespacedName{
|
||||||
Namespace: repository.GetNamespace(),
|
Namespace: repository.GetNamespace(),
|
||||||
Name: repository.Spec.SecretRef.Name,
|
Name: repository.Spec.SecretRef.Name,
|
||||||
}
|
}
|
||||||
|
|
||||||
var secret corev1.Secret
|
secret := &corev1.Secret{}
|
||||||
err = r.Client.Get(ctx, name, &secret)
|
err = r.Client.Get(ctx, name, secret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fmt.Errorf("auth secret error: %w", err)
|
err = fmt.Errorf("auth secret error: %w", err)
|
||||||
return sourcev1.GitRepositoryNotReady(repository, sourcev1.AuthenticationFailedReason, err.Error()), 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 {
|
if err != nil {
|
||||||
err = fmt.Errorf("auth error: %w", err)
|
|
||||||
return sourcev1.GitRepositoryNotReady(repository, sourcev1.AuthenticationFailedReason, err.Error()), err
|
return sourcev1.GitRepositoryNotReady(repository, sourcev1.AuthenticationFailedReason, err.Error()), err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
checkoutOpts := git.CheckoutOptions{RecurseSubmodules: repository.Spec.RecurseSubmodules}
|
||||||
checkoutStrategy, err := strategy.CheckoutStrategyForRef(
|
if ref := repository.Spec.Reference; ref != nil {
|
||||||
repository.Spec.Reference,
|
checkoutOpts.Branch = ref.Branch
|
||||||
git.CheckoutOptions{
|
checkoutOpts.Commit = ref.Commit
|
||||||
GitImplementation: repository.Spec.GitImplementation,
|
checkoutOpts.Tag = ref.Tag
|
||||||
RecurseSubmodules: repository.Spec.RecurseSubmodules,
|
checkoutOpts.SemVer = ref.SemVer
|
||||||
},
|
}
|
||||||
)
|
checkoutStrategy, err := strategy.CheckoutStrategyForImplementation(ctx,
|
||||||
|
git.Implementation(repository.Spec.GitImplementation), checkoutOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return sourcev1.GitRepositoryNotReady(repository, sourcev1.GitOperationFailedReason, err.Error()), err
|
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)
|
gitCtx, cancel := context.WithTimeout(ctx, repository.Spec.Timeout.Duration)
|
||||||
defer cancel()
|
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 {
|
if err != nil {
|
||||||
return sourcev1.GitRepositoryNotReady(repository, sourcev1.GitOperationFailedReason, err.Error()), err
|
return sourcev1.GitRepositoryNotReady(repository, sourcev1.GitOperationFailedReason, err.Error()), err
|
||||||
}
|
}
|
||||||
|
artifact := r.Storage.NewArtifactFor(repository.Kind, repository.GetObjectMeta(), commit.String(), fmt.Sprintf("%s.tar.gz", commit.Hash.String()))
|
||||||
artifact := r.Storage.NewArtifactFor(repository.Kind, repository.GetObjectMeta(), revision, fmt.Sprintf("%s.tar.gz", commit.Hash()))
|
|
||||||
|
|
||||||
// copy all included repository into the artifact
|
// copy all included repository into the artifact
|
||||||
includedArtifacts := []*sourcev1.Artifact{}
|
includedArtifacts := []*sourcev1.Artifact{}
|
||||||
|
@ -309,14 +298,17 @@ func (r *GitRepositoryReconciler) reconcile(ctx context.Context, repository sour
|
||||||
Namespace: repository.Namespace,
|
Namespace: repository.Namespace,
|
||||||
Name: repository.Spec.Verification.SecretRef.Name,
|
Name: repository.Spec.Verification.SecretRef.Name,
|
||||||
}
|
}
|
||||||
var secret corev1.Secret
|
secret := &corev1.Secret{}
|
||||||
if err := r.Client.Get(ctx, publicKeySecret, &secret); err != nil {
|
if err := r.Client.Get(ctx, publicKeySecret, secret); err != nil {
|
||||||
err = fmt.Errorf("PGP public keys secret error: %w", err)
|
err = fmt.Errorf("PGP public keys secret error: %w", err)
|
||||||
return sourcev1.GitRepositoryNotReady(repository, sourcev1.VerificationFailedReason, err.Error()), err
|
return sourcev1.GitRepositoryNotReady(repository, sourcev1.VerificationFailedReason, err.Error()), err
|
||||||
}
|
}
|
||||||
|
|
||||||
err := commit.Verify(secret)
|
var keyRings []string
|
||||||
if err != nil {
|
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
|
return sourcev1.GitRepositoryNotReady(repository, sourcev1.VerificationFailedReason, err.Error()), err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,11 +23,9 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -251,7 +249,7 @@ var _ = Describe("GitRepositoryReconciler", func() {
|
||||||
reference: &sourcev1.GitRepositoryRef{SemVer: "1.2.3.4"},
|
reference: &sourcev1.GitRepositoryRef{SemVer: "1.2.3.4"},
|
||||||
waitForReason: sourcev1.GitOperationFailedReason,
|
waitForReason: sourcev1.GitOperationFailedReason,
|
||||||
expectStatus: metav1.ConditionFalse,
|
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{
|
Entry("semver no match", refTestCase{
|
||||||
reference: &sourcev1.GitRepositoryRef{SemVer: "1.0.0"},
|
reference: &sourcev1.GitRepositoryRef{SemVer: "1.0.0"},
|
||||||
|
@ -265,7 +263,7 @@ var _ = Describe("GitRepositoryReconciler", func() {
|
||||||
},
|
},
|
||||||
waitForReason: sourcev1.GitOperationSucceedReason,
|
waitForReason: sourcev1.GitOperationSucceedReason,
|
||||||
expectStatus: metav1.ConditionTrue,
|
expectStatus: metav1.ConditionTrue,
|
||||||
expectRevision: "master",
|
expectRevision: "HEAD",
|
||||||
}),
|
}),
|
||||||
Entry("commit in branch", refTestCase{
|
Entry("commit in branch", refTestCase{
|
||||||
reference: &sourcev1.GitRepositoryRef{
|
reference: &sourcev1.GitRepositoryRef{
|
||||||
|
@ -284,7 +282,7 @@ var _ = Describe("GitRepositoryReconciler", func() {
|
||||||
},
|
},
|
||||||
waitForReason: sourcev1.GitOperationFailedReason,
|
waitForReason: sourcev1.GitOperationFailedReason,
|
||||||
expectStatus: metav1.ConditionFalse,
|
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"},
|
reference: &sourcev1.GitRepositoryRef{Branch: "main"},
|
||||||
waitForReason: sourcev1.GitOperationFailedReason,
|
waitForReason: sourcev1.GitOperationFailedReason,
|
||||||
expectStatus: metav1.ConditionFalse,
|
expectStatus: metav1.ConditionFalse,
|
||||||
expectMessage: "error: user rejected certificate",
|
expectMessage: "unable to clone: user rejected certificate",
|
||||||
gitImplementation: sourcev1.LibGit2Implementation,
|
gitImplementation: sourcev1.LibGit2Implementation,
|
||||||
}),
|
}),
|
||||||
Entry("self signed libgit2 with CA", refTestCase{
|
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)
|
v, err := semver.NewVersion(helmChart.Metadata.Version)
|
||||||
if err != nil {
|
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
|
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, "/")
|
splitRev := strings.Split(artifact.Revision, "/")
|
||||||
v, err := v.SetMetadata(splitRev[len(splitRev)-1])
|
v, err := v.SetMetadata(splitRev[len(splitRev)-1])
|
||||||
if err != nil {
|
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
|
return sourcev1.HelmChartNotReady(chart, sourcev1.StorageOperationFailedReason, err.Error()), err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -273,6 +273,21 @@ spec:
|
||||||
commit: 363a6a8fe6a7f13e05d34c163b0ef02a777da20a
|
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:
|
Pull a specific tag:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
|
5
go.mod
5
go.mod
|
@ -8,9 +8,10 @@ require (
|
||||||
cloud.google.com/go v0.93.3 // indirect
|
cloud.google.com/go v0.93.3 // indirect
|
||||||
cloud.google.com/go/storage v1.16.0
|
cloud.google.com/go/storage v1.16.0
|
||||||
github.com/Masterminds/semver/v3 v3.1.1
|
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/cyphar/filepath-securejoin v0.2.2
|
||||||
github.com/fluxcd/pkg/apis/meta v0.10.0
|
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/gitutil v0.1.0
|
||||||
github.com/fluxcd/pkg/helmtestserver v0.2.0
|
github.com/fluxcd/pkg/helmtestserver v0.2.0
|
||||||
github.com/fluxcd/pkg/lockedfile v0.1.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/ssh v0.1.0
|
||||||
github.com/fluxcd/pkg/untar v0.1.0
|
github.com/fluxcd/pkg/untar v0.1.0
|
||||||
github.com/fluxcd/pkg/version v0.1.0
|
github.com/fluxcd/pkg/version v0.1.0
|
||||||
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-billy/v5 v5.3.1
|
||||||
github.com/go-git/go-git/v5 v5.4.2
|
github.com/go-git/go-git/v5 v5.4.2
|
||||||
github.com/go-logr/logr v0.4.0
|
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/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 h1:N7wVGHC1cyPdT87hrDC7UwCwRwnZdQM46PBSLjG2rlE=
|
||||||
github.com/fluxcd/pkg/apis/meta v0.10.0/go.mod h1:CW9X9ijMTpNe7BwnokiUOrLl/h13miwVr/3abEQLbKE=
|
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.4.1 h1:knghRrVEEPnpO0VJYjoz0H2YMc4fnKAVt5hDGsB1IHc=
|
||||||
github.com/fluxcd/pkg/gittestserver v0.3.0/go.mod h1:8j36Z6B0BuKNZZ6exAWoyDEpyQoFcjz1IX3WBT7PZNg=
|
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 h1:VO3kJY/CKOCO4ysDNqfdpTg04icAKBOSb3lbR5uE/IE=
|
||||||
github.com/fluxcd/pkg/gitutil v0.1.0/go.mod h1:Ybz50Ck5gkcnvF0TagaMwtlRy3X3wXuiri1HVsK5id4=
|
github.com/fluxcd/pkg/gitutil v0.1.0/go.mod h1:Ybz50Ck5gkcnvF0TagaMwtlRy3X3wXuiri1HVsK5id4=
|
||||||
github.com/fluxcd/pkg/helmtestserver v0.2.0 h1:cE7YHDmrWI0hr9QpaaeQ0vQ16Z0IiqZKiINDpqdY610=
|
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-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-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-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-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-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/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
|
package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5/plumbing/transport"
|
"github.com/ProtonMail/go-crypto/openpgp"
|
||||||
git2go "github.com/libgit2/git2go/v31"
|
|
||||||
corev1 "k8s.io/api/core/v1"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
type Implementation string
|
||||||
DefaultOrigin = "origin"
|
|
||||||
DefaultBranch = "master"
|
|
||||||
DefaultPublicKeyAuthUser = "git"
|
|
||||||
CAFile = "caFile"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Commit interface {
|
type Hash []byte
|
||||||
Verify(secret corev1.Secret) error
|
|
||||||
Hash() string
|
// 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 {
|
type CheckoutStrategy interface {
|
||||||
Checkout(ctx context.Context, path, url string, auth *Auth) (Commit, string, error)
|
Checkout(ctx context.Context, path, url string, config *AuthOptions) (*Commit, 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)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"sort"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Masterminds/semver/v3"
|
"github.com/Masterminds/semver/v3"
|
||||||
extgogit "github.com/go-git/go-git/v5"
|
extgogit "github.com/go-git/go-git/v5"
|
||||||
"github.com/go-git/go-git/v5/plumbing"
|
"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/gitutil"
|
||||||
"github.com/fluxcd/pkg/version"
|
"github.com/fluxcd/pkg/version"
|
||||||
|
|
||||||
sourcev1 "github.com/fluxcd/source-controller/api/v1beta1"
|
|
||||||
"github.com/fluxcd/source-controller/pkg/git"
|
"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 {
|
switch {
|
||||||
case ref == nil:
|
case opts.Commit != "":
|
||||||
return &CheckoutBranch{branch: git.DefaultBranch}
|
return &CheckoutCommit{Branch: opts.Branch, Commit: opts.Commit, RecurseSubmodules: opts.RecurseSubmodules}
|
||||||
case ref.SemVer != "":
|
case opts.SemVer != "":
|
||||||
return &CheckoutSemVer{semVer: ref.SemVer, recurseSubmodules: opt.RecurseSubmodules}
|
return &CheckoutSemVer{SemVer: opts.SemVer, RecurseSubmodules: opts.RecurseSubmodules}
|
||||||
case ref.Tag != "":
|
case opts.Tag != "":
|
||||||
return &CheckoutTag{tag: ref.Tag, recurseSubmodules: opt.RecurseSubmodules}
|
return &CheckoutTag{Tag: opts.Tag, RecurseSubmodules: opts.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}
|
|
||||||
default:
|
default:
|
||||||
return &CheckoutBranch{branch: git.DefaultBranch}
|
branch := opts.Branch
|
||||||
|
if branch == "" {
|
||||||
|
branch = git.DefaultBranch
|
||||||
|
}
|
||||||
|
return &CheckoutBranch{Branch: branch, RecurseSubmodules: opts.RecurseSubmodules}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type CheckoutBranch struct {
|
type CheckoutBranch struct {
|
||||||
branch string
|
Branch string
|
||||||
recurseSubmodules bool
|
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{
|
repo, err := extgogit.PlainCloneContext(ctx, path, false, &extgogit.CloneOptions{
|
||||||
URL: url,
|
URL: url,
|
||||||
Auth: auth.AuthMethod,
|
Auth: authMethod,
|
||||||
RemoteName: git.DefaultOrigin,
|
RemoteName: git.DefaultOrigin,
|
||||||
ReferenceName: plumbing.NewBranchReferenceName(c.branch),
|
ReferenceName: plumbing.NewBranchReferenceName(c.Branch),
|
||||||
SingleBranch: true,
|
SingleBranch: true,
|
||||||
NoCheckout: false,
|
NoCheckout: false,
|
||||||
Depth: 1,
|
Depth: 1,
|
||||||
RecurseSubmodules: recurseSubmodules(c.recurseSubmodules),
|
RecurseSubmodules: recurseSubmodules(c.RecurseSubmodules),
|
||||||
Progress: nil,
|
Progress: nil,
|
||||||
Tags: extgogit.NoTags,
|
Tags: extgogit.NoTags,
|
||||||
CABundle: auth.CABundle,
|
CABundle: caBundle(opts),
|
||||||
})
|
})
|
||||||
if err != nil {
|
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()
|
head, err := repo.Head()
|
||||||
if err != nil {
|
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 {
|
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 {
|
type CheckoutTag struct {
|
||||||
tag string
|
Tag string
|
||||||
recurseSubmodules bool
|
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{
|
repo, err := extgogit.PlainCloneContext(ctx, path, false, &extgogit.CloneOptions{
|
||||||
URL: url,
|
URL: url,
|
||||||
Auth: auth.AuthMethod,
|
Auth: authMethod,
|
||||||
RemoteName: git.DefaultOrigin,
|
RemoteName: git.DefaultOrigin,
|
||||||
ReferenceName: plumbing.NewTagReferenceName(c.tag),
|
ReferenceName: plumbing.NewTagReferenceName(c.Tag),
|
||||||
SingleBranch: true,
|
SingleBranch: true,
|
||||||
NoCheckout: false,
|
NoCheckout: false,
|
||||||
Depth: 1,
|
Depth: 1,
|
||||||
RecurseSubmodules: recurseSubmodules(c.recurseSubmodules),
|
RecurseSubmodules: recurseSubmodules(c.RecurseSubmodules),
|
||||||
Progress: nil,
|
Progress: nil,
|
||||||
Tags: extgogit.NoTags,
|
Tags: extgogit.NoTags,
|
||||||
CABundle: auth.CABundle,
|
CABundle: caBundle(opts),
|
||||||
})
|
})
|
||||||
if err != nil {
|
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()
|
head, err := repo.Head()
|
||||||
if err != nil {
|
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 {
|
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 {
|
type CheckoutCommit struct {
|
||||||
branch string
|
Branch string
|
||||||
commit string
|
Commit string
|
||||||
recurseSubmodules bool
|
RecurseSubmodules bool
|
||||||
}
|
}
|
||||||
|
|
||||||
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 := extgogit.PlainCloneContext(ctx, path, false, &extgogit.CloneOptions{
|
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,
|
URL: url,
|
||||||
Auth: auth.AuthMethod,
|
Auth: authMethod,
|
||||||
RemoteName: git.DefaultOrigin,
|
RemoteName: git.DefaultOrigin,
|
||||||
ReferenceName: plumbing.NewBranchReferenceName(c.branch),
|
SingleBranch: false,
|
||||||
SingleBranch: true,
|
NoCheckout: true,
|
||||||
NoCheckout: false,
|
RecurseSubmodules: recurseSubmodules(c.RecurseSubmodules),
|
||||||
RecurseSubmodules: recurseSubmodules(c.recurseSubmodules),
|
|
||||||
Progress: nil,
|
Progress: nil,
|
||||||
Tags: extgogit.NoTags,
|
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 {
|
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()
|
w, err := repo.Worktree()
|
||||||
if err != nil {
|
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 {
|
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{
|
err = w.Checkout(&extgogit.CheckoutOptions{
|
||||||
Hash: commit.Hash,
|
Hash: cc.Hash,
|
||||||
Force: true,
|
Force: true,
|
||||||
})
|
})
|
||||||
if err != nil {
|
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 {
|
type CheckoutSemVer struct {
|
||||||
semVer string
|
SemVer string
|
||||||
recurseSubmodules bool
|
RecurseSubmodules bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CheckoutSemVer) Checkout(ctx context.Context, path, url string, auth *git.Auth) (git.Commit, string, error) {
|
func (c *CheckoutSemVer) Checkout(ctx context.Context, path, url string, opts *git.AuthOptions) (*git.Commit, error) {
|
||||||
verConstraint, err := semver.NewConstraint(c.semVer)
|
verConstraint, err := semver.NewConstraint(c.SemVer)
|
||||||
if err != nil {
|
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{
|
repo, err := extgogit.PlainCloneContext(ctx, path, false, &extgogit.CloneOptions{
|
||||||
URL: url,
|
URL: url,
|
||||||
Auth: auth.AuthMethod,
|
Auth: authMethod,
|
||||||
RemoteName: git.DefaultOrigin,
|
RemoteName: git.DefaultOrigin,
|
||||||
NoCheckout: false,
|
NoCheckout: false,
|
||||||
Depth: 1,
|
Depth: 1,
|
||||||
RecurseSubmodules: recurseSubmodules(c.recurseSubmodules),
|
RecurseSubmodules: recurseSubmodules(c.RecurseSubmodules),
|
||||||
Progress: nil,
|
Progress: nil,
|
||||||
Tags: extgogit.AllTags,
|
Tags: extgogit.AllTags,
|
||||||
CABundle: auth.CABundle,
|
CABundle: caBundle(opts),
|
||||||
})
|
})
|
||||||
if err != nil {
|
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()
|
repoTags, err := repo.Tags()
|
||||||
if err != nil {
|
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)
|
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]
|
tags[t.Name().Short()] = t.Strings()[1]
|
||||||
return nil
|
return nil
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return nil, "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var matchedVersions semver.Collection
|
var matchedVersions semver.Collection
|
||||||
|
@ -223,7 +246,7 @@ func (c *CheckoutSemVer) Checkout(ctx context.Context, path, url string, auth *g
|
||||||
matchedVersions = append(matchedVersions, v)
|
matchedVersions = append(matchedVersions, v)
|
||||||
}
|
}
|
||||||
if len(matchedVersions) == 0 {
|
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
|
// Sort versions
|
||||||
|
@ -246,27 +269,61 @@ func (c *CheckoutSemVer) Checkout(ctx context.Context, path, url string, auth *g
|
||||||
|
|
||||||
w, err := repo.Worktree()
|
w, err := repo.Worktree()
|
||||||
if err != nil {
|
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{
|
err = w.Checkout(&extgogit.CheckoutOptions{
|
||||||
Branch: plumbing.NewTagReferenceName(t),
|
Branch: ref,
|
||||||
})
|
})
|
||||||
if err != nil {
|
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()
|
head, err := repo.Head()
|
||||||
if err != nil {
|
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)
|
||||||
}
|
}
|
||||||
|
cc, err := repo.CommitObject(head.Hash())
|
||||||
commit, err := repo.CommitObject(head.Hash())
|
|
||||||
if err != nil {
|
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 {
|
func recurseSubmodules(recurse bool) extgogit.SubmoduleRescursivity {
|
||||||
|
|
|
@ -18,37 +18,420 @@ package gogit
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"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) {
|
func TestCheckoutBranch_Checkout(t *testing.T) {
|
||||||
auth := &git.Auth{}
|
repo, path, err := initRepo()
|
||||||
tag := CheckoutTag{
|
if err != nil {
|
||||||
tag: "v1.7.0",
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(path)
|
||||||
|
|
||||||
|
firstCommit, err := commitFile(repo, "branch", "init", time.Now())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = createBranch(repo, "test"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
secondCommit, err := commitFile(repo, "branch", "second", time.Now())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
branch string
|
||||||
|
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")
|
tmpDir, _ := os.MkdirTemp("", "test")
|
||||||
defer os.RemoveAll(tmpDir)
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
cTag, _, err := tag.Checkout(context.TODO(), tmpDir, "https://github.com/projectcontour/contour", auth)
|
cc, err := branch.Checkout(context.TODO(), tmpDir, path, nil)
|
||||||
if err != nil {
|
if tt.expectedErr != "" {
|
||||||
t.Error(err)
|
g.Expect(err.Error()).To(ContainSubstring(tt.expectedErr))
|
||||||
|
g.Expect(cc).To(BeNil())
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
g.Expect(err).To(BeNil())
|
||||||
semVer := CheckoutSemVer{
|
g.Expect(cc.String()).To(Equal(tt.branch + "/" + tt.expectedCommit))
|
||||||
semVer: ">=1.0.0 <=1.7.0",
|
})
|
||||||
}
|
}
|
||||||
tmpDir2, _ := os.MkdirTemp("", "test")
|
}
|
||||||
defer os.RemoveAll(tmpDir2)
|
|
||||||
|
func TestCheckoutTag_Checkout(t *testing.T) {
|
||||||
cSemVer, _, err := semVer.Checkout(context.TODO(), tmpDir2, "https://github.com/projectcontour/contour", auth)
|
tests := []struct {
|
||||||
if err != nil {
|
name string
|
||||||
t.Error(err)
|
tag string
|
||||||
}
|
annotated bool
|
||||||
|
checkoutTag string
|
||||||
if cTag.Hash() != cSemVer.Hash() {
|
expectTag string
|
||||||
t.Errorf("expected semver hash %s, got %s", cTag.Hash(), cSemVer.Hash())
|
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 (
|
import (
|
||||||
"fmt"
|
"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/http"
|
||||||
"github.com/go-git/go-git/v5/plumbing/transport/ssh"
|
"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/pkg/ssh/knownhosts"
|
||||||
|
|
||||||
"github.com/fluxcd/source-controller/pkg/git"
|
"github.com/fluxcd/source-controller/pkg/git"
|
||||||
)
|
)
|
||||||
|
|
||||||
func AuthSecretStrategyForURL(URL string) (git.AuthSecretStrategy, error) {
|
// transportAuth constructs the transport.AuthMethod for the git.Transport of
|
||||||
u, err := url.Parse(URL)
|
// the given git.AuthOptions. It returns the result, or an error.
|
||||||
if err != nil {
|
func transportAuth(opts *git.AuthOptions) (transport.AuthMethod, error) {
|
||||||
return nil, fmt.Errorf("failed to parse URL to determine auth strategy: %w", err)
|
if opts == nil {
|
||||||
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
switch opts.Transport {
|
||||||
switch {
|
case git.HTTPS, git.HTTP:
|
||||||
case u.Scheme == "http", u.Scheme == "https":
|
return &http.BasicAuth{
|
||||||
return &BasicAuth{}, nil
|
Username: opts.Username,
|
||||||
case u.Scheme == "ssh":
|
Password: opts.Password,
|
||||||
return &PublicKeyAuth{user: u.User.Username()}, nil
|
}, nil
|
||||||
default:
|
case git.SSH:
|
||||||
return nil, fmt.Errorf("no auth secret strategy for scheme %s", u.Scheme)
|
if len(opts.Identity) > 0 {
|
||||||
}
|
pk, err := ssh.NewPublicKeys(opts.Username, opts.Identity, opts.Password)
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if len(opts.KnownHosts) > 0 {
|
||||||
callback, err := knownhosts.New(knownHosts)
|
callback, err := knownhosts.New(opts.KnownHosts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
pk.HostKeyCallback = callback
|
pk.HostKeyCallback = callback
|
||||||
|
}
|
||||||
return &git.Auth{AuthMethod: pk}, nil
|
return pk, nil
|
||||||
|
}
|
||||||
|
case "":
|
||||||
|
return nil, fmt.Errorf("no transport type set")
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown transport '%s'", opts.Transport)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// caBundle returns the CA bundle from the given git.AuthOptions.
|
||||||
|
func caBundle(opts *git.AuthOptions) []byte {
|
||||||
|
if opts == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return opts.CAFile
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,19 +17,21 @@ limitations under the License.
|
||||||
package gogit
|
package gogit
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"reflect"
|
"errors"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"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/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"
|
"github.com/fluxcd/source-controller/pkg/git"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// secretKeyFixture is a randomly generated password less
|
// privateKeyFixture is a randomly generated password less
|
||||||
// 512bit RSA private key.
|
// 512bit RSA private key.
|
||||||
secretKeyFixture string = `-----BEGIN RSA PRIVATE KEY-----
|
privateKeyFixture = `-----BEGIN RSA PRIVATE KEY-----
|
||||||
MIICXAIBAAKBgQCrakELAKxozvwJijQEggYlTvS1QTZx1DaBwOhW/4kRSuR21plu
|
MIICXAIBAAKBgQCrakELAKxozvwJijQEggYlTvS1QTZx1DaBwOhW/4kRSuR21plu
|
||||||
xuQeyuUiztoWeb9jgW7wjzG4j1PIJjdbsgjPIcIZ4PBY7JeEW+QRopfwuN8MHXNp
|
xuQeyuUiztoWeb9jgW7wjzG4j1PIJjdbsgjPIcIZ4PBY7JeEW+QRopfwuN8MHXNp
|
||||||
uTLgIHbkmhoOg5qBEcjzO/lEOOPpV0EmbObgqv3+wRmLJrgfzWl/cTtRewIDAQAB
|
uTLgIHbkmhoOg5qBEcjzO/lEOOPpV0EmbObgqv3+wRmLJrgfzWl/cTtRewIDAQAB
|
||||||
|
@ -45,9 +47,9 @@ Ngkgu4mLjc3RfenEhJECQAx8zjWUE6kHHPGAd9DfiAIQ4bChqnyS0Nwb9+Gd4hSE
|
||||||
P0Ah10mHiK/M0o3T8Eanwum0gbQHPnOwqZgsPkwXRqQ=
|
P0Ah10mHiK/M0o3T8Eanwum0gbQHPnOwqZgsPkwXRqQ=
|
||||||
-----END RSA PRIVATE KEY-----`
|
-----END RSA PRIVATE KEY-----`
|
||||||
|
|
||||||
// secretKeyFixture is a randomly generated
|
// privateKeyPassphraseFixture is a randomly generated
|
||||||
// 512bit RSA private key with password foobar.
|
// 512bit RSA private key with password foobar.
|
||||||
secretPassphraseFixture = `-----BEGIN RSA PRIVATE KEY-----
|
privateKeyPassphraseFixture = `-----BEGIN RSA PRIVATE KEY-----
|
||||||
Proc-Type: 4,ENCRYPTED
|
Proc-Type: 4,ENCRYPTED
|
||||||
DEK-Info: AES-256-CBC,0B016973B2A761D31E6B388D0F327C35
|
DEK-Info: AES-256-CBC,0B016973B2A761D31E6B388D0F327C35
|
||||||
|
|
||||||
|
@ -60,138 +62,145 @@ wGctSx4kHsZGhJv5qwKqqPEFPhUzph8D2tm2TABk8HJa5KJFDbGrcfvk2uODAoZr
|
||||||
MbcpIxCfl8oB09bWfY6tDQjyvwSYYo2Phdwm7kT92xc=
|
MbcpIxCfl8oB09bWfY6tDQjyvwSYYo2Phdwm7kT92xc=
|
||||||
-----END RSA PRIVATE KEY-----`
|
-----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
|
// knownHostsFixture is known_hosts fixture in the expected
|
||||||
// format.
|
// format.
|
||||||
knownHostsFixture string = `github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==`
|
knownHostsFixture string = `github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==`
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
func Test_transportAuth(t *testing.T) {
|
||||||
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) {
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
url string
|
opts *git.AuthOptions
|
||||||
want git.AuthSecretStrategy
|
wantFunc func(g *WithT, t transport.AuthMethod, opts *git.AuthOptions)
|
||||||
wantErr bool
|
wantErr error
|
||||||
}{
|
}{
|
||||||
{"HTTP", "http://git.example.com/org/repo.git", &BasicAuth{}, false},
|
{
|
||||||
{"HTTPS", "https://git.example.com/org/repo.git", &BasicAuth{}, false},
|
name: "HTTP basic auth",
|
||||||
{"SSH", "ssh://git.example.com:2222/org/repo.git", &PublicKeyAuth{}, false},
|
opts: &git.AuthOptions{
|
||||||
{"SSH with username", "ssh://example@git.example.com:2222/org/repo.git", &PublicKeyAuth{user: "example"}, false},
|
Transport: git.HTTP,
|
||||||
{"unsupported", "protocol://example.com", nil, true},
|
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 {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
got, err := AuthSecretStrategyForURL(tt.url)
|
g := NewWithT(t)
|
||||||
if (err != nil) != tt.wantErr {
|
|
||||||
t.Errorf("AuthSecretStrategyForURL() error = %v, wantErr %v", err, tt.wantErr)
|
got, err := transportAuth(tt.opts)
|
||||||
|
if tt.wantErr != nil {
|
||||||
|
g.Expect(err).To(Equal(tt.wantErr))
|
||||||
|
g.Expect(got).To(BeNil())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(got, tt.want) {
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
t.Errorf("AuthSecretStrategyForURL() got = %v, want %v", got, tt.want)
|
if tt.wantFunc != nil {
|
||||||
|
tt.wantFunc(g, got, tt.opts)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBasicAuthStrategy_Method(t *testing.T) {
|
func Test_caBundle(t *testing.T) {
|
||||||
tests := []struct {
|
g := NewWithT(t)
|
||||||
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 TestPublicKeyStrategy_Method(t *testing.T) {
|
g.Expect(caBundle(&git.AuthOptions{CAFile: []byte("foo")})).To(BeEquivalentTo("foo"))
|
||||||
tests := []struct {
|
g.Expect(caBundle(nil)).To(BeNil())
|
||||||
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
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,142 +24,135 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Masterminds/semver/v3"
|
"github.com/Masterminds/semver/v3"
|
||||||
|
"github.com/go-logr/logr"
|
||||||
git2go "github.com/libgit2/git2go/v31"
|
git2go "github.com/libgit2/git2go/v31"
|
||||||
|
|
||||||
"github.com/fluxcd/pkg/gitutil"
|
"github.com/fluxcd/pkg/gitutil"
|
||||||
"github.com/fluxcd/pkg/version"
|
"github.com/fluxcd/pkg/version"
|
||||||
|
|
||||||
sourcev1 "github.com/fluxcd/source-controller/api/v1beta1"
|
|
||||||
"github.com/fluxcd/source-controller/pkg/git"
|
"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
|
||||||
switch {
|
// git.CheckoutOptions.
|
||||||
case ref == nil:
|
func CheckoutStrategyForOptions(ctx context.Context, opt git.CheckoutOptions) git.CheckoutStrategy {
|
||||||
return &CheckoutBranch{branch: git.DefaultBranch}
|
if opt.RecurseSubmodules {
|
||||||
case ref.SemVer != "":
|
logr.FromContextOrDiscard(ctx).Info("git submodule recursion not supported by '%s'", Implementation)
|
||||||
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
|
switch {
|
||||||
case ref.Branch != "":
|
case opt.Commit != "":
|
||||||
return &CheckoutBranch{branch: ref.Branch}
|
return &CheckoutCommit{Commit: opt.Commit}
|
||||||
|
case opt.SemVer != "":
|
||||||
|
return &CheckoutSemVer{SemVer: opt.SemVer}
|
||||||
|
case opt.Tag != "":
|
||||||
|
return &CheckoutTag{Tag: opt.Tag}
|
||||||
default:
|
default:
|
||||||
return &CheckoutBranch{branch: git.DefaultBranch}
|
branch := opt.Branch
|
||||||
|
if branch == "" {
|
||||||
|
branch = git.DefaultBranch
|
||||||
|
}
|
||||||
|
return &CheckoutBranch{Branch: branch}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type CheckoutBranch struct {
|
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{
|
repo, err := git2go.Clone(url, path, &git2go.CloneOptions{
|
||||||
FetchOptions: &git2go.FetchOptions{
|
FetchOptions: &git2go.FetchOptions{
|
||||||
DownloadTags: git2go.DownloadTagsNone,
|
DownloadTags: git2go.DownloadTagsNone,
|
||||||
RemoteCallbacks: git2go.RemoteCallbacks{
|
RemoteCallbacks: RemoteCallbacks(opts),
|
||||||
CredentialsCallback: auth.CredCallback,
|
|
||||||
CertificateCheckCallback: auth.CertCallback,
|
|
||||||
},
|
},
|
||||||
},
|
CheckoutBranch: c.Branch,
|
||||||
CheckoutBranch: c.branch,
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
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()
|
head, err := repo.Head()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", fmt.Errorf("git resolve HEAD error: %w", err)
|
return nil, fmt.Errorf("git resolve HEAD error: %w", err)
|
||||||
}
|
}
|
||||||
defer head.Free()
|
defer head.Free()
|
||||||
commit, err := repo.LookupCommit(head.Target())
|
cc, err := repo.LookupCommit(head.Target())
|
||||||
if err != nil {
|
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 {
|
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{
|
repo, err := git2go.Clone(url, path, &git2go.CloneOptions{
|
||||||
FetchOptions: &git2go.FetchOptions{
|
FetchOptions: &git2go.FetchOptions{
|
||||||
DownloadTags: git2go.DownloadTagsAll,
|
DownloadTags: git2go.DownloadTagsAll,
|
||||||
RemoteCallbacks: git2go.RemoteCallbacks{
|
RemoteCallbacks: RemoteCallbacks(opts),
|
||||||
CredentialsCallback: auth.CredCallback,
|
|
||||||
CertificateCheckCallback: auth.CertCallback,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
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 {
|
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 {
|
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{
|
repo, err := git2go.Clone(url, path, &git2go.CloneOptions{
|
||||||
FetchOptions: &git2go.FetchOptions{
|
FetchOptions: &git2go.FetchOptions{
|
||||||
DownloadTags: git2go.DownloadTagsNone,
|
DownloadTags: git2go.DownloadTagsNone,
|
||||||
RemoteCallbacks: git2go.RemoteCallbacks{
|
RemoteCallbacks: RemoteCallbacks(opts),
|
||||||
CredentialsCallback: auth.CredCallback,
|
|
||||||
CertificateCheckCallback: auth.CertCallback,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
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()
|
||||||
oid, err := git2go.NewOid(c.commit)
|
oid, err := git2go.NewOid(c.Commit)
|
||||||
if err != nil {
|
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 {
|
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 {
|
type CheckoutSemVer struct {
|
||||||
semVer string
|
SemVer string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CheckoutSemVer) Checkout(ctx context.Context, path, url string, auth *git.Auth) (git.Commit, string, error) {
|
func (c *CheckoutSemVer) Checkout(ctx context.Context, path, url string, opts *git.AuthOptions) (*git.Commit, error) {
|
||||||
verConstraint, err := semver.NewConstraint(c.semVer)
|
verConstraint, err := semver.NewConstraint(c.SemVer)
|
||||||
if err != nil {
|
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{
|
repo, err := git2go.Clone(url, path, &git2go.CloneOptions{
|
||||||
FetchOptions: &git2go.FetchOptions{
|
FetchOptions: &git2go.FetchOptions{
|
||||||
DownloadTags: git2go.DownloadTagsAll,
|
DownloadTags: git2go.DownloadTagsAll,
|
||||||
RemoteCallbacks: git2go.RemoteCallbacks{
|
RemoteCallbacks: RemoteCallbacks(opts),
|
||||||
CredentialsCallback: auth.CredCallback,
|
|
||||||
CertificateCheckCallback: auth.CertCallback,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
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)
|
tags := make(map[string]string)
|
||||||
tagTimestamps := make(map[string]time.Time)
|
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
|
tags[t.Name()] = name
|
||||||
return nil
|
return nil
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return nil, "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var matchedVersions semver.Collection
|
var matchedVersions semver.Collection
|
||||||
|
@ -209,7 +202,7 @@ func (c *CheckoutSemVer) Checkout(ctx context.Context, path, url string, auth *g
|
||||||
matchedVersions = append(matchedVersions, v)
|
matchedVersions = append(matchedVersions, v)
|
||||||
}
|
}
|
||||||
if len(matchedVersions) == 0 {
|
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
|
// Sort versions
|
||||||
|
@ -230,8 +223,12 @@ func (c *CheckoutSemVer) Checkout(ctx context.Context, path, url string, auth *g
|
||||||
v := matchedVersions[len(matchedVersions)-1]
|
v := matchedVersions[len(matchedVersions)-1]
|
||||||
t := v.Original()
|
t := v.Original()
|
||||||
|
|
||||||
commit, err := checkoutDetachedDwim(repo, t)
|
cc, err := checkoutDetachedDwim(repo, t)
|
||||||
return &Commit{commit}, fmt.Sprintf("%s/%s", t, commit.Id().String()), nil
|
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
|
// 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)
|
return nil, fmt.Errorf("could not get commit for ref '%s': %w", ref.Name(), err)
|
||||||
}
|
}
|
||||||
defer c.Free()
|
defer c.Free()
|
||||||
commit, err := c.AsCommit()
|
cc, err := c.AsCommit()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not get commit object for ref '%s': %w", ref.Name(), err)
|
return nil, fmt.Errorf("could not get commit object for ref '%s': %w", ref.Name(), err)
|
||||||
}
|
}
|
||||||
defer commit.Free()
|
defer cc.Free()
|
||||||
return checkoutDetachedHEAD(repo, commit.Id())
|
return checkoutDetachedHEAD(repo, cc.Id())
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkoutDetachedHEAD attempts to perform a detached HEAD checkout for the given commit.
|
// checkoutDetachedHEAD attempts to perform a detached HEAD checkout for the given commit.
|
||||||
func checkoutDetachedHEAD(repo *git2go.Repository, oid *git2go.Oid) (*git2go.Commit, error) {
|
func checkoutDetachedHEAD(repo *git2go.Repository, oid *git2go.Oid) (*git2go.Commit, error) {
|
||||||
commit, err := repo.LookupCommit(oid)
|
cc, err := repo.LookupCommit(oid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("git commit '%s' not found: %w", oid.String(), err)
|
return nil, fmt.Errorf("git commit '%s' not found: %w", oid.String(), err)
|
||||||
}
|
}
|
||||||
if err = repo.SetHeadDetached(commit.Id()); err != nil {
|
if err = repo.SetHeadDetached(cc.Id()); err != nil {
|
||||||
commit.Free()
|
cc.Free()
|
||||||
return nil, fmt.Errorf("could not detach HEAD at '%s': %w", oid.String(), err)
|
return nil, fmt.Errorf("could not detach HEAD at '%s': %w", oid.String(), err)
|
||||||
}
|
}
|
||||||
if err = repo.CheckoutHead(&git2go.CheckoutOptions{
|
if err = repo.CheckoutHead(&git2go.CheckoutOptions{
|
||||||
Strategy: git2go.CheckoutForce,
|
Strategy: git2go.CheckoutForce,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
commit.Free()
|
cc.Free()
|
||||||
return nil, fmt.Errorf("git checkout error: %w", err)
|
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.
|
// 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
|
return nil, err
|
||||||
}
|
}
|
||||||
defer head.Free()
|
defer head.Free()
|
||||||
|
c, err := repo.LookupCommit(head.Target())
|
||||||
commit, err := repo.LookupCommit(head.Target())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
return c, nil
|
||||||
return commit, 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"
|
git2go "github.com/libgit2/git2go/v31"
|
||||||
. "github.com/onsi/gomega"
|
. "github.com/onsi/gomega"
|
||||||
|
|
||||||
"github.com/fluxcd/source-controller/pkg/git"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCheckoutBranch_Checkout(t *testing.T) {
|
func TestCheckoutBranch_Checkout(t *testing.T) {
|
||||||
|
@ -79,19 +77,20 @@ func TestCheckoutBranch_Checkout(t *testing.T) {
|
||||||
g := NewWithT(t)
|
g := NewWithT(t)
|
||||||
|
|
||||||
branch := CheckoutBranch{
|
branch := CheckoutBranch{
|
||||||
branch: tt.branch,
|
Branch: tt.branch,
|
||||||
}
|
}
|
||||||
tmpDir, _ := os.MkdirTemp("", "test")
|
tmpDir, _ := os.MkdirTemp("", "test")
|
||||||
defer os.RemoveAll(tmpDir)
|
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 != "" {
|
if tt.expectedErr != "" {
|
||||||
|
g.Expect(err).To(HaveOccurred())
|
||||||
g.Expect(err.Error()).To(ContainSubstring(tt.expectedErr))
|
g.Expect(err.Error()).To(ContainSubstring(tt.expectedErr))
|
||||||
g.Expect(ref).To(BeEmpty())
|
g.Expect(cc).To(BeNil())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
g.Expect(ref).To(Equal(tt.branch + "/" + tt.expectedCommit))
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
g.Expect(err).To(BeNil())
|
g.Expect(cc.String()).To(Equal(tt.branch + "/" + tt.expectedCommit))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -149,22 +148,23 @@ func TestCheckoutTag_Checkout(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
tag := CheckoutTag{
|
tag := CheckoutTag{
|
||||||
tag: tt.checkoutTag,
|
Tag: tt.checkoutTag,
|
||||||
}
|
}
|
||||||
tmpDir, _ := os.MkdirTemp("", "test")
|
tmpDir, _ := os.MkdirTemp("", "test")
|
||||||
defer os.RemoveAll(tmpDir)
|
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 != "" {
|
if tt.expectErr != "" {
|
||||||
g.Expect(err.Error()).To(Equal(tt.expectErr))
|
g.Expect(err).To(HaveOccurred())
|
||||||
g.Expect(ref).To(BeEmpty())
|
g.Expect(err.Error()).To(ContainSubstring(tt.expectErr))
|
||||||
|
g.Expect(cc).To(BeNil())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if tt.expectTag != "" {
|
|
||||||
g.Expect(ref).To(Equal(tt.expectTag + "/" + commit.Id().String()))
|
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(filepath.Join(tmpDir, "tag")).To(BeARegularFile())
|
||||||
g.Expect(os.ReadFile(filepath.Join(tmpDir, "tag"))).To(BeEquivalentTo(tt.tag))
|
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 := CheckoutCommit{
|
||||||
commit: c.String(),
|
Commit: c.String(),
|
||||||
branch: "main",
|
|
||||||
}
|
}
|
||||||
tmpDir, _ := os.MkdirTemp("", "git2go")
|
tmpDir, _ := os.MkdirTemp("", "git2go")
|
||||||
defer os.RemoveAll(tmpDir)
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
_, ref, err := commit.Checkout(context.TODO(), tmpDir, repo.Path(), &git.Auth{})
|
cc, err := commit.Checkout(context.TODO(), tmpDir, repo.Path(), nil)
|
||||||
g.Expect(err).To(BeNil())
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
g.Expect(ref).To(Equal("main/" + c.String()))
|
g.Expect(cc).ToNot(BeNil())
|
||||||
|
g.Expect(cc.String()).To(Equal("HEAD/" + c.String()))
|
||||||
g.Expect(filepath.Join(tmpDir, "commit")).To(BeARegularFile())
|
g.Expect(filepath.Join(tmpDir, "commit")).To(BeARegularFile())
|
||||||
g.Expect(os.ReadFile(filepath.Join(tmpDir, "commit"))).To(BeEquivalentTo("init"))
|
g.Expect(os.ReadFile(filepath.Join(tmpDir, "commit"))).To(BeEquivalentTo("init"))
|
||||||
|
|
||||||
commit = CheckoutCommit{
|
commit = CheckoutCommit{
|
||||||
commit: "4dc3185c5fc94eb75048376edeb44571cece25f4",
|
Commit: "4dc3185c5fc94eb75048376edeb44571cece25f4",
|
||||||
}
|
}
|
||||||
tmpDir2, _ := os.MkdirTemp("", "git2go")
|
tmpDir2, _ := os.MkdirTemp("", "git2go")
|
||||||
defer os.RemoveAll(tmpDir)
|
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(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) {
|
func TestCheckoutTagSemVer_Checkout(t *testing.T) {
|
||||||
|
@ -307,19 +308,20 @@ func TestCheckoutTagSemVer_Checkout(t *testing.T) {
|
||||||
g := NewWithT(t)
|
g := NewWithT(t)
|
||||||
|
|
||||||
semVer := CheckoutSemVer{
|
semVer := CheckoutSemVer{
|
||||||
semVer: tt.constraint,
|
SemVer: tt.constraint,
|
||||||
}
|
}
|
||||||
tmpDir, _ := os.MkdirTemp("", "test")
|
tmpDir, _ := os.MkdirTemp("", "test")
|
||||||
defer os.RemoveAll(tmpDir)
|
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 {
|
if tt.expectErr != nil {
|
||||||
g.Expect(err).To(Equal(tt.expectErr))
|
g.Expect(err).To(Equal(tt.expectErr))
|
||||||
g.Expect(ref).To(BeEmpty())
|
g.Expect(cc).To(BeNil())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
g.Expect(err).ToNot(HaveOccurred())
|
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(filepath.Join(tmpDir, "tag")).To(BeARegularFile())
|
||||||
g.Expect(os.ReadFile(filepath.Join(tmpDir, "tag"))).To(BeEquivalentTo(tt.expectTag))
|
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()
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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) {
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
if annotated {
|
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)
|
return repo.Tags.CreateLightweight(tag, commit, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func signature(time time.Time) *git2go.Signature {
|
func mockSignature(time time.Time) *git2go.Signature {
|
||||||
return &git2go.Signature{
|
return &git2go.Signature{
|
||||||
Name: "Jane Doe",
|
Name: "Jane Doe",
|
||||||
Email: "author@example.com",
|
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"
|
"crypto/x509"
|
||||||
"fmt"
|
"fmt"
|
||||||
"hash"
|
"hash"
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/url"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
git2go "github.com/libgit2/git2go/v31"
|
git2go "github.com/libgit2/git2go/v31"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
"golang.org/x/crypto/ssh/knownhosts"
|
"golang.org/x/crypto/ssh/knownhosts"
|
||||||
corev1 "k8s.io/api/core/v1"
|
|
||||||
|
|
||||||
"github.com/fluxcd/source-controller/pkg/git"
|
"github.com/fluxcd/source-controller/pkg/git"
|
||||||
)
|
)
|
||||||
|
|
||||||
func AuthSecretStrategyForURL(URL string) (git.AuthSecretStrategy, error) {
|
var (
|
||||||
u, err := url.Parse(URL)
|
now = time.Now
|
||||||
if err != nil {
|
)
|
||||||
return nil, fmt.Errorf("failed to parse URL to determine auth strategy: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
// RemoteCallbacks constructs RemoteCallbacks with credentialsCallback and
|
||||||
case u.Scheme == "http", u.Scheme == "https":
|
// certificateCallback, and the given options if the given opts is not nil.
|
||||||
return &BasicAuth{}, nil
|
func RemoteCallbacks(opts *git.AuthOptions) git2go.RemoteCallbacks {
|
||||||
case u.Scheme == "ssh":
|
if opts != nil {
|
||||||
return &PublicKeyAuth{user: u.User.Username(), host: u.Host}, nil
|
return git2go.RemoteCallbacks{
|
||||||
default:
|
CredentialsCallback: credentialsCallback(opts),
|
||||||
return nil, fmt.Errorf("no auth secret strategy for scheme %s", u.Scheme)
|
CertificateCheckCallback: certificateCallback(opts),
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return git2go.RemoteCallbacks{}
|
||||||
}
|
}
|
||||||
|
|
||||||
type BasicAuth struct{}
|
// credentialsCallback constructs CredentialsCallbacks with the given options
|
||||||
|
// for git.Transport, and returns the result.
|
||||||
func (s *BasicAuth) Method(secret corev1.Secret) (*git.Auth, error) {
|
func credentialsCallback(opts *git.AuthOptions) git2go.CredentialsCallback {
|
||||||
var credCallback git2go.CredentialsCallback
|
return func(url string, username string, allowedTypes git2go.CredentialType) (*git2go.Credential, error) {
|
||||||
var username string
|
if allowedTypes&(git2go.CredentialTypeSSHKey|git2go.CredentialTypeSSHCustom|git2go.CredentialTypeSSHMemory) != 0 {
|
||||||
if d, ok := secret.Data["username"]; ok {
|
var (
|
||||||
username = string(d)
|
signer ssh.Signer
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if opts.Password != "" {
|
||||||
|
signer, err = ssh.ParsePrivateKeyWithPassphrase(opts.Identity, []byte(opts.Password))
|
||||||
|
} else {
|
||||||
|
signer, err = ssh.ParsePrivateKey(opts.Identity)
|
||||||
}
|
}
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return cred, nil
|
return git2go.NewCredentialSSHKeyFromSigner(opts.Username, signer)
|
||||||
}
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var certCallback git2go.CertificateCheckCallback
|
// certificateCallback constructs CertificateCallback with the given options
|
||||||
if caFile, ok := secret.Data[git.CAFile]; ok {
|
// for git.Transport if the given opts is not nil, and returns the result.
|
||||||
certCallback = func(cert *git2go.Certificate, valid bool, hostname string) git2go.ErrorCode {
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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()
|
roots := x509.NewCertPool()
|
||||||
ok := roots.AppendCertsFromPEM(caFile)
|
if ok := roots.AppendCertsFromPEM(caBundle); !ok {
|
||||||
if !ok {
|
|
||||||
return git2go.ErrorCodeCertificate
|
return git2go.ErrorCodeCertificate
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := x509.VerifyOptions{
|
opts := x509.VerifyOptions{
|
||||||
Roots: roots,
|
Roots: roots,
|
||||||
DNSName: hostname,
|
DNSName: hostname,
|
||||||
|
CurrentTime: now(),
|
||||||
}
|
}
|
||||||
_, err := cert.X509.Verify(opts)
|
if _, err := cert.X509.Verify(opts); err != nil {
|
||||||
if err != nil {
|
|
||||||
return git2go.ErrorCodeCertificate
|
return git2go.ErrorCodeCertificate
|
||||||
}
|
}
|
||||||
return git2go.ErrorCodeOK
|
return git2go.ErrorCodeOK
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return &git.Auth{CredCallback: credCallback, CertCallback: certCallback}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PublicKeyAuth struct {
|
// knownHostCallback returns a CertificateCheckCallback that verifies
|
||||||
user string
|
// the key of Git server against the given host and known_hosts for
|
||||||
host string
|
// git.SSH Transports.
|
||||||
}
|
func knownHostsCallback(host string, knownHosts []byte) git2go.CertificateCheckCallback {
|
||||||
|
return func(cert *git2go.Certificate, valid bool, hostname string) git2go.ErrorCode {
|
||||||
func (s *PublicKeyAuth) Method(secret corev1.Secret) (*git.Auth, error) {
|
kh, err := parseKnownHosts(string(knownHosts))
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
kk, err := parseKnownHosts(string(knownHosts))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return git2go.ErrorCodeCertificate
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
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
|
// First, attempt to split the configured host and port to validate
|
||||||
// the port-less hostname given to the callback.
|
// the port-less hostname given to the callback.
|
||||||
host, _, err := net.SplitHostPort(s.host)
|
h, _, err := net.SplitHostPort(host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// SplitHostPort returns an error if the host is missing
|
// SplitHostPort returns an error if the host is missing
|
||||||
// a port, assume the host has no port.
|
// a port, assume the host has no port.
|
||||||
host = s.host
|
h = host
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the configured host matches the hostname given to
|
// Check if the configured host matches the hostname given to
|
||||||
// the callback.
|
// the callback.
|
||||||
if host != hostname {
|
if h != hostname {
|
||||||
return git2go.ErrorCodeUser
|
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
|
// given to the callback match. Use the configured host (that
|
||||||
// includes the port), and normalize it, so we can check if there
|
// includes the port), and normalize it, so we can check if there
|
||||||
// is an entry for the hostname _and_ port.
|
// is an entry for the hostname _and_ port.
|
||||||
host = knownhosts.Normalize(s.host)
|
h = knownhosts.Normalize(host)
|
||||||
for _, k := range kk {
|
for _, k := range kh {
|
||||||
if k.matches(host, cert.Hostkey) {
|
if k.matches(h, cert.Hostkey) {
|
||||||
return git2go.ErrorCodeOK
|
return git2go.ErrorCodeOK
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return git2go.ErrorCodeCertificate
|
return git2go.ErrorCodeCertificate
|
||||||
}
|
}
|
||||||
|
|
||||||
return &git.Auth{CredCallback: credCallback, CertCallback: certCallback}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type knownKey struct {
|
type knownKey struct {
|
||||||
|
@ -187,6 +169,11 @@ func parseKnownHosts(s string) ([]knownKey, error) {
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
_, hosts, pubKey, _, _, err := ssh.ParseKnownHosts(scanner.Bytes())
|
_, hosts, pubKey, _, _, err := ssh.ParseKnownHosts(scanner.Bytes())
|
||||||
if err != nil {
|
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
|
return []knownKey{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -234,6 +221,5 @@ func containsHost(hosts []string, host string) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,163 +17,241 @@ limitations under the License.
|
||||||
package libgit2
|
package libgit2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/x509"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"reflect"
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
git2go "github.com/libgit2/git2go/v31"
|
git2go "github.com/libgit2/git2go/v31"
|
||||||
corev1 "k8s.io/api/core/v1"
|
. "github.com/onsi/gomega"
|
||||||
|
|
||||||
"github.com/fluxcd/source-controller/pkg/git"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// secretKeyFixture is a randomly generated password less
|
geoTrustRootFixture = `-----BEGIN CERTIFICATE-----
|
||||||
// 512bit RSA private key.
|
MIIDVDCCAjygAwIBAgIDAjRWMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT
|
||||||
secretKeyFixture string = `-----BEGIN RSA PRIVATE KEY-----
|
MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i
|
||||||
MIICXAIBAAKBgQCrakELAKxozvwJijQEggYlTvS1QTZx1DaBwOhW/4kRSuR21plu
|
YWwgQ0EwHhcNMDIwNTIxMDQwMDAwWhcNMjIwNTIxMDQwMDAwWjBCMQswCQYDVQQG
|
||||||
xuQeyuUiztoWeb9jgW7wjzG4j1PIJjdbsgjPIcIZ4PBY7JeEW+QRopfwuN8MHXNp
|
EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEbMBkGA1UEAxMSR2VvVHJ1c3Qg
|
||||||
uTLgIHbkmhoOg5qBEcjzO/lEOOPpV0EmbObgqv3+wRmLJrgfzWl/cTtRewIDAQAB
|
R2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2swYYzD9
|
||||||
AoGAawKFImpEN5Xn78iwWpQVZBsbV0AjzgHuGSiloxIZrorzf2DPHkHZzYNaclVx
|
9BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9mOSm9BXiLnTjoBbdq
|
||||||
/o/4tBTsfg7WumH3qr541qyZJDgU7iRMABwmx0v1vm2wQiX7NJzLzH2E9vlMC3mw
|
fnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIuT8rxh0PBFpVXLVDv
|
||||||
d8S99g9EqRuNH98XX8su34B9WGRPqiKvEm0RW8Hideo2/KkCQQDbs6rHcriKQyPB
|
iS2Aelet8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6cJmTM386DGXHKTubU
|
||||||
paidHZAfguu0eVbyHT2EgLgRboWE+tEAqFEW2ycqNL3VPz9fRvwexbB6rpOcPpQJ
|
1XupGc1V3sjs0l44U+VcT4wt/lAjNvxm5suOpDkZALeVAjmRCw7+OC7RHQWa9k0+
|
||||||
DEL4XB2XAkEAx7xJz8YlCQ2H38xggK8R8EUXF9Zhb0fqMJHMNmao1HCHVMtbsa8I
|
bw8HHa8sHo9gOeL6NlMTOdReJivbPagUvTLrGAMoUgRx5aszPeE4uwc2hGKceeoW
|
||||||
jR2EGyQ4CaIqNG5tdWukXQSJrPYDRWNvvQJAZX3rP7XUYDLB2twvN12HzbbKMhX3
|
MPRfwCvocWvk+QIDAQABo1MwUTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTA
|
||||||
v2MYnxRjc9INpi/Dyzz2MMvOnOW+aDuOh/If2AtVCmeJUx1pf4CFk3viQwJBAKyC
|
ephojYn7qwVkDBF9qn1luMrMTjAfBgNVHSMEGDAWgBTAephojYn7qwVkDBF9qn1l
|
||||||
t824+evjv+NQBlme3AOF6PgxtV4D4wWoJ5Uk/dTejER0j/Hbl6sqPxuiILRRV9qJ
|
uMrMTjANBgkqhkiG9w0BAQUFAAOCAQEANeMpauUvXVSOKVCUn5kaFOSPeCpilKIn
|
||||||
Ngkgu4mLjc3RfenEhJECQAx8zjWUE6kHHPGAd9DfiAIQ4bChqnyS0Nwb9+Gd4hSE
|
Z57QzxpeR+nBsqTP3UEaBU6bS+5Kb1VSsyShNwrrZHYqLizz/Tt1kL/6cdjHPTfS
|
||||||
P0Ah10mHiK/M0o3T8Eanwum0gbQHPnOwqZgsPkwXRqQ=
|
tQWVYrmm3ok9Nns4d0iXrKYgjy6myQzCsplFAMfOEVEiIuCl6rYVSAlk6l5PdPcF
|
||||||
-----END RSA PRIVATE KEY-----`
|
PseKUgzbFbS9bZvlxrFUaKnjaZC2mqUPuLk/IH2uSrW4nOQdtqvmlKXBx4Ot2/Un
|
||||||
|
hw4EbNX/3aBd7YdStysVAq45pmp06drE57xNNB6pXE0zX5IJL4hmXXeXxx12E6nV
|
||||||
|
5fEWCRE11azbJHFwLJhWC9kXtNHjUStedejV0NxPNO3CBWaAocvmMw==
|
||||||
|
-----END CERTIFICATE-----`
|
||||||
|
|
||||||
// secretKeyFixture is a randomly generated
|
giag2IntermediateFixture = `-----BEGIN CERTIFICATE-----
|
||||||
// 512bit RSA private key with password foobar.
|
MIIEBDCCAuygAwIBAgIDAjppMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT
|
||||||
secretPassphraseFixture = `-----BEGIN RSA PRIVATE KEY-----
|
MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i
|
||||||
Proc-Type: 4,ENCRYPTED
|
YWwgQ0EwHhcNMTMwNDA1MTUxNTU1WhcNMTUwNDA0MTUxNTU1WjBJMQswCQYDVQQG
|
||||||
DEK-Info: AES-256-CBC,0B016973B2A761D31E6B388D0F327C35
|
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
|
googleLeafFixture = `-----BEGIN CERTIFICATE-----
|
||||||
HGCqPAeMO/ofGDGtDULWW6iqDUFRu5gPgEVSCnnbqoHNU325WHhXdhejVAItwObC
|
MIIEdjCCA16gAwIBAgIIcR5k4dkoe04wDQYJKoZIhvcNAQEFBQAwSTELMAkGA1UE
|
||||||
IpL/zYfs2+gDHXct/n9FJ/9D/EGXZihwPqYaK8GQSfZAxz0QjLuh0wU1qpbm3y3N
|
BhMCVVMxEzARBgNVBAoTCkdvb2dsZSBJbmMxJTAjBgNVBAMTHEdvb2dsZSBJbnRl
|
||||||
q+o9FLv3b2Ys/tCJOUsYVQOYLSrZEI77y1ii3nWgQ8lXiTJbBUKzuq4f1YWeO8Ah
|
cm5ldCBBdXRob3JpdHkgRzIwHhcNMTQwMzEyMDkzODMwWhcNMTQwNjEwMDAwMDAw
|
||||||
RZbdhTa57AF5lUaRtL7Nrm3HJUrK1alBbU7HHyjeW4Q4n/D3fiRDC1Mh2Bi4EOOn
|
WjBoMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwN
|
||||||
wGctSx4kHsZGhJv5qwKqqPEFPhUzph8D2tm2TABk8HJa5KJFDbGrcfvk2uODAoZr
|
TW91bnRhaW4gVmlldzETMBEGA1UECgwKR29vZ2xlIEluYzEXMBUGA1UEAwwOd3d3
|
||||||
MbcpIxCfl8oB09bWfY6tDQjyvwSYYo2Phdwm7kT92xc=
|
Lmdvb2dsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4zYCe
|
||||||
-----END RSA PRIVATE KEY-----`
|
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==`
|
knownHostsFixture string = `github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==`
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
func Test_x509Callback(t *testing.T) {
|
||||||
basicAuthSecretFixture = corev1.Secret{
|
now = func() time.Time { return time.Unix(1395785200, 0) }
|
||||||
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 TestAuthSecretStrategyForURL(t *testing.T) {
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
url string
|
certificate string
|
||||||
want git.AuthSecretStrategy
|
host string
|
||||||
wantErr bool
|
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},
|
name: "Valid certificate authority bundle",
|
||||||
{"SSH", "ssh://git.example.com:2222/org/repo.git", &PublicKeyAuth{host: "git.example.com:2222"}, false},
|
certificate: googleLeafFixture,
|
||||||
{"SSH with username", "ssh://example@git.example.com:2222/org/repo.git", &PublicKeyAuth{user: "example", host: "git.example.com:2222"}, false},
|
host: "www.google.com",
|
||||||
{"unsupported", "protocol://example.com", nil, true},
|
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 {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
got, err := AuthSecretStrategyForURL(tt.url)
|
g := NewWithT(t)
|
||||||
if (err != nil) != tt.wantErr {
|
|
||||||
t.Errorf("AuthSecretStrategyForURL() error = %v, wantErr %v", err, tt.wantErr)
|
cert := &git2go.Certificate{}
|
||||||
return
|
if tt.certificate != "" {
|
||||||
}
|
x509Cert, err := certificateFromPEM(tt.certificate)
|
||||||
if !reflect.DeepEqual(got, tt.want) {
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
t.Errorf("AuthSecretStrategyForURL() got = %v, want %v", got, tt.want)
|
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 {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
secret corev1.Secret
|
host string
|
||||||
modify func(secret *corev1.Secret)
|
expectedHost string
|
||||||
wantErr bool
|
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 {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
secret := tt.secret.DeepCopy()
|
g := NewWithT(t)
|
||||||
if tt.modify != nil {
|
|
||||||
tt.modify(secret)
|
cert := &git2go.Certificate{Hostkey: tt.hostkey}
|
||||||
}
|
callback := knownHostsCallback(tt.expectedHost, tt.knownHosts)
|
||||||
s := &BasicAuth{}
|
g.Expect(callback(cert, false, tt.host)).To(Equal(tt.want))
|
||||||
_, err := s.Method(*secret)
|
|
||||||
if (err != nil) != tt.wantErr {
|
|
||||||
t.Errorf("Method() error = %v, wantErr %v", err, tt.wantErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPublicKeyStrategy_Method(t *testing.T) {
|
func Test_parseKnownHosts_matches(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) {
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
hostkey git2go.HostkeyCertificate
|
hostkey git2go.HostkeyCertificate
|
||||||
|
@ -189,16 +267,80 @@ func TestKnownKeyHash(t *testing.T) {
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
g := NewWithT(t)
|
||||||
|
|
||||||
knownKeys, err := parseKnownHosts(knownHostsFixture)
|
knownKeys, err := parseKnownHosts(knownHostsFixture)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
matches := knownKeys[0].matches("github.com", tt.hostkey)
|
matches := knownKeys[0].matches("github.com", tt.hostkey)
|
||||||
if matches != tt.wantMatches {
|
g.Expect(matches).To(Equal(tt.wantMatches))
|
||||||
t.Errorf("Method() matches = %v, wantMatches %v", matches, tt.wantMatches)
|
})
|
||||||
return
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
func md5Fingerprint(in string) [16]byte {
|
||||||
var out [16]byte
|
var out [16]byte
|
||||||
copy(out[:], []byte(in))
|
copy(out[:], in)
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -229,3 +371,11 @@ func sha256Fingerprint(in string) [32]byte {
|
||||||
copy(out[:], d)
|
copy(out[:], d)
|
||||||
return out
|
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
|
package strategy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
sourcev1 "github.com/fluxcd/source-controller/api/v1beta1"
|
|
||||||
"github.com/fluxcd/source-controller/pkg/git"
|
"github.com/fluxcd/source-controller/pkg/git"
|
||||||
"github.com/fluxcd/source-controller/pkg/git/gogit"
|
"github.com/fluxcd/source-controller/pkg/git/gogit"
|
||||||
"github.com/fluxcd/source-controller/pkg/git/libgit2"
|
"github.com/fluxcd/source-controller/pkg/git/libgit2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func CheckoutStrategyForRef(ref *sourcev1.GitRepositoryRef, opt git.CheckoutOptions) (git.CheckoutStrategy, error) {
|
// CheckoutStrategyForImplementation returns the CheckoutStrategy for the given
|
||||||
switch opt.GitImplementation {
|
// git.Implementation and git.CheckoutOptions.
|
||||||
case sourcev1.GoGitImplementation:
|
func CheckoutStrategyForImplementation(ctx context.Context, impl git.Implementation, opts git.CheckoutOptions) (git.CheckoutStrategy, error) {
|
||||||
return gogit.CheckoutStrategyForRef(ref, opt), nil
|
switch impl {
|
||||||
case sourcev1.LibGit2Implementation:
|
case gogit.Implementation:
|
||||||
return libgit2.CheckoutStrategyForRef(ref, opt), nil
|
return gogit.CheckoutStrategyForOptions(ctx, opts), nil
|
||||||
|
case libgit2.Implementation:
|
||||||
|
return libgit2.CheckoutStrategyForOptions(ctx, opts), nil
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("invalid Git implementation %s", opt.GitImplementation)
|
return nil, fmt.Errorf("unsupported Git implementation '%s'", impl)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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