1
0
Fork 0
mirror of https://github.com/imjasonh/git-k8s synced 2026-07-09 15:16:56 +00:00
git-k8s/test/e2e/e2e_test.go
Claude 6f803bf10e
Add comprehensive e2e tests for repo-watcher-controller
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
2026-02-28 14:15:28 +00:00

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
}