mirror of https://github.com/rancher/gitjob.git
470 lines
12 KiB
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
|
|
}
|