diff --git a/prplanner/outputs.go b/prplanner/outputs.go index d99b583..b99b004 100644 --- a/prplanner/outputs.go +++ b/prplanner/outputs.go @@ -10,136 +10,127 @@ import ( "regexp" "strings" - tfaplv1beta1 "github.com/utilitywarehouse/terraform-applier/api/v1beta1" + "k8s.io/apimachinery/pkg/types" ) -func (ps *Server) getPendingPlans(ctx context.Context, planRequests *map[string]*tfaplv1beta1.Request, pr pr, repo gitHubRepo, prModules []tfaplv1beta1.Module) { - if ps.isNewPR(pr.Comments.Nodes) { - for _, module := range prModules { - annotated, err := ps.isModuleAnnotated(ctx, module.NamespacedName()) - if err != nil { - ps.Log.Error("error retreiving module annotation", err) - } +func (ps *Server) serveOutputRequests(ctx context.Context, repo gitHubRepo, pr pr) { + outputs := ps.getPendinPRUpdates(ctx, pr) - if annotated { - continue // Skip annotated modules - } - - ps.addNewRequest(ctx, planRequests, module, pr, repo) - ps.Log.Debug("new pr found. creating new plan request", "namespace", module.ObjectMeta.Namespace, "module", module.Name) - } - return + for _, output := range outputs { + ps.postPlanOutput(output, repo) } - - ps.checkLastPRCommit(ctx, planRequests, pr, repo, prModules) - ps.analysePRCommentsForRun(ctx, planRequests, pr, repo, prModules) } -func (ps *Server) getPendinPRUpdates(ctx context.Context, outputs []output, pr pr, prModules []tfaplv1beta1.Module) []output { +func (ps *Server) getPendinPRUpdates(ctx context.Context, pr pr) []output { + var outputs []output // Go through PR comments in reverse order - for _, module := range prModules { - - for i := len(pr.Comments.Nodes) - 1; i >= 0; i-- { - comment := pr.Comments.Nodes[i] - - if strings.Contains(comment.Body, "Received terraform plan request") { - prCommentModule, prCommentReqID, err := ps.findModuleNameInComment(comment.Body) - if err != nil { - ps.Log.Error("error getting module name and req ID from PR comment", err) - return nil - } - - if module.Name == prCommentModule { - planOutput, err := ps.getPlanOutputFromRedis(ctx, module, prCommentReqID, pr) - if err != nil { - ps.Log.Error("can't check plan output in Redis:", err) - break - } - - if planOutput == "" { - break // plan output is not ready yet - } - - commentBody := prComment{ - Body: fmt.Sprintf( - "Terraform plan output for module `%s`\n```terraform\n%s\n```", - module.Name, - planOutput, - ), - } - newOutput := output{ - module: module, - commentID: comment.DatabaseID, - prNumber: pr.Number, - body: commentBody, - } - outputs = append(outputs, newOutput) - break - } - } - } + for i := len(pr.Comments.Nodes) - 1; i >= 0; i-- { + comment := pr.Comments.Nodes[i] + ps.checkPRCommentsForOutputRequests(ctx, &outputs, pr, comment) } return outputs } -func (ps *Server) findModuleNameInComment(commentBody string) (string, string, error) { +func (ps *Server) checkPRCommentsForOutputRequests(ctx context.Context, outputs *[]output, pr pr, comment prComment) { + + if strings.Contains(comment.Body, "Received terraform plan request") { + prCommentModule, err := ps.findModuleNameInComment(comment.Body) + if err != nil { + ps.Log.Error("error getting module name and req ID from PR comment", err) + return + } + + // if module.Name == prCommentModule { + planOutput, err := ps.getPlanOutputFromRedis(ctx, pr, "", prCommentModule) + if err != nil { + ps.Log.Error("can't check plan output in Redis:", err) + return + } + + if planOutput == "" { + return // plan output is not ready yet + } + + commentBody := prComment{ + Body: fmt.Sprintf( + "Terraform plan output for module `%s`\n```terraform\n%s\n```", + prCommentModule, + planOutput, + ), + } + newOutput := output{ + module: prCommentModule, + commentID: comment.DatabaseID, + prNumber: pr.Number, + body: commentBody, + } + *outputs = append(*outputs, newOutput) + // } + } +} + +// TODO: move re1 outside of func +func (ps *Server) findModuleNameInComment(commentBody string) (types.NamespacedName, error) { // Search for module name and req ID re1 := regexp.MustCompile(`Module: ` + "`" + `(.+?)` + "`" + ` Request ID: ` + "`" + `(.+?)` + "`") - matches1 := re1.FindStringSubmatch(commentBody) - if len(matches1) == 3 { - return matches1[1], matches1[2], nil + matches := re1.FindStringSubmatch(commentBody) + + if len(matches) == 3 { + namespacedName := strings.Split(matches[1], "/") + return types.NamespacedName{Namespace: namespacedName[0], Name: namespacedName[1]}, nil + } + return types.NamespacedName{}, nil +} + +func (ps *Server) findModuleNameInRunRequestComment(commentBody string) (string, error) { + // TODO: Match "@terraform-applier plan " // Search for module name only re2 := regexp.MustCompile("`([^`]*)`") - matches2 := re2.FindStringSubmatch(commentBody) + matches := re2.FindStringSubmatch(commentBody) - if len(matches2) > 1 { - return matches2[1], "", nil + if len(matches) > 1 { + return matches[1], nil } - return "", "", fmt.Errorf("module data not found") + return "", fmt.Errorf("module data not found") } -func (ps *Server) postPlanOutput(outputs []output) { - for _, output := range outputs { - _, err := ps.postToGitHub(output.module.Spec.RepoURL, "PATCH", output.commentID, output.prNumber, output.body) - if err != nil { - ps.Log.Error("error posting PR comment:", err) - } +func (ps *Server) postPlanOutput(output output, repo gitHubRepo) { + _, err := ps.postToGitHub(repo, "PATCH", output.commentID, output.prNumber, output.body) + if err != nil { + ps.Log.Error("error posting PR comment:", err) } } -func (ps *Server) getPlanOutputFromRedis(ctx context.Context, module tfaplv1beta1.Module, prCommentReqID string, pr pr) (string, error) { - lastRun, err := ps.RedisClient.PRLastRun(ctx, module.NamespacedName(), pr.Number) +func (ps *Server) getPlanOutputFromRedis(ctx context.Context, pr pr, prCommentReqID string, module types.NamespacedName) (string, error) { + moduleRuns, err := ps.RedisClient.Runs(ctx, module) if err != nil { return "", err } - if lastRun == nil { - return "", nil - } - - if prCommentReqID == lastRun.Request.ID { - return lastRun.Output, nil + for _, run := range moduleRuns { + if run.Request.ID == prCommentReqID { + return run.Output, nil + } } return "", nil } -func (ps *Server) postToGitHub(repoURL, method string, commentID, prNumber int, commentBody prComment) (int, error) { + +func (ps *Server) postToGitHub(repo gitHubRepo, method string, commentID, prNumber int, commentBody prComment) (int, error) { // TODO: Update credentials // Temporarily using my own github user and token username := "DTLP" token := os.Getenv("GITHUB_TOKEN") - repoName := repoNameFromURL(repoURL) - // Post a comment - url := fmt.Sprintf("https://api.github.com/repos/%s/issues/%d/comments", repoName, prNumber) + url := fmt.Sprintf("https://api.github.com/repos/%s/%s/issues/%d/comments", repo.owner, repo.name, prNumber) if method == "PATCH" { - url = fmt.Sprintf("https://api.github.com/repos/%s/issues/comments/%d", repoName, commentID) + url = fmt.Sprintf("https://api.github.com/repos/%s/%/issues/comments/%d", repo.owner, repo.name, commentID) } // Marshal the comment object to JSON @@ -183,11 +174,11 @@ func (ps *Server) postToGitHub(repoURL, method string, commentID, prNumber int, return commentResponse.ID, nil } -func repoNameFromURL(url string) string { - trimmedURL := strings.TrimSuffix(url, ".git") - parts := strings.Split(trimmedURL, ":") - if len(parts) < 2 { - return "" - } - return parts[1] -} +// func repoNameFromURL(url string) string { +// trimmedURL := strings.TrimSuffix(url, ".git") +// parts := strings.Split(trimmedURL, ":") +// if len(parts) < 2 { +// return "" +// } +// return parts[1] +// } diff --git a/prplanner/requests.go b/prplanner/requests.go index b6c26d9..f290bb2 100644 --- a/prplanner/requests.go +++ b/prplanner/requests.go @@ -14,26 +14,155 @@ import ( "k8s.io/apimachinery/pkg/types" ) -func (ps *Server) pathBelongsToModule(pathList []string, module tfaplv1beta1.Module) bool { - for _, path := range pathList { - if strings.Contains(path, module.Spec.Path) { +func (ps *Server) servePlanRequests(ctx context.Context, repo gitHubRepo, pr pr, module tfaplv1beta1.Module) { + planRequests := make(map[string]*tfaplv1beta1.Request) + ps.getPendingPlans(ctx, &planRequests, repo, pr, module) + ps.requestPlan(ctx, &planRequests, pr, repo) +} + +func (ps *Server) getPendingPlans(ctx context.Context, planRequests *map[string]*tfaplv1beta1.Request, repo gitHubRepo, pr pr, module tfaplv1beta1.Module) { + // 1. Check if module is annotated + annotated, err := ps.isModuleAnnotated(ctx, module.NamespacedName()) + if err != nil { + ps.Log.Error("error checking module annotation", err) + } + + if annotated { + return // no need to proceed if there's already a plan request for the module + } + + // 2. loop through commits from latest to oldest + ps.checkPRCommits(ctx, planRequests, repo, pr, module) + + // 3. loop through comments + ps.checkPRCommentsForPlanRequests(ctx, planRequests, pr, repo, module) + +} + +func (ps *Server) checkPRCommits(ctx context.Context, planRequests *map[string]*tfaplv1beta1.Request, repo gitHubRepo, pr pr, module tfaplv1beta1.Module) { + for i := len(pr.Commits.Nodes) - 1; i >= 0; i-- { + commit := pr.Commits.Nodes[i].Commit + + // 0. check comments if output posted for the commit hash + outputPosted := ps.commentPostedForCommit(pr, commit.Oid) + if outputPosted { + return + } + + // 1. check commit hashes in redis + _, err := ps.RedisClient.PRRun(ctx, module.NamespacedName(), pr.Number, commit.Oid) + if err == nil { + break + } + + // 2. verify module needs to be planned based on files changed + commitBelongsToModule, err := ps.doesCommitBelongToModule(repo, commit.Oid, module) + if err != nil { + ps.Log.Error("", err) + } + if !commitBelongsToModule { + return + } + + // 3. request run + ps.addNewRequest(ctx, planRequests, module, pr, repo) + } +} + +func (ps *Server) commentPostedForCommit(pr pr, commitID string) bool { + for i := len(pr.Comments.Nodes) - 1; i >= 0; i-- { + comment := pr.Comments.Nodes[i] + + if strings.Contains(comment.Body, commitID) { return true } } + return false } -func (ps *Server) isNewPR(prComments []prComment) bool { - // A PR is considered new when there are no comments posted by terraform-applier - for _, comment := range prComments { - if strings.Contains(comment.Body, "Received terraform plan request") || strings.Contains(comment.Body, "Terraform plan output for module") { - return false +func (ps *Server) doesCommitBelongToModule(repo gitHubRepo, commitHash string, module tfaplv1beta1.Module) (bool, error) { + + filesChangedInCommit, err := ps.getCommitFilesChanged(repo.name, commitHash) + if err != nil { + return false, fmt.Errorf("error getting commit info: %w", err) + } + + moduleUpdated := ps.pathBelongsToModule(filesChangedInCommit, module) + if !moduleUpdated { + return false, nil + } + + return true, nil +} + +func (ps *Server) checkPRCommentsForPlanRequests(ctx context.Context, planRequests *map[string]*tfaplv1beta1.Request, pr pr, repo gitHubRepo, module tfaplv1beta1.Module) { + // TODO: Allow users manually request plan runs for PRs with a large number of modules, + // but only ONE module at a time + + // Go through PR comments in reverse order + for i := len(pr.Comments.Nodes) - 1; i >= 0; i-- { + comment := pr.Comments.Nodes[i] + + // Check if user requested terraform plan run + if strings.Contains(comment.Body, "@terraform-applier plan") { + + // Give users ability to request plan for all modules or just one + // terraform-applier plan [``] + prCommentModule, _ := ps.findModuleNameInComment(comment.Body) + + if prCommentModule.Name != "" && module.Name != prCommentModule.Name { + break + } + + ps.addNewRequest(ctx, planRequests, module, pr, repo) + ps.Log.Debug("new plan request received. creating new plan request", "namespace", module.ObjectMeta.Namespace, "module", module.Name) } } +} + +// func (ps *Server) getPendingPlans(ctx context.Context, planRequests *map[string]*tfaplv1beta1.Request, pr pr, repo gitHubRepo, prModules []tfaplv1beta1.Module) { +// if ps.isNewPR(pr.Comments.Nodes) { +// for _, module := range prModules { +// annotated, err := ps.isModuleAnnotated(ctx, module.NamespacedName()) +// if err != nil { +// ps.Log.Error("error retreiving module annotation", err) +// } +// +// if annotated { +// continue // Skip annotated modules +// } +// +// ps.addNewRequest(ctx, planRequests, module, pr, repo) +// ps.Log.Debug("new pr found. creating new plan request", "namespace", module.ObjectMeta.Namespace, "module", module.Name) +// } +// return +// } +// +// ps.checkLastPRCommit(ctx, planRequests, pr, repo, prModules) +// ps.analysePRCommentsForRun(ctx, planRequests, pr, repo, prModules) +// } - return true +func (ps *Server) pathBelongsToModule(pathList []string, module tfaplv1beta1.Module) bool { + for _, path := range pathList { + if strings.Contains(path, module.Spec.Path) { + return true + } + } + return false } +// func (ps *Server) isNewPR(prComments []prComment) bool { +// // A PR is considered new when there are no comments posted by terraform-applier +// for _, comment := range prComments { +// if strings.Contains(comment.Body, "Received terraform plan request") || strings.Contains(comment.Body, "Terraform plan output for module") { +// return false +// } +// } +// +// return true +// } + func (ps *Server) addNewRequest(ctx context.Context, requests *map[string]*tfaplv1beta1.Request, module tfaplv1beta1.Module, pr pr, repo gitHubRepo) { if _, exists := (*requests)[module.Name]; exists { return // module is already in the requests list @@ -44,7 +173,7 @@ func (ps *Server) addNewRequest(ctx context.Context, requests *map[string]*tfapl commentBody := prComment{ Body: fmt.Sprintf("Received terraform plan request. Module: `%s` Request ID: `%s`", module.Name, newReq.ID), } - commentID, err := ps.postToGitHub(module.Spec.RepoURL, "POST", 0, pr.Number, commentBody) + commentID, err := ps.postToGitHub(repo, "POST", 0, pr.Number, commentBody) if err != nil { ps.Log.Error("error posting PR comment:", err) } @@ -68,51 +197,51 @@ func (ps *Server) addNewRequest(ctx context.Context, requests *map[string]*tfapl } -func (ps *Server) checkLastPRCommit(ctx context.Context, planRequests *map[string]*tfaplv1beta1.Request, pr pr, repo gitHubRepo, prModules []tfaplv1beta1.Module) { - for _, module := range prModules { - // TODO: Change order of checks? - // What's the point in doing all of that if module might be annotated at the end? - // - prLastCommitHash := pr.Commits.Nodes[0].Commit.Oid - localRepoCommitHash, err := ps.Repos.Hash(ctx, module.Spec.RepoURL, pr.HeadRefName, module.Spec.Path) - key := sysutil.DefaultPRLastRunsKey(module.NamespacedName(), pr.Number) - redisCommitHash, err := ps.RedisClient.GetCommitHash(ctx, key) - if err != nil { - ps.Log.Error("error getting module data from redis", err) - } - - // TODO: - // Get PR last commit hash - // Find plan output in redis for the same commit - - if prLastCommitHash != localRepoCommitHash { - // Skip check as local git repo is not up-to-date yet - continue - } - - if prLastCommitHash != redisCommitHash { - // Request plan run if corresponding plan output is not yet in redis - moduleUpdated, err := ps.isModuleUpdated(repo, prLastCommitHash, module) - if err != nil { - ps.Log.Error("error getting a list of modules", err) - } - - if moduleUpdated { - // Continue if this commit changes the files related to current module - annotated, err := ps.isModuleAnnotated(ctx, module.NamespacedName()) - if err != nil { - ps.Log.Error("error retreiving module annotation", err) - } - - if !annotated { - // Create a new request if kube module is not yet annotated - ps.addNewRequest(ctx, planRequests, module, pr, repo) - ps.Log.Debug("new commit found. creating new plan request", "namespace", module.ObjectMeta.Namespace, "module", module.Name) - } - } - } - } -} +// func (ps *Server) checkLastPRCommit(ctx context.Context, planRequests *map[string]*tfaplv1beta1.Request, pr pr, repo gitHubRepo, prModules []tfaplv1beta1.Module) { +// for _, module := range prModules { +// // TODO: Change order of checks? +// // What's the point in doing all of that if module might be annotated at the end? +// // +// prLastCommitHash := pr.Commits.Nodes[0].Commit.Oid +// localRepoCommitHash, err := ps.Repos.Hash(ctx, module.Spec.RepoURL, pr.HeadRefName, module.Spec.Path) +// key := sysutil.DefaultPRLastRunsKey(module.NamespacedName(), pr.Number) +// redisCommitHash, err := ps.RedisClient.GetCommitHash(ctx, key) +// if err != nil { +// ps.Log.Error("error getting module data from redis", err) +// } +// +// // TODO: +// // Get PR last commit hash +// // Find plan output in redis for the same commit +// +// if prLastCommitHash != localRepoCommitHash { +// // Skip check as local git repo is not up-to-date yet +// continue +// } +// +// if prLastCommitHash != redisCommitHash { +// // Request plan run if corresponding plan output is not yet in redis +// moduleUpdated, err := ps.isModuleUpdated(repo, prLastCommitHash, module) +// if err != nil { +// ps.Log.Error("error getting a list of modules", err) +// } +// +// if moduleUpdated { +// // Continue if this commit changes the files related to current module +// annotated, err := ps.isModuleAnnotated(ctx, module.NamespacedName()) +// if err != nil { +// ps.Log.Error("error retreiving module annotation", err) +// } +// +// if !annotated { +// // Create a new request if kube module is not yet annotated +// ps.addNewRequest(ctx, planRequests, module, pr, repo) +// ps.Log.Debug("new commit found. creating new plan request", "namespace", module.ObjectMeta.Namespace, "module", module.Name) +// } +// } +// } +// } +// } func (ps *Server) isModuleAnnotated(ctx context.Context, key types.NamespacedName) (bool, error) { module, err := sysutil.GetModule(ctx, ps.ClusterClt, key) @@ -129,19 +258,19 @@ func (ps *Server) isModuleAnnotated(ctx context.Context, key types.NamespacedNam return false, nil } -func (ps *Server) isModuleUpdated(repo gitHubRepo, commitHash string, module tfaplv1beta1.Module) (bool, error) { - filesChangedInCommit, err := ps.getCommitFilesChanged(repo.name, commitHash) - if err != nil { - return false, fmt.Errorf("error getting commit info: %w", err) - } - - moduleUpdated := ps.pathBelongsToModule(filesChangedInCommit, module) - if !moduleUpdated { - return false, nil - } - - return true, nil -} +// func (ps *Server) isModuleUpdated(repo gitHubRepo, commitHash string, module tfaplv1beta1.Module) (bool, error) { +// filesChangedInCommit, err := ps.getCommitFilesChanged(repo.name, commitHash) +// if err != nil { +// return false, fmt.Errorf("error getting commit info: %w", err) +// } +// +// moduleUpdated := ps.pathBelongsToModule(filesChangedInCommit, module) +// if !moduleUpdated { +// return false, nil +// } +// +// return true, nil +// } func (ps *Server) getCommitFilesChanged(repoName, commitHash string) ([]string, error) { // TODO: Replace go-git package with git command @@ -169,41 +298,41 @@ func (ps *Server) getCommitFilesChanged(repoName, commitHash string) ([]string, return filesChanged, nil } -func (ps *Server) analysePRCommentsForRun(ctx context.Context, planRequests *map[string]*tfaplv1beta1.Request, pr pr, repo gitHubRepo, prModules []tfaplv1beta1.Module) { - for _, module := range prModules { - - // Go through PR comments in reverse order - for i := len(pr.Comments.Nodes) - 1; i >= 0; i-- { - comment := pr.Comments.Nodes[i] - - // Skip module if plan job request is already received or completed - if strings.Contains(comment.Body, "Received terraform plan request") || strings.Contains(comment.Body, "Terraform plan output for module") { - prCommentModule, _, err := ps.findModuleNameInComment(comment.Body) - if err != nil { - ps.Log.Error("error getting module name from PR comment", err) - } - - if module.Name == prCommentModule { - break // skip as plan output or request request ack is already posted for the module - } - } - - // Check if user requested terraform plan run - if strings.Contains(comment.Body, "@terraform-applier plan") { - // Give users ability to request plan for all modules or just one - // terraform-applier plan [``] - prCommentModule, _, _ := ps.findModuleNameInComment(comment.Body) - - if prCommentModule != "" && module.Name != prCommentModule { - break - } - - ps.addNewRequest(ctx, planRequests, module, pr, repo) - ps.Log.Debug("new plan request received. creating new plan request", "namespace", module.ObjectMeta.Namespace, "module", module.Name) - } - } - } -} +// func (ps *Server) analysePRCommentsForRun(ctx context.Context, planRequests *map[string]*tfaplv1beta1.Request, pr pr, repo gitHubRepo, prModules []tfaplv1beta1.Module) { +// for _, module := range prModules { +// +// // Go through PR comments in reverse order +// for i := len(pr.Comments.Nodes) - 1; i >= 0; i-- { +// comment := pr.Comments.Nodes[i] +// +// // Skip module if plan job request is already received or completed +// if strings.Contains(comment.Body, "Received terraform plan request") || strings.Contains(comment.Body, "Terraform plan output for module") { +// prCommentModule, _, err := ps.findModuleNameInComment(comment.Body) +// if err != nil { +// ps.Log.Error("error getting module name from PR comment", err) +// } +// +// if module.Name == prCommentModule { +// break // skip as plan output or request request ack is already posted for the module +// } +// } +// +// // Check if user requested terraform plan run +// if strings.Contains(comment.Body, "@terraform-applier plan") { +// // Give users ability to request plan for all modules or just one +// // terraform-applier plan [``] +// prCommentModule, _, _ := ps.findModuleNameInComment(comment.Body) +// +// if prCommentModule != "" && module.Name != prCommentModule { +// break +// } +// +// ps.addNewRequest(ctx, planRequests, module, pr, repo) +// ps.Log.Debug("new plan request received. creating new plan request", "namespace", module.ObjectMeta.Namespace, "module", module.Name) +// } +// } +// } +// } func (ps *Server) requestPlan(ctx context.Context, planRequests *map[string]*tfaplv1beta1.Request, pr pr, repo gitHubRepo) { for module, req := range *planRequests { diff --git a/prplanner/server.go b/prplanner/server.go index f530b60..1dff9e2 100644 --- a/prplanner/server.go +++ b/prplanner/server.go @@ -14,6 +14,7 @@ import ( "github.com/go-resty/resty/v2" + "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/kubernetes" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -78,7 +79,7 @@ type prFiles []struct { } type output struct { - module tfaplv1beta1.Module + module types.NamespacedName body prComment commentID int prNumber int @@ -128,24 +129,58 @@ func (ps *Server) Start(ctx context.Context) { // Loop through all open PRs for _, pr := range response.Data.Repository.PullRequests.Nodes { + // 1. Verify if pr belongs to module based on files changed prModules, err := ps.getPRModuleList(pr.Files.Nodes, kubeModuleList) if err != nil { ps.Log.Error("error getting a list of modules in PR", err) } - planRequests := make(map[string]*tfaplv1beta1.Request) - ps.getPendingPlans(ctx, &planRequests, pr, repo, prModules) - ps.requestPlan(ctx, &planRequests, pr, repo) + // 2. compare remote and local repos last commit hashes + upToDate, err := ps.isLocalRepoUpToDate(ctx, repo, pr) + if err != nil { + ps.Log.Error("error fetching local repo last commit hash", err) + } - var outputs []output - outputs = ps.getPendinPRUpdates(ctx, outputs, pr, prModules) - ps.postPlanOutput(outputs) + if !upToDate { + break // skip as local repo isn't yet in sync with the remote + } + + // 3. loop through pr modules + ps.actionOnPRModules(ctx, repo, pr, prModules) } } } } } +func (ps *Server) actionOnPRModules(ctx context.Context, repo gitHubRepo, pr pr, prModules []tfaplv1beta1.Module) { + + for _, module := range prModules { + fmt.Println("module:", module.Name) + + // 1. look for pending plan requests + ps.servePlanRequests(ctx, repo, pr, module) + } + + // 2. look for pending outputs + ps.serveOutputRequests(ctx, repo, pr) +} + +func (ps *Server) isLocalRepoUpToDate(ctx context.Context, repo gitHubRepo, pr pr) (bool, error) { + repoURL := "git@github.com:" + repo.owner + "/" + repo.name + ".git" + prLastCommitHash := pr.Commits.Nodes[0].Commit.Oid + localRepoCommitHash, err := ps.Repos.Hash(ctx, repoURL, pr.HeadRefName, ".") + if err != nil { + return false, nil + } + + if prLastCommitHash != localRepoCommitHash { + return false, nil + } + + return true, nil +} + func (ps *Server) getKubeModuleList(ctx context.Context) ([]tfaplv1beta1.Module, error) { moduleList := &tfaplv1beta1.ModuleList{} modules := []tfaplv1beta1.Module{} @@ -195,7 +230,7 @@ func (ps *Server) getOpenPullRequests(ctx context.Context, repoOwner, repoName s nodes { number headRefName - commits(last: 1) { + commits(last: 20) { nodes { commit { oid diff --git a/runner/runner.go b/runner/runner.go index 5599e5f..5366f6a 100644 --- a/runner/runner.go +++ b/runner/runner.go @@ -458,7 +458,7 @@ func (r *Runner) updateRedis(ctx context.Context, run *tfaplv1beta1.Run) error { // if its PR run only update relevant PR key if run.Request.Type == tfaplv1beta1.PRPlan { - return r.Redis.SetPRLastRun(ctx, run) + return r.Redis.SetPRRun(ctx, run) } // set default last run diff --git a/sysutil/redis.go b/sysutil/redis.go index 5e48139..a581662 100644 --- a/sysutil/redis.go +++ b/sysutil/redis.go @@ -21,13 +21,13 @@ var ( type RedisInterface interface { DefaultLastRun(ctx context.Context, module types.NamespacedName) (*tfaplv1beta1.Run, error) DefaultApply(ctx context.Context, module types.NamespacedName) (*tfaplv1beta1.Run, error) - PRLastRun(ctx context.Context, module types.NamespacedName, pr int) (*tfaplv1beta1.Run, error) + PRRun(ctx context.Context, module types.NamespacedName, pr int, hash string) (*tfaplv1beta1.Run, error) Runs(ctx context.Context, module types.NamespacedName) ([]*tfaplv1beta1.Run, error) GetCommitHash(ctx context.Context, key string) (string, error) SetDefaultLastRun(ctx context.Context, run *tfaplv1beta1.Run) error SetDefaultApply(ctx context.Context, run *tfaplv1beta1.Run) error - SetPRLastRun(ctx context.Context, run *tfaplv1beta1.Run) error + SetPRRun(ctx context.Context, run *tfaplv1beta1.Run) error } type Redis struct { @@ -46,8 +46,8 @@ func defaultLastApplyKey(module types.NamespacedName) string { return fmt.Sprintf("%sdefault:lastApply", keyPrefix(module)) } -func DefaultPRLastRunsKey(module types.NamespacedName, pr int) string { - return fmt.Sprintf("%sPR:%d:lastRun", keyPrefix(module), pr) +func DefaultPRLastRunsKey(module types.NamespacedName, pr int, hash string) string { + return fmt.Sprintf("%sPR:%d:%s", keyPrefix(module), pr, hash) } // DefaultLastRun will return last run result for the default branch @@ -61,8 +61,8 @@ func (r Redis) DefaultApply(ctx context.Context, module types.NamespacedName) (* } // PRLastRun will return last run result for the given PR branch -func (r Redis) PRLastRun(ctx context.Context, module types.NamespacedName, pr int) (*tfaplv1beta1.Run, error) { - return r.getKV(ctx, DefaultPRLastRunsKey(module, pr)) +func (r Redis) PRRun(ctx context.Context, module types.NamespacedName, pr int, hash string) (*tfaplv1beta1.Run, error) { + return r.getKV(ctx, DefaultPRLastRunsKey(module, pr, hash)) } // Runs will return all the runs stored for the given module @@ -94,9 +94,9 @@ func (r Redis) SetDefaultApply(ctx context.Context, run *tfaplv1beta1.Run) error return r.setKV(ctx, defaultLastApplyKey(run.Module), run, 0) } -// SetPRLastRun puts given run in to cache with expiration -func (r Redis) SetPRLastRun(ctx context.Context, run *tfaplv1beta1.Run) error { - return r.setKV(ctx, DefaultPRLastRunsKey(run.Module, run.Request.PR.Number), run, PRKeyExpirationDur) +// SetPRRun puts given run in to cache with expiration +func (r Redis) SetPRRun(ctx context.Context, run *tfaplv1beta1.Run) error { + return r.setKV(ctx, DefaultPRLastRunsKey(run.Module, run.Request.PR.Number, run.CommitHash), run, PRKeyExpirationDur) } func (r Redis) setKV(ctx context.Context, key string, run *tfaplv1beta1.Run, exp time.Duration) error { diff --git a/sysutil/redis_mock.go b/sysutil/redis_mock.go index 0a52d9a..81a28c8 100644 --- a/sysutil/redis_mock.go +++ b/sysutil/redis_mock.go @@ -66,19 +66,34 @@ func (mr *MockRedisInterfaceMockRecorder) DefaultLastRun(arg0, arg1 interface{}) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DefaultLastRun", reflect.TypeOf((*MockRedisInterface)(nil).DefaultLastRun), arg0, arg1) } -// PRLastRun mocks base method. -func (m *MockRedisInterface) PRLastRun(arg0 context.Context, arg1 types.NamespacedName, arg2 int) (*v1beta1.Run, error) { +// GetCommitHash mocks base method. +func (m *MockRedisInterface) GetCommitHash(arg0 context.Context, arg1 string) (string, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "PRLastRun", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "GetCommitHash", arg0, arg1) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCommitHash indicates an expected call of GetCommitHash. +func (mr *MockRedisInterfaceMockRecorder) GetCommitHash(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCommitHash", reflect.TypeOf((*MockRedisInterface)(nil).GetCommitHash), arg0, arg1) +} + +// PRRun mocks base method. +func (m *MockRedisInterface) PRRun(arg0 context.Context, arg1 types.NamespacedName, arg2 int, arg3 string) (*v1beta1.Run, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PRRun", arg0, arg1, arg2, arg3) ret0, _ := ret[0].(*v1beta1.Run) ret1, _ := ret[1].(error) return ret0, ret1 } -// PRLastRun indicates an expected call of PRLastRun. -func (mr *MockRedisInterfaceMockRecorder) PRLastRun(arg0, arg1, arg2 interface{}) *gomock.Call { +// PRRun indicates an expected call of PRRun. +func (mr *MockRedisInterfaceMockRecorder) PRRun(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PRLastRun", reflect.TypeOf((*MockRedisInterface)(nil).PRLastRun), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PRRun", reflect.TypeOf((*MockRedisInterface)(nil).PRRun), arg0, arg1, arg2, arg3) } // Runs mocks base method. @@ -124,16 +139,16 @@ func (mr *MockRedisInterfaceMockRecorder) SetDefaultLastRun(arg0, arg1 interface return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDefaultLastRun", reflect.TypeOf((*MockRedisInterface)(nil).SetDefaultLastRun), arg0, arg1) } -// SetPRLastRun mocks base method. -func (m *MockRedisInterface) SetPRLastRun(arg0 context.Context, arg1 *v1beta1.Run) error { +// SetPRRun mocks base method. +func (m *MockRedisInterface) SetPRRun(arg0 context.Context, arg1 *v1beta1.Run) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SetPRLastRun", arg0, arg1) + ret := m.ctrl.Call(m, "SetPRRun", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } -// SetPRLastRun indicates an expected call of SetPRLastRun. -func (mr *MockRedisInterfaceMockRecorder) SetPRLastRun(arg0, arg1 interface{}) *gomock.Call { +// SetPRRun indicates an expected call of SetPRRun. +func (mr *MockRedisInterfaceMockRecorder) SetPRRun(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetPRLastRun", reflect.TypeOf((*MockRedisInterface)(nil).SetPRLastRun), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetPRRun", reflect.TypeOf((*MockRedisInterface)(nil).SetPRRun), arg0, arg1) }