gitjob/integrationtests/git/git_test.go

470 lines
12 KiB
Go

package integrationtests
import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"time"
gogit "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/go-git/go-git/v5/plumbing/transport"
httpgit "github.com/go-git/go-git/v5/plumbing/transport/http"
"github.com/gogits/go-gogs-client"
cp "github.com/otiai10/copy"
gitjobv1 "github.com/rancher/gitjob/pkg/apis/gitjob.cattle.io/v1"
"github.com/rancher/gitjob/pkg/git"
"github.com/stretchr/testify/require"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
"golang.org/x/crypto/ssh"
v1 "k8s.io/api/core/v1"
kerrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime/schema"
)
/*
These tests use gogs for testing integration with a git server. Gogs container uses data from assets/gitserver, which
contains one user, one public repository, and another private repository. Initial commits and fingerprint are provided as consts.
*/
const (
gogsFingerPrint = "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBOLWGeeq/e1mK/zH47UeQeMtdh+NEz6j7xp5cAINcV2pPWgAsuyh5dumMv1RkC1rr0pmWekCoMnR2c4+PllRqrQ="
gogsUser = "test"
gogsPass = "pass"
startupTimeout = 60 * time.Second
)
var (
gogsClient *gogs.Client
latestCommitPublicRepo string
latestCommitPrivateRepo string
)
func TestLatestCommit_NoAuth(t *testing.T) {
ctx := context.Background()
container, url, err := createGogsContainer(ctx, createTempFolder(t))
require.NoError(t, err, "creating gogs container failed")
defer terminateContainer(ctx, container, t)
tests := map[string]struct {
gitjob *gitjobv1.GitJob
expectedCommit string
expectedErr error
}{
"public repo": {
gitjob: &gitjobv1.GitJob{
Spec: gitjobv1.GitJobSpec{
Git: gitjobv1.GitInfo{
Repo: url + "/test/public-repo",
Branch: "master",
},
},
},
expectedCommit: latestCommitPublicRepo,
expectedErr: nil,
},
"private repo": {
gitjob: &gitjobv1.GitJob{
Spec: gitjobv1.GitJobSpec{
Git: gitjobv1.GitInfo{
Repo: url + "/test/private-repo",
Branch: "master",
},
},
},
expectedCommit: "",
expectedErr: transport.ErrAuthenticationRequired,
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
secretGetter := &secretGetterMock{err: kerrors.NewNotFound(schema.GroupResource{}, "notfound")}
latestCommit, err := git.LatestCommit(test.gitjob, secretGetter)
if err != test.expectedErr {
t.Errorf("expecter error is: %v, but got %v", test.expectedErr, err)
}
if latestCommit != test.expectedCommit {
t.Errorf("latestCommit doesn't match. got %s, expected %s", latestCommit, test.expectedCommit)
}
})
}
}
func TestLatestCommit_BasicAuth(t *testing.T) {
ctx := context.Background()
container, url, err := createGogsContainer(ctx, createTempFolder(t))
require.NoError(t, err, "creating gogs container failed")
defer terminateContainer(ctx, container, t)
tests := map[string]struct {
gitjob *gitjobv1.GitJob
expectedCommit string
expectedErr error
}{
"public repo": {
gitjob: &gitjobv1.GitJob{
Spec: gitjobv1.GitJobSpec{
Git: gitjobv1.GitInfo{
Repo: url + "/test/public-repo",
Branch: "master",
},
},
},
expectedCommit: latestCommitPublicRepo,
expectedErr: nil,
},
"private repo": {
gitjob: &gitjobv1.GitJob{
Spec: gitjobv1.GitJobSpec{
Git: gitjobv1.GitInfo{
Repo: url + "/test/private-repo",
Branch: "master",
},
},
},
expectedCommit: latestCommitPrivateRepo,
expectedErr: nil,
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
secret := &v1.Secret{
Data: map[string][]byte{v1.BasicAuthUsernameKey: []byte(gogsUser), v1.BasicAuthPasswordKey: []byte(gogsPass)},
Type: v1.SecretTypeBasicAuth,
}
secretGetter := &secretGetterMock{secret: secret}
latestCommit, err := git.LatestCommit(test.gitjob, secretGetter)
if err != test.expectedErr {
t.Errorf("expecter error is: %v, but got %v", test.expectedErr, err)
}
if latestCommit != test.expectedCommit {
t.Errorf("latestCommit doesn't match. got %s, expected %s", latestCommit, test.expectedCommit)
}
})
}
}
func TestLatestCommitSSH(t *testing.T) {
ctx := context.Background()
container, _, err := createGogsContainer(ctx, createTempFolder(t))
require.NoError(t, err, "creating gogs container failed")
defer terminateContainer(ctx, container, t)
privateKey, err := createAndAddKeys()
require.NoError(t, err)
sshPort, err := container.MappedPort(ctx, "22")
require.NoError(t, err)
gogsKnownHosts := []byte("[localhost]:" + sshPort.Port() + " " + gogsFingerPrint)
tests := map[string]struct {
gitjob *gitjobv1.GitJob
knownHosts []byte
expectedCommit string
expectedErr error
}{
"public repo": {
gitjob: &gitjobv1.GitJob{
Spec: gitjobv1.GitJobSpec{
Git: gitjobv1.GitInfo{
Repo: "ssh://git@localhost:" + sshPort.Port() + "/test/" + "public-repo",
Branch: "master",
},
},
},
knownHosts: gogsKnownHosts,
expectedCommit: latestCommitPublicRepo,
expectedErr: nil,
},
"private repo with known hosts": {
gitjob: &gitjobv1.GitJob{
Spec: gitjobv1.GitJobSpec{
Git: gitjobv1.GitInfo{
Repo: "ssh://git@localhost:" + sshPort.Port() + "/test/" + "private-repo",
Branch: "master",
},
},
},
knownHosts: gogsKnownHosts,
expectedCommit: latestCommitPrivateRepo,
expectedErr: nil,
},
"private repo without known hosts": {
gitjob: &gitjobv1.GitJob{
Spec: gitjobv1.GitJobSpec{
Git: gitjobv1.GitInfo{
Repo: "ssh://git@localhost:" + sshPort.Port() + "/test/" + "private-repo",
Branch: "master",
},
},
},
knownHosts: nil,
expectedCommit: latestCommitPrivateRepo,
expectedErr: nil,
},
"private repo with known host with a wrong host url": {
gitjob: &gitjobv1.GitJob{
Spec: gitjobv1.GitJobSpec{
Git: gitjobv1.GitInfo{
Repo: "ssh://git@localhost:" + sshPort.Port() + "/test/" + "private-repo",
Branch: "master",
},
},
},
knownHosts: []byte("doesntexist " + gogsFingerPrint),
expectedCommit: "",
expectedErr: fmt.Errorf("ssh: handshake failed: knownhosts: key is unknown"),
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
require := require.New(t)
secret := &v1.Secret{
Data: map[string][]byte{
v1.SSHAuthPrivateKey: []byte(privateKey),
"known_hosts": test.knownHosts,
},
Type: v1.SecretTypeSSHAuth,
}
secretGetter := &secretGetterMock{secret: secret}
latestCommit, err := git.LatestCommit(test.gitjob, secretGetter)
// works with nil and wrapped errors
if test.expectedErr == nil {
require.NoError(err)
} else {
require.Contains(test.expectedErr.Error(), err.Error())
}
if latestCommit != test.expectedCommit {
t.Errorf("latestCommit doesn't match. got %s, expected %s", latestCommit, test.expectedCommit)
}
})
}
}
func createGogsContainer(ctx context.Context, tmpDir string) (testcontainers.Container, string, error) {
err := cp.Copy("../assets/gitserver", tmpDir)
if err != nil {
return nil, "", err
}
req := testcontainers.ContainerRequest{
Image: "gogs/gogs:0.13",
ExposedPorts: []string{"3000/tcp", "22/tcp"},
WaitingFor: wait.ForAll(
wait.ForHTTP("/").WithPort("3000/tcp").WithStartupTimeout(startupTimeout),
wait.ForListeningPort("22/tcp").WithStartupTimeout(startupTimeout),
),
Mounts: testcontainers.ContainerMounts{
{
Source: testcontainers.GenericBindMountSource{HostPath: tmpDir},
Target: "/data",
},
},
}
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
if err != nil {
return nil, "", err
}
url, err := getURL(ctx, container)
if err != nil {
return nil, "", err
}
c := gogs.NewClient(url, "")
token, err := c.CreateAccessToken(gogsUser, gogsPass, gogs.CreateAccessTokenOption{
Name: "test",
})
if err != nil {
return nil, "", err
}
gogsClient = gogs.NewClient(url, token.Sha1)
latestCommitPublicRepo, err = initRepo(url, "public-repo", false)
if err != nil {
return nil, "", err
}
latestCommitPrivateRepo, err = initRepo(url, "private-repo", true)
if err != nil {
return nil, "", err
}
return container, url, nil
}
// initRepo creates a git repo and adds an initial commit.
func initRepo(url string, name string, private bool) (string, error) {
// create repo
_, err := gogsClient.CreateRepo(gogs.CreateRepoOption{
Name: name,
Private: private,
})
if err != nil {
return "", err
}
repoURL := url + "/" + gogsUser + "/" + name
// add initial commit
tmp, err := os.MkdirTemp("", name)
if err != nil {
return "", err
}
defer os.RemoveAll(tmp)
r, err := gogit.PlainInit(tmp, false)
if err != nil {
return "", err
}
r, err = gogit.PlainOpen(tmp)
if err != nil {
return "", err
}
filename := filepath.Join(tmp, "example-git-file")
err = os.WriteFile(filename, []byte("test"), 0600)
if err != nil {
return "", err
}
w, err := r.Worktree()
if err != nil {
return "", err
}
_, err = w.Add("example-git-file")
if err != nil {
return "", err
}
commit, err := w.Commit("test commit", &gogit.CommitOptions{
Author: &object.Signature{
Name: "Test user",
Email: "test@test.com",
When: time.Now(),
},
})
if err != nil {
return "", err
}
cfg, err := r.Config()
if err != nil {
return "", err
}
cfg.Remotes["upstream"] = &config.RemoteConfig{
Name: "upstream",
URLs: []string{repoURL},
}
err = r.SetConfig(cfg)
if err != nil {
return "", err
}
err = r.Push(&gogit.PushOptions{
RemoteName: "upstream",
RemoteURL: repoURL,
Auth: &httpgit.BasicAuth{
Username: gogsUser,
Password: gogsPass,
},
})
if err != nil {
return "", err
}
return commit.String(), nil
}
func getURL(ctx context.Context, container testcontainers.Container) (string, error) {
mappedPort, err := container.MappedPort(ctx, "3000")
if err != nil {
return "", err
}
host, err := container.Host(ctx)
if err != nil {
return "", err
}
url := "http://" + host + ":" + mappedPort.Port()
return url, nil
}
// createTempFolder uses testing tempDir if running in local, which will cleanup the files at the end of the tests.
// cleanup fails in github actions, that's why we use os.MkdirTemp instead. Container will be removed at the end in
// github actions, so no resources are left orphaned.
func createTempFolder(t *testing.T) string {
if os.Getenv("GITHUB_ACTIONS") == "true" {
tmp, err := os.MkdirTemp("", "gogs")
require.NoError(t, err)
return tmp
}
return t.TempDir()
}
// createAndAddKeys creates a public private key pair. It adds the public key to gogs, and returns the private key.
func createAndAddKeys() (string, error) {
publicKey, privateKey, err := makeSSHKeyPair()
if err != nil {
return "", err
}
_, err = gogsClient.CreatePublicKey(gogs.CreateKeyOption{
Title: "test",
Key: publicKey,
})
if err != nil {
return "", err
}
return privateKey, nil
}
func makeSSHKeyPair() (string, string, error) {
privateKey, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil {
return "", "", err
}
var privKeyBuf strings.Builder
privateKeyPEM := &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)}
if err := pem.Encode(&privKeyBuf, privateKeyPEM); err != nil {
return "", "", err
}
pub, err := ssh.NewPublicKey(&privateKey.PublicKey)
if err != nil {
return "", "", err
}
var pubKeyBuf strings.Builder
pubKeyBuf.Write(ssh.MarshalAuthorizedKey(pub))
return pubKeyBuf.String(), privKeyBuf.String(), nil
}
func terminateContainer(ctx context.Context, container testcontainers.Container, t *testing.T) {
if err := container.Terminate(ctx); err != nil {
t.Fatalf("failed to terminate container: %s", err.Error())
}
}
type secretGetterMock struct {
secret *v1.Secret
err error
}
func (s *secretGetterMock) Get(string, string) (*v1.Secret, error) {
if s.err != nil {
return nil, s.err
}
return s.secret, nil
}