diff --git a/server/controllers/logstreaming_controller.go b/server/controllers/logstreaming_controller.go index 6d7007a7a..6bfd10e0a 100644 --- a/server/controllers/logstreaming_controller.go +++ b/server/controllers/logstreaming_controller.go @@ -43,11 +43,12 @@ func (p *pullInfo) String() string { type projectInfo struct { projectName string + workspace string pullInfo } func (p *projectInfo) String() string { - return fmt.Sprintf("%s/%s/%d/%s", p.org, p.repo, p.pull, p.projectName) + return fmt.Sprintf("%s/%s/%d/%s/%s", p.org, p.repo, p.pull, p.projectName, p.workspace) } func newPullInfo(r *http.Request) (*pullInfo, error) { @@ -87,9 +88,15 @@ func newProjectInfo(r *http.Request) (*projectInfo, error) { return nil, fmt.Errorf("Internal error: no project in route") } + workspace, ok := mux.Vars(r)["workspace"] + if !ok { + return nil, fmt.Errorf("Internal error: no workspace in route") + } + return &projectInfo{ pullInfo: *pullInfo, projectName: project, + workspace: workspace, }, nil } diff --git a/server/events/models/models.go b/server/events/models/models.go index 87c9457bb..b978f6ba0 100644 --- a/server/events/models/models.go +++ b/server/events/models/models.go @@ -425,11 +425,19 @@ func (p ProjectCommandContext) GetShowResultFileName() string { // Gets a unique identifier for the current pull request as a single string func (p ProjectCommandContext) PullInfo() string { - return BuildPullInfo(p.BaseRepo.FullName, p.Pull.Num, p.ProjectName) + return BuildPullInfo(p.BaseRepo.FullName, p.Pull.Num, p.ProjectName, p.RepoRelDir, p.Workspace) } -func BuildPullInfo(repoName string, pullNum int, projectName string) string { - return fmt.Sprintf("%s/%d/%s", repoName, pullNum, projectName) +func BuildPullInfo(repoName string, pullNum int, projectName string, relDir string, workspace string) string { + projectIdentifier := GetProjectIdentifier(relDir, projectName) + return fmt.Sprintf("%s/%d/%s/%s", repoName, pullNum, projectIdentifier, workspace) +} + +func GetProjectIdentifier(relRepoDir string, projectName string) string { + if projectName != "" { + return projectName + } + return strings.ReplaceAll(relRepoDir, "/", "-") } // SplitRepoFullName splits a repo full name up into its owner and repo diff --git a/server/events/pull_closed_executor.go b/server/events/pull_closed_executor.go index 20e83f4ce..955a573fe 100644 --- a/server/events/pull_closed_executor.go +++ b/server/events/pull_closed_executor.go @@ -88,7 +88,7 @@ func (p *PullClosedExecutor) CleanUpPull(repo models.Repo, pull models.PullReque // with same dir and workspace. If a project name has not been set, we'll use the dir and // workspace to build project key. // Source: https://www.runatlantis.io/docs/repo-level-atlantis-yaml.html#reference - projectKey := models.BuildPullInfo(pullStatus.Pull.BaseRepo.FullName, pull.Num, project.ProjectName) + projectKey := models.BuildPullInfo(pullStatus.Pull.BaseRepo.FullName, pull.Num, project.ProjectName, project.RepoRelDir, project.Workspace) p.LogStreamResourceCleaner.CleanUp(projectKey) } } diff --git a/server/events/pull_closed_executor_test.go b/server/events/pull_closed_executor_test.go index fc8354b55..a6d379367 100644 --- a/server/events/pull_closed_executor_test.go +++ b/server/events/pull_closed_executor_test.go @@ -206,6 +206,7 @@ func TestCleanUpLogStreaming(t *testing.T) { BaseRepo: fixtures.GithubRepo, Pull: fixtures.Pull, ProjectName: *fixtures.Project.Name, + Workspace: "default", } go prjCmdOutHandler.Handle() diff --git a/server/router.go b/server/router.go index f66e63eba..560729470 100644 --- a/server/router.go +++ b/server/router.go @@ -41,16 +41,16 @@ func (r *Router) GenerateLockURL(lockID string) string { func (r *Router) GenerateProjectJobURL(ctx models.ProjectCommandContext) (string, error) { pull := ctx.Pull - + projectIdentifier := models.GetProjectIdentifier(ctx.RepoRelDir, ctx.ProjectName) jobURL, err := r.Underlying.Get(r.ProjectJobsViewRouteName).URL( "org", pull.BaseRepo.Owner, "repo", pull.BaseRepo.Name, "pull", fmt.Sprintf("%d", pull.Num), - "project", ctx.ProjectName, + "project", projectIdentifier, + "workspace", ctx.Workspace, ) - if err != nil { - return "", errors.Wrapf(err, "creating job url for %s/%d/%s", pull.BaseRepo.FullName, pull.Num, ctx.ProjectName) + return "", errors.Wrapf(err, "creating job url for %s/%d/%s/%s", pull.BaseRepo.FullName, pull.Num, projectIdentifier, ctx.Workspace) } return r.AtlantisURL.String() + jobURL.String(), nil diff --git a/server/router_test.go b/server/router_test.go index 3a79d5640..ccabee44d 100644 --- a/server/router_test.go +++ b/server/router_test.go @@ -6,6 +6,7 @@ import ( "github.com/gorilla/mux" "github.com/runatlantis/atlantis/server" + "github.com/runatlantis/atlantis/server/events/models" . "github.com/runatlantis/atlantis/testing" ) @@ -60,3 +61,57 @@ func TestRouter_GenerateLockURL(t *testing.T) { }) } } + +func setupJobsRouter(t *testing.T) *server.Router { + atlantisURL, err := server.ParseAtlantisURL("http://localhost:4141") + Ok(t, err) + + underlyingRouter := mux.NewRouter() + underlyingRouter.HandleFunc("/jobs/{org}/{repo}/{pull}/{project}/{workspace}", func(_ http.ResponseWriter, _ *http.Request) {}).Methods("GET").Name("project-jobs-detail") + + return &server.Router{ + AtlantisURL: atlantisURL, + Underlying: underlyingRouter, + ProjectJobsViewRouteName: "project-jobs-detail", + } +} + +func TestGenerateProjectJobURL_ShouldGenerateURLWithProjectNameWhenProjectNameSpecified(t *testing.T) { + router := setupJobsRouter(t) + ctx := models.ProjectCommandContext{ + Pull: models.PullRequest{ + BaseRepo: models.Repo{ + Owner: "test-owner", + Name: "test-repo", + }, + Num: 1, + }, + ProjectName: "test-project", + Workspace: "default", + } + expectedURL := "http://localhost:4141/jobs/test-owner/test-repo/1/test-project/default" + gotURL, err := router.GenerateProjectJobURL(ctx) + Ok(t, err) + + Equals(t, expectedURL, gotURL) +} + +func TestGenerateProjectJobURL_ShouldGenerateURLWithDirectoryAndWorkspaceWhenProjectNameNotSpecified(t *testing.T) { + router := setupJobsRouter(t) + ctx := models.ProjectCommandContext{ + Pull: models.PullRequest{ + BaseRepo: models.Repo{ + Owner: "test-owner", + Name: "test-repo", + }, + Num: 1, + }, + RepoRelDir: "ops/terraform/test-root", + Workspace: "default", + } + expectedURL := "http://localhost:4141/jobs/test-owner/test-repo/1/ops-terraform-test-root/default" + gotURL, err := router.GenerateProjectJobURL(ctx) + Ok(t, err) + + Equals(t, expectedURL, gotURL) +} diff --git a/server/server.go b/server/server.go index 6357597bc..13bd88a55 100644 --- a/server/server.go +++ b/server/server.go @@ -75,7 +75,6 @@ const ( // mux.Router.Get(LockViewRouteName).URL(LockViewRouteIDQueryParam, "my id") LockViewRouteIDQueryParam = "id" // ProjectJobsViewRouteName is the named route in mux.Router for the log stream view. - // Can be retrieved by mux.Router.Get(ProjectJobsViewRouteName) ProjectJobsViewRouteName = "project-jobs-detail" // binDirName is the name of the directory inside our data dir where // we download binaries. @@ -852,8 +851,9 @@ func (s *Server) Start() error { 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) - s.Router.HandleFunc("/jobs/{org}/{repo}/{pull}/{project}", s.JobsController.GetProjectJobs).Methods("GET").Name(ProjectJobsViewRouteName) - s.Router.HandleFunc("/jobs/{org}/{repo}/{pull}/{project}/ws", s.JobsController.GetProjectJobsWS).Methods("GET") + s.Router.HandleFunc("/jobs/{org}/{repo}/{pull}/{project}/{workspace}", s.JobsController.GetProjectJobs).Methods("GET").Name(ProjectJobsViewRouteName) + s.Router.HandleFunc("/jobs/{org}/{repo}/{pull}/{project}/{workspace}/ws", s.JobsController.GetProjectJobsWS).Methods("GET") + n := negroni.New(&negroni.Recovery{ Logger: log.New(os.Stdout, "", log.LstdFlags), PrintStack: false,