From ac83457c36f521c580d4a63d743c9b14b495dffb Mon Sep 17 00:00:00 2001 From: Luke Kysow Date: Sat, 1 Jul 2017 18:40:43 -0700 Subject: [PATCH] Fork workflows (#64) * Name LockURL route * Support pull requests from forks --- server/apply_executor.go | 22 +++++++++++----------- server/command_handler.go | 13 +++++++------ server/event_parser.go | 26 +++++++++++++++++--------- server/github_client.go | 25 +++++++++++++++++++------ server/github_status.go | 2 +- server/help_executor.go | 2 +- server/plan_executor.go | 20 ++++++++++---------- server/server.go | 22 ++++++++++++---------- server/workspace.go | 11 ++++------- 9 files changed, 82 insertions(+), 61 deletions(-) diff --git a/server/apply_executor.go b/server/apply_executor.go index aeecd57343..a2068225ed 100644 --- a/server/apply_executor.go +++ b/server/apply_executor.go @@ -61,18 +61,18 @@ func (n NoPlansFailure) Template() *CompiledTemplate { } func (a *ApplyExecutor) execute(ctx *CommandContext, github *GithubClient) { - if a.concurrentRunLocker.TryLock(ctx.Repo.FullName, ctx.Command.environment, ctx.Pull.Num) != true { + if a.concurrentRunLocker.TryLock(ctx.BaseRepo.FullName, ctx.Command.environment, ctx.Pull.Num) != true { ctx.Log.Info("run was locked by a concurrent run") - github.CreateComment(ctx.Repo, ctx.Pull, "This environment is currently locked by another command that is running for this pull request. Wait until command is complete and try again") + github.CreateComment(ctx.BaseRepo, ctx.Pull, "This environment is currently locked by another command that is running for this pull request. Wait until command is complete and try again") return } - defer a.concurrentRunLocker.Unlock(ctx.Repo.FullName, ctx.Command.environment, ctx.Pull.Num) + defer a.concurrentRunLocker.Unlock(ctx.BaseRepo.FullName, ctx.Command.environment, ctx.Pull.Num) - a.githubStatus.Update(ctx.Repo, ctx.Pull, Pending, ApplyStep) + a.githubStatus.Update(ctx.BaseRepo, ctx.Pull, Pending, ApplyStep) res := a.setupAndApply(ctx) res.Command = Apply comment := a.githubCommentRenderer.render(res, ctx.Log.History.String(), ctx.Command.verbose) - github.CreateComment(ctx.Repo, ctx.Pull, comment) + github.CreateComment(ctx.BaseRepo, ctx.Pull, comment) } func (a *ApplyExecutor) setupAndApply(ctx *CommandContext) ExecutionResult { @@ -86,7 +86,7 @@ func (a *ApplyExecutor) setupAndApply(ctx *CommandContext) ExecutionResult { repoDir, err := a.workspace.GetWorkspace(ctx) if err != nil { ctx.Log.Err(err.Error()) - a.githubStatus.Update(ctx.Repo, ctx.Pull, Error, ApplyStep) + a.githubStatus.Update(ctx.BaseRepo, ctx.Pull, Error, ApplyStep) return ExecutionResult{SetupError: GeneralError{errors.New("Workspace missing, please plan again")}} } @@ -100,7 +100,7 @@ func (a *ApplyExecutor) setupAndApply(ctx *CommandContext) ExecutionResult { if !info.IsDir() && info.Name() == ctx.Command.environment+".tfplan" { rel, _ := filepath.Rel(repoDir, filepath.Dir(path)) plans = append(plans, models.Plan{ - Project: models.NewProject(ctx.Repo.FullName, rel), + Project: models.NewProject(ctx.BaseRepo.FullName, rel), LocalPath: path, }) } @@ -109,7 +109,7 @@ func (a *ApplyExecutor) setupAndApply(ctx *CommandContext) ExecutionResult { if len(plans) == 0 { failure := "found 0 plans for that environment" ctx.Log.Warn(failure) - a.githubStatus.Update(ctx.Repo, ctx.Pull, Failure, ApplyStep) + a.githubStatus.Update(ctx.BaseRepo, ctx.Pull, Failure, ApplyStep) return ExecutionResult{SetupFailure: NoPlansFailure{}} } @@ -240,16 +240,16 @@ func (a *ApplyExecutor) apply(ctx *CommandContext, repoDir string, plan models.P } func (a *ApplyExecutor) isApproved(ctx *CommandContext) (bool, ExecutionResult) { - ok, err := a.github.PullIsApproved(ctx.Repo, ctx.Pull) + ok, err := a.github.PullIsApproved(ctx.BaseRepo, ctx.Pull) if err != nil { msg := fmt.Sprintf("failed to determine if pull request was approved: %v", err) ctx.Log.Err(msg) - a.githubStatus.Update(ctx.Repo, ctx.Pull, Error, ApplyStep) + a.githubStatus.Update(ctx.BaseRepo, ctx.Pull, Error, ApplyStep) return false, ExecutionResult{SetupError: GeneralError{errors.New(msg)}} } if !ok { ctx.Log.Info("pull request was not approved") - a.githubStatus.Update(ctx.Repo, ctx.Pull, Failure, ApplyStep) + a.githubStatus.Update(ctx.BaseRepo, ctx.Pull, Failure, ApplyStep) return false, ExecutionResult{SetupFailure: PullNotApprovedFailure{}} } return true, ExecutionResult{} diff --git a/server/command_handler.go b/server/command_handler.go index 0d722830a2..59e1f45faf 100644 --- a/server/command_handler.go +++ b/server/command_handler.go @@ -44,27 +44,28 @@ type Command struct { } func (c *CommandHandler) ExecuteCommand(ctx *CommandContext) { - src := fmt.Sprintf("%s/pull/%d", ctx.Repo.FullName, ctx.Pull.Num) - // it'e safe to reuse the underlying logger e.logger.Log + src := fmt.Sprintf("%s/pull/%d", ctx.BaseRepo.FullName, ctx.Pull.Num) + // it's safe to reuse the underlying logger e.logger.Log ctx.Log = logging.NewSimpleLogger(src, c.logger.Log, true, c.logger.Level) defer c.recover(ctx) // need to get additional data from the PR - ghPull, _, err := c.githubClient.GetPullRequest(ctx.Repo, ctx.Pull.Num) + ghPull, _, err := c.githubClient.GetPullRequest(ctx.BaseRepo, ctx.Pull.Num) if err != nil { ctx.Log.Err("pull request data api call failed: %v", err) return } - pull, err := c.eventParser.ExtractPullData(ghPull) + pull, headRepo, err := c.eventParser.ExtractPullData(ghPull) if err != nil { ctx.Log.Err("failed to extract required fields from comment data: %v", err) return } ctx.Pull = pull + ctx.HeadRepo = headRepo if ghPull.GetState() != "open" { ctx.Log.Info("command run on closed pull request") - c.githubClient.CreateComment(ctx.Repo, ctx.Pull, "Atlantis commands can't be run on closed pull requests") + c.githubClient.CreateComment(ctx.BaseRepo, ctx.Pull, "Atlantis commands can't be run on closed pull requests") return } @@ -88,7 +89,7 @@ func (c *CommandHandler) SetLockURL(f func(id string) (url string)) { func (c *CommandHandler) recover(ctx *CommandContext) { if err := recover(); err != nil { stack := recovery.Stack(3) - c.githubClient.CreateComment(ctx.Repo, ctx.Pull, fmt.Sprintf("**Error: goroutine panic. This is a bug.**\n```\n%s\n%s```", err, stack)) + c.githubClient.CreateComment(ctx.BaseRepo, ctx.Pull, fmt.Sprintf("**Error: goroutine panic. This is a bug.**\n```\n%s\n%s```", err, stack)) ctx.Log.Err("PANIC: %s\n%s", err, stack) } } diff --git a/server/event_parser.go b/server/event_parser.go index 0c61d352b4..89b0281fc5 100644 --- a/server/event_parser.go +++ b/server/event_parser.go @@ -75,7 +75,7 @@ func (e *EventParser) ExtractCommentData(comment *github.IssueCommentEvent, ctx if htmlURL == "" { return errors.New("comment.issue.html_url is null") } - ctx.Repo = repo + ctx.BaseRepo = repo ctx.User = models.User{ Username: commentorUsername, } @@ -85,32 +85,40 @@ func (e *EventParser) ExtractCommentData(comment *github.IssueCommentEvent, ctx return nil } -func (e *EventParser) ExtractPullData(pull *github.PullRequest) (models.PullRequest, error) { +func (e *EventParser) ExtractPullData(pull *github.PullRequest) (models.PullRequest, models.Repo, error) { var pullModel models.PullRequest + var headRepoModel models.Repo + commit := pull.Head.GetSHA() if commit == "" { - return pullModel, errors.New("head.sha is null") + return pullModel, headRepoModel, errors.New("head.sha is null") } base := pull.Base.GetSHA() if base == "" { - return pullModel, errors.New("base.sha is null") + return pullModel, headRepoModel, errors.New("base.sha is null") } url := pull.GetHTMLURL() if url == "" { - return pullModel, errors.New("html_url is null") + return pullModel, headRepoModel, errors.New("html_url is null") } branch := pull.Head.GetRef() if branch == "" { - return pullModel, errors.New("head.ref is null") + return pullModel, headRepoModel, errors.New("head.ref is null") } authorUsername := pull.User.GetLogin() if authorUsername == "" { - return pullModel, errors.New("user.login is null") + return pullModel, headRepoModel, errors.New("user.login is null") } num := pull.GetNumber() if num == 0 { - return pullModel, errors.New("number is null") + return pullModel, headRepoModel, errors.New("number is null") + } + + headRepoModel, err := e.ExtractRepoData(pull.Head.Repo) + if err != nil { + return pullModel, headRepoModel, err } + return models.PullRequest{ BaseCommit: base, Author: authorUsername, @@ -118,7 +126,7 @@ func (e *EventParser) ExtractPullData(pull *github.PullRequest) (models.PullRequ HeadCommit: commit, URL: url, Num: num, - }, nil + }, headRepoModel, nil } func (e *EventParser) ExtractRepoData(ghRepo *github.Repository) (models.Repo, error) { diff --git a/server/github_client.go b/server/github_client.go index cb5f7613a9..682ee59adc 100644 --- a/server/github_client.go +++ b/server/github_client.go @@ -17,12 +17,25 @@ type GithubClient struct { // The names include the path to the file from the repo root, ex. parent/child/file.txt func (g *GithubClient) GetModifiedFiles(repo models.Repo, pull models.PullRequest) ([]string, error) { var files []string - comparison, _, err := g.client.Repositories.CompareCommits(g.ctx, repo.Owner, repo.Name, pull.BaseCommit, pull.HeadCommit) - if err != nil { - return files, err - } - for _, file := range comparison.Files { - files = append(files, *file.Filename) + nextPage := 0 + for { + opts := github.ListOptions{ + PerPage: 300, + } + if nextPage != 0 { + opts.Page = nextPage + } + pageFiles, resp, err := g.client.PullRequests.ListFiles(g.ctx, repo.Owner, repo.Name, pull.Num, &opts) + if err != nil { + return files, err + } + for _, f := range pageFiles { + files = append(files, f.GetFilename()) + } + if resp.NextPage == 0 { + break + } + nextPage = resp.NextPage } return files, nil } diff --git a/server/github_status.go b/server/github_status.go index 161df49790..c695b47699 100644 --- a/server/github_status.go +++ b/server/github_status.go @@ -53,7 +53,7 @@ func (g *GithubStatus) UpdatePathResult(ctx *CommandContext, pathResults []PathR statuses = append(statuses, p.Status) } worst := g.worstStatus(statuses) - return g.Update(ctx.Repo, ctx.Pull, worst, ctx.Command.commandType.String()) + return g.Update(ctx.BaseRepo, ctx.Pull, worst, ctx.Command.commandType.String()) } func (g *GithubStatus) worstStatus(ss []Status) Status { diff --git a/server/help_executor.go b/server/help_executor.go index c69025228f..2a1e9597a2 100644 --- a/server/help_executor.go +++ b/server/help_executor.go @@ -32,6 +32,6 @@ atlantis apply func (h *HelpExecutor) execute(ctx *CommandContext, github *GithubClient) { ctx.Log.Info("generating help comment....") - github.CreateComment(ctx.Repo, ctx.Pull, helpComment) + github.CreateComment(ctx.BaseRepo, ctx.Pull, helpComment) return } diff --git a/server/plan_executor.go b/server/plan_executor.go index 58382742e5..7bca2c470e 100644 --- a/server/plan_executor.go +++ b/server/plan_executor.go @@ -73,39 +73,39 @@ func (e EnvironmentFailure) Template() *CompiledTemplate { } func (p *PlanExecutor) execute(ctx *CommandContext, github *GithubClient) { - if p.concurrentRunLocker.TryLock(ctx.Repo.FullName, ctx.Command.environment, ctx.Pull.Num) != true { + if p.concurrentRunLocker.TryLock(ctx.BaseRepo.FullName, ctx.Command.environment, ctx.Pull.Num) != true { ctx.Log.Info("run was locked by a concurrent run") - github.CreateComment(ctx.Repo, ctx.Pull, "This environment is currently locked by another command that is running for this pull request. Wait until command is complete and try again") + github.CreateComment(ctx.BaseRepo, ctx.Pull, "This environment is currently locked by another command that is running for this pull request. Wait until command is complete and try again") return } - defer p.concurrentRunLocker.Unlock(ctx.Repo.FullName, ctx.Command.environment, ctx.Pull.Num) + defer p.concurrentRunLocker.Unlock(ctx.BaseRepo.FullName, ctx.Command.environment, ctx.Pull.Num) res := p.setupAndPlan(ctx) res.Command = Plan comment := p.githubCommentRenderer.render(res, ctx.Log.History.String(), ctx.Command.verbose) - github.CreateComment(ctx.Repo, ctx.Pull, comment) + github.CreateComment(ctx.BaseRepo, ctx.Pull, comment) } func (p *PlanExecutor) setupAndPlan(ctx *CommandContext) ExecutionResult { - p.githubStatus.Update(ctx.Repo, ctx.Pull, Pending, PlanStep) + p.githubStatus.Update(ctx.BaseRepo, ctx.Pull, Pending, PlanStep) // figure out what projects have been modified so we know where to run plan ctx.Log.Info("listing modified files from pull request") - modifiedFiles, err := p.github.GetModifiedFiles(ctx.Repo, ctx.Pull) + modifiedFiles, err := p.github.GetModifiedFiles(ctx.BaseRepo, ctx.Pull) if err != nil { return p.setupError(ctx, errors.Wrap(err, "getting modified files")) } modifiedTerraformFiles := p.filterToTerraform(modifiedFiles) if len(modifiedTerraformFiles) == 0 { ctx.Log.Info("no modified terraform files found, exiting") - p.githubStatus.Update(ctx.Repo, ctx.Pull, Failure, PlanStep) + p.githubStatus.Update(ctx.BaseRepo, ctx.Pull, Failure, PlanStep) return ExecutionResult{SetupError: GeneralError{errors.New("Plan Failed: no modified terraform files found")}} } ctx.Log.Debug("Found %d modified terraform files: %v", len(modifiedTerraformFiles), modifiedTerraformFiles) - projects := p.ModifiedProjects(ctx.Repo.FullName, modifiedTerraformFiles) + projects := p.ModifiedProjects(ctx.BaseRepo.FullName, modifiedTerraformFiles) if len(projects) == 0 { ctx.Log.Info("no Terraform projects were modified") - p.githubStatus.Update(ctx.Repo, ctx.Pull, Failure, PlanStep) + p.githubStatus.Update(ctx.BaseRepo, ctx.Pull, Failure, PlanStep) return ExecutionResult{SetupError: GeneralError{errors.New("Plan Failed: we determined that no terraform projects were modified")}} } @@ -316,6 +316,6 @@ func (p *PlanExecutor) getProjectPath(modifiedFilePath string) string { func (p *PlanExecutor) setupError(ctx *CommandContext, err error) ExecutionResult { ctx.Log.Err(err.Error()) - p.githubStatus.Update(ctx.Repo, ctx.Pull, Error, PlanStep) + p.githubStatus.Update(ctx.BaseRepo, ctx.Pull, Error, PlanStep) return ExecutionResult{SetupError: GeneralError{err}} } diff --git a/server/server.go b/server/server.go index a146aaf1eb..0c75fac560 100644 --- a/server/server.go +++ b/server/server.go @@ -12,7 +12,6 @@ import ( "time" "github.com/aws/aws-sdk-go/aws/session" - homedir "github.com/mitchellh/go-homedir" "github.com/elazarl/go-bindata-assetfs" "github.com/google/go-github/github" "github.com/gorilla/mux" @@ -23,12 +22,14 @@ import ( "github.com/hootsuite/atlantis/middleware" "github.com/hootsuite/atlantis/models" "github.com/hootsuite/atlantis/prerun" + homedir "github.com/mitchellh/go-homedir" "github.com/pkg/errors" "github.com/urfave/cli" "github.com/urfave/negroni" ) const ( + lockRoute = "lock-detail" LockingFileBackend = "file" LockingDynamoDBBackend = "dynamodb" ) @@ -62,11 +63,12 @@ type ServerConfig struct { } type CommandContext struct { - Repo models.Repo - Pull models.PullRequest - User models.User - Command *Command - Log *logging.SimpleLogger + BaseRepo models.Repo + HeadRepo models.Repo + Pull models.PullRequest + User models.User + Command *Command + Log *logging.SimpleLogger } // todo: These structs have nothing to do with the server. Move to a different file/package #refactor @@ -213,7 +215,7 @@ func (s *Server) Start() error { s.router.PathPrefix("/static/").Handler(http.FileServer(&assetfs.AssetFS{Asset: Asset, AssetDir: AssetDir, AssetInfo: AssetInfo})) s.router.HandleFunc("/hooks", s.postHooks).Methods("POST") s.router.HandleFunc("/locks", s.deleteLock).Methods("DELETE").Queries("id", "{id:.*}") - lockRoute := s.router.HandleFunc("/lock", s.lock).Methods("GET").Queries("id", "{id}") + lockRoute := s.router.HandleFunc("/lock", s.lock).Methods("GET").Queries("id", "{id}").Name(lockRoute) // function that planExecutor can use to construct detail view url // injecting this here because this is the earliest routes are created s.commandHandler.SetLockURL(func(lockID string) string { @@ -248,9 +250,9 @@ func (s *Server) index(w http.ResponseWriter, r *http.Request) { } var results []lock for id, v := range locks { + url, _ := s.router.Get(lockRoute).URL("id", url.QueryEscape(id)) results = append(results, lock{ - // todo: make LockURL use the router to get /lock endpoint - LockURL: fmt.Sprintf("/lock?id=%s", url.QueryEscape(id)), + LockURL: url.String(), RepoFullName: v.Project.RepoFullName, PullNum: v.Pull.Num, Time: v.Time, @@ -380,7 +382,7 @@ func (s *Server) handlePullRequestEvent(w http.ResponseWriter, pullEvent *github fmt.Fprintln(w, "Ignoring") return } - pull, err := s.eventParser.ExtractPullData(pullEvent.PullRequest) + pull, _, err := s.eventParser.ExtractPullData(pullEvent.PullRequest) if err != nil { s.logger.Err("parsing pull data: %s", err) w.WriteHeader(http.StatusBadRequest) diff --git a/server/workspace.go b/server/workspace.go index f016251e1f..975c4b24d9 100644 --- a/server/workspace.go +++ b/server/workspace.go @@ -32,13 +32,10 @@ func (w *Workspace) Clone(ctx *CommandContext) (string, error) { return "", errors.Wrap(err, "creating new workspace") } - // Check if ssh key is set and create git ssh wrapper - cloneCmd := exec.Command("git", "clone", ctx.Repo.SSHURL, cloneDir) - - // clone the repo - ctx.Log.Info("git cloning %q into %q", ctx.Repo.SSHURL, cloneDir) + ctx.Log.Info("git cloning %q into %q", ctx.HeadRepo.SSHURL, cloneDir) + cloneCmd := exec.Command("git", "clone", ctx.HeadRepo.SSHURL, cloneDir) if output, err := cloneCmd.CombinedOutput(); err != nil { - return "", errors.Wrapf(err, "cloning %s: %s", ctx.Repo.SSHURL, string(output)) + return "", errors.Wrapf(err, "cloning %s: %s", ctx.HeadRepo.SSHURL, string(output)) } // check out the branch for this PR @@ -69,5 +66,5 @@ func (w *Workspace) repoPullDir(repo models.Repo, pull models.PullRequest) strin } func (w *Workspace) cloneDir(ctx *CommandContext) string { - return filepath.Join(w.repoPullDir(ctx.Repo, ctx.Pull), ctx.Command.environment) + return filepath.Join(w.repoPullDir(ctx.BaseRepo, ctx.Pull), ctx.Command.environment) }