chore: make e2e code generic to vcs (#4732)

This commit is contained in:
Luke Massa
2024-07-07 17:35:03 -04:00
committed by GitHub
parent 23418918e8
commit 441671614b
4 changed files with 190 additions and 113 deletions

View File

@@ -20,24 +20,19 @@ import (
"os"
"os/exec"
"time"
"github.com/google/go-github/v59/github"
)
type E2ETester struct {
githubClient *GithubClient
repoURL string
ownerName string
repoName string
vcsClient VCSClient
hookID int64
cloneDirRoot string
projectType Project
}
type E2EResult struct {
projectType string
githubPullRequestURL string
testResult string
projectType string
pullRequestURL string
testResult string
}
var testFileData = `
@@ -59,11 +54,9 @@ func (t *E2ETester) Start(ctx context.Context) (*E2EResult, error) {
return e2eResult, fmt.Errorf("failed to create dir %q prior to cloning, attempting to continue: %v", cloneDir, err)
}
cloneCmd := exec.Command("git", "clone", t.repoURL, cloneDir)
// git clone the repo
log.Printf("git cloning into %q", cloneDir)
if output, err := cloneCmd.CombinedOutput(); err != nil {
return e2eResult, fmt.Errorf("failed to clone repository: %v: %s", err, string(output))
err := t.vcsClient.Clone(cloneDir)
if err != nil {
return e2eResult, err
}
// checkout a new branch for the project
@@ -78,7 +71,7 @@ func (t *E2ETester) Start(ctx context.Context) (*E2EResult, error) {
randomData := []byte(testFileData)
filePath := fmt.Sprintf("%s/%s/%s", cloneDir, t.projectType.Name, testFileName)
log.Printf("creating file to commit %q", filePath)
err := os.WriteFile(filePath, randomData, 0644)
err = os.WriteFile(filePath, randomData, 0644)
if err != nil {
return e2eResult, fmt.Errorf("couldn't write file %s: %v", filePath, err)
}
@@ -109,23 +102,19 @@ func (t *E2ETester) Start(ctx context.Context) (*E2EResult, error) {
// create a new pr
title := fmt.Sprintf("This is a test pull request for atlantis e2e test for %s project type", t.projectType.Name)
head := fmt.Sprintf("%s:%s", t.ownerName, branchName)
body := ""
base := "main"
newPullRequest := &github.NewPullRequest{Title: &title, Head: &head, Body: &body, Base: &base}
url, pullId, err := t.vcsClient.CreatePullRequest(ctx, title, branchName)
pull, _, err := t.githubClient.client.PullRequests.Create(ctx, t.ownerName, t.repoName, newPullRequest)
if err != nil {
return e2eResult, fmt.Errorf("error while creating new pull request: %v", err)
return e2eResult, err
}
// set pull request url
e2eResult.githubPullRequestURL = pull.GetHTMLURL()
e2eResult.pullRequestURL = url
log.Printf("created pull request %s", pull.GetHTMLURL())
log.Printf("created pull request %s", url)
// defer closing pull request and delete remote branch
defer cleanUp(ctx, t, pull.GetNumber(), branchName) // nolint: errcheck
defer cleanUp(ctx, t, pullId, branchName) // nolint: errcheck
// wait for atlantis to respond to webhook and autoplan.
time.Sleep(2 * time.Second)
@@ -136,7 +125,7 @@ func (t *E2ETester) Start(ctx context.Context) (*E2EResult, error) {
i := 0
for ; i < maxLoops && checkStatus(state); i++ {
time.Sleep(2 * time.Second)
state, _ = getAtlantisStatus(ctx, t, branchName)
state, _ = t.vcsClient.GetAtlantisStatus(ctx, branchName)
if state == "" {
log.Println("atlantis run hasn't started")
continue
@@ -157,22 +146,6 @@ func (t *E2ETester) Start(ctx context.Context) (*E2EResult, error) {
return e2eResult, nil
}
func getAtlantisStatus(ctx context.Context, t *E2ETester, branchName string) (string, error) {
// check repo status
combinedStatus, _, err := t.githubClient.client.Repositories.GetCombinedStatus(ctx, t.ownerName, t.repoName, branchName, nil)
if err != nil {
return "", err
}
for _, status := range combinedStatus.Statuses {
if status.GetContext() == "atlantis/plan" {
return status.GetState(), nil
}
}
return "", nil
}
func checkStatus(state string) bool {
for _, s := range []string{"success", "error", "failure"} {
if state == s {
@@ -184,14 +157,14 @@ func checkStatus(state string) bool {
func cleanUp(ctx context.Context, t *E2ETester, pullRequestNumber int, branchName string) error {
// clean up
pullClosed, _, err := t.githubClient.client.PullRequests.Edit(ctx, t.ownerName, t.repoName, pullRequestNumber, &github.PullRequest{State: github.String("closed")})
err := t.vcsClient.ClosePullRequest(ctx, pullRequestNumber)
if err != nil {
return fmt.Errorf("error while closing new pull request: %v", err)
return err
}
log.Printf("closed pull request %d", pullClosed.GetNumber())
log.Printf("closed pull request %d", pullRequestNumber)
deleteBranchName := fmt.Sprintf("%s/%s", "heads", branchName)
_, err = t.githubClient.client.Git.DeleteRef(ctx, t.ownerName, t.repoName, deleteBranchName)
err = t.vcsClient.DeleteBranch(ctx, deleteBranchName)
if err != nil {
return fmt.Errorf("error while deleting branch %s: %v", deleteBranchName, err)
}

View File

@@ -14,10 +14,148 @@
package main
import (
"context"
"fmt"
"log"
"os"
"os/exec"
"strings"
"github.com/google/go-github/v59/github"
)
type GithubClient struct {
client *github.Client
username string
client *github.Client
username string
ownerName string
repoName string
token string
}
func NewGithubClient() *GithubClient {
githubUsername := os.Getenv("ATLANTISBOT_GITHUB_USERNAME")
if githubUsername == "" {
log.Fatalf("ATLANTISBOT_GITHUB_USERNAME cannot be empty")
}
githubToken := os.Getenv("ATLANTISBOT_GITHUB_TOKEN")
if githubToken == "" {
log.Fatalf("ATLANTISBOT_GITHUB_TOKEN cannot be empty")
}
ownerName := os.Getenv("GITHUB_REPO_OWNER_NAME")
if ownerName == "" {
ownerName = "runatlantis"
}
repoName := os.Getenv("GITHUB_REPO_NAME")
if repoName == "" {
repoName = "atlantis-tests"
}
// create github client
tp := github.BasicAuthTransport{
Username: strings.TrimSpace(githubUsername),
Password: strings.TrimSpace(githubToken),
}
ghClient := github.NewClient(tp.Client())
return &GithubClient{
client: ghClient,
username: githubUsername,
ownerName: ownerName,
repoName: repoName,
token: githubToken,
}
}
func (g GithubClient) Clone(cloneDir string) error {
repoURL := fmt.Sprintf("https://%s:%s@github.com/%s/%s.git", g.username, g.token, g.ownerName, g.repoName)
cloneCmd := exec.Command("git", "clone", repoURL, cloneDir)
// git clone the repo
log.Printf("git cloning into %q", cloneDir)
if output, err := cloneCmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to clone repository: %v: %s", err, string(output))
}
return nil
}
func (g GithubClient) CreateAtlantisWebhook(ctx context.Context, hookURL string) (int64, error) {
// create atlantis hook
atlantisHook := &github.Hook{
Events: []string{"issue_comment", "pull_request", "push"},
Config: map[string]interface{}{
"url": hookURL,
"content_type": "json",
},
Active: github.Bool(true),
}
hook, _, err := g.client.Repositories.CreateHook(ctx, g.ownerName, g.repoName, atlantisHook)
if err != nil {
return 0, err
}
log.Println(hook.GetURL())
return hook.GetID(), nil
}
func (g GithubClient) DeleteAtlantisHook(ctx context.Context, hookID int64) error {
_, err := g.client.Repositories.DeleteHook(ctx, g.ownerName, g.repoName, hookID)
if err != nil {
return err
}
log.Printf("deleted webhook id %d", hookID)
return nil
}
func (g GithubClient) CreatePullRequest(ctx context.Context, title, branchName string) (string, int, error) {
head := fmt.Sprintf("%s:%s", g.ownerName, branchName)
body := ""
base := "main"
newPullRequest := &github.NewPullRequest{Title: &title, Head: &head, Body: &body, Base: &base}
pull, _, err := g.client.PullRequests.Create(ctx, g.ownerName, g.repoName, newPullRequest)
if err != nil {
return "", 0, fmt.Errorf("error while creating new pull request: %v", err)
}
// set pull request url
return pull.GetHTMLURL(), pull.GetNumber(), nil
}
func (g GithubClient) GetAtlantisStatus(ctx context.Context, branchName string) (string, error) {
// check repo status
combinedStatus, _, err := g.client.Repositories.GetCombinedStatus(ctx, g.ownerName, g.repoName, branchName, nil)
if err != nil {
return "", err
}
for _, status := range combinedStatus.Statuses {
if status.GetContext() == "atlantis/plan" {
return status.GetState(), nil
}
}
return "", nil
}
func (g GithubClient) ClosePullRequest(ctx context.Context, pullRequestNumber int) error {
// clean up
_, _, err := g.client.PullRequests.Edit(ctx, g.ownerName, g.repoName, pullRequestNumber, &github.PullRequest{State: github.String("closed")})
if err != nil {
return fmt.Errorf("error while closing new pull request: %v", err)
}
return nil
}
func (g GithubClient) DeleteBranch(ctx context.Context, branchName string) error {
_, err := g.client.Git.DeleteRef(ctx, g.ownerName, g.repoName, branchName)
if err != nil {
return fmt.Errorf("error while deleting branch %s: %v", branchName, err)
}
return nil
}

View File

@@ -17,11 +17,9 @@ import (
"context"
"log"
"os"
"strings"
"fmt"
"github.com/google/go-github/v59/github"
multierror "github.com/hashicorp/go-multierror"
)
@@ -38,30 +36,13 @@ type Project struct {
func main() {
githubUsername := os.Getenv("ATLANTISBOT_GITHUB_USERNAME")
if githubUsername == "" {
log.Fatalf("ATLANTISBOT_GITHUB_USERNAME cannot be empty")
}
githubToken := os.Getenv("ATLANTISBOT_GITHUB_TOKEN")
if githubToken == "" {
log.Fatalf("ATLANTISBOT_GITHUB_TOKEN cannot be empty")
}
atlantisURL := os.Getenv("ATLANTIS_URL")
if atlantisURL == "" {
atlantisURL = defaultAtlantisURL
}
// add /events to the url
atlantisURL = fmt.Sprintf("%s/events", atlantisURL)
ownerName := os.Getenv("GITHUB_REPO_OWNER_NAME")
if ownerName == "" {
ownerName = "runatlantis"
}
repoName := os.Getenv("GITHUB_REPO_NAME")
if repoName == "" {
repoName = "atlantis-tests"
}
// using https to clone the repo
repoURL := fmt.Sprintf("https://%s:%s@github.com/%s/%s.git", githubUsername, githubToken, ownerName, repoName)
cloneDirRoot := os.Getenv("CLONE_DIR")
if cloneDirRoot == "" {
cloneDirRoot = "/tmp/atlantis-tests"
@@ -74,28 +55,18 @@ func main() {
log.Fatalf("failed to clean dir %q before cloning, attempting to continue: %v", cloneDirRoot, err)
}
// create github client
tp := github.BasicAuthTransport{
Username: strings.TrimSpace(githubUsername),
Password: strings.TrimSpace(githubToken),
}
ghClient := github.NewClient(tp.Client())
githubClient := &GithubClient{client: ghClient, username: githubUsername}
githubClient := NewGithubClient()
ctx := context.Background()
// we create atlantis hook once for the repo, since the atlantis server can handle multiple requests
log.Printf("creating atlantis webhook with %s url", atlantisURL)
hookID, err := createAtlantisWebhook(ctx, githubClient, ownerName, repoName, atlantisURL)
hookID, err := githubClient.CreateAtlantisWebhook(ctx, atlantisURL)
if err != nil {
log.Fatalf("error creating atlantis webhook: %v", err)
}
// create e2e test
e2e := E2ETester{
githubClient: githubClient,
repoURL: repoURL,
ownerName: ownerName,
repoName: repoName,
vcsClient: githubClient,
hookID: hookID,
cloneDirRoot: cloneDirRoot,
}
@@ -105,7 +76,7 @@ func main() {
log.Printf("Test Results\n---------------------------\n")
for _, result := range results {
fmt.Printf("Project Type: %s \n", result.projectType)
fmt.Printf("Pull Request Link: %s \n", result.githubPullRequestURL)
fmt.Printf("Pull Request Link: %s \n", result.pullRequestURL)
fmt.Printf("Atlantis Run Status: %s \n", result.testResult)
fmt.Println("---------------------------")
}
@@ -115,37 +86,6 @@ func main() {
}
func createAtlantisWebhook(ctx context.Context, g *GithubClient, ownerName string, repoName string, hookURL string) (int64, error) {
// create atlantis hook
atlantisHook := &github.Hook{
Events: []string{"issue_comment", "pull_request", "push"},
Config: map[string]interface{}{
"url": hookURL,
"content_type": "json",
},
Active: github.Bool(true),
}
// moved to github.go
hook, _, err := g.client.Repositories.CreateHook(ctx, ownerName, repoName, atlantisHook)
if err != nil {
return 0, err
}
log.Println(hook.GetURL())
return hook.GetID(), nil
}
func deleteAtlantisHook(ctx context.Context, g *GithubClient, ownerName string, repoName string, hookID int64) error {
_, err := g.client.Repositories.DeleteHook(ctx, ownerName, repoName, hookID)
if err != nil {
return err
}
log.Printf("deleted webhook id %d", hookID)
return nil
}
func cleanDir(path string) error {
return os.RemoveAll(path)
}
@@ -154,7 +94,7 @@ func startTests(ctx context.Context, e2e E2ETester) ([]*E2EResult, error) {
var testResults []*E2EResult
var testErrors *multierror.Error
// delete webhook when we are done running tests
defer deleteAtlantisHook(ctx, e2e.githubClient, e2e.ownerName, e2e.repoName, e2e.hookID) // nolint: errcheck
defer e2e.vcsClient.DeleteAtlantisHook(ctx, e2e.hookID) // nolint: errcheck
for _, projectType := range projectTypes {
log.Printf("starting e2e test for project type %q", projectType.Name)

26
e2e/vcs.go Normal file
View File

@@ -0,0 +1,26 @@
// Copyright 2017 HootSuite Media Inc.
//
// Licensed under the Apache License, Version 2.0 (the License);
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an AS IS BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Modified hereafter by contributors to runatlantis/atlantis.
package main
import "context"
type VCSClient interface {
Clone(cloneDir string) error
CreateAtlantisWebhook(ctx context.Context, hookURL string) (int64, error)
DeleteAtlantisHook(ctx context.Context, hookID int64) error
CreatePullRequest(ctx context.Context, title, branchName string) (string, int, error)
GetAtlantisStatus(ctx context.Context, branchName string) (string, error)
ClosePullRequest(ctx context.Context, pullRequestNumber int) error
DeleteBranch(ctx context.Context, branchName string) error
}