diff --git a/locking/boltdb/boltdb.go b/locking/boltdb/boltdb.go index 41c1352d66..e7167f6673 100644 --- a/locking/boltdb/boltdb.go +++ b/locking/boltdb/boltdb.go @@ -81,13 +81,26 @@ func (b *Backend) TryLock(newLock models.ProjectLock) (bool, models.ProjectLock, return lockAcquired, currLock, nil } -func (b Backend) Unlock(p models.Project, env string) error { +func (b Backend) Unlock(p models.Project, env string) (*models.ProjectLock, error) { + var lock models.ProjectLock + foundLock := false key := b.key(p, env) err := b.db.Update(func(tx *bolt.Tx) error { - locks := tx.Bucket(b.bucket) - return locks.Delete([]byte(key)) + bucket := tx.Bucket(b.bucket) + serialized := bucket.Get([]byte(key)) + if serialized != nil { + if err := json.Unmarshal(serialized, &lock); err != nil { + return errors.Wrap(err, "failed to deserialize lock") + } + foundLock = true + } + return bucket.Delete([]byte(key)) }) - return errors.Wrap(err, "DB transaction failed") + err = errors.Wrap(err, "DB transaction failed") + if foundLock { + return &lock, err + } + return nil, err } func (b Backend) List() ([]models.ProjectLock, error) { @@ -117,8 +130,7 @@ func (b Backend) List() ([]models.ProjectLock, error) { return locks, nil } -func (b Backend) UnlockByPull(repoFullName string, pullNum int) error { - // get the locks that match that pull request +func (b Backend) UnlockByPull(repoFullName string, pullNum int) ([]models.ProjectLock, error) { var locks []models.ProjectLock err := b.db.View(func(tx *bolt.Tx) error { c := tx.Bucket(b.bucket).Cursor() @@ -138,11 +150,11 @@ func (b Backend) UnlockByPull(repoFullName string, pullNum int) error { // delete the locks for _, lock := range locks { - if err = b.Unlock(lock.Project, lock.Env); err != nil { - return errors.Wrapf(err, "unlocking repo %s, path %s, env %s", lock.Project.RepoFullName, lock.Project.Path, lock.Env) + if _, err = b.Unlock(lock.Project, lock.Env); err != nil { + return locks, errors.Wrapf(err, "unlocking repo %s, path %s, env %s", lock.Project.RepoFullName, lock.Project.Path, lock.Env) } } - return nil + return locks, nil } func (b Backend) key(p models.Project, env string) string { diff --git a/locking/dynamodb/dynamodb.go b/locking/dynamodb/dynamodb.go index 39b65b4805..378c7d31f6 100644 --- a/locking/dynamodb/dynamodb.go +++ b/locking/dynamodb/dynamodb.go @@ -95,16 +95,27 @@ func (b Backend) TryLock(newLock models.ProjectLock) (bool, models.ProjectLock, return true, newLock, nil } -func (b Backend) Unlock(project models.Project, env string) error { +func (b Backend) Unlock(project models.Project, env string) (*models.ProjectLock, error) { key := b.key(project, env) params := &dynamodb.DeleteItemInput{ Key: map[string]*dynamodb.AttributeValue{ "LockKey": {S: aws.String(key)}, }, TableName: aws.String(b.LockTable), + ReturnValues: aws.String("ALL_OLD"), } - _, err := b.DB.DeleteItem(params) - return errors.Wrap(err, "deleting lock") + output, err := b.DB.DeleteItem(params) + if err != nil { + return nil, errors.Wrap(err, "deleting lock") + } + + // deserialize the lock so we can return it + var dLock dynamoLock + if err := dynamodbattribute.UnmarshalMap(output.Attributes, &dLock); err != nil { + return nil, errors.Wrap(err, "found an existing lock at that key but it could not be deserialized. We suggest manually deleting this key from DynamoDB") + } + lock := b.fromDynamo(dLock) + return &lock, nil } func (b Backend) List() ([]models.ProjectLock, error) { @@ -131,7 +142,7 @@ func (b Backend) List() ([]models.ProjectLock, error) { return locks, errors.Wrap(err, "scanning dynamodb") } -func (b Backend) UnlockByPull(repoFullName string, pullNum int) error { +func (b Backend) UnlockByPull(repoFullName string, pullNum int) ([]models.ProjectLock, error) { params := &dynamodb.ScanInput{ ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{ ":pullNum": { @@ -146,10 +157,11 @@ func (b Backend) UnlockByPull(repoFullName string, pullNum int) error { } // scan DynamoDB for locks that match the pull request - var locks []dynamoLock + var dLocks []dynamoLock + var locks []models.ProjectLock var err, internalErr error err = b.DB.ScanPages(params, func(out *dynamodb.ScanOutput, lastPage bool) bool { - if err := dynamodbattribute.UnmarshalListOfMaps(out.Items, &locks); err != nil { + if err := dynamodbattribute.UnmarshalListOfMaps(out.Items, &dLocks); err != nil { internalErr = errors.Wrap(err, "deserializing locks") return false } @@ -159,16 +171,17 @@ func (b Backend) UnlockByPull(repoFullName string, pullNum int) error { err = internalErr } if err != nil { - return errors.Wrap(err, "scanning dynamodb") + return locks, errors.Wrap(err, "scanning dynamodb") } // now we can unlock all of them - for _, lock := range locks { - if err := b.Unlock(models.NewProject(lock.RepoFullName, lock.Path), lock.Env); err != nil { - return errors.Wrapf(err, "unlocking repo %s, path %s, env %s", lock.RepoFullName, lock.Path, lock.Env) + for _, lock := range dLocks { + if _, err := b.Unlock(models.NewProject(lock.RepoFullName, lock.Path), lock.Env); err != nil { + return locks, errors.Wrapf(err, "unlocking repo %s, path %s, env %s", lock.RepoFullName, lock.Path, lock.Env) } + locks = append(locks, b.fromDynamo(lock)) } - return nil + return locks, nil } func (b Backend) toDynamo(key string, l models.ProjectLock) dynamoLock { diff --git a/locking/locking.go b/locking/locking.go index 41121b9c51..08139e290f 100644 --- a/locking/locking.go +++ b/locking/locking.go @@ -10,9 +10,9 @@ import ( type Backend interface { TryLock(lock models.ProjectLock) (bool, models.ProjectLock, error) - Unlock(project models.Project, env string) error + Unlock(project models.Project, env string) (*models.ProjectLock, error) List() ([]models.ProjectLock, error) - UnlockByPull(repoFullName string, pullNum int) error + UnlockByPull(repoFullName string, pullNum int) ([]models.ProjectLock, error) } type TryLockResponse struct { @@ -49,10 +49,10 @@ func (c *Client) TryLock(p models.Project, env string, pull models.PullRequest, return TryLockResponse{lockAcquired, currLock, c.key(p, env)}, nil } -func (c *Client) Unlock(key string) error { +func (c *Client) Unlock(key string) (*models.ProjectLock, error) { matches := keyRegex.FindStringSubmatch(key) if len(matches) != 4 { - return errors.New("invalid key format") + return nil, errors.New("invalid key format") } return c.backend.Unlock(models.Project{matches[1], matches[2]}, matches[3]) } @@ -69,7 +69,7 @@ func (c *Client) List() (map[string]models.ProjectLock, error) { return m, nil } -func (c *Client) UnlockByPull(repoFullName string, pullNum int) error { +func (c *Client) UnlockByPull(repoFullName string, pullNum int) ([]models.ProjectLock, error) { return c.backend.UnlockByPull(repoFullName, pullNum) } diff --git a/plan/backend.go b/plan/backend.go index 3de19bc555..4961d2c69a 100644 --- a/plan/backend.go +++ b/plan/backend.go @@ -7,6 +7,8 @@ import ( type Backend interface { SavePlan(path string, project models.Project, env string, pullNum int) error CopyPlans(dstRepoPath string, repoFullName string, env string, pullNum int) ([]Plan, error) + DeletePlan(project models.Project, env string, pullNum int) error + DeletePlansByPull(repoFullName string, pullNum int) error } type Plan struct { diff --git a/plan/file/file.go b/plan/file/file.go index be760093a3..e722a0e92b 100644 --- a/plan/file/file.go +++ b/plan/file/file.go @@ -15,8 +15,10 @@ type Backend struct { baseDir string } +const planPath = "plans" + func New(baseDir string) (*Backend, error) { - baseDir = filepath.Clean(baseDir) + baseDir = filepath.Join(filepath.Clean(baseDir), planPath) if err := os.MkdirAll(baseDir, 0755); err != nil { return nil, err } @@ -25,11 +27,11 @@ func New(baseDir string) (*Backend, error) { // save plans to baseDir/owner/repo/pullNum/path/env.tfplan func (b *Backend) SavePlan(path string, project models.Project, env string, pullNum int) error { - savePath := b.path(project, pullNum) - if err := os.MkdirAll(savePath, 0755); err != nil { + file := b.path(project, env, pullNum) + if err := os.MkdirAll(filepath.Dir(file), 0755); err != nil { return errors.Wrap(err, "creating save directory") } - if err := b.copy(path, filepath.Join(savePath, env+".tfplan")); err != nil { + if err := b.copy(path, file); err != nil { return errors.Wrap(err, "saving plan") } return nil @@ -76,6 +78,14 @@ func (b *Backend) CopyPlans(dstRepo string, repoFullName string, env string, pul return plans, nil } +func (b *Backend) DeletePlan(project models.Project, env string, pullNum int) error { + return os.Remove(b.path(project, env, pullNum)) +} + +func (b *Backend) DeletePlansByPull(repoFullName string, pullNum int) error { + return os.RemoveAll(filepath.Join(b.baseDir, repoFullName, strconv.Itoa(pullNum))) +} + func (b *Backend) copy(src string, dst string) error { data, err := ioutil.ReadFile(src) if err != nil { @@ -88,6 +98,6 @@ func (b *Backend) copy(src string, dst string) error { return nil } -func (b *Backend) path(p models.Project, pullNum int) string { - return filepath.Join(b.baseDir, p.RepoFullName, strconv.Itoa(pullNum), p.Path) +func (b *Backend) path(p models.Project, env string, pullNum int) string { + return filepath.Join(b.baseDir, p.RepoFullName, strconv.Itoa(pullNum), p.Path, env+".tfplan") } diff --git a/plan/s3/s3.go b/plan/s3/s3.go index 29fd51217e..3c3154c44f 100644 --- a/plan/s3/s3.go +++ b/plan/s3/s3.go @@ -87,7 +87,7 @@ func (b *Backend) SavePlan(path string, project models.Project, env string, pull return errors.Wrapf(err, "opening plan at %s", path) } - key := pathutil.Join(b.keyPrefix, project.RepoFullName, strconv.Itoa(pullNum), project.Path, env+".tfplan") + key := b.path(project, env, pullNum) _, err = b.uploader.Upload(&s3manager.UploadInput{ Bucket: aws.String(b.bucket), Key: &key, @@ -104,3 +104,35 @@ func (b *Backend) SavePlan(path string, project models.Project, env string, pull } return nil } + +func (b *Backend) DeletePlan(project models.Project, env string, pullNum int) error { + _, err := b.s3.DeleteObject(&s3.DeleteObjectInput{ + Bucket: aws.String(b.bucket), + Key: aws.String(b.path(project, env, pullNum)), + }) + return err +} + +func (b *Backend) DeletePlansByPull(repoFullName string, pullNum int) error { + // first list the plans with the correct prefix + prefix := pathutil.Join(b.keyPrefix, repoFullName, strconv.Itoa(pullNum)) + list, err := b.s3.ListObjects(&s3.ListObjectsInput{Bucket: aws.String(b.bucket), Prefix: &prefix}) + if err != nil { + return errors.Wrap(err, "listing plans") + } + + var deleteList []*s3.ObjectIdentifier + for _, obj := range list.Contents { + deleteList = append(deleteList, &s3.ObjectIdentifier{Key: obj.Key}) + } + + _, err = b.s3.DeleteObjects(&s3.DeleteObjectsInput{ + Bucket: aws.String(b.bucket), + Delete: &s3.Delete{Objects: deleteList}, + }) + return err +} + +func (b *Backend) path(project models.Project, env string, pullNum int) string { + return pathutil.Join(b.keyPrefix, project.RepoFullName, strconv.Itoa(pullNum), project.Path, env+".tfplan") +} diff --git a/server/apply_executor.go b/server/apply_executor.go index a196ffd6f3..c64765af7d 100644 --- a/server/apply_executor.go +++ b/server/apply_executor.go @@ -24,7 +24,7 @@ type ApplyExecutor struct { githubCommentRenderer *GithubCommentRenderer lockingClient *locking.Client requireApproval bool - planStorage plan.Backend + planBackend plan.Backend } /** Result Types **/ @@ -63,7 +63,7 @@ func (a *ApplyExecutor) execute(ctx *CommandContext, github *GithubClient) { res := a.setupAndApply(ctx) res.Command = Apply comment := a.githubCommentRenderer.render(res, ctx.Log.History.String(), ctx.Command.verbose) - github.CreateComment(ctx, comment) + github.CreateComment(ctx.Repo, ctx.Pull, comment) } func (a *ApplyExecutor) setupAndApply(ctx *CommandContext) ExecutionResult { @@ -73,7 +73,7 @@ func (a *ApplyExecutor) setupAndApply(ctx *CommandContext) ExecutionResult { // todo: reclone repo and switch branch, don't assume it's already there repoDir := filepath.Join(a.scratchDir, ctx.Repo.FullName, strconv.Itoa(ctx.Pull.Num)) - plans, err := a.planStorage.CopyPlans(repoDir, ctx.Repo.FullName, ctx.Command.environment, ctx.Pull.Num) + plans, err := a.planBackend.CopyPlans(repoDir, ctx.Repo.FullName, ctx.Command.environment, ctx.Pull.Num) if err != nil { errMsg := fmt.Sprintf("failed to get plans: %s", err) ctx.Log.Err(errMsg) @@ -209,6 +209,9 @@ func (a *ApplyExecutor) apply(ctx *CommandContext, repoDir string, plan plan.Pla // clean up, delete local plan file os.Remove(plan.LocalPath) // swallow errors, okay if we failed to delete + if err := a.planBackend.DeletePlan(plan.Project, ctx.Command.environment, ctx.Pull.Num); err != nil { + ctx.Log.Err("deleting plan for repo %s, path %s, env %s: %s", plan.Project.RepoFullName, plan.Project.Path, ctx.Command.environment, err) + } return PathResult{ Status: Success, Result: ApplySuccess{output}, diff --git a/server/command_handler.go b/server/command_handler.go index b5cbe784c3..90ef576b9c 100644 --- a/server/command_handler.go +++ b/server/command_handler.go @@ -81,7 +81,7 @@ func (s *CommandHandler) SetDeleteLockURL(f func(id string) (url string)) { func (s *CommandHandler) recover(ctx *CommandContext) { if err := recover(); err != nil { stack := recovery.Stack(3) - s.githubClient.CreateComment(ctx, fmt.Sprintf("**Error: goroutine panic. This is a bug.**\n```\n%s\n%s```", err, stack)) + s.githubClient.CreateComment(ctx.Repo, 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 f5173f4b4e..523b0082f0 100644 --- a/server/event_parser.go +++ b/server/event_parser.go @@ -49,21 +49,9 @@ func (e *EventParser) DetermineCommand(comment *github.IssueCommentEvent) (*Comm } func (e *EventParser) ExtractCommentData(comment *github.IssueCommentEvent, ctx *CommandContext) error { - repoFullName := comment.Repo.GetFullName() - if repoFullName == "" { - return errors.New("repository.full_name is null") - } - repoOwner := comment.Repo.Owner.GetLogin() - if repoOwner == "" { - return errors.New("repository.owner.login is null") - } - repoName := comment.Repo.GetName() - if repoName == "" { - return errors.New("repository.name is null") - } - repoSSHURL := comment.Repo.GetSSHURL() - if repoSSHURL == "" { - return errors.New("comment.repository.ssh_url is null") + repo, err := e.ExtractRepoData(comment.Repo) + if err != nil { + return err } pullNum := comment.Issue.GetNumber() if pullNum == 0 { @@ -81,12 +69,7 @@ func (e *EventParser) ExtractCommentData(comment *github.IssueCommentEvent, ctx if htmlURL == "" { return errors.New("comment.issue.html_url is null") } - ctx.Repo = models.Repo{ - FullName: repoFullName, - Owner: repoOwner, - Name: repoName, - SSHURL: repoSSHURL, - } + ctx.Repo = repo ctx.User = models.User{ Username: commentorUsername, } @@ -131,3 +114,29 @@ func (e *EventParser) ExtractPullData(pull *github.PullRequest) (models.PullRequ Num: num, }, nil } + +func (e *EventParser) ExtractRepoData(ghRepo *github.Repository) (models.Repo, error) { + var repo models.Repo + repoFullName := ghRepo.GetFullName() + if repoFullName == "" { + return repo, errors.New("repository.full_name is null") + } + repoOwner := ghRepo.Owner.GetLogin() + if repoOwner == "" { + return repo, errors.New("repository.owner.login is null") + } + repoName := ghRepo.GetName() + if repoName == "" { + return repo, errors.New("repository.name is null") + } + repoSSHURL := ghRepo.GetSSHURL() + if repoSSHURL == "" { + return repo, errors.New("repository.ssh_url is null") + } + return models.Repo{ + Owner: repoOwner, + FullName: repoFullName, + SSHURL: repoSSHURL, + Name: repoName, + }, nil +} diff --git a/server/github_client.go b/server/github_client.go index f84d257ded..cb5f7613a9 100644 --- a/server/github_client.go +++ b/server/github_client.go @@ -27,8 +27,8 @@ func (g *GithubClient) GetModifiedFiles(repo models.Repo, pull models.PullReques return files, nil } -func (g *GithubClient) CreateComment(ctx *CommandContext, comment string) error { - _, _, err := g.client.Issues.CreateComment(g.ctx, ctx.Repo.Owner, ctx.Repo.Name, ctx.Pull.Num, &github.IssueComment{Body: &comment}) +func (g *GithubClient) CreateComment(repo models.Repo, pull models.PullRequest, comment string) error { + _, _, err := g.client.Issues.CreateComment(g.ctx, repo.Owner, repo.Name, pull.Num, &github.IssueComment{Body: &comment}) return err } diff --git a/server/help_executor.go b/server/help_executor.go index b801795de5..c69025228f 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, helpComment) + github.CreateComment(ctx.Repo, ctx.Pull, helpComment) return } diff --git a/server/plan_executor.go b/server/plan_executor.go index e9ae5fde8c..172be4a8c8 100644 --- a/server/plan_executor.go +++ b/server/plan_executor.go @@ -28,7 +28,7 @@ type PlanExecutor struct { lockingClient *locking.Client // DeleteLockURL is a function that given a lock id will return a url for deleting the lock DeleteLockURL func(id string) (url string) - planStorage plan.Backend + planBackend plan.Backend } /** Result Types **/ @@ -76,7 +76,7 @@ func (p *PlanExecutor) execute(ctx *CommandContext, github *GithubClient) { res := p.setupAndPlan(ctx) res.Command = Plan comment := p.githubCommentRenderer.render(res, ctx.Log.History.String(), ctx.Command.verbose) - github.CreateComment(ctx, comment) + github.CreateComment(ctx.Repo, ctx.Pull, comment) } func (p *PlanExecutor) setupAndPlan(ctx *CommandContext) ExecutionResult { @@ -283,7 +283,7 @@ func (p *PlanExecutor) plan( } ctx.Log.Err("error running terraform plan: %v", output) ctx.Log.Info("unlocking state since plan failed") - if err := p.lockingClient.Unlock(lockAttempt.LockKey); err != nil { + if _, err := p.lockingClient.Unlock(lockAttempt.LockKey); err != nil { ctx.Log.Err("error unlocking state: %v", err) } return PathResult{ @@ -292,10 +292,10 @@ func (p *PlanExecutor) plan( } } // Save the plan - if err := p.planStorage.SavePlan(planFile, project, tfEnv, ctx.Pull.Num); err != nil { + if err := p.planBackend.SavePlan(planFile, project, tfEnv, ctx.Pull.Num); err != nil { ctx.Log.Err("saving plan: %s", err) // there was an error planning so unlock - if err := p.lockingClient.Unlock(lockAttempt.LockKey); err != nil { + if _, err := p.lockingClient.Unlock(lockAttempt.LockKey); err != nil { ctx.Log.Err("error unlocking: %v", err) } return PathResult{ diff --git a/server/pull_closed_executor.go b/server/pull_closed_executor.go new file mode 100644 index 0000000000..3c40d5baf1 --- /dev/null +++ b/server/pull_closed_executor.go @@ -0,0 +1,79 @@ +package server + +import ( + "github.com/hootsuite/atlantis/locking" + "github.com/hootsuite/atlantis/models" + "github.com/pkg/errors" + "github.com/hootsuite/atlantis/plan" + "fmt" + "strings" + "text/template" + "bytes" +) + +type PullClosedExecutor struct { + locking *locking.Client + github *GithubClient + planBackend plan.Backend +} + +type templatedProject struct { + Path string + Envs string +} +var pullClosedTemplate = template.Must(template.New("").Parse("Locks and plans deleted for the projects and environments modified in this pull request:\n" + +"{{ range . }}\n" + +"- path: `{{ .Path }}` {{ .Envs }}{{ end }}")) + +func (p *PullClosedExecutor) CleanUpPull(repo models.Repo, pull models.PullRequest) error { + locks, err := p.locking.UnlockByPull(repo.FullName, pull.Num) + if err != nil { + return errors.Wrap(err, "cleaning up locks") + } + + // if there are no locks then there's no need to comment + if len(locks) == 0 { + return nil + } + + err = p.planBackend.DeletePlansByPull(repo.FullName, pull.Num) + if err != nil { + return errors.Wrap(err, "cleaning up plans") + } + + templateData := p.buildTemplateData(locks) + var buf bytes.Buffer + if err = pullClosedTemplate.Execute(&buf, templateData); err != nil { + return errors.Wrap(err, "rendering template for comment") + } + return p.github.CreateComment(repo, pull, buf.String()) +} + +// buildTemplateData formats the lock data into a slice that can easily be templated +// for the GitHub comment. We organize all the environments by their respective project paths +// so the comment can look like: path: {path}, environments: {all-envs} +func (p *PullClosedExecutor) buildTemplateData(locks []models.ProjectLock) []templatedProject { + envsByPath := make(map[string][]string) + for _, l := range locks { + path := l.Project.RepoFullName + "/" + l.Project.Path + envsByPath[path] = append(envsByPath[path], l.Env) + } + + var projects []templatedProject + for p, e := range envsByPath { + envsStr := fmt.Sprintf("`%s`", strings.Join(e, "`, `")) + if len(e) == 1 { + projects = append(projects, templatedProject{ + Path: p, + Envs: "environment: " + envsStr, + }) + } else { + projects = append(projects, templatedProject{ + Path: p, + Envs: "environments: " + envsStr, + }) + + } + } + return projects +} diff --git a/server/server.go b/server/server.go index 309f67d027..6b544bb71e 100644 --- a/server/server.go +++ b/server/server.go @@ -43,6 +43,7 @@ type Server struct { router *mux.Router port int commandHandler *CommandHandler + pullClosedExecutor *PullClosedExecutor logger *logging.SimpleLogger eventParser *EventParser lockingClient *locking.Client @@ -170,7 +171,7 @@ func NewServer(config ServerConfig) (*Server, error) { githubCommentRenderer: githubComments, lockingClient: lockingClient, requireApproval: config.RequireApproval, - planStorage: planBackend, + planBackend: planBackend, } planExecutor := &PlanExecutor{ github: githubClient, @@ -181,9 +182,14 @@ func NewServer(config ServerConfig) (*Server, error) { terraform: terraformClient, githubCommentRenderer: githubComments, lockingClient: lockingClient, - planStorage: planBackend, + planBackend: planBackend, } helpExecutor := &HelpExecutor{} + pullClosedExecutor := &PullClosedExecutor{ + planBackend: planBackend, + github: githubClient, + locking: lockingClient, + } logger := logging.NewSimpleLogger("server", log.New(os.Stderr, "", log.LstdFlags), false, logging.ToLogLevel(config.LogLevel)) eventParser := &EventParser{} commandHandler := &CommandHandler{ @@ -199,6 +205,7 @@ func NewServer(config ServerConfig) (*Server, error) { router: router, port: config.Port, commandHandler: commandHandler, + pullClosedExecutor: pullClosedExecutor, eventParser: eventParser, logger: logger, lockingClient: lockingClient, @@ -275,11 +282,17 @@ func (s *Server) deleteLock(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadRequest) fmt.Fprint(w, "invalid lock id") } - if err := s.lockingClient.Unlock(idUnencoded); err != nil { + lock, err := s.lockingClient.Unlock(idUnencoded) + if err != nil { w.WriteHeader(http.StatusBadRequest) fmt.Fprintf(w, "Failed to unlock: %s", err) return } + if lock == nil { + w.WriteHeader(http.StatusBadRequest) + fmt.Fprint(w, "No lock at that key") + return + } fmt.Fprint(w, "Unlocked successfully") } @@ -331,18 +344,29 @@ func (s *Server) handlePullRequestEvent(w http.ResponseWriter, pullEvent *github fmt.Fprintln(w, "Ignoring") return } - repo := pullEvent.Repo.GetFullName() - pullNum := pullEvent.PullRequest.GetNumber() - - s.logger.Debug("Unlocking locks for repo %s and pull %d %s", repo, pullNum, githubReqID) - err := s.lockingClient.UnlockByPull(repo, pullNum) + pull, err := s.eventParser.ExtractPullData(pullEvent.PullRequest) if err != nil { - s.logger.Err("unlocking locks for repo %s pull %d: %v", repo, pullNum, err) - w.WriteHeader(http.StatusServiceUnavailable) - fmt.Fprintf(w, "Error unlocking locks: %v\n", err) + s.logger.Err("parsing pull data: %s", err) + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "error parsing request: %s", err) + return + } + repo, err := s.eventParser.ExtractRepoData(pullEvent.Repo) + if err != nil { + s.logger.Err("parsing repo data: %s", err) + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "error parsing request: %s", err) + return + } + + s.logger.Info("cleaning up locks and plans for repo %s and pull %d", repo.FullName, pull.Num) + if err := s.pullClosedExecutor.CleanUpPull(repo, pull); err != nil { + s.logger.Err("cleaning pull request: %s", err) + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, "Error cleaning pull request: %s", err) return } - fmt.Fprintln(w, "Locks unlocked") + fmt.Fprint(w, "Pull request cleaned successfully") } func (s *Server) handleCommentEvent(w http.ResponseWriter, event *github.IssueCommentEvent, githubReqID string) {