Skip to content

Commit

Permalink
Merge pull request #357 from runatlantis/basepath
Browse files Browse the repository at this point in the history
Allow running Atlantis behind a path-based proxy
  • Loading branch information
lkysow authored Nov 21, 2018
2 parents c6dcd65 + fa4aad4 commit f1cced0
Show file tree
Hide file tree
Showing 8 changed files with 235 additions and 46 deletions.
5 changes: 3 additions & 2 deletions cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ const redTermEnd = "\033[39m"
var stringFlags = []stringFlag{
{
name: AtlantisURLFlag,
description: "URL that Atlantis can be reached at. Defaults to http://$(hostname):$port where $port is from --" + PortFlag + ".",
description: "URL that Atlantis can be reached at. Defaults to http://$(hostname):$port where $port is from --" + PortFlag + ". Supports a base path ex. https://example.com/basepath.",
},
{
name: BitbucketUserFlag,
Expand Down Expand Up @@ -254,7 +254,7 @@ func (s *ServerCmd) Init() *cobra.Command {
Short: "Start the atlantis server",
Long: `Start the atlantis server and listen for webhook calls.`,
SilenceErrors: true,
SilenceUsage: s.SilenceOutput,
SilenceUsage: true,
PreRunE: s.withErrPrint(func(cmd *cobra.Command, args []string) error {
return s.preRun()
}),
Expand Down Expand Up @@ -344,6 +344,7 @@ func (s *ServerCmd) run() error {
server, err := s.ServerCreator.NewServer(userConfig, server.Config{
AllowForkPRsFlag: AllowForkPRsFlag,
AllowRepoConfigFlag: AllowRepoConfigFlag,
AtlantisURLFlag: AtlantisURLFlag,
AtlantisVersion: s.AtlantisVersion,
})
if err != nil {
Expand Down
7 changes: 6 additions & 1 deletion server/locks_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
// LocksController handles all requests relating to Atlantis locks.
type LocksController struct {
AtlantisVersion string
AtlantisURL *url.URL
Locker locking.Locker
Logger *logging.SimpleLogger
VCSClient vcs.ClientProxy
Expand Down Expand Up @@ -57,8 +58,12 @@ func (l *LocksController) GetLock(w http.ResponseWriter, r *http.Request) {
LockedBy: lock.Pull.Author,
Workspace: lock.Workspace,
AtlantisVersion: l.AtlantisVersion,
CleanedBasePath: l.AtlantisURL.Path,
}
err = l.LockDetailTemplate.Execute(w, viewData)
if err != nil {
l.Logger.Err(err.Error())
}
l.LockDetailTemplate.Execute(w, viewData) // nolint: errcheck
}

// DeleteLock handles deleting the lock at id and commenting back on the
Expand Down
6 changes: 6 additions & 0 deletions server/locks_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"testing"

Expand All @@ -18,6 +19,7 @@ import (
vcsmocks "github.com/runatlantis/atlantis/server/events/vcs/mocks"
"github.com/runatlantis/atlantis/server/logging"
sMocks "github.com/runatlantis/atlantis/server/mocks"
. "github.com/runatlantis/atlantis/testing"
)

func AnyRepo() models.Repo {
Expand Down Expand Up @@ -90,11 +92,14 @@ func TestGetLock_Success(t *testing.T) {
Workspace: "workspace",
}, nil)
tmpl := sMocks.NewMockTemplateWriter()
atlantisURL, err := url.Parse("https://example.com/basepath")
Ok(t, err)
lc := server.LocksController{
Logger: logging.NewNoopLogger(),
Locker: l,
LockDetailTemplate: tmpl,
AtlantisVersion: "1300135",
AtlantisURL: atlantisURL,
}
req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil))
req = mux.SetURLVars(req, map[string]string{"id": "id"})
Expand All @@ -109,6 +114,7 @@ func TestGetLock_Success(t *testing.T) {
LockedBy: "lkysow",
Workspace: "workspace",
AtlantisVersion: "1300135",
CleanedBasePath: "/basepath",
})
responseContains(t, w, http.StatusOK, "")
}
Expand Down
16 changes: 10 additions & 6 deletions server/router.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package server

import (
"fmt"
"net/url"

"github.com/gorilla/mux"
Expand All @@ -19,13 +18,18 @@ type Router struct {
// LockViewRouteIDQueryParam is the query parameter needed to construct the
// lock view: underlying.Get(LockViewRouteName).URL(LockViewRouteIDQueryParam, "my id").
LockViewRouteIDQueryParam string
// AtlantisURL is the fully qualified URL (scheme included) that Atlantis is
// being served at, ex: https://example.com.
AtlantisURL string
// AtlantisURL is the fully qualified URL that Atlantis is
// accessible from externally.
AtlantisURL *url.URL
}

// GenerateLockURL returns a fully qualified URL to view the lock at lockID.
func (r *Router) GenerateLockURL(lockID string) string {
path, _ := r.Underlying.Get(r.LockViewRouteName).URL(r.LockViewRouteIDQueryParam, url.QueryEscape(lockID))
return fmt.Sprintf("%s%s", r.AtlantisURL, path)
lockURL, _ := r.Underlying.Get(r.LockViewRouteName).URL(r.LockViewRouteIDQueryParam, url.QueryEscape(lockID))
// At this point, lockURL will just be a path because r.Underlying isn't
// configured with host or scheme information. So to generate the fully
// qualified LockURL we just append the router's url to our base url.
// We're not doing anything fancy here with the actual url object because
// golang likes to double escape the lockURL path when using url.Parse().
return r.AtlantisURL.String() + lockURL.String()
}
55 changes: 45 additions & 10 deletions server/router_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,53 @@ import (
)

func TestRouter_GenerateLockURL(t *testing.T) {
queryParam := "queryparam"
routeName := "routename"
atlantisURL := "https://example.com"
cases := []struct {
AtlantisURL string
ExpURL string
}{
{
"http://localhost:4141",
"http://localhost:4141/lock?id=lkysow%252Fatlantis-example%252F.%252Fdefault",
},
{
"https://localhost:4141",
"https://localhost:4141/lock?id=lkysow%252Fatlantis-example%252F.%252Fdefault",
},
{
"https://localhost:4141/",
"https://localhost:4141/lock?id=lkysow%252Fatlantis-example%252F.%252Fdefault",
},
{
"https://example.com/basepath",
"https://example.com/basepath/lock?id=lkysow%252Fatlantis-example%252F.%252Fdefault",
},
{
"https://example.com/basepath/",
"https://example.com/basepath/lock?id=lkysow%252Fatlantis-example%252F.%252Fdefault",
},
{
"https://example.com/path/1/",
"https://example.com/path/1/lock?id=lkysow%252Fatlantis-example%252F.%252Fdefault",
},
}

queryParam := "id"
routeName := "routename"
underlyingRouter := mux.NewRouter()
underlyingRouter.HandleFunc("/lock", func(_ http.ResponseWriter, _ *http.Request) {}).Methods("GET").Queries(queryParam, "{queryparam}").Name(routeName)
underlyingRouter.HandleFunc("/lock", func(_ http.ResponseWriter, _ *http.Request) {}).Methods("GET").Queries(queryParam, "{id}").Name(routeName)

for _, c := range cases {
t.Run(c.AtlantisURL, func(t *testing.T) {
atlantisURL, err := server.ParseAtlantisURL(c.AtlantisURL)
Ok(t, err)

router := &server.Router{
AtlantisURL: atlantisURL,
LockViewRouteIDQueryParam: queryParam,
LockViewRouteName: routeName,
Underlying: underlyingRouter,
router := &server.Router{
AtlantisURL: atlantisURL,
LockViewRouteIDQueryParam: queryParam,
LockViewRouteName: routeName,
Underlying: underlyingRouter,
}
Equals(t, c.ExpURL, router.GenerateLockURL("lkysow/atlantis-example/./default"))
})
}
Equals(t, "https://example.com/lock?queryparam=myid", router.GenerateLockURL("myid"))
}
40 changes: 36 additions & 4 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ const (
// Server runs the Atlantis web server.
type Server struct {
AtlantisVersion string
AtlantisURL *url.URL
Router *mux.Router
Port int
CommandRunner *events.DefaultCommandRunner
Expand Down Expand Up @@ -114,6 +115,7 @@ type UserConfig struct {
type Config struct {
AllowForkPRsFlag string
AllowRepoConfigFlag string
AtlantisURLFlag string
AtlantisVersion string
}

Expand Down Expand Up @@ -229,9 +231,14 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
projectLocker := &events.DefaultProjectLocker{
Locker: lockingClient,
}
parsedURL, err := ParseAtlantisURL(userConfig.AtlantisURL)
if err != nil {
return nil, errors.Wrapf(err,
"parsing --%s flag %q", config.AtlantisURLFlag, userConfig.AtlantisURL)
}
underlyingRouter := mux.NewRouter()
router := &Router{
AtlantisURL: userConfig.AtlantisURL,
AtlantisURL: parsedURL,
LockViewRouteIDQueryParam: LockViewRouteIDQueryParam,
LockViewRouteName: LockViewRouteName,
Underlying: underlyingRouter,
Expand Down Expand Up @@ -309,6 +316,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
}
locksController := &LocksController{
AtlantisVersion: config.AtlantisVersion,
AtlantisURL: parsedURL,
Locker: lockingClient,
Logger: logger,
VCSClient: vcsClient,
Expand All @@ -334,6 +342,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
}
return &Server{
AtlantisVersion: config.AtlantisVersion,
AtlantisURL: parsedURL,
Router: underlyingRouter,
Port: userConfig.Port,
CommandRunner: commandRunner,
Expand Down Expand Up @@ -411,17 +420,22 @@ func (s *Server) Index(w http.ResponseWriter, _ *http.Request) {
for id, v := range locks {
lockURL, _ := s.Router.Get(LockViewRouteName).URL("id", url.QueryEscape(id))
lockResults = append(lockResults, LockIndexData{
LockURL: lockURL.String(),
// NOTE: must use .String() instead of .Path because we need the
// query params as part of the lock URL.
LockPath: lockURL.String(),
RepoFullName: v.Project.RepoFullName,
PullNum: v.Pull.Num,
Time: v.Time,
})
}
// nolint: errcheck
s.IndexTemplate.Execute(w, IndexData{
err = s.IndexTemplate.Execute(w, IndexData{
Locks: lockResults,
AtlantisVersion: s.AtlantisVersion,
CleanedBasePath: s.AtlantisURL.Path,
})
if err != nil {
s.Logger.Err(err.Error())
}
}

// Healthz returns the health check response. It always returns a 200 currently.
Expand All @@ -439,3 +453,21 @@ func (s *Server) Healthz(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write(data) // nolint: errcheck
}

// ParseAtlantisURL parses the user-passed atlantis URL to ensure it is valid
// and we can use it in our templates.
// It removes any trailing slashes from the path so we can concatenate it
// with other paths without checking.
func ParseAtlantisURL(u string) (*url.URL, error) {
parsed, err := url.Parse(u)
if err != nil {
return nil, err
}
if !(parsed.Scheme == "http" || parsed.Scheme == "https") {
return nil, errors.New("http or https must be specified")
}
// We want the path to end without a trailing slash so we know how to
// use it in the rest of the program.
parsed.Path = strings.TrimSuffix(parsed.Path, "/")
return parsed, nil
}
Loading

0 comments on commit f1cced0

Please sign in to comment.