mirror of
https://github.com/imjasonh/git-k8s
synced 2026-07-09 15:16:56 +00:00
Tests cover the key behaviors of the repo-watcher: - Branch discovery: auto-creates GitBranch CRDs from remote refs - Owner references: created branches correctly reference GitRepository - New branch detection: branches added on Gitea are picked up - Commit update detection: new pushes to Gitea update headCommit - Branch deletion: branches removed from Gitea trigger CRD deletion - Multiple branches: all branches on a repo are discovered - LastFetchTime: status is updated and advances on each poll - Labels: repository label is set on created branches - Slash branch names: feature/foo naming handled correctly Also adds shared e2e helpers for Gitea branch/file operations and updates CI workflow to deploy and monitor the repo-watcher-controller. https://claude.ai/code/session_01P7xfBqARU5DFS8QJ4uDPVj
463 lines
14 KiB
Go
463 lines
14 KiB
Go
//go:build e2e
|
|
|
|
package e2e
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
corev1 "k8s.io/api/core/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/client-go/dynamic"
|
|
"k8s.io/client-go/kubernetes"
|
|
"k8s.io/client-go/tools/clientcmd"
|
|
|
|
gitv1alpha1 "github.com/imjasonh/git-k8s/pkg/apis/git/v1alpha1"
|
|
gitclient "github.com/imjasonh/git-k8s/pkg/client"
|
|
)
|
|
|
|
const (
|
|
testNamespace = "default"
|
|
giteaUsername = "testadmin"
|
|
giteaPassword = "testpassword123"
|
|
|
|
pollInterval = 1 * time.Second
|
|
pollTimeout = 2 * time.Minute
|
|
)
|
|
|
|
var (
|
|
gitClient *gitclient.GitV1alpha1Client
|
|
kubeClient kubernetes.Interface
|
|
giteaURL string // localhost URL for test process (via port-forward)
|
|
giteaInURL string // in-cluster URL for controllers
|
|
)
|
|
|
|
func TestMain(m *testing.M) {
|
|
// Resolve Gitea URLs.
|
|
giteaURL = envOrDefault("GITEA_URL", "http://localhost:3000")
|
|
giteaInURL = envOrDefault("GITEA_INTERNAL_URL", "http://gitea.git-system.svc.cluster.local:3000")
|
|
|
|
// Build clients from KUBECONFIG.
|
|
kubeconfig := os.Getenv("KUBECONFIG")
|
|
if kubeconfig == "" {
|
|
kubeconfig = clientcmd.RecommendedHomeFile
|
|
}
|
|
config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "building kubeconfig: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
kubeClient, err = kubernetes.NewForConfig(config)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "creating kubernetes client: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
dynClient, err := dynamic.NewForConfig(config)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "creating dynamic client: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
gitClient = gitclient.NewFromDynamic(dynClient)
|
|
|
|
os.Exit(m.Run())
|
|
}
|
|
|
|
func envOrDefault(key, def string) string {
|
|
if v := os.Getenv(key); v != "" {
|
|
return v
|
|
}
|
|
return def
|
|
}
|
|
|
|
// createGiteaRepo creates a repository on Gitea via the API.
|
|
func createGiteaRepo(t *testing.T, name string) {
|
|
t.Helper()
|
|
body, _ := json.Marshal(map[string]interface{}{
|
|
"name": name,
|
|
"auto_init": true,
|
|
"default_branch": "main",
|
|
})
|
|
req, err := http.NewRequest("POST", giteaURL+"/api/v1/user/repos", bytes.NewReader(body))
|
|
if err != nil {
|
|
t.Fatalf("creating request: %v", err)
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.SetBasicAuth(giteaUsername, giteaPassword)
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("creating Gitea repo %q: %v", name, err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusCreated {
|
|
t.Fatalf("creating Gitea repo %q: status %d", name, resp.StatusCode)
|
|
}
|
|
t.Logf("Created Gitea repo %q", name)
|
|
}
|
|
|
|
// getGiteaBranchCommit fetches the HEAD commit of a branch from Gitea.
|
|
func getGiteaBranchCommit(t *testing.T, repo, branch string) string {
|
|
t.Helper()
|
|
url := fmt.Sprintf("%s/api/v1/repos/%s/%s/branches/%s", giteaURL, giteaUsername, repo, branch)
|
|
req, _ := http.NewRequest("GET", url, nil)
|
|
req.SetBasicAuth(giteaUsername, giteaPassword)
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("getting branch %s/%s: %v", repo, branch, err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("getting branch %s/%s: status %d", repo, branch, resp.StatusCode)
|
|
}
|
|
|
|
var result struct {
|
|
Commit struct {
|
|
ID string `json:"id"`
|
|
} `json:"commit"`
|
|
}
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
t.Fatalf("decoding branch response: %v", err)
|
|
}
|
|
return result.Commit.ID
|
|
}
|
|
|
|
// giteaBranchExists checks if a branch exists on Gitea.
|
|
func giteaBranchExists(t *testing.T, repo, branch string) bool {
|
|
t.Helper()
|
|
url := fmt.Sprintf("%s/api/v1/repos/%s/%s/branches/%s", giteaURL, giteaUsername, repo, branch)
|
|
req, _ := http.NewRequest("GET", url, nil)
|
|
req.SetBasicAuth(giteaUsername, giteaPassword)
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("checking branch %s/%s: %v", repo, branch, err)
|
|
}
|
|
resp.Body.Close()
|
|
return resp.StatusCode == http.StatusOK
|
|
}
|
|
|
|
// createCredentialsSecret creates a Secret with Gitea credentials.
|
|
// It returns the secret name. The secret is cleaned up when the test finishes.
|
|
func createCredentialsSecret(t *testing.T, name string) string {
|
|
t.Helper()
|
|
ctx := context.Background()
|
|
secret := &corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: name,
|
|
Namespace: testNamespace,
|
|
},
|
|
StringData: map[string]string{
|
|
"username": giteaUsername,
|
|
"password": giteaPassword,
|
|
},
|
|
}
|
|
_, err := kubeClient.CoreV1().Secrets(testNamespace).Create(ctx, secret, metav1.CreateOptions{})
|
|
if err != nil {
|
|
t.Fatalf("creating secret %q: %v", name, err)
|
|
}
|
|
t.Cleanup(func() {
|
|
kubeClient.CoreV1().Secrets(testNamespace).Delete(ctx, name, metav1.DeleteOptions{})
|
|
})
|
|
return name
|
|
}
|
|
|
|
// createGitRepository creates a GitRepository CR pointing at a Gitea repo.
|
|
func createGitRepository(t *testing.T, name, repoName, secretName string) *gitv1alpha1.GitRepository {
|
|
t.Helper()
|
|
ctx := context.Background()
|
|
repo := &gitv1alpha1.GitRepository{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: name,
|
|
Namespace: testNamespace,
|
|
},
|
|
Spec: gitv1alpha1.GitRepositorySpec{
|
|
URL: fmt.Sprintf("%s/%s/%s.git", giteaInURL, giteaUsername, repoName),
|
|
DefaultBranch: "main",
|
|
Auth: &gitv1alpha1.GitAuth{
|
|
SecretRef: &gitv1alpha1.SecretRef{Name: secretName},
|
|
},
|
|
},
|
|
}
|
|
created, err := gitClient.GitRepositories(testNamespace).Create(ctx, repo, metav1.CreateOptions{})
|
|
if err != nil {
|
|
t.Fatalf("creating GitRepository %q: %v", name, err)
|
|
}
|
|
t.Cleanup(func() {
|
|
gitClient.GitRepositories(testNamespace).Delete(ctx, name, metav1.DeleteOptions{})
|
|
})
|
|
return created
|
|
}
|
|
|
|
// createGitBranch creates a GitBranch CR.
|
|
func createGitBranch(t *testing.T, name, repoRef, branchName string) *gitv1alpha1.GitBranch {
|
|
t.Helper()
|
|
ctx := context.Background()
|
|
branch := &gitv1alpha1.GitBranch{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: name,
|
|
Namespace: testNamespace,
|
|
},
|
|
Spec: gitv1alpha1.GitBranchSpec{
|
|
RepositoryRef: repoRef,
|
|
BranchName: branchName,
|
|
},
|
|
}
|
|
created, err := gitClient.GitBranches(testNamespace).Create(ctx, branch, metav1.CreateOptions{})
|
|
if err != nil {
|
|
t.Fatalf("creating GitBranch %q: %v", name, err)
|
|
}
|
|
t.Cleanup(func() {
|
|
gitClient.GitBranches(testNamespace).Delete(ctx, name, metav1.DeleteOptions{})
|
|
})
|
|
return created
|
|
}
|
|
|
|
// createGiteaBranch creates a new branch on Gitea from an existing branch.
|
|
func createGiteaBranch(t *testing.T, repo, newBranch, fromBranch string) {
|
|
t.Helper()
|
|
body, _ := json.Marshal(map[string]interface{}{
|
|
"new_branch_name": newBranch,
|
|
"old_branch_name": fromBranch,
|
|
})
|
|
url := fmt.Sprintf("%s/api/v1/repos/%s/%s/branches", giteaURL, giteaUsername, repo)
|
|
req, err := http.NewRequest("POST", url, bytes.NewReader(body))
|
|
if err != nil {
|
|
t.Fatalf("creating request: %v", err)
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.SetBasicAuth(giteaUsername, giteaPassword)
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("creating Gitea branch %q: %v", newBranch, err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusCreated {
|
|
t.Fatalf("creating Gitea branch %q: status %d", newBranch, resp.StatusCode)
|
|
}
|
|
t.Logf("Created Gitea branch %q on repo %q", newBranch, repo)
|
|
}
|
|
|
|
// deleteGiteaBranch deletes a branch on Gitea.
|
|
func deleteGiteaBranch(t *testing.T, repo, branch string) {
|
|
t.Helper()
|
|
url := fmt.Sprintf("%s/api/v1/repos/%s/%s/branches/%s", giteaURL, giteaUsername, repo, branch)
|
|
req, _ := http.NewRequest("DELETE", url, nil)
|
|
req.SetBasicAuth(giteaUsername, giteaPassword)
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("deleting Gitea branch %q: %v", branch, err)
|
|
}
|
|
resp.Body.Close()
|
|
if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("deleting Gitea branch %q: status %d", branch, resp.StatusCode)
|
|
}
|
|
t.Logf("Deleted Gitea branch %q on repo %q", branch, repo)
|
|
}
|
|
|
|
// createGiteaFile creates a file on Gitea, producing a new commit on the given branch.
|
|
// Returns the new commit SHA.
|
|
func createGiteaFile(t *testing.T, repo, branch, filePath, content string) string {
|
|
t.Helper()
|
|
body, _ := json.Marshal(map[string]interface{}{
|
|
"content": content,
|
|
"branch": branch,
|
|
"new_branch": branch,
|
|
"message": fmt.Sprintf("add %s", filePath),
|
|
})
|
|
url := fmt.Sprintf("%s/api/v1/repos/%s/%s/contents/%s", giteaURL, giteaUsername, repo, filePath)
|
|
req, err := http.NewRequest("POST", url, bytes.NewReader(body))
|
|
if err != nil {
|
|
t.Fatalf("creating request: %v", err)
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.SetBasicAuth(giteaUsername, giteaPassword)
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("creating file %q: %v", filePath, err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusCreated {
|
|
t.Fatalf("creating file %q: status %d", filePath, resp.StatusCode)
|
|
}
|
|
|
|
var result struct {
|
|
Commit struct {
|
|
SHA string `json:"sha"`
|
|
} `json:"commit"`
|
|
}
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
t.Fatalf("decoding create file response: %v", err)
|
|
}
|
|
t.Logf("Created file %q on %s/%s, commit: %s", filePath, repo, branch, result.Commit.SHA)
|
|
return result.Commit.SHA
|
|
}
|
|
|
|
// createGitRepositoryWithPollInterval creates a GitRepository CR with a custom poll interval.
|
|
func createGitRepositoryWithPollInterval(t *testing.T, name, repoName, secretName string, interval metav1.Duration) *gitv1alpha1.GitRepository {
|
|
t.Helper()
|
|
ctx := context.Background()
|
|
repo := &gitv1alpha1.GitRepository{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: name,
|
|
Namespace: testNamespace,
|
|
},
|
|
Spec: gitv1alpha1.GitRepositorySpec{
|
|
URL: fmt.Sprintf("%s/%s/%s.git", giteaInURL, giteaUsername, repoName),
|
|
DefaultBranch: "main",
|
|
Auth: &gitv1alpha1.GitAuth{
|
|
SecretRef: &gitv1alpha1.SecretRef{Name: secretName},
|
|
},
|
|
PollInterval: &interval,
|
|
},
|
|
}
|
|
created, err := gitClient.GitRepositories(testNamespace).Create(ctx, repo, metav1.CreateOptions{})
|
|
if err != nil {
|
|
t.Fatalf("creating GitRepository %q: %v", name, err)
|
|
}
|
|
t.Cleanup(func() {
|
|
gitClient.GitRepositories(testNamespace).Delete(ctx, name, metav1.DeleteOptions{})
|
|
})
|
|
return created
|
|
}
|
|
|
|
// waitForGitBranchExists polls until a GitBranch with the given name exists.
|
|
func waitForGitBranchExists(t *testing.T, name string) *gitv1alpha1.GitBranch {
|
|
t.Helper()
|
|
ctx := context.Background()
|
|
deadline := time.Now().Add(pollTimeout)
|
|
|
|
for time.Now().Before(deadline) {
|
|
branch, err := gitClient.GitBranches(testNamespace).Get(ctx, name, metav1.GetOptions{})
|
|
if err == nil {
|
|
return branch
|
|
}
|
|
time.Sleep(pollInterval)
|
|
}
|
|
t.Fatalf("timed out waiting for GitBranch %q to exist", name)
|
|
return nil
|
|
}
|
|
|
|
// waitForGitBranchDeleted polls until a GitBranch with the given name no longer exists.
|
|
func waitForGitBranchDeleted(t *testing.T, name string) {
|
|
t.Helper()
|
|
ctx := context.Background()
|
|
deadline := time.Now().Add(pollTimeout)
|
|
|
|
for time.Now().Before(deadline) {
|
|
_, err := gitClient.GitBranches(testNamespace).Get(ctx, name, metav1.GetOptions{})
|
|
if err != nil {
|
|
t.Logf("GitBranch %q deleted", name)
|
|
return
|
|
}
|
|
time.Sleep(pollInterval)
|
|
}
|
|
t.Fatalf("timed out waiting for GitBranch %q to be deleted", name)
|
|
}
|
|
|
|
// waitForGitBranchHeadCommit polls until the GitBranch has the expected headCommit.
|
|
func waitForGitBranchHeadCommit(t *testing.T, name, expectedCommit string) {
|
|
t.Helper()
|
|
ctx := context.Background()
|
|
deadline := time.Now().Add(pollTimeout)
|
|
|
|
for time.Now().Before(deadline) {
|
|
branch, err := gitClient.GitBranches(testNamespace).Get(ctx, name, metav1.GetOptions{})
|
|
if err != nil {
|
|
time.Sleep(pollInterval)
|
|
continue
|
|
}
|
|
if branch.Status.HeadCommit == expectedCommit {
|
|
t.Logf("GitBranch %q headCommit matches: %s", name, expectedCommit)
|
|
return
|
|
}
|
|
t.Logf("GitBranch %q headCommit = %q, waiting for %q", name, branch.Status.HeadCommit, expectedCommit)
|
|
time.Sleep(pollInterval)
|
|
}
|
|
t.Fatalf("timed out waiting for GitBranch %q to have headCommit %q", name, expectedCommit)
|
|
}
|
|
|
|
// waitForPushTransaction polls until the GitPushTransaction reaches a terminal phase.
|
|
func waitForPushTransaction(t *testing.T, name string) *gitv1alpha1.GitPushTransaction {
|
|
t.Helper()
|
|
ctx := context.Background()
|
|
deadline := time.Now().Add(pollTimeout)
|
|
|
|
var lastTxn *gitv1alpha1.GitPushTransaction
|
|
for time.Now().Before(deadline) {
|
|
txn, err := gitClient.GitPushTransactions(testNamespace).Get(ctx, name, metav1.GetOptions{})
|
|
if err != nil {
|
|
t.Logf("getting push transaction %q: %v (retrying)", name, err)
|
|
time.Sleep(pollInterval)
|
|
continue
|
|
}
|
|
lastTxn = txn
|
|
t.Logf("GitPushTransaction %q phase: %s", name, txn.Status.Phase)
|
|
switch txn.Status.Phase {
|
|
case gitv1alpha1.TransactionPhaseSucceeded:
|
|
return txn
|
|
case gitv1alpha1.TransactionPhaseFailed:
|
|
t.Fatalf("GitPushTransaction %q failed: %s", name, txn.Status.Message)
|
|
}
|
|
time.Sleep(pollInterval)
|
|
}
|
|
if lastTxn != nil {
|
|
t.Logf("Last seen state: phase=%q message=%q resourceVersion=%s", lastTxn.Status.Phase, lastTxn.Status.Message, lastTxn.ResourceVersion)
|
|
}
|
|
t.Fatalf("timed out waiting for GitPushTransaction %q", name)
|
|
return nil
|
|
}
|
|
|
|
// waitForGitBranchCommit polls until the GitBranch has a non-empty headCommit.
|
|
func waitForGitBranchCommit(t *testing.T, name string) string {
|
|
t.Helper()
|
|
ctx := context.Background()
|
|
deadline := time.Now().Add(pollTimeout)
|
|
|
|
for time.Now().Before(deadline) {
|
|
branch, err := gitClient.GitBranches(testNamespace).Get(ctx, name, metav1.GetOptions{})
|
|
if err != nil {
|
|
time.Sleep(pollInterval)
|
|
continue
|
|
}
|
|
if branch.Status.HeadCommit != "" {
|
|
return branch.Status.HeadCommit
|
|
}
|
|
time.Sleep(pollInterval)
|
|
}
|
|
t.Fatalf("timed out waiting for GitBranch %q headCommit", name)
|
|
return ""
|
|
}
|
|
|
|
// waitForSyncPhase polls until the GitRepoSync reaches a non-empty phase.
|
|
func waitForSyncPhase(t *testing.T, name string) *gitv1alpha1.GitRepoSync {
|
|
t.Helper()
|
|
ctx := context.Background()
|
|
deadline := time.Now().Add(pollTimeout)
|
|
|
|
for time.Now().Before(deadline) {
|
|
syncObj, err := gitClient.GitRepoSyncs(testNamespace).Get(ctx, name, metav1.GetOptions{})
|
|
if err != nil {
|
|
time.Sleep(pollInterval)
|
|
continue
|
|
}
|
|
if syncObj.Status.Phase != "" {
|
|
t.Logf("GitRepoSync %q phase: %s", name, syncObj.Status.Phase)
|
|
return syncObj
|
|
}
|
|
time.Sleep(pollInterval)
|
|
}
|
|
t.Fatalf("timed out waiting for GitRepoSync %q phase", name)
|
|
return nil
|
|
}
|