From c8afddf906272e29f18812904f6af220adc23265 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Lapeyre?= Date: Thu, 16 Apr 2020 15:25:51 +0200 Subject: [PATCH 1/9] Add the /plan and /apply endpoints --- Makefile | 4 +- cmd/server.go | 4 + server/events/command_runner.go | 7 +- server/events/event_parser.go | 6 + server/events/models/models.go | 17 ++ server/events/vcs/azuredevops_client.go | 4 + server/events/vcs/bitbucketcloud/client.go | 4 + server/events/vcs/bitbucketserver/client.go | 4 + server/events/vcs/client.go | 1 + server/events/vcs/github_client.go | 4 + server/events/vcs/gitlab_client.go | 8 + .../events/vcs/not_configured_vcs_client.go | 3 + server/events/vcs/proxy.go | 4 + server/events_controller.go | 175 ++++++++++++++++++ server/server.go | 3 + server/user_config.go | 1 + 16 files changed, 245 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 3cb2f76197..553973f5b5 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,8 @@ WORKSPACE := $(shell pwd) PKG := $(shell go list ./... | grep -v e2e | grep -v vendor | grep -v static | grep -v mocks | grep -v testing) PKG_COMMAS := $(shell go list ./... | grep -v e2e | grep -v vendor | grep -v static | grep -v mocks | grep -v testing | tr '\n' ',') IMAGE_NAME := runatlantis/atlantis +GOOS := linux +GOARCH := amd64 .PHONY: test @@ -20,7 +22,7 @@ debug: ## Output internal make variables @echo PKG = $(PKG) build-service: ## Build the main Go service - CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -mod=vendor -v -o atlantis . + CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(GOARCH) go build -mod=vendor -v -o atlantis . go-generate: ## Run go generate in all packages go generate $(PKG) diff --git a/cmd/server.go b/cmd/server.go index 37d6c725e0..c01d01cabc 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -62,6 +62,7 @@ const ( GitlabTokenFlag = "gitlab-token" GitlabUserFlag = "gitlab-user" GitlabWebhookSecretFlag = "gitlab-webhook-secret" // nolint: gosec + APISecretFlag = "api-secret" LogLevelFlag = "log-level" PortFlag = "port" RepoConfigFlag = "repo-config" @@ -184,6 +185,9 @@ var stringFlags = map[string]stringFlag{ "This means that an attacker could spoof calls to Atlantis and cause it to perform malicious actions. " + "Should be specified via the ATLANTIS_GITLAB_WEBHOOK_SECRET environment variable.", }, + APISecretFlag: { + description: "Secret to validate requests made to the API", + }, LogLevelFlag: { description: "Log level. Either debug, info, warn, or error.", defaultValue: DefaultLogLevel, diff --git a/server/events/command_runner.go b/server/events/command_runner.go index 11ec0a979b..b74aa0dd9b 100644 --- a/server/events/command_runner.go +++ b/server/events/command_runner.go @@ -36,6 +36,7 @@ type CommandRunner interface { // and then calling the appropriate services to finish executing the command. RunCommentCommand(baseRepo models.Repo, maybeHeadRepo *models.Repo, maybePull *models.PullRequest, user models.User, pullNum int, cmd *CommentCommand) RunAutoplanCommand(baseRepo models.Repo, headRepo models.Repo, pull models.PullRequest, user models.User) + RunProjectCmds(cmds []models.ProjectCommandContext, cmdName models.CommandName) CommandResult } //go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_github_pull_getter.go GithubPullGetter @@ -141,7 +142,7 @@ func (c *DefaultCommandRunner) RunAutoplanCommand(baseRepo models.Repo, headRepo ctx.Log.Warn("unable to update commit status: %s", err) } - result := c.runProjectCmds(projectCmds, models.PlanCommand) + result := c.RunProjectCmds(projectCmds, models.PlanCommand) if c.automergeEnabled(ctx, projectCmds) && result.HasErrors() { ctx.Log.Info("deleting plans because there were errors and automerge requires all plans succeed") c.deletePlans(ctx) @@ -251,7 +252,7 @@ func (c *DefaultCommandRunner) RunCommentCommand(baseRepo models.Repo, maybeHead return } - result := c.runProjectCmds(projectCmds, cmd.Name) + result := c.RunProjectCmds(projectCmds, cmd.Name) if cmd.Name == models.PlanCommand && c.automergeEnabled(ctx, projectCmds) && result.HasErrors() { ctx.Log.Info("deleting plans because there were errors and automerge requires all plans succeed") c.deletePlans(ctx) @@ -336,7 +337,7 @@ func (c *DefaultCommandRunner) automerge(ctx *CommandContext, pullStatus models. } } -func (c *DefaultCommandRunner) runProjectCmds(cmds []models.ProjectCommandContext, cmdName models.CommandName) CommandResult { +func (c *DefaultCommandRunner) RunProjectCmds(cmds []models.ProjectCommandContext, cmdName models.CommandName) CommandResult { var results []models.ProjectResult for _, pCmd := range cmds { var res models.ProjectResult diff --git a/server/events/event_parser.go b/server/events/event_parser.go index 6a64110ff0..4b346f720d 100644 --- a/server/events/event_parser.go +++ b/server/events/event_parser.go @@ -187,6 +187,8 @@ type EventParsing interface { // that returns a merge request. ParseGitlabMergeRequest(mr *gitlab.MergeRequest, baseRepo models.Repo) models.PullRequest + ParseAPIPlanRequest(path, cloneURL string) (baseRepo models.Repo, err error) + // ParseBitbucketCloudPullEvent parses a pull request event from Bitbucket // Cloud (bitbucket.org). // pull is the parsed pull request. @@ -273,6 +275,10 @@ type EventParser struct { AzureDevopsUser string } +func (e *EventParser) ParseAPIPlanRequest(repoFullName, cloneURL string) (models.Repo, error) { + return models.NewRepo(models.Gitlab, repoFullName, cloneURL, e.GitlabUser, e.GitlabToken) +} + // GetBitbucketCloudPullEventType returns the type of the pull request // event given the Bitbucket Cloud header. func (e *EventParser) GetBitbucketCloudPullEventType(eventTypeHeader string) models.PullRequestEventType { diff --git a/server/events/models/models.go b/server/events/models/models.go index 8e1ec53798..80dec16a79 100644 --- a/server/events/models/models.go +++ b/server/events/models/models.go @@ -296,6 +296,23 @@ func (h VCSHostType) String() string { return "" } +func NewVCSHostType(t string) (VCSHostType, error) { + switch t { + case "Github": + return Github, nil + case "Gitlab": + return Gitlab, nil + case "BitbucketCloud": + return BitbucketCloud, nil + case "BitbucketServer": + return BitbucketServer, nil + case "AzureDevops": + return AzureDevops, nil + } + + return 0, fmt.Errorf("%q is not a valid type", t) +} + // ProjectCommandContext defines the context for a plan or apply stage that will // be executed for a project. type ProjectCommandContext struct { diff --git a/server/events/vcs/azuredevops_client.go b/server/events/vcs/azuredevops_client.go index b3eee8d9eb..c8b6878e95 100644 --- a/server/events/vcs/azuredevops_client.go +++ b/server/events/vcs/azuredevops_client.go @@ -315,3 +315,7 @@ func SplitAzureDevopsRepoFullName(repoFullName string) (owner string, project st } return repoFullName[:lastSlashIdx], "", repoFullName[lastSlashIdx+1:] } + +func (g *AzureDevopsClient) GetCloneURL(VCSHostType models.VCSHostType, repo string) (string, error) { + return "", fmt.Errorf("not yet implemented") +} diff --git a/server/events/vcs/bitbucketcloud/client.go b/server/events/vcs/bitbucketcloud/client.go index d55b7c598f..917bbee9e7 100644 --- a/server/events/vcs/bitbucketcloud/client.go +++ b/server/events/vcs/bitbucketcloud/client.go @@ -234,3 +234,7 @@ func (b *Client) makeRequest(method string, path string, reqBody io.Reader) ([]b } return respBody, nil } + +func (g *Client) GetCloneURL(VCSHostType models.VCSHostType, repo string) (string, error) { + return "", fmt.Errorf("not yet implemented") +} diff --git a/server/events/vcs/bitbucketserver/client.go b/server/events/vcs/bitbucketserver/client.go index bff3b6258a..42bcdffb77 100644 --- a/server/events/vcs/bitbucketserver/client.go +++ b/server/events/vcs/bitbucketserver/client.go @@ -302,3 +302,7 @@ func (b *Client) makeRequest(method string, path string, reqBody io.Reader) ([]b } return respBody, nil } + +func (b *Client) GetCloneURL(VCSHostType models.VCSHostType, repo string) (string, error) { + return "", fmt.Errorf("not yet implemented") +} diff --git a/server/events/vcs/client.go b/server/events/vcs/client.go index 153af1265f..d2c77fe76a 100644 --- a/server/events/vcs/client.go +++ b/server/events/vcs/client.go @@ -36,4 +36,5 @@ type Client interface { // about this status. UpdateStatus(repo models.Repo, pull models.PullRequest, state models.CommitStatus, src string, description string, url string) error MergePull(pull models.PullRequest) error + GetCloneURL(VCSHostType models.VCSHostType, repo string) (string, error) } diff --git a/server/events/vcs/github_client.go b/server/events/vcs/github_client.go index 11220b1958..54e7fbe717 100644 --- a/server/events/vcs/github_client.go +++ b/server/events/vcs/github_client.go @@ -232,3 +232,7 @@ func (g *GithubClient) MergePull(pull models.PullRequest) error { } return nil } + +func (g *GithubClient) GetCloneURL(VCSHostType models.VCSHostType, repo string) (string, error) { + return "", fmt.Errorf("not yet implemented") +} diff --git a/server/events/vcs/gitlab_client.go b/server/events/vcs/gitlab_client.go index 002822805e..b4db0b4148 100644 --- a/server/events/vcs/gitlab_client.go +++ b/server/events/vcs/gitlab_client.go @@ -254,3 +254,11 @@ func MustConstraint(constraint string) version.Constraints { } return c } + +func (g *GitlabClient) GetCloneURL(VCSHostType models.VCSHostType, repo string) (string, error) { + project, _, err := g.Client.Projects.GetProject(repo, nil) + if err != nil { + return "", err + } + return project.HTTPURLToRepo, nil +} diff --git a/server/events/vcs/not_configured_vcs_client.go b/server/events/vcs/not_configured_vcs_client.go index 98f5ab8ebd..3f24b1d5b4 100644 --- a/server/events/vcs/not_configured_vcs_client.go +++ b/server/events/vcs/not_configured_vcs_client.go @@ -47,3 +47,6 @@ func (a *NotConfiguredVCSClient) MergePull(pull models.PullRequest) error { func (a *NotConfiguredVCSClient) err() error { return fmt.Errorf("atlantis was not configured to support repos from %s", a.Host.String()) } +func (a *NotConfiguredVCSClient) GetCloneURL(VCSHostType models.VCSHostType, repo string) (string, error) { + return "", a.err() +} diff --git a/server/events/vcs/proxy.go b/server/events/vcs/proxy.go index 976eb3e851..93d8e517ca 100644 --- a/server/events/vcs/proxy.go +++ b/server/events/vcs/proxy.go @@ -75,3 +75,7 @@ func (d *ClientProxy) UpdateStatus(repo models.Repo, pull models.PullRequest, st func (d *ClientProxy) MergePull(pull models.PullRequest) error { return d.clients[pull.BaseRepo.VCSHost.Type].MergePull(pull) } + +func (d *ClientProxy) GetCloneURL(VCSHostType models.VCSHostType, repo string) (string, error) { + return d.clients[VCSHostType].GetCloneURL(VCSHostType, repo) +} diff --git a/server/events_controller.go b/server/events_controller.go index 2b62a2017d..ca25e2a741 100644 --- a/server/events_controller.go +++ b/server/events_controller.go @@ -14,6 +14,7 @@ package server import ( + "encoding/json" "fmt" "io/ioutil" "net/http" @@ -30,6 +31,7 @@ import ( "github.com/runatlantis/atlantis/server/events/vcs/bitbucketserver" "github.com/runatlantis/atlantis/server/logging" gitlab "github.com/xanzy/go-gitlab" + "gopkg.in/go-playground/validator.v9" ) const githubHeader = "X-Github-Event" @@ -83,6 +85,8 @@ type EventsController struct { // Azure DevOps Team Project. If empty, no request validation is done. AzureDevopsWebhookBasicPassword []byte AzureDevopsRequestValidator AzureDevopsRequestValidator + + APISecret []byte } // Post handles POST webhook requests. @@ -552,3 +556,174 @@ func (e *EventsController) commentNotWhitelisted(baseRepo models.Repo, pullNum i e.Logger.Err("unable to comment on pull request: %s", err) } } + +type APIRequest struct { + Repository string `validate:"required"` + Ref string `validate:"required"` + Type string `validate:"required"` + Projects []string + Paths []struct { + Directory string + Workspace string + } +} + +func (a *APIRequest) getCommands(ctx *events.CommandContext, cmdBuilder func(*events.CommandContext, *events.CommentCommand) ([]models.ProjectCommandContext, error)) ([]models.ProjectCommandContext, error) { + cc := make([]*events.CommentCommand, 0) + + for _, project := range a.Projects { + cc = append(cc, &events.CommentCommand{ + ProjectName: project, + }) + } + for _, path := range a.Paths { + cc = append(cc, &events.CommentCommand{ + RepoRelDir: strings.TrimRight(path.Directory, "/"), + Workspace: path.Workspace, + }) + } + + cmds := make([]models.ProjectCommandContext, 0) + for _, commentCommand := range cc { + projectCmds, err := cmdBuilder(ctx, commentCommand) + if err != nil { + return nil, fmt.Errorf("Failed to build command: %v", err) + } + for _, cmd := range projectCmds { + cmds = append(cmds, cmd) + } + } + + return cmds, nil +} + +func (e *EventsController) apiReportError(w http.ResponseWriter, code int, err error) { + response, _ := json.Marshal(map[string]string{ + "error": err.Error(), + }) + e.respond(w, logging.Warn, code, string(response)) +} + +func (e *EventsController) APIPlan(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + request, ctx, code, err := e.apiParseAndValidate(w, r) + + if err != nil { + e.apiReportError(w, code, err) + return + } + + runner := e.CommandRunner.(*events.DefaultCommandRunner) + cmds, err := request.getCommands(ctx, runner.ProjectCommandBuilder.BuildPlanCommands) + if err != nil { + e.apiReportError(w, http.StatusInternalServerError, err) + return + } + + defer runner.ProjectCommandRunner.(*events.DefaultProjectCommandRunner).Locker.(*events.DefaultProjectLocker).Locker.UnlockByPull(ctx.BaseRepo.FullName, -1) + result := runner.RunProjectCmds(cmds, models.PlanCommand) + + code = http.StatusOK + if result.HasErrors() { + code = http.StatusInternalServerError + } + + // TODO: make a better response + response, _ := json.Marshal(result) + e.respond(w, logging.Debug, code, string(response)) +} + +func (e *EventsController) APIApply(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + request, ctx, code, err := e.apiParseAndValidate(w, r) + + if err != nil { + e.apiReportError(w, code, err) + return + } + runner := e.CommandRunner.(*events.DefaultCommandRunner) + + // We must first make the plan for all projects + cmds, err := request.getCommands(ctx, runner.ProjectCommandBuilder.BuildPlanCommands) + if err != nil { + e.apiReportError(w, http.StatusInternalServerError, err) + return + } + + defer runner.ProjectCommandRunner.(*events.DefaultProjectCommandRunner).Locker.(*events.DefaultProjectLocker).Locker.UnlockByPull(ctx.BaseRepo.FullName, -1) + result := runner.RunProjectCmds(cmds, models.PlanCommand) + + // We can now prepare and run the apply step + cmds, err = request.getCommands(ctx, runner.ProjectCommandBuilder.BuildApplyCommands) + if err != nil { + e.apiReportError(w, http.StatusInternalServerError, err) + return + } + result = runner.RunProjectCmds(cmds, models.ApplyCommand) + + code = http.StatusOK + if result.HasErrors() { + code = http.StatusInternalServerError + } + + response, _ := json.Marshal(result) + e.respond(w, logging.Debug, http.StatusOK, string(response)) +} + +func (e *EventsController) apiParseAndValidate(w http.ResponseWriter, r *http.Request) (*APIRequest, *events.CommandContext, int, error) { + if len(e.APISecret) == 0 { + return nil, nil, http.StatusBadRequest, fmt.Errorf("Ignoring request since API is disabled") + } + + // Validate the secret token + header := "X-Atlantis-Token" + secret := r.Header.Get(header) + if secret != string(e.APISecret) { + return nil, nil, http.StatusUnauthorized, fmt.Errorf("header %s did not match expected secret", header) + } + + // Parse the JSON payload + bytes, err := ioutil.ReadAll(r.Body) + if err != nil { + return nil, nil, http.StatusBadRequest, fmt.Errorf("failed to read request") + } + var request APIRequest + if err = json.Unmarshal(bytes, &request); err != nil { + return nil, nil, http.StatusBadRequest, fmt.Errorf("Failed to parse request: %v", err.Error()) + } + if err = validator.New().Struct(request); err != nil { + return nil, nil, http.StatusBadRequest, fmt.Errorf("Request %q is missing fields", string(bytes)) + } + + VCSHostType, err := models.NewVCSHostType(request.Type) + if err != nil { + return nil, nil, http.StatusBadRequest, err + } + cloneURL, err := e.VCSClient.GetCloneURL(VCSHostType, request.Repository) + if err != nil { + return nil, nil, http.StatusInternalServerError, err + } + + baseRepo, err := e.Parser.ParseAPIPlanRequest(request.Repository, cloneURL) + if err != nil { + return nil, nil, http.StatusBadRequest, fmt.Errorf("failed to parse request: %v", err) + } + + // Check if the repo is whitelisted + if !e.RepoWhitelistChecker.IsWhitelisted(baseRepo.FullName, baseRepo.VCSHost.Hostname) { + return nil, nil, http.StatusForbidden, fmt.Errorf("Repo not whitelisted") + } + + return &request, &events.CommandContext{ + BaseRepo: baseRepo, + HeadRepo: baseRepo, + Pull: models.PullRequest{ + Num: -1, + BaseBranch: request.Ref, + HeadBranch: request.Ref, + HeadCommit: request.Ref, + }, + }, 0, nil +} diff --git a/server/server.go b/server/server.go index 70d245793a..3925d9b78b 100644 --- a/server/server.go +++ b/server/server.go @@ -387,6 +387,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { GithubRequestValidator: &DefaultGithubRequestValidator{}, GitlabRequestParserValidator: &DefaultGitlabRequestParserValidator{}, GitlabWebhookSecret: []byte(userConfig.GitlabWebhookSecret), + APISecret: []byte(userConfig.APISecret), RepoWhitelistChecker: repoWhitelist, SilenceWhitelistErrors: userConfig.SilenceWhitelistErrors, SupportedVCSHosts: supportedVCSHosts, @@ -421,6 +422,8 @@ func (s *Server) Start() error { s.Router.HandleFunc("/healthz", s.Healthz).Methods("GET") s.Router.PathPrefix("/static/").Handler(http.FileServer(&assetfs.AssetFS{Asset: static.Asset, AssetDir: static.AssetDir, AssetInfo: static.AssetInfo})) s.Router.HandleFunc("/events", s.EventsController.Post).Methods("POST") + s.Router.HandleFunc("/plan", s.EventsController.APIPlan).Methods("POST") + s.Router.HandleFunc("/apply", s.EventsController.APIApply).Methods("POST") s.Router.HandleFunc("/locks", s.LocksController.DeleteLock).Methods("DELETE").Queries("id", "{id:.*}") s.Router.HandleFunc("/lock", s.LocksController.GetLock).Methods("GET"). Queries(LockViewRouteIDQueryParam, fmt.Sprintf("{%s}", LockViewRouteIDQueryParam)).Name(LockViewRouteName) diff --git a/server/user_config.go b/server/user_config.go index 28ca724e9f..186a48885a 100644 --- a/server/user_config.go +++ b/server/user_config.go @@ -32,6 +32,7 @@ type UserConfig struct { GitlabToken string `mapstructure:"gitlab-token"` GitlabUser string `mapstructure:"gitlab-user"` GitlabWebhookSecret string `mapstructure:"gitlab-webhook-secret"` + APISecret string `mapstructure:"api-secret"` LogLevel string `mapstructure:"log-level"` Port int `mapstructure:"port"` RepoConfig string `mapstructure:"repo-config"` From 7f7114223c65f8b4e27b712d47d7a230d64a251c Mon Sep 17 00:00:00 2001 From: Li Lin Date: Tue, 19 Jul 2022 14:59:44 -0700 Subject: [PATCH 2/9] Resolve conflicts --- .../controllers/events/events_controller.go | 66 ++++++++++++------- server/events/models/models.go | 62 ++++++++++++++++- server/server.go | 8 ++- 3 files changed, 111 insertions(+), 25 deletions(-) diff --git a/server/controllers/events/events_controller.go b/server/controllers/events/events_controller.go index cf8ef28d95..0bbc04d752 100644 --- a/server/controllers/events/events_controller.go +++ b/server/controllers/events/events_controller.go @@ -17,6 +17,7 @@ import ( "encoding/json" "fmt" "io" + "io/ioutil" "net/http" "strings" @@ -24,7 +25,9 @@ import ( "github.com/mcdafydd/go-azuredevops/azuredevops" "github.com/microcosm-cc/bluemonday" "github.com/pkg/errors" + "github.com/runatlantis/atlantis/server/core/locking" "github.com/runatlantis/atlantis/server/events" + "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/events/models" "github.com/runatlantis/atlantis/server/events/vcs" "github.com/runatlantis/atlantis/server/events/vcs/bitbucketcloud" @@ -89,7 +92,11 @@ type VCSEventsController struct { AzureDevopsWebhookBasicPassword []byte AzureDevopsRequestValidator AzureDevopsRequestValidator - APISecret []byte + APISecret []byte + ProjectCommandBuilder events.ProjectCommandBuilder + ProjectPlanCommandRunner events.ProjectPlanCommandRunner + ProjectApplyCommandRunner events.ProjectApplyCommandRunner + Locker locking.Locker } // Post handles POST webhook requests. @@ -729,7 +736,7 @@ type APIRequest struct { } } -func (a *APIRequest) getCommands(ctx *events.CommandContext, cmdBuilder func(*events.CommandContext, *events.CommentCommand) ([]models.ProjectCommandContext, error)) ([]models.ProjectCommandContext, error) { +func (a *APIRequest) getCommands(ctx *command.Context, cmdBuilder func(*command.Context, *events.CommentCommand) ([]command.ProjectContext, error)) ([]command.ProjectContext, error) { cc := make([]*events.CommentCommand, 0) for _, project := range a.Projects { @@ -744,7 +751,7 @@ func (a *APIRequest) getCommands(ctx *events.CommandContext, cmdBuilder func(*ev }) } - cmds := make([]models.ProjectCommandContext, 0) + cmds := make([]command.ProjectContext, 0) for _, commentCommand := range cc { projectCmds, err := cmdBuilder(ctx, commentCommand) if err != nil { @@ -758,14 +765,14 @@ func (a *APIRequest) getCommands(ctx *events.CommandContext, cmdBuilder func(*ev return cmds, nil } -func (e *EventsController) apiReportError(w http.ResponseWriter, code int, err error) { +func (e *VCSEventsController) apiReportError(w http.ResponseWriter, code int, err error) { response, _ := json.Marshal(map[string]string{ "error": err.Error(), }) e.respond(w, logging.Warn, code, string(response)) } -func (e *EventsController) APIPlan(w http.ResponseWriter, r *http.Request) { +func (e *VCSEventsController) APIPlan(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") request, ctx, code, err := e.apiParseAndValidate(w, r) @@ -775,15 +782,19 @@ func (e *EventsController) APIPlan(w http.ResponseWriter, r *http.Request) { return } - runner := e.CommandRunner.(*events.DefaultCommandRunner) - cmds, err := request.getCommands(ctx, runner.ProjectCommandBuilder.BuildPlanCommands) + cmds, err := request.getCommands(ctx, e.ProjectCommandBuilder.BuildPlanCommands) if err != nil { e.apiReportError(w, http.StatusInternalServerError, err) return } - defer runner.ProjectCommandRunner.(*events.DefaultProjectCommandRunner).Locker.(*events.DefaultProjectLocker).Locker.UnlockByPull(ctx.BaseRepo.FullName, -1) - result := runner.RunProjectCmds(cmds, models.PlanCommand) + defer e.Locker.UnlockByPull(ctx.HeadRepo.FullName, 0) + var projectResults []command.ProjectResult + for _, cmd := range cmds { + res := e.ProjectPlanCommandRunner.Plan(cmd) + projectResults = append(projectResults, res) + } + result := command.Result{ProjectResults: projectResults} code = http.StatusOK if result.HasErrors() { @@ -795,7 +806,7 @@ func (e *EventsController) APIPlan(w http.ResponseWriter, r *http.Request) { e.respond(w, logging.Debug, code, string(response)) } -func (e *EventsController) APIApply(w http.ResponseWriter, r *http.Request) { +func (e *VCSEventsController) APIApply(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") request, ctx, code, err := e.apiParseAndValidate(w, r) @@ -804,25 +815,34 @@ func (e *EventsController) APIApply(w http.ResponseWriter, r *http.Request) { e.apiReportError(w, code, err) return } - runner := e.CommandRunner.(*events.DefaultCommandRunner) // We must first make the plan for all projects - cmds, err := request.getCommands(ctx, runner.ProjectCommandBuilder.BuildPlanCommands) + cmds, err := request.getCommands(ctx, e.ProjectCommandBuilder.BuildPlanCommands) if err != nil { e.apiReportError(w, http.StatusInternalServerError, err) return } - defer runner.ProjectCommandRunner.(*events.DefaultProjectCommandRunner).Locker.(*events.DefaultProjectLocker).Locker.UnlockByPull(ctx.BaseRepo.FullName, -1) - result := runner.RunProjectCmds(cmds, models.PlanCommand) + defer e.Locker.UnlockByPull(ctx.HeadRepo.FullName, 0) + var projectResults []command.ProjectResult + for _, cmd := range cmds { + res := e.ProjectPlanCommandRunner.Plan(cmd) + projectResults = append(projectResults, res) + } + result := command.Result{ProjectResults: projectResults} // We can now prepare and run the apply step - cmds, err = request.getCommands(ctx, runner.ProjectCommandBuilder.BuildApplyCommands) + cmds, err = request.getCommands(ctx, e.ProjectCommandBuilder.BuildApplyCommands) if err != nil { e.apiReportError(w, http.StatusInternalServerError, err) return } - result = runner.RunProjectCmds(cmds, models.ApplyCommand) + projectResults = nil + for _, cmd := range cmds { + res := e.ProjectApplyCommandRunner.Apply(cmd) + projectResults = append(projectResults, res) + } + result = command.Result{ProjectResults: projectResults} code = http.StatusOK if result.HasErrors() { @@ -833,7 +853,7 @@ func (e *EventsController) APIApply(w http.ResponseWriter, r *http.Request) { e.respond(w, logging.Debug, http.StatusOK, string(response)) } -func (e *EventsController) apiParseAndValidate(w http.ResponseWriter, r *http.Request) (*APIRequest, *events.CommandContext, int, error) { +func (e *VCSEventsController) apiParseAndValidate(w http.ResponseWriter, r *http.Request) (*APIRequest, *command.Context, int, error) { if len(e.APISecret) == 0 { return nil, nil, http.StatusBadRequest, fmt.Errorf("Ignoring request since API is disabled") } @@ -873,18 +893,20 @@ func (e *EventsController) apiParseAndValidate(w http.ResponseWriter, r *http.Re } // Check if the repo is whitelisted - if !e.RepoWhitelistChecker.IsWhitelisted(baseRepo.FullName, baseRepo.VCSHost.Hostname) { - return nil, nil, http.StatusForbidden, fmt.Errorf("Repo not whitelisted") + if !e.RepoAllowlistChecker.IsAllowlisted(baseRepo.FullName, baseRepo.VCSHost.Hostname) { + return nil, nil, http.StatusForbidden, fmt.Errorf("Repo not allowlisted") } - return &request, &events.CommandContext{ - BaseRepo: baseRepo, + return &request, &command.Context{ HeadRepo: baseRepo, Pull: models.PullRequest{ - Num: -1, + Num: 0, BaseBranch: request.Ref, HeadBranch: request.Ref, HeadCommit: request.Ref, + BaseRepo: baseRepo, }, + Scope: e.Scope, + Log: e.Logger, }, 0, nil } diff --git a/server/events/models/models.go b/server/events/models/models.go index 8473fff6a5..8271b0f0d7 100644 --- a/server/events/models/models.go +++ b/server/events/models/models.go @@ -18,6 +18,8 @@ package models import ( "fmt" + "github.com/hashicorp/go-version" + "github.com/runatlantis/atlantis/server/core/config/valid" "net/url" paths "path" "regexp" @@ -328,7 +330,65 @@ func NewVCSHostType(t string) (VCSHostType, error) { return AzureDevops, nil } - return 0, fmt.Errorf("%q is not a valid type", t) + return -1, fmt.Errorf("%q is not a valid type", t) +} + +// ProjectCommandContext defines the context for a plan or apply stage that will +// be executed for a project. +type ProjectCommandContext struct { + // ApplyCmd is the command that users should run to apply this plan. If + // this is an apply then this will be empty. + ApplyCmd string + // ApplyRequirements is the list of requirements that must be satisfied + // before we will run the apply stage. + ApplyRequirements []string + // AutoplanEnabled is true if automerge is enabled for the repo that this + // project is in. + AutomergeEnabled bool + // AutoplanEnabled is true if autoplanning is enabled for this project. + AutoplanEnabled bool + // BaseRepo is the repository that the pull request will be merged into. + BaseRepo Repo + // EscapedCommentArgs are the extra arguments that were added to the atlantis + // command, ex. atlantis plan -- -target=resource. We then escape them + // by adding a \ before each character so that they can be used within + // sh -c safely, i.e. sh -c "terraform plan $(touch bad)". + EscapedCommentArgs []string + // HeadRepo is the repository that is getting merged into the BaseRepo. + // If the pull request branch is from the same repository then HeadRepo will + // be the same as BaseRepo. + HeadRepo Repo + // Log is a logger that's been set up for this context. + Log *logging.SimpleLogging + // PullMergeable is true if the pull request for this project is able to be merged. + PullMergeable bool + // Pull is the pull request we're responding to. + Pull PullRequest + // ProjectName is the name of the project set in atlantis.yaml. If there was + // no name this will be an empty string. + ProjectName string + // RepoConfigVersion is the version of the repo's atlantis.yaml file. If + // there was no file, this will be 0. + RepoConfigVersion int + // RePlanCmd is the command that users should run to re-plan this project. + // If this is an apply then this will be empty. + RePlanCmd string + // RepoRelDir is the directory of this project relative to the repo root. + RepoRelDir string + // Steps are the sequence of commands we need to run for this project and this + // stage. + Steps []valid.Step + // TerraformVersion is the version of terraform we should use when executing + // commands for this project. This can be set to nil in which case we will + // use the default Atlantis terraform version. + TerraformVersion *version.Version + // User is the user that triggered this command. + User User + // Verbose is true when the user would like verbose output. + Verbose bool + // Workspace is the Terraform workspace this project is in. It will always + // be set. + Workspace string } // SplitRepoFullName splits a repo full name up into its owner and repo diff --git a/server/server.go b/server/server.go index f42b17ddb6..16375dc6dc 100644 --- a/server/server.go +++ b/server/server.go @@ -749,6 +749,10 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { GitlabRequestParserValidator: &events_controllers.DefaultGitlabRequestParserValidator{}, GitlabWebhookSecret: []byte(userConfig.GitlabWebhookSecret), APISecret: []byte(userConfig.APISecret), + ProjectCommandBuilder: projectCommandBuilder, + ProjectPlanCommandRunner: instrumentedProjectCmdRunner, + ProjectApplyCommandRunner: instrumentedProjectCmdRunner, + Locker: lockingClient, RepoAllowlistChecker: repoAllowlist, SilenceAllowlistErrors: userConfig.SilenceAllowlistErrors, SupportedVCSHosts: supportedVCSHosts, @@ -813,8 +817,8 @@ func (s *Server) Start() error { s.Router.HandleFunc("/status", s.StatusController.Get).Methods("GET") s.Router.PathPrefix("/static/").Handler(http.FileServer(&assetfs.AssetFS{Asset: static.Asset, AssetDir: static.AssetDir, AssetInfo: static.AssetInfo})) s.Router.HandleFunc("/events", s.VCSEventsController.Post).Methods("POST") - s.Router.HandleFunc("/plan", s.EventsController.APIPlan).Methods("POST") - s.Router.HandleFunc("/apply", s.EventsController.APIApply).Methods("POST") + s.Router.HandleFunc("/plan", s.VCSEventsController.APIPlan).Methods("POST") + s.Router.HandleFunc("/apply", s.VCSEventsController.APIApply).Methods("POST") s.Router.HandleFunc("/github-app/exchange-code", s.GithubAppController.ExchangeCode).Methods("GET") s.Router.HandleFunc("/github-app/setup", s.GithubAppController.New).Methods("GET") s.Router.HandleFunc("/apply/lock", s.LocksController.LockApply).Methods("POST").Queries() From 40f76f6bb079c4fbcc02e7d83e3f10dd437b8311 Mon Sep 17 00:00:00 2001 From: Li Lin Date: Tue, 19 Jul 2022 15:05:12 -0700 Subject: [PATCH 3/9] Fix wrong merge --- server/events/models/models.go | 60 ---------------------------------- 1 file changed, 60 deletions(-) diff --git a/server/events/models/models.go b/server/events/models/models.go index 8271b0f0d7..7da8b57b48 100644 --- a/server/events/models/models.go +++ b/server/events/models/models.go @@ -18,8 +18,6 @@ package models import ( "fmt" - "github.com/hashicorp/go-version" - "github.com/runatlantis/atlantis/server/core/config/valid" "net/url" paths "path" "regexp" @@ -333,64 +331,6 @@ func NewVCSHostType(t string) (VCSHostType, error) { return -1, fmt.Errorf("%q is not a valid type", t) } -// ProjectCommandContext defines the context for a plan or apply stage that will -// be executed for a project. -type ProjectCommandContext struct { - // ApplyCmd is the command that users should run to apply this plan. If - // this is an apply then this will be empty. - ApplyCmd string - // ApplyRequirements is the list of requirements that must be satisfied - // before we will run the apply stage. - ApplyRequirements []string - // AutoplanEnabled is true if automerge is enabled for the repo that this - // project is in. - AutomergeEnabled bool - // AutoplanEnabled is true if autoplanning is enabled for this project. - AutoplanEnabled bool - // BaseRepo is the repository that the pull request will be merged into. - BaseRepo Repo - // EscapedCommentArgs are the extra arguments that were added to the atlantis - // command, ex. atlantis plan -- -target=resource. We then escape them - // by adding a \ before each character so that they can be used within - // sh -c safely, i.e. sh -c "terraform plan $(touch bad)". - EscapedCommentArgs []string - // HeadRepo is the repository that is getting merged into the BaseRepo. - // If the pull request branch is from the same repository then HeadRepo will - // be the same as BaseRepo. - HeadRepo Repo - // Log is a logger that's been set up for this context. - Log *logging.SimpleLogging - // PullMergeable is true if the pull request for this project is able to be merged. - PullMergeable bool - // Pull is the pull request we're responding to. - Pull PullRequest - // ProjectName is the name of the project set in atlantis.yaml. If there was - // no name this will be an empty string. - ProjectName string - // RepoConfigVersion is the version of the repo's atlantis.yaml file. If - // there was no file, this will be 0. - RepoConfigVersion int - // RePlanCmd is the command that users should run to re-plan this project. - // If this is an apply then this will be empty. - RePlanCmd string - // RepoRelDir is the directory of this project relative to the repo root. - RepoRelDir string - // Steps are the sequence of commands we need to run for this project and this - // stage. - Steps []valid.Step - // TerraformVersion is the version of terraform we should use when executing - // commands for this project. This can be set to nil in which case we will - // use the default Atlantis terraform version. - TerraformVersion *version.Version - // User is the user that triggered this command. - User User - // Verbose is true when the user would like verbose output. - Verbose bool - // Workspace is the Terraform workspace this project is in. It will always - // be set. - Workspace string -} - // SplitRepoFullName splits a repo full name up into its owner and repo // name segments. If the repoFullName is malformed, may return empty // strings for owner or repo. From 439623f5eaabc4df4f3b16d87dd918670c1f0cbb Mon Sep 17 00:00:00 2001 From: Li Lin Date: Tue, 19 Jul 2022 15:46:28 -0700 Subject: [PATCH 4/9] Add missing methods for mocks --- server/events/mocks/mock_event_parsing.go | 19 +++++++++++++++++++ server/events/vcs/mocks/mock_client.go | 19 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/server/events/mocks/mock_event_parsing.go b/server/events/mocks/mock_event_parsing.go index 14d3fbde95..f528ccdc06 100644 --- a/server/events/mocks/mock_event_parsing.go +++ b/server/events/mocks/mock_event_parsing.go @@ -228,6 +228,25 @@ func (mock *MockEventParsing) ParseGitlabMergeRequest(mr *go_gitlab.MergeRequest return ret0 } +func (mock *MockEventParsing) ParseAPIPlanRequest(repoFullName string, cloneURL string) (models.Repo, error) { + if mock == nil { + panic("mock must not be nil. Use myMock := NewMockEventParsing().") + } + params := []pegomock.Param{repoFullName, cloneURL} + result := pegomock.GetGenericMockFrom(mock).Invoke("ParseAPIPlanRequest", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*string)(nil)).Elem()}) + var ret0 models.Repo + var ret1 error + if len(result) != 0 { + if result[0] != nil { + ret0 = result[0].(models.Repo) + } + if result[1] != nil { + ret1 = result[1].(error) + } + } + return ret0, ret1 +} + func (mock *MockEventParsing) ParseBitbucketCloudPullEvent(body []byte) (models.PullRequest, models.Repo, models.Repo, models.User, error) { if mock == nil { panic("mock must not be nil. Use myMock := NewMockEventParsing().") diff --git a/server/events/vcs/mocks/mock_client.go b/server/events/vcs/mocks/mock_client.go index 0f70008577..3522cc8e0f 100644 --- a/server/events/vcs/mocks/mock_client.go +++ b/server/events/vcs/mocks/mock_client.go @@ -204,6 +204,25 @@ func (mock *MockClient) SupportsSingleFileDownload(_param0 models.Repo) bool { return ret0 } +func (mock *MockClient) GetCloneURL(_param0 models.VCSHostType, _param1 string) (string, error) { + if mock == nil { + panic("mock must not be nil. Use myMock := NewMockClient().") + } + params := []pegomock.Param{_param0, _param1} + result := pegomock.GetGenericMockFrom(mock).Invoke("GetCloneURL", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) + var ret0 string + var ret1 error + if len(result) != 0 { + if result[0] != nil { + ret0 = result[0].(string) + } + if result[1] != nil { + ret1 = result[1].(error) + } + } + return ret0, ret1 +} + func (mock *MockClient) UpdateStatus(_param0 models.Repo, _param1 models.PullRequest, _param2 models.CommitStatus, _param3 string, _param4 string, _param5 string) error { if mock == nil { panic("mock must not be nil. Use myMock := NewMockClient().") From 34be22a2bb363aa603faa7ba4dc9569809df0a45 Mon Sep 17 00:00:00 2001 From: Li Lin Date: Tue, 19 Jul 2022 16:03:24 -0700 Subject: [PATCH 5/9] Fix linting error --- server/controllers/events/events_controller.go | 12 ++++-------- server/events/vcs/bitbucketcloud/client.go | 2 +- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/server/controllers/events/events_controller.go b/server/controllers/events/events_controller.go index 0bbc04d752..0091228755 100644 --- a/server/controllers/events/events_controller.go +++ b/server/controllers/events/events_controller.go @@ -757,9 +757,7 @@ func (a *APIRequest) getCommands(ctx *command.Context, cmdBuilder func(*command. if err != nil { return nil, fmt.Errorf("Failed to build command: %v", err) } - for _, cmd := range projectCmds { - cmds = append(cmds, cmd) - } + cmds = append(cmds, projectCmds...) } return cmds, nil @@ -788,7 +786,7 @@ func (e *VCSEventsController) APIPlan(w http.ResponseWriter, r *http.Request) { return } - defer e.Locker.UnlockByPull(ctx.HeadRepo.FullName, 0) + defer e.Locker.UnlockByPull(ctx.HeadRepo.FullName, 0) // nolint: errcheck var projectResults []command.ProjectResult for _, cmd := range cmds { res := e.ProjectPlanCommandRunner.Plan(cmd) @@ -796,7 +794,6 @@ func (e *VCSEventsController) APIPlan(w http.ResponseWriter, r *http.Request) { } result := command.Result{ProjectResults: projectResults} - code = http.StatusOK if result.HasErrors() { code = http.StatusInternalServerError } @@ -823,7 +820,7 @@ func (e *VCSEventsController) APIApply(w http.ResponseWriter, r *http.Request) { return } - defer e.Locker.UnlockByPull(ctx.HeadRepo.FullName, 0) + defer e.Locker.UnlockByPull(ctx.HeadRepo.FullName, 0) // nolint: errcheck var projectResults []command.ProjectResult for _, cmd := range cmds { res := e.ProjectPlanCommandRunner.Plan(cmd) @@ -844,7 +841,6 @@ func (e *VCSEventsController) APIApply(w http.ResponseWriter, r *http.Request) { } result = command.Result{ProjectResults: projectResults} - code = http.StatusOK if result.HasErrors() { code = http.StatusInternalServerError } @@ -908,5 +904,5 @@ func (e *VCSEventsController) apiParseAndValidate(w http.ResponseWriter, r *http }, Scope: e.Scope, Log: e.Logger, - }, 0, nil + }, http.StatusOK, nil } diff --git a/server/events/vcs/bitbucketcloud/client.go b/server/events/vcs/bitbucketcloud/client.go index 8388dc4082..4bdad53128 100644 --- a/server/events/vcs/bitbucketcloud/client.go +++ b/server/events/vcs/bitbucketcloud/client.go @@ -267,6 +267,6 @@ func (b *Client) DownloadRepoConfigFile(pull models.PullRequest) (bool, []byte, return false, []byte{}, fmt.Errorf("Not Implemented") } -func (g *Client) GetCloneURL(VCSHostType models.VCSHostType, repo string) (string, error) { +func (b *Client) GetCloneURL(VCSHostType models.VCSHostType, repo string) (string, error) { return "", fmt.Errorf("not yet implemented") } From 08c7a4daa6e78483af85d95417172108a851ec45 Mon Sep 17 00:00:00 2001 From: Li Lin Date: Tue, 19 Jul 2022 16:07:45 -0700 Subject: [PATCH 6/9] Fix linting error --- server/controllers/events/events_controller.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/controllers/events/events_controller.go b/server/controllers/events/events_controller.go index 0091228755..02e69d4cf5 100644 --- a/server/controllers/events/events_controller.go +++ b/server/controllers/events/events_controller.go @@ -846,7 +846,7 @@ func (e *VCSEventsController) APIApply(w http.ResponseWriter, r *http.Request) { } response, _ := json.Marshal(result) - e.respond(w, logging.Debug, http.StatusOK, string(response)) + e.respond(w, logging.Debug, code, string(response)) } func (e *VCSEventsController) apiParseAndValidate(w http.ResponseWriter, r *http.Request) (*APIRequest, *command.Context, int, error) { From bd36139d4637d21ca4be01fe54826d770aada88f Mon Sep 17 00:00:00 2001 From: Li Lin Date: Wed, 20 Jul 2022 15:38:52 -0700 Subject: [PATCH 7/9] Move api plan/apply into APIController --- server/controllers/api_controller.go | 229 ++++++++++++++++++ server/controllers/api_controller_test.go | 103 ++++++++ .../controllers/events/events_controller.go | 193 --------------- server/server.go | 23 +- 4 files changed, 348 insertions(+), 200 deletions(-) create mode 100644 server/controllers/api_controller.go create mode 100644 server/controllers/api_controller_test.go diff --git a/server/controllers/api_controller.go b/server/controllers/api_controller.go new file mode 100644 index 0000000000..1f381a02c1 --- /dev/null +++ b/server/controllers/api_controller.go @@ -0,0 +1,229 @@ +package controllers + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + + "github.com/runatlantis/atlantis/server/core/locking" + "github.com/runatlantis/atlantis/server/events" + "github.com/runatlantis/atlantis/server/events/command" + "github.com/runatlantis/atlantis/server/events/models" + "github.com/runatlantis/atlantis/server/events/vcs" + "github.com/runatlantis/atlantis/server/logging" + "github.com/uber-go/tally" + "gopkg.in/go-playground/validator.v9" +) + +const atlantisTokenHeader = "X-Atlantis-Token" + +type APIController struct { + APISecret []byte + Locker locking.Locker + Logger logging.SimpleLogging + Parser events.EventParsing + ProjectCommandBuilder events.ProjectCommandBuilder + ProjectPlanCommandRunner events.ProjectPlanCommandRunner + ProjectApplyCommandRunner events.ProjectApplyCommandRunner + RepoAllowlistChecker *events.RepoAllowlistChecker + Scope tally.Scope + VCSClient vcs.Client +} + +type APIRequest struct { + Repository string `validate:"required"` + Ref string `validate:"required"` + Type string `validate:"required"` + Projects []string + Paths []struct { + Directory string + Workspace string + } +} + +func (a *APIRequest) getCommands(ctx *command.Context, cmdBuilder func(*command.Context, *events.CommentCommand) ([]command.ProjectContext, error)) ([]command.ProjectContext, error) { + cc := make([]*events.CommentCommand, 0) + + for _, project := range a.Projects { + cc = append(cc, &events.CommentCommand{ + ProjectName: project, + }) + } + for _, path := range a.Paths { + cc = append(cc, &events.CommentCommand{ + RepoRelDir: strings.TrimRight(path.Directory, "/"), + Workspace: path.Workspace, + }) + } + + cmds := make([]command.ProjectContext, 0) + for _, commentCommand := range cc { + projectCmds, err := cmdBuilder(ctx, commentCommand) + if err != nil { + return nil, fmt.Errorf("failed to build command: %v", err) + } + cmds = append(cmds, projectCmds...) + } + + return cmds, nil +} + +func (a *APIController) apiReportError(w http.ResponseWriter, code int, err error) { + response, _ := json.Marshal(map[string]string{ + "error": err.Error(), + }) + a.respond(w, logging.Warn, code, string(response)) +} + +func (a *APIController) Plan(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + request, ctx, code, err := a.apiParseAndValidate(r) + + if err != nil { + a.apiReportError(w, code, err) + return + } + + cmds, err := request.getCommands(ctx, a.ProjectCommandBuilder.BuildPlanCommands) + if err != nil { + a.apiReportError(w, http.StatusInternalServerError, err) + return + } + + defer a.Locker.UnlockByPull(ctx.HeadRepo.FullName, 0) // nolint: errcheck + var projectResults []command.ProjectResult + for _, cmd := range cmds { + res := a.ProjectPlanCommandRunner.Plan(cmd) + projectResults = append(projectResults, res) + } + result := command.Result{ProjectResults: projectResults} + + if result.HasErrors() { + code = http.StatusInternalServerError + } + + // TODO: make a better response + response, err := json.Marshal(result) + if err != nil { + a.apiReportError(w, http.StatusInternalServerError, err) + return + } + a.respond(w, logging.Debug, code, string(response)) +} + +func (a *APIController) Apply(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + request, ctx, code, err := a.apiParseAndValidate(r) + + if err != nil { + a.apiReportError(w, code, err) + return + } + + // We must first make the plan for all projects + cmds, err := request.getCommands(ctx, a.ProjectCommandBuilder.BuildPlanCommands) + if err != nil { + a.apiReportError(w, http.StatusInternalServerError, err) + return + } + + defer a.Locker.UnlockByPull(ctx.HeadRepo.FullName, 0) // nolint: errcheck + var projectResults []command.ProjectResult + for _, cmd := range cmds { + res := a.ProjectPlanCommandRunner.Plan(cmd) + projectResults = append(projectResults, res) + } + result := command.Result{ProjectResults: projectResults} + + // We can now prepare and run the apply step + cmds, err = request.getCommands(ctx, a.ProjectCommandBuilder.BuildApplyCommands) + if err != nil { + a.apiReportError(w, http.StatusInternalServerError, err) + return + } + projectResults = nil + for _, cmd := range cmds { + res := a.ProjectApplyCommandRunner.Apply(cmd) + projectResults = append(projectResults, res) + } + result = command.Result{ProjectResults: projectResults} + + if result.HasErrors() { + code = http.StatusInternalServerError + } + + response, err := json.Marshal(result) + if err != nil { + a.apiReportError(w, http.StatusInternalServerError, err) + return + } + a.respond(w, logging.Debug, code, string(response)) +} + +func (a *APIController) apiParseAndValidate(r *http.Request) (*APIRequest, *command.Context, int, error) { + if len(a.APISecret) == 0 { + return nil, nil, http.StatusBadRequest, fmt.Errorf("ignoring request since API is disabled") + } + + // Validate the secret token + secret := r.Header.Get(atlantisTokenHeader) + if secret != string(a.APISecret) { + return nil, nil, http.StatusUnauthorized, fmt.Errorf("header %s did not match expected secret", atlantisTokenHeader) + } + + // Parse the JSON payload + bytes, err := ioutil.ReadAll(r.Body) + if err != nil { + return nil, nil, http.StatusBadRequest, fmt.Errorf("failed to read request") + } + var request APIRequest + if err = json.Unmarshal(bytes, &request); err != nil { + return nil, nil, http.StatusBadRequest, fmt.Errorf("failed to parse request: %v", err.Error()) + } + if err = validator.New().Struct(request); err != nil { + return nil, nil, http.StatusBadRequest, fmt.Errorf("request %q is missing fields", string(bytes)) + } + + VCSHostType, err := models.NewVCSHostType(request.Type) + if err != nil { + return nil, nil, http.StatusBadRequest, err + } + cloneURL, err := a.VCSClient.GetCloneURL(VCSHostType, request.Repository) + if err != nil { + return nil, nil, http.StatusInternalServerError, err + } + + baseRepo, err := a.Parser.ParseAPIPlanRequest(request.Repository, cloneURL) + if err != nil { + return nil, nil, http.StatusBadRequest, fmt.Errorf("failed to parse request: %v", err) + } + + // Check if the repo is allowlisted + if !a.RepoAllowlistChecker.IsAllowlisted(baseRepo.FullName, baseRepo.VCSHost.Hostname) { + return nil, nil, http.StatusForbidden, fmt.Errorf("repo not allowlisted") + } + + return &request, &command.Context{ + HeadRepo: baseRepo, + Pull: models.PullRequest{ + Num: 0, + BaseBranch: request.Ref, + HeadBranch: request.Ref, + HeadCommit: request.Ref, + BaseRepo: baseRepo, + }, + Scope: a.Scope, + Log: a.Logger, + }, http.StatusOK, nil +} + +func (a *APIController) respond(w http.ResponseWriter, lvl logging.LogLevel, responseCode int, format string, args ...interface{}) { + response := fmt.Sprintf(format, args...) + a.Logger.Log(lvl, response) + w.WriteHeader(responseCode) + fmt.Fprintln(w, response) +} diff --git a/server/controllers/api_controller_test.go b/server/controllers/api_controller_test.go new file mode 100644 index 0000000000..8cae678b63 --- /dev/null +++ b/server/controllers/api_controller_test.go @@ -0,0 +1,103 @@ +package controllers_test + +import ( + "bytes" + "encoding/json" + "github.com/runatlantis/atlantis/server/events/command" + "github.com/runatlantis/atlantis/server/events/models" + "net/http" + "net/http/httptest" + "testing" + + . "github.com/petergtz/pegomock" + "github.com/runatlantis/atlantis/server/controllers" + . "github.com/runatlantis/atlantis/server/core/locking/mocks" + "github.com/runatlantis/atlantis/server/events" + . "github.com/runatlantis/atlantis/server/events/mocks" + . "github.com/runatlantis/atlantis/server/events/mocks/matchers" + . "github.com/runatlantis/atlantis/server/events/vcs/mocks" + "github.com/runatlantis/atlantis/server/logging" + "github.com/runatlantis/atlantis/server/metrics" + . "github.com/runatlantis/atlantis/testing" +) + +const atlantisTokenHeader = "X-Atlantis-Token" +const atlantisToken = "token" + +func TestAPIController_Plan(t *testing.T) { + ac, projectCommandBuilder, projectCommandRunner := setup(t) + body, _ := json.Marshal(controllers.APIRequest{ + Repository: "Repo", + Ref: "main", + Type: "Gitlab", + Projects: []string{"default"}, + }) + req, _ := http.NewRequest("POST", "", bytes.NewBuffer(body)) + req.Header.Set(atlantisTokenHeader, atlantisToken) + w := httptest.NewRecorder() + ac.Plan(w, req) + ResponseContains(t, w, http.StatusOK, "") + projectCommandBuilder.VerifyWasCalledOnce().BuildPlanCommands(AnyPtrToEventsCommandContext(), AnyPtrToEventsCommentCommand()) + projectCommandRunner.VerifyWasCalledOnce().Plan(AnyModelsProjectCommandContext()) +} + +func TestAPIController_Apply(t *testing.T) { + ac, projectCommandBuilder, projectCommandRunner := setup(t) + body, _ := json.Marshal(controllers.APIRequest{ + Repository: "Repo", + Ref: "main", + Type: "Gitlab", + Projects: []string{"default"}, + }) + req, _ := http.NewRequest("POST", "", bytes.NewBuffer(body)) + req.Header.Set(atlantisTokenHeader, atlantisToken) + w := httptest.NewRecorder() + ac.Apply(w, req) + ResponseContains(t, w, http.StatusOK, "") + projectCommandBuilder.VerifyWasCalledOnce().BuildApplyCommands(AnyPtrToEventsCommandContext(), AnyPtrToEventsCommentCommand()) + projectCommandRunner.VerifyWasCalledOnce().Plan(AnyModelsProjectCommandContext()) + projectCommandRunner.VerifyWasCalledOnce().Apply(AnyModelsProjectCommandContext()) +} + +func setup(t *testing.T) (controllers.APIController, *MockProjectCommandBuilder, *MockProjectCommandRunner) { + RegisterMockTestingT(t) + locker := NewMockLocker() + logger := logging.NewNoopLogger(t) + scope, _, _ := metrics.NewLoggingScope(logger, "null") + parser := NewMockEventParsing() + vcsClient := NewMockClient() + repoAllowlistChecker, err := events.NewRepoAllowlistChecker("*") + Ok(t, err) + + projectCommandBuilder := NewMockProjectCommandBuilder() + When(projectCommandBuilder.BuildPlanCommands(AnyPtrToEventsCommandContext(), AnyPtrToEventsCommentCommand())). + ThenReturn([]command.ProjectContext{{ + CommandName: command.Plan, + }}, nil) + When(projectCommandBuilder.BuildApplyCommands(AnyPtrToEventsCommandContext(), AnyPtrToEventsCommentCommand())). + ThenReturn([]command.ProjectContext{{ + CommandName: command.Apply, + }}, nil) + + projectCommandRunner := NewMockProjectCommandRunner() + When(projectCommandRunner.Plan(AnyModelsProjectCommandContext())).ThenReturn(command.ProjectResult{ + PlanSuccess: &models.PlanSuccess{}, + }) + When(projectCommandRunner.Apply(AnyModelsProjectCommandContext())).ThenReturn(command.ProjectResult{ + ApplySuccess: "success", + }) + + ac := controllers.APIController{ + APISecret: []byte(atlantisToken), + Locker: locker, + Logger: logger, + Scope: scope, + Parser: parser, + ProjectCommandBuilder: projectCommandBuilder, + ProjectPlanCommandRunner: projectCommandRunner, + ProjectApplyCommandRunner: projectCommandRunner, + VCSClient: vcsClient, + RepoAllowlistChecker: repoAllowlistChecker, + } + return ac, projectCommandBuilder, projectCommandRunner +} diff --git a/server/controllers/events/events_controller.go b/server/controllers/events/events_controller.go index 02e69d4cf5..f2697edff9 100644 --- a/server/controllers/events/events_controller.go +++ b/server/controllers/events/events_controller.go @@ -14,10 +14,8 @@ package events import ( - "encoding/json" "fmt" "io" - "io/ioutil" "net/http" "strings" @@ -25,9 +23,7 @@ import ( "github.com/mcdafydd/go-azuredevops/azuredevops" "github.com/microcosm-cc/bluemonday" "github.com/pkg/errors" - "github.com/runatlantis/atlantis/server/core/locking" "github.com/runatlantis/atlantis/server/events" - "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/events/models" "github.com/runatlantis/atlantis/server/events/vcs" "github.com/runatlantis/atlantis/server/events/vcs/bitbucketcloud" @@ -35,7 +31,6 @@ import ( "github.com/runatlantis/atlantis/server/logging" "github.com/uber-go/tally" gitlab "github.com/xanzy/go-gitlab" - "gopkg.in/go-playground/validator.v9" ) const githubHeader = "X-Github-Event" @@ -91,12 +86,6 @@ type VCSEventsController struct { // Azure DevOps Team Project. If empty, no request validation is done. AzureDevopsWebhookBasicPassword []byte AzureDevopsRequestValidator AzureDevopsRequestValidator - - APISecret []byte - ProjectCommandBuilder events.ProjectCommandBuilder - ProjectPlanCommandRunner events.ProjectPlanCommandRunner - ProjectApplyCommandRunner events.ProjectApplyCommandRunner - Locker locking.Locker } // Post handles POST webhook requests. @@ -724,185 +713,3 @@ func (e *VCSEventsController) commentNotAllowlisted(baseRepo models.Repo, pullNu e.Logger.Err("unable to comment on pull request: %s", err) } } - -type APIRequest struct { - Repository string `validate:"required"` - Ref string `validate:"required"` - Type string `validate:"required"` - Projects []string - Paths []struct { - Directory string - Workspace string - } -} - -func (a *APIRequest) getCommands(ctx *command.Context, cmdBuilder func(*command.Context, *events.CommentCommand) ([]command.ProjectContext, error)) ([]command.ProjectContext, error) { - cc := make([]*events.CommentCommand, 0) - - for _, project := range a.Projects { - cc = append(cc, &events.CommentCommand{ - ProjectName: project, - }) - } - for _, path := range a.Paths { - cc = append(cc, &events.CommentCommand{ - RepoRelDir: strings.TrimRight(path.Directory, "/"), - Workspace: path.Workspace, - }) - } - - cmds := make([]command.ProjectContext, 0) - for _, commentCommand := range cc { - projectCmds, err := cmdBuilder(ctx, commentCommand) - if err != nil { - return nil, fmt.Errorf("Failed to build command: %v", err) - } - cmds = append(cmds, projectCmds...) - } - - return cmds, nil -} - -func (e *VCSEventsController) apiReportError(w http.ResponseWriter, code int, err error) { - response, _ := json.Marshal(map[string]string{ - "error": err.Error(), - }) - e.respond(w, logging.Warn, code, string(response)) -} - -func (e *VCSEventsController) APIPlan(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - - request, ctx, code, err := e.apiParseAndValidate(w, r) - - if err != nil { - e.apiReportError(w, code, err) - return - } - - cmds, err := request.getCommands(ctx, e.ProjectCommandBuilder.BuildPlanCommands) - if err != nil { - e.apiReportError(w, http.StatusInternalServerError, err) - return - } - - defer e.Locker.UnlockByPull(ctx.HeadRepo.FullName, 0) // nolint: errcheck - var projectResults []command.ProjectResult - for _, cmd := range cmds { - res := e.ProjectPlanCommandRunner.Plan(cmd) - projectResults = append(projectResults, res) - } - result := command.Result{ProjectResults: projectResults} - - if result.HasErrors() { - code = http.StatusInternalServerError - } - - // TODO: make a better response - response, _ := json.Marshal(result) - e.respond(w, logging.Debug, code, string(response)) -} - -func (e *VCSEventsController) APIApply(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - - request, ctx, code, err := e.apiParseAndValidate(w, r) - - if err != nil { - e.apiReportError(w, code, err) - return - } - - // We must first make the plan for all projects - cmds, err := request.getCommands(ctx, e.ProjectCommandBuilder.BuildPlanCommands) - if err != nil { - e.apiReportError(w, http.StatusInternalServerError, err) - return - } - - defer e.Locker.UnlockByPull(ctx.HeadRepo.FullName, 0) // nolint: errcheck - var projectResults []command.ProjectResult - for _, cmd := range cmds { - res := e.ProjectPlanCommandRunner.Plan(cmd) - projectResults = append(projectResults, res) - } - result := command.Result{ProjectResults: projectResults} - - // We can now prepare and run the apply step - cmds, err = request.getCommands(ctx, e.ProjectCommandBuilder.BuildApplyCommands) - if err != nil { - e.apiReportError(w, http.StatusInternalServerError, err) - return - } - projectResults = nil - for _, cmd := range cmds { - res := e.ProjectApplyCommandRunner.Apply(cmd) - projectResults = append(projectResults, res) - } - result = command.Result{ProjectResults: projectResults} - - if result.HasErrors() { - code = http.StatusInternalServerError - } - - response, _ := json.Marshal(result) - e.respond(w, logging.Debug, code, string(response)) -} - -func (e *VCSEventsController) apiParseAndValidate(w http.ResponseWriter, r *http.Request) (*APIRequest, *command.Context, int, error) { - if len(e.APISecret) == 0 { - return nil, nil, http.StatusBadRequest, fmt.Errorf("Ignoring request since API is disabled") - } - - // Validate the secret token - header := "X-Atlantis-Token" - secret := r.Header.Get(header) - if secret != string(e.APISecret) { - return nil, nil, http.StatusUnauthorized, fmt.Errorf("header %s did not match expected secret", header) - } - - // Parse the JSON payload - bytes, err := ioutil.ReadAll(r.Body) - if err != nil { - return nil, nil, http.StatusBadRequest, fmt.Errorf("failed to read request") - } - var request APIRequest - if err = json.Unmarshal(bytes, &request); err != nil { - return nil, nil, http.StatusBadRequest, fmt.Errorf("Failed to parse request: %v", err.Error()) - } - if err = validator.New().Struct(request); err != nil { - return nil, nil, http.StatusBadRequest, fmt.Errorf("Request %q is missing fields", string(bytes)) - } - - VCSHostType, err := models.NewVCSHostType(request.Type) - if err != nil { - return nil, nil, http.StatusBadRequest, err - } - cloneURL, err := e.VCSClient.GetCloneURL(VCSHostType, request.Repository) - if err != nil { - return nil, nil, http.StatusInternalServerError, err - } - - baseRepo, err := e.Parser.ParseAPIPlanRequest(request.Repository, cloneURL) - if err != nil { - return nil, nil, http.StatusBadRequest, fmt.Errorf("failed to parse request: %v", err) - } - - // Check if the repo is whitelisted - if !e.RepoAllowlistChecker.IsAllowlisted(baseRepo.FullName, baseRepo.VCSHost.Hostname) { - return nil, nil, http.StatusForbidden, fmt.Errorf("Repo not allowlisted") - } - - return &request, &command.Context{ - HeadRepo: baseRepo, - Pull: models.PullRequest{ - Num: 0, - BaseBranch: request.Ref, - HeadBranch: request.Ref, - HeadCommit: request.Ref, - BaseRepo: baseRepo, - }, - Scope: e.Scope, - Log: e.Logger, - }, http.StatusOK, nil -} diff --git a/server/server.go b/server/server.go index 16375dc6dc..f7ba8d19f7 100644 --- a/server/server.go +++ b/server/server.go @@ -104,6 +104,7 @@ type Server struct { LocksController *controllers.LocksController StatusController *controllers.StatusController JobsController *controllers.JobsController + APIController *controllers.APIController IndexTemplate templates.TemplateWriter LockDetailTemplate templates.TemplateWriter ProjectJobsTemplate templates.TemplateWriter @@ -735,6 +736,18 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { KeyGenerator: controllers.JobIDKeyGenerator{}, StatsScope: statsScope.SubScope("api"), } + apiController := &controllers.APIController{ + APISecret: []byte(userConfig.APISecret), + Locker: lockingClient, + Logger: logger, + Parser: eventParser, + ProjectCommandBuilder: projectCommandBuilder, + ProjectPlanCommandRunner: instrumentedProjectCmdRunner, + ProjectApplyCommandRunner: instrumentedProjectCmdRunner, + RepoAllowlistChecker: repoAllowlist, + Scope: statsScope.SubScope("api"), + VCSClient: vcsClient, + } eventsController := &events_controllers.VCSEventsController{ CommandRunner: commandRunner, @@ -748,11 +761,6 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { GithubRequestValidator: &events_controllers.DefaultGithubRequestValidator{}, GitlabRequestParserValidator: &events_controllers.DefaultGitlabRequestParserValidator{}, GitlabWebhookSecret: []byte(userConfig.GitlabWebhookSecret), - APISecret: []byte(userConfig.APISecret), - ProjectCommandBuilder: projectCommandBuilder, - ProjectPlanCommandRunner: instrumentedProjectCmdRunner, - ProjectApplyCommandRunner: instrumentedProjectCmdRunner, - Locker: lockingClient, RepoAllowlistChecker: repoAllowlist, SilenceAllowlistErrors: userConfig.SilenceAllowlistErrors, SupportedVCSHosts: supportedVCSHosts, @@ -793,6 +801,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { LocksController: locksController, JobsController: jobsController, StatusController: statusController, + APIController: apiController, IndexTemplate: templates.IndexTemplate, LockDetailTemplate: templates.LockTemplate, ProjectJobsTemplate: templates.ProjectJobsTemplate, @@ -817,8 +826,8 @@ func (s *Server) Start() error { s.Router.HandleFunc("/status", s.StatusController.Get).Methods("GET") s.Router.PathPrefix("/static/").Handler(http.FileServer(&assetfs.AssetFS{Asset: static.Asset, AssetDir: static.AssetDir, AssetInfo: static.AssetInfo})) s.Router.HandleFunc("/events", s.VCSEventsController.Post).Methods("POST") - s.Router.HandleFunc("/plan", s.VCSEventsController.APIPlan).Methods("POST") - s.Router.HandleFunc("/apply", s.VCSEventsController.APIApply).Methods("POST") + s.Router.HandleFunc("/api/plan", s.APIController.Plan).Methods("POST") + s.Router.HandleFunc("/api/apply", s.APIController.Apply).Methods("POST") s.Router.HandleFunc("/github-app/exchange-code", s.GithubAppController.ExchangeCode).Methods("GET") s.Router.HandleFunc("/github-app/setup", s.GithubAppController.New).Methods("GET") s.Router.HandleFunc("/apply/lock", s.LocksController.LockApply).Methods("POST").Queries() From 4511dfd7515af42299da1e42effabf09013eb849 Mon Sep 17 00:00:00 2001 From: Li Lin Date: Wed, 20 Jul 2022 16:26:59 -0700 Subject: [PATCH 8/9] Extract commond code into helper functions --- server/controllers/api_controller.go | 58 +++++++++++++++------------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/server/controllers/api_controller.go b/server/controllers/api_controller.go index 1f381a02c1..7484206be7 100644 --- a/server/controllers/api_controller.go +++ b/server/controllers/api_controller.go @@ -81,26 +81,17 @@ func (a *APIController) Plan(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") request, ctx, code, err := a.apiParseAndValidate(r) - if err != nil { a.apiReportError(w, code, err) return } - cmds, err := request.getCommands(ctx, a.ProjectCommandBuilder.BuildPlanCommands) + result, err := a.apiPlan(request, ctx) if err != nil { a.apiReportError(w, http.StatusInternalServerError, err) return } - defer a.Locker.UnlockByPull(ctx.HeadRepo.FullName, 0) // nolint: errcheck - var projectResults []command.ProjectResult - for _, cmd := range cmds { - res := a.ProjectPlanCommandRunner.Plan(cmd) - projectResults = append(projectResults, res) - } - result := command.Result{ProjectResults: projectResults} - if result.HasErrors() { code = http.StatusInternalServerError } @@ -118,40 +109,25 @@ func (a *APIController) Apply(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") request, ctx, code, err := a.apiParseAndValidate(r) - if err != nil { a.apiReportError(w, code, err) return } // We must first make the plan for all projects - cmds, err := request.getCommands(ctx, a.ProjectCommandBuilder.BuildPlanCommands) + _, err = a.apiPlan(request, ctx) if err != nil { a.apiReportError(w, http.StatusInternalServerError, err) return } - defer a.Locker.UnlockByPull(ctx.HeadRepo.FullName, 0) // nolint: errcheck - var projectResults []command.ProjectResult - for _, cmd := range cmds { - res := a.ProjectPlanCommandRunner.Plan(cmd) - projectResults = append(projectResults, res) - } - result := command.Result{ProjectResults: projectResults} // We can now prepare and run the apply step - cmds, err = request.getCommands(ctx, a.ProjectCommandBuilder.BuildApplyCommands) + result, err := a.apiApply(request, ctx) if err != nil { a.apiReportError(w, http.StatusInternalServerError, err) return } - projectResults = nil - for _, cmd := range cmds { - res := a.ProjectApplyCommandRunner.Apply(cmd) - projectResults = append(projectResults, res) - } - result = command.Result{ProjectResults: projectResults} - if result.HasErrors() { code = http.StatusInternalServerError } @@ -164,6 +140,34 @@ func (a *APIController) Apply(w http.ResponseWriter, r *http.Request) { a.respond(w, logging.Debug, code, string(response)) } +func (a *APIController) apiPlan(request *APIRequest, ctx *command.Context) (*command.Result, error) { + cmds, err := request.getCommands(ctx, a.ProjectCommandBuilder.BuildPlanCommands) + if err != nil { + return nil, err + } + + var projectResults []command.ProjectResult + for _, cmd := range cmds { + res := a.ProjectPlanCommandRunner.Plan(cmd) + projectResults = append(projectResults, res) + } + return &command.Result{ProjectResults: projectResults}, nil +} + +func (a *APIController) apiApply(request *APIRequest, ctx *command.Context) (*command.Result, error) { + cmds, err := request.getCommands(ctx, a.ProjectCommandBuilder.BuildApplyCommands) + if err != nil { + return nil, err + } + + var projectResults []command.ProjectResult + for _, cmd := range cmds { + res := a.ProjectApplyCommandRunner.Apply(cmd) + projectResults = append(projectResults, res) + } + return &command.Result{ProjectResults: projectResults}, nil +} + func (a *APIController) apiParseAndValidate(r *http.Request) (*APIRequest, *command.Context, int, error) { if len(a.APISecret) == 0 { return nil, nil, http.StatusBadRequest, fmt.Errorf("ignoring request since API is disabled") From c4438a7afd521835ec1682b319f0eb0f33ca91b7 Mon Sep 17 00:00:00 2001 From: Li Lin Date: Wed, 20 Jul 2022 17:10:10 -0700 Subject: [PATCH 9/9] Implement GetCloneURL for GitHub --- server/controllers/api_controller.go | 2 +- server/events/event_parser.go | 12 +++++++++--- server/events/mocks/mock_event_parsing.go | 6 +++--- server/events/vcs/github_client.go | 7 ++++++- 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/server/controllers/api_controller.go b/server/controllers/api_controller.go index 7484206be7..3138419146 100644 --- a/server/controllers/api_controller.go +++ b/server/controllers/api_controller.go @@ -201,7 +201,7 @@ func (a *APIController) apiParseAndValidate(r *http.Request) (*APIRequest, *comm return nil, nil, http.StatusInternalServerError, err } - baseRepo, err := a.Parser.ParseAPIPlanRequest(request.Repository, cloneURL) + baseRepo, err := a.Parser.ParseAPIPlanRequest(VCSHostType, request.Repository, cloneURL) if err != nil { return nil, nil, http.StatusBadRequest, fmt.Errorf("failed to parse request: %v", err) } diff --git a/server/events/event_parser.go b/server/events/event_parser.go index 626b58afba..87cf264672 100644 --- a/server/events/event_parser.go +++ b/server/events/event_parser.go @@ -216,7 +216,7 @@ type EventParsing interface { // that returns a merge request. ParseGitlabMergeRequest(mr *gitlab.MergeRequest, baseRepo models.Repo) models.PullRequest - ParseAPIPlanRequest(path, cloneURL string) (baseRepo models.Repo, err error) + ParseAPIPlanRequest(vcsHostType models.VCSHostType, path string, cloneURL string) (baseRepo models.Repo, err error) // ParseBitbucketCloudPullEvent parses a pull request event from Bitbucket // Cloud (bitbucket.org). @@ -305,8 +305,14 @@ type EventParser struct { AzureDevopsUser string } -func (e *EventParser) ParseAPIPlanRequest(repoFullName, cloneURL string) (models.Repo, error) { - return models.NewRepo(models.Gitlab, repoFullName, cloneURL, e.GitlabUser, e.GitlabToken) +func (e *EventParser) ParseAPIPlanRequest(vcsHostType models.VCSHostType, repoFullName string, cloneURL string) (models.Repo, error) { + switch vcsHostType { + case models.Github: + return models.NewRepo(vcsHostType, repoFullName, cloneURL, e.GithubUser, e.GithubToken) + case models.Gitlab: + return models.NewRepo(vcsHostType, repoFullName, cloneURL, e.GitlabUser, e.GitlabToken) + } + return models.Repo{}, fmt.Errorf("not implemented") } // GetBitbucketCloudPullEventType returns the type of the pull request diff --git a/server/events/mocks/mock_event_parsing.go b/server/events/mocks/mock_event_parsing.go index f528ccdc06..54e8e6215a 100644 --- a/server/events/mocks/mock_event_parsing.go +++ b/server/events/mocks/mock_event_parsing.go @@ -228,12 +228,12 @@ func (mock *MockEventParsing) ParseGitlabMergeRequest(mr *go_gitlab.MergeRequest return ret0 } -func (mock *MockEventParsing) ParseAPIPlanRequest(repoFullName string, cloneURL string) (models.Repo, error) { +func (mock *MockEventParsing) ParseAPIPlanRequest(vcsHostType models.VCSHostType, repoFullName string, cloneURL string) (models.Repo, error) { if mock == nil { panic("mock must not be nil. Use myMock := NewMockEventParsing().") } - params := []pegomock.Param{repoFullName, cloneURL} - result := pegomock.GetGenericMockFrom(mock).Invoke("ParseAPIPlanRequest", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*string)(nil)).Elem()}) + params := []pegomock.Param{vcsHostType, repoFullName, cloneURL} + result := pegomock.GetGenericMockFrom(mock).Invoke("ParseAPIPlanRequest", params, []reflect.Type{reflect.TypeOf((*models.VCSHostType)(nil)).Elem(), reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*string)(nil)).Elem()}) var ret0 models.Repo var ret1 error if len(result) != 0 { diff --git a/server/events/vcs/github_client.go b/server/events/vcs/github_client.go index 93808dc74c..8b396ad557 100644 --- a/server/events/vcs/github_client.go +++ b/server/events/vcs/github_client.go @@ -503,5 +503,10 @@ func (g *GithubClient) SupportsSingleFileDownload(repo models.Repo) bool { } func (g *GithubClient) GetCloneURL(VCSHostType models.VCSHostType, repo string) (string, error) { - return "", fmt.Errorf("not yet implemented") + parts := strings.Split(repo, "/") + repository, _, err := g.client.Repositories.Get(g.ctx, parts[0], parts[1]) + if err != nil { + return "", err + } + return repository.GetCloneURL(), nil }