diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 046a9d7..0e4dc75 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -84,13 +84,6 @@ jobs: --namespace=git-system --timeout=120s done - - name: Verify CRDs are installed - run: | - kubectl get crd gitrepositories.git-k8s.imjasonh.com - kubectl get crd gitbranches.git-k8s.imjasonh.com - kubectl get crd gitpushtransactions.git-k8s.imjasonh.com - kubectl get crd gitreposyncs.git-k8s.imjasonh.com - - name: Deploy Gitea git server run: | kubectl apply -f - <<'EOF' @@ -159,7 +152,6 @@ jobs: - name: Wait for Gitea run: | kubectl rollout status deployment/gitea -n git-system --timeout=120s - # Wait for Gitea API to be fully responsive inside the pod. for i in $(seq 1 30); do if kubectl exec -n git-system deploy/gitea -- \ curl -sf http://localhost:3000/api/v1/version; then @@ -181,256 +173,37 @@ jobs: --email admin@test.local \ --must-change-password=false - - name: Create test repository + - name: Run e2e tests run: | - kubectl exec -n git-system deploy/gitea -- \ - curl -sf -X POST http://localhost:3000/api/v1/user/repos \ - -H 'Content-Type: application/json' \ - -u testadmin:testpassword123 \ - -d '{"name":"test-repo","auto_init":true,"default_branch":"main"}' - echo "" - echo "Test repository created" - - - name: Create Git credentials Secret - run: | - kubectl create secret generic gitea-creds \ - --namespace=default \ - --from-literal=username=testadmin \ - --from-literal=password=testpassword123 - - - name: Create GitRepository - run: | - cat <<'EOF' | kubectl apply -f - - apiVersion: git-k8s.imjasonh.com/v1alpha1 - kind: GitRepository - metadata: - name: test-repo - namespace: default - spec: - url: http://gitea.git-system.svc.cluster.local:3000/testadmin/test-repo.git - defaultBranch: main - auth: - secretRef: - name: gitea-creds - EOF - kubectl get gitrepositories -A - - - name: Create GitBranch - run: | - cat <<'EOF' | kubectl apply -f - - apiVersion: git-k8s.imjasonh.com/v1alpha1 - kind: GitBranch - metadata: - name: test-repo-feature - namespace: default - spec: - repositoryRef: test-repo - branchName: feature-test - EOF - kubectl get gitbranches -A - - - name: Push main to a new branch via GitPushTransaction - run: | - cat <<'EOF' | kubectl apply -f - - apiVersion: git-k8s.imjasonh.com/v1alpha1 - kind: GitPushTransaction - metadata: - name: test-push - namespace: default - spec: - repositoryRef: test-repo - refSpecs: - - source: refs/heads/main - destination: refs/heads/feature-test - EOF - kubectl get gitpushtransactions -A - - - name: Wait for push transaction to succeed - run: | - echo "Waiting for GitPushTransaction to complete..." - for i in $(seq 1 60); do - PHASE=$(kubectl get gitpushtransaction test-push -n default \ - -o jsonpath='{.status.phase}' 2>/dev/null) - echo " attempt ${i}: phase=${PHASE:-Pending}" - if [ "$PHASE" = "Succeeded" ]; then - echo "Push transaction succeeded!" - break - elif [ "$PHASE" = "Failed" ]; then - echo "Push transaction FAILED!" - kubectl get gitpushtransaction test-push -n default -o yaml - kubectl logs -n git-system deploy/push-controller --tail=100 - exit 1 - fi - sleep 2 - done - PHASE=$(kubectl get gitpushtransaction test-push -n default \ - -o jsonpath='{.status.phase}') - if [ "$PHASE" != "Succeeded" ]; then - echo "Timed out waiting for push transaction (phase: ${PHASE:-empty})" - kubectl logs -n git-system deploy/push-controller --tail=100 - exit 1 - fi - - - name: Verify push transaction result - run: | - echo "=== GitPushTransaction status ===" - kubectl get gitpushtransaction test-push -n default -o yaml - - COMMIT=$(kubectl get gitpushtransaction test-push -n default \ - -o jsonpath='{.status.resultCommit}') - echo "Result commit: $COMMIT" - if [ -z "$COMMIT" ]; then - echo "FAIL: resultCommit is empty" - exit 1 - fi - - - name: Verify GitBranch was updated by controller - run: | - echo "=== GitBranch status ===" - kubectl get gitbranch test-repo-feature -n default -o yaml - - COMMIT=$(kubectl get gitbranch test-repo-feature -n default \ - -o jsonpath='{.status.headCommit}') - echo "GitBranch headCommit: $COMMIT" - if [ -z "$COMMIT" ]; then - echo "FAIL: GitBranch headCommit was not updated by the controller" - exit 1 - fi - - - name: Verify branch exists on git server - run: | - echo "Listing branches on Gitea..." - kubectl exec -n git-system deploy/gitea -- \ - curl -sf http://localhost:3000/api/v1/repos/testadmin/test-repo/branches/feature-test \ - -u testadmin:testpassword123 - echo "" - echo "Branch feature-test verified on git server!" - - # --- GitRepoSync e2e test --- - # Create a second repo on Gitea to test sync between two repos. - - name: Create second test repository on Gitea - run: | - kubectl exec -n git-system deploy/gitea -- \ - curl -sf -X POST http://localhost:3000/api/v1/user/repos \ - -H 'Content-Type: application/json' \ - -u testadmin:testpassword123 \ - -d '{"name":"test-repo-b","auto_init":true,"default_branch":"main"}' - echo "" - echo "Second test repository created" - - - name: Create second GitRepository CR - run: | - cat <<'EOF' | kubectl apply -f - - apiVersion: git-k8s.imjasonh.com/v1alpha1 - kind: GitRepository - metadata: - name: test-repo-b - namespace: default - spec: - url: http://gitea.git-system.svc.cluster.local:3000/testadmin/test-repo-b.git - defaultBranch: main - auth: - secretRef: - name: gitea-creds - EOF - - - name: Create GitBranch CRs for sync test - run: | - # Get the head commit from each repo via Gitea API - COMMIT_A=$(kubectl exec -n git-system deploy/gitea -- \ - curl -sf http://localhost:3000/api/v1/repos/testadmin/test-repo/branches/main \ - -u testadmin:testpassword123 | python3 -c "import sys,json; print(json.load(sys.stdin)['commit']['id'])") - echo "Repo A main commit: $COMMIT_A" - - COMMIT_B=$(kubectl exec -n git-system deploy/gitea -- \ - curl -sf http://localhost:3000/api/v1/repos/testadmin/test-repo-b/branches/main \ - -u testadmin:testpassword123 | python3 -c "import sys,json; print(json.load(sys.stdin)['commit']['id'])") - echo "Repo B main commit: $COMMIT_B" - - # Create GitBranch CRs - cat <<'EOF' | kubectl apply -f - - apiVersion: git-k8s.imjasonh.com/v1alpha1 - kind: GitBranch - metadata: - name: test-repo-main - namespace: default - spec: - repositoryRef: test-repo - branchName: main - EOF - - cat <<'EOF' | kubectl apply -f - - apiVersion: git-k8s.imjasonh.com/v1alpha1 - kind: GitBranch - metadata: - name: test-repo-b-main - namespace: default - spec: - repositoryRef: test-repo-b - branchName: main - EOF - - # Set headCommit via status subresource patch - kubectl patch gitbranch test-repo-main -n default \ - --type=merge --subresource=status \ - -p "{\"status\":{\"headCommit\":\"${COMMIT_A}\"}}" - - kubectl patch gitbranch test-repo-b-main -n default \ - --type=merge --subresource=status \ - -p "{\"status\":{\"headCommit\":\"${COMMIT_B}\"}}" - - kubectl get gitbranches -A -o wide - - - name: Create GitRepoSync - run: | - cat <<'EOF' | kubectl apply -f - - apiVersion: git-k8s.imjasonh.com/v1alpha1 - kind: GitRepoSync - metadata: - name: test-sync - namespace: default - spec: - repoA: - name: test-repo - repoB: - name: test-repo-b - branchName: main - EOF - kubectl get gitreposyncs -A - - - name: Wait for sync controller to process - run: | - echo "Waiting for GitRepoSync to be processed..." - for i in $(seq 1 30); do - PHASE=$(kubectl get gitreposync test-sync -n default \ - -o jsonpath='{.status.phase}' 2>/dev/null) - MSG=$(kubectl get gitreposync test-sync -n default \ - -o jsonpath='{.status.message}' 2>/dev/null) - echo " attempt ${i}: phase=${PHASE:-empty} message=${MSG:-empty}" - if [ -n "$PHASE" ]; then - echo "Sync controller has set phase: $PHASE" + # Port-forward Gitea so Go tests can reach it from the runner. + kubectl port-forward -n git-system svc/gitea 3000:3000 & + PF_PID=$! + # Wait for port-forward to be ready. + for i in $(seq 1 15); do + if curl -sf http://localhost:3000/api/v1/version > /dev/null 2>&1; then break fi - sleep 2 + sleep 1 done - - name: Verify GitRepoSync status - run: | - echo "=== GitRepoSync status ===" - kubectl get gitreposync test-sync -n default -o yaml + GITEA_URL=http://localhost:3000 \ + GITEA_INTERNAL_URL=http://gitea.git-system.svc.cluster.local:3000 \ + go test -v -count=1 -timeout=10m -tags=e2e ./test/e2e/ - PHASE=$(kubectl get gitreposync test-sync -n default \ - -o jsonpath='{.status.phase}') - echo "Phase: $PHASE" - # The sync controller should have processed this - phase should be set - if [ -z "$PHASE" ]; then - echo "FAIL: sync controller did not set a phase" - kubectl logs -n git-system deploy/sync-controller --tail=100 - exit 1 - fi - echo "GitRepoSync e2e test passed! Phase: $PHASE" + kill $PF_PID 2>/dev/null || true - name: Collect diagnostics + if: ${{ failure() }} + run: | + echo "=== Controller logs ===" + for ctrl in push-controller sync-controller resolver-controller; do + echo "--- ${ctrl} ---" + kubectl logs -n git-system deploy/${ctrl} --tail=50 2>/dev/null || true + done + echo "=== CRD resources ===" + kubectl get gitrepositories,gitbranches,gitpushtransactions,gitreposyncs -A -o yaml 2>/dev/null || true + + - name: Collect KinD diagnostics if: ${{ failure() }} uses: chainguard-dev/actions/kind-diag@main with: diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go new file mode 100644 index 0000000..a59898b --- /dev/null +++ b/test/e2e/e2e_test.go @@ -0,0 +1,291 @@ +//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 = 2 * 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 +} + +// 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) + + 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 + } + 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) + } + 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 +} diff --git a/test/e2e/push_test.go b/test/e2e/push_test.go new file mode 100644 index 0000000..d69ac63 --- /dev/null +++ b/test/e2e/push_test.go @@ -0,0 +1,118 @@ +//go:build e2e + +package e2e + +import ( + "context" + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + gitv1alpha1 "github.com/imjasonh/git-k8s/pkg/apis/git/v1alpha1" +) + +func TestPushTransaction(t *testing.T) { + t.Parallel() + + repoName := "e2e-push" + createGiteaRepo(t, repoName) + + secretName := createCredentialsSecret(t, "e2e-push-creds") + createGitRepository(t, "e2e-push-repo", repoName, secretName) + createGitBranch(t, "e2e-push-feature", "e2e-push-repo", "feature-push") + + // Create a push transaction: push main to a new branch. + ctx := context.Background() + txn := &gitv1alpha1.GitPushTransaction{ + ObjectMeta: metav1.ObjectMeta{ + Name: "e2e-push-txn", + Namespace: testNamespace, + }, + Spec: gitv1alpha1.GitPushTransactionSpec{ + RepositoryRef: "e2e-push-repo", + RefSpecs: []gitv1alpha1.PushRefSpec{ + { + Source: "refs/heads/main", + Destination: "refs/heads/feature-push", + }, + }, + }, + } + _, err := gitClient.GitPushTransactions(testNamespace).Create(ctx, txn, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("creating GitPushTransaction: %v", err) + } + t.Cleanup(func() { + gitClient.GitPushTransactions(testNamespace).Delete(ctx, "e2e-push-txn", metav1.DeleteOptions{}) + }) + + // Wait for the push to succeed. + result := waitForPushTransaction(t, "e2e-push-txn") + if result.Status.ResultCommit == "" { + t.Fatal("push transaction succeeded but resultCommit is empty") + } + t.Logf("Push transaction result commit: %s", result.Status.ResultCommit) + + // Verify the GitBranch was updated by the controller. + headCommit := waitForGitBranchCommit(t, "e2e-push-feature") + t.Logf("GitBranch headCommit: %s", headCommit) + + // Verify the branch actually exists on the git server. + if !giteaBranchExists(t, repoName, "feature-push") { + t.Fatal("branch feature-push does not exist on Gitea after push") + } +} + +func TestPushTransactionMultipleRefSpecs(t *testing.T) { + t.Parallel() + + repoName := "e2e-push-multi" + createGiteaRepo(t, repoName) + + secretName := createCredentialsSecret(t, "e2e-push-multi-creds") + createGitRepository(t, "e2e-push-multi-repo", repoName, secretName) + createGitBranch(t, "e2e-push-multi-b1", "e2e-push-multi-repo", "branch-one") + createGitBranch(t, "e2e-push-multi-b2", "e2e-push-multi-repo", "branch-two") + + // Push main to two branches at once. + ctx := context.Background() + txn := &gitv1alpha1.GitPushTransaction{ + ObjectMeta: metav1.ObjectMeta{ + Name: "e2e-push-multi-txn", + Namespace: testNamespace, + }, + Spec: gitv1alpha1.GitPushTransactionSpec{ + RepositoryRef: "e2e-push-multi-repo", + RefSpecs: []gitv1alpha1.PushRefSpec{ + { + Source: "refs/heads/main", + Destination: "refs/heads/branch-one", + }, + { + Source: "refs/heads/main", + Destination: "refs/heads/branch-two", + }, + }, + }, + } + _, err := gitClient.GitPushTransactions(testNamespace).Create(ctx, txn, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("creating GitPushTransaction: %v", err) + } + t.Cleanup(func() { + gitClient.GitPushTransactions(testNamespace).Delete(ctx, "e2e-push-multi-txn", metav1.DeleteOptions{}) + }) + + result := waitForPushTransaction(t, "e2e-push-multi-txn") + if result.Status.ResultCommit == "" { + t.Fatal("push transaction succeeded but resultCommit is empty") + } + + // Both branches should exist on Gitea. + if !giteaBranchExists(t, repoName, "branch-one") { + t.Error("branch-one does not exist on Gitea") + } + if !giteaBranchExists(t, repoName, "branch-two") { + t.Error("branch-two does not exist on Gitea") + } +} diff --git a/test/e2e/sync_test.go b/test/e2e/sync_test.go new file mode 100644 index 0000000..c6e91d2 --- /dev/null +++ b/test/e2e/sync_test.go @@ -0,0 +1,77 @@ +//go:build e2e + +package e2e + +import ( + "context" + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + gitv1alpha1 "github.com/imjasonh/git-k8s/pkg/apis/git/v1alpha1" +) + +func TestRepoSync(t *testing.T) { + t.Parallel() + + // Create two repos on Gitea. + createGiteaRepo(t, "e2e-sync-a") + createGiteaRepo(t, "e2e-sync-b") + + secretName := createCredentialsSecret(t, "e2e-sync-creds") + createGitRepository(t, "e2e-sync-repo-a", "e2e-sync-a", secretName) + createGitRepository(t, "e2e-sync-repo-b", "e2e-sync-b", secretName) + + // Create GitBranch CRs and populate their headCommit via the push controller. + // First, push main to itself to get the push controller to set headCommit. + branchA := createGitBranch(t, "e2e-sync-branch-a", "e2e-sync-repo-a", "main") + branchB := createGitBranch(t, "e2e-sync-branch-b", "e2e-sync-repo-b", "main") + + // We need headCommit set on both branches for the sync controller. + // Patch them with the actual commit from Gitea. + commitA := getGiteaBranchCommit(t, "e2e-sync-a", "main") + commitB := getGiteaBranchCommit(t, "e2e-sync-b", "main") + t.Logf("Repo A main commit: %s", commitA) + t.Logf("Repo B main commit: %s", commitB) + + ctx := context.Background() + branchA.Status.HeadCommit = commitA + if _, err := gitClient.GitBranches(testNamespace).UpdateStatus(ctx, branchA, metav1.UpdateOptions{}); err != nil { + t.Fatalf("updating branch A status: %v", err) + } + branchB.Status.HeadCommit = commitB + if _, err := gitClient.GitBranches(testNamespace).UpdateStatus(ctx, branchB, metav1.UpdateOptions{}); err != nil { + t.Fatalf("updating branch B status: %v", err) + } + + // Create a GitRepoSync between the two repos. + syncObj := &gitv1alpha1.GitRepoSync{ + ObjectMeta: metav1.ObjectMeta{ + Name: "e2e-sync", + Namespace: testNamespace, + }, + Spec: gitv1alpha1.GitRepoSyncSpec{ + RepoA: gitv1alpha1.SyncRepoRef{Name: "e2e-sync-repo-a"}, + RepoB: gitv1alpha1.SyncRepoRef{Name: "e2e-sync-repo-b"}, + BranchName: "main", + }, + } + _, err := gitClient.GitRepoSyncs(testNamespace).Create(ctx, syncObj, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("creating GitRepoSync: %v", err) + } + t.Cleanup(func() { + gitClient.GitRepoSyncs(testNamespace).Delete(ctx, "e2e-sync", metav1.DeleteOptions{}) + }) + + // Wait for the sync controller to set a phase. + result := waitForSyncPhase(t, "e2e-sync") + t.Logf("GitRepoSync phase: %s, message: %s", result.Status.Phase, result.Status.Message) + + // The sync controller should have processed this. + // Since both repos are different auto_init repos, commits differ, + // so we expect Conflicted (no common ancestor) or another non-empty phase. + if result.Status.Phase == "" { + t.Fatal("sync controller did not set a phase") + } +}