Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prefix URLs in templates with AtlantisURL #314

Merged
merged 1 commit into from
Nov 21, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion 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, e.g. https://example.com/basepath",
},
{
name: BitbucketUserFlag,
Expand Down
2 changes: 2 additions & 0 deletions 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,6 +58,7 @@ func (l *LocksController) GetLock(w http.ResponseWriter, r *http.Request) {
LockedBy: lock.Pull.Author,
Workspace: lock.Workspace,
AtlantisVersion: l.AtlantisVersion,
AtlantisURL: l.AtlantisURL,
}
l.LockDetailTemplate.Execute(w, viewData) // nolint: errcheck
}
Expand Down
4 changes: 2 additions & 2 deletions server/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ type Router struct {
LockViewRouteIDQueryParam string
// AtlantisURL is the fully qualified URL (scheme included) that Atlantis is
// being served at, ex: https://example.com.
AtlantisURL string
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)
return fmt.Sprintf("%s%s", r.AtlantisURL.String(), path)
}
6 changes: 4 additions & 2 deletions server/router_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package server_test

import (
"net/http"
"net/url"
"testing"

"github.com/gorilla/mux"
Expand All @@ -12,13 +13,14 @@ import (
func TestRouter_GenerateLockURL(t *testing.T) {
queryParam := "queryparam"
routeName := "routename"
atlantisURL := "https://example.com"
atlantisURL, err := url.Parse("https://example.com")
Ok(t, err)

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

router := &server.Router{
AtlantisURL: atlantisURL,
AtlantisURL: *atlantisURL,
LockViewRouteIDQueryParam: queryParam,
LockViewRouteName: routeName,
Underlying: underlyingRouter,
Expand Down
16 changes: 14 additions & 2 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 @@ -229,9 +230,17 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
projectLocker := &events.DefaultProjectLocker{
Locker: lockingClient,
}
atlantisURL, err := url.Parse(userConfig.AtlantisURL)
if err != nil {
return nil, errors.Wrap(err, "parsing atlantis URL")
}
atlantisURL, err = NormalizeBaseURL(atlantisURL)
if err != nil {
return nil, errors.Wrap(err, "normalizing atlantis URL")
}
underlyingRouter := mux.NewRouter()
router := &Router{
AtlantisURL: userConfig.AtlantisURL,
AtlantisURL: *atlantisURL,
LockViewRouteIDQueryParam: LockViewRouteIDQueryParam,
LockViewRouteName: LockViewRouteName,
Underlying: underlyingRouter,
Expand Down Expand Up @@ -309,6 +318,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
}
locksController := &LocksController{
AtlantisVersion: config.AtlantisVersion,
AtlantisURL: *atlantisURL,
Locker: lockingClient,
Logger: logger,
VCSClient: vcsClient,
Expand All @@ -334,6 +344,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
}
return &Server{
AtlantisVersion: config.AtlantisVersion,
AtlantisURL: *atlantisURL,
Router: underlyingRouter,
Port: userConfig.Port,
CommandRunner: commandRunner,
Expand Down Expand Up @@ -411,7 +422,7 @@ 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(),
LockURL: *lockURL,
RepoFullName: v.Project.RepoFullName,
PullNum: v.Pull.Num,
Time: v.Time,
Expand All @@ -421,6 +432,7 @@ func (s *Server) Index(w http.ResponseWriter, _ *http.Request) {
s.IndexTemplate.Execute(w, IndexData{
Locks: lockResults,
AtlantisVersion: s.AtlantisVersion,
AtlantisURL: s.AtlantisURL,
})
}

Expand Down
6 changes: 4 additions & 2 deletions server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"io/ioutil"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"
Expand All @@ -37,7 +38,8 @@ func TestNewServer(t *testing.T) {
tmpDir, err := ioutil.TempDir("", "")
Ok(t, err)
_, err = server.NewServer(server.UserConfig{
DataDir: tmpDir,
DataDir: tmpDir,
AtlantisURL: "http://example.com",
}, server.Config{})
Ok(t, err)
}
Expand Down Expand Up @@ -91,7 +93,7 @@ func TestIndex_Success(t *testing.T) {
it.VerifyWasCalledOnce().Execute(w, server.IndexData{
Locks: []server.LockIndexData{
{
LockURL: "",
LockURL: url.URL{},
RepoFullName: "owner/repo",
PullNum: 9,
Time: now,
Expand Down
24 changes: 24 additions & 0 deletions server/url.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package server

import (
"fmt"
"net/url"
"strings"
)

// NormalizeBaseURL ensures the given URL is a valid base URL for Atlantis.
//
// URLs that are fundamentally invalid (e.g. "hi") will return an error.
// Otherwise, the returned URL will have no trailing slashes and be guaranteed
// to be suitable for use as a base URL.
func NormalizeBaseURL(u *url.URL) (*url.URL, error) {
if !u.IsAbs() {
return nil, fmt.Errorf("Base URLs must be absolute.")
}
if !(u.Scheme == "http" || u.Scheme == "https") {
return nil, fmt.Errorf("Base URLs must be HTTP or HTTPS.")
}
out := *u
out.Path = strings.TrimRight(out.Path, "/")
return &out, nil
}
62 changes: 62 additions & 0 deletions server/url_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package server_test

import (
"net/url"
"testing"

"github.com/runatlantis/atlantis/server"
. "github.com/runatlantis/atlantis/testing"
)

func TestNormalizeBaseURL_Valid(t *testing.T) {
t.Log("When given a valid base URL, NormalizeBaseURL returns such URLs unchanged.")
examples := []string{
"https://example.com",
"https://example.com/some/path",
"http://example.com:8080",
}
for _, example := range examples {
url, err := url.Parse(example)
Ok(t, err)
normalized, err := server.NormalizeBaseURL(url)
Ok(t, err)
Equals(t, url, normalized)
}
}

func TestNormalizeBaseURL_Relative(t *testing.T) {
t.Log("We do not allow relative URLs as base URLs.")
_, err := server.NormalizeBaseURL(&url.URL{Path: "hi"})
Assert(t, err != nil, "should be an error")
Equals(t, "Base URLs must be absolute.", err.Error())
}

func TestNormalizeBaseURL_NonHTTP(t *testing.T) {
t.Log("Base URLs must be http or https.")
_, err := server.NormalizeBaseURL(&url.URL{Scheme: "ftp", Host: "example", Path: "hi"})
Assert(t, err != nil, "should be an error")
Equals(t, "Base URLs must be HTTP or HTTPS.", err.Error())
}

func TestNormalizeBaseURL_TrailingSlashes(t *testing.T) {
t.Log("We strip off any trailing slashes from the base URL.")
examples := []struct {
input string
output string
}{
{"https://example.com/", "https://example.com"},
{"https://example.com/some/path/", "https://example.com/some/path"},
{"http://example.com:8080/", "http://example.com:8080"},
{"https://example.com//", "https://example.com"},
{"https://example.com/path///", "https://example.com/path"},
}
for _, example := range examples {
inputURL, err := url.Parse(example.input)
Ok(t, err)
outputURL, err := url.Parse(example.output)
Ok(t, err)
normalized, err := server.NormalizeBaseURL(inputURL)
Ok(t, err)
Equals(t, outputURL, normalized)
}
}
36 changes: 19 additions & 17 deletions server/web_templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package server
import (
"html/template"
"io"
"net/url"
"time"
)

Expand All @@ -31,7 +32,7 @@ type TemplateWriter interface {

// LockIndexData holds the fields needed to display the index view for locks.
type LockIndexData struct {
LockURL string
LockURL url.URL
RepoFullName string
PullNum int
Time time.Time
Expand All @@ -41,6 +42,7 @@ type LockIndexData struct {
type IndexData struct {
Locks []LockIndexData
AtlantisVersion string
AtlantisURL url.URL
}

var indexTemplate = template.Must(template.New("index.html.tmpl").Parse(`
Expand All @@ -52,7 +54,7 @@ var indexTemplate = template.Must(template.New("index.html.tmpl").Parse(`
<meta name="description" content="">
<meta name="author" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="/static/js/jquery-3.2.1.min.js"></script>
<script src="{{ .AtlantisURL }}/static/js/jquery-3.2.1.min.js"></script>
<script>
$(document).ready(function () {
$("p.js-discard-success").toggle(document.URL.indexOf("discard=true") !== -1);
Expand All @@ -61,15 +63,15 @@ var indexTemplate = template.Must(template.New("index.html.tmpl").Parse(`
$("p.js-discard-success").fadeOut('slow');
}, 5000); // <-- time in milliseconds
</script>
<link rel="stylesheet" href="/static/css/normalize.css">
<link rel="stylesheet" href="/static/css/skeleton.css">
<link rel="stylesheet" href="/static/css/custom.css">
<link rel="icon" type="image/png" href="/static/images/atlantis-icon.png">
<link rel="stylesheet" href="{{ .AtlantisURL }}/static/css/normalize.css">
<link rel="stylesheet" href="{{ .AtlantisURL }}/static/css/skeleton.css">
<link rel="stylesheet" href="{{ .AtlantisURL }}/static/css/custom.css">
<link rel="icon" type="image/png" href="{{ .AtlantisURL }}/static/images/atlantis-icon.png">
</head>
<body>
<div class="container">
<section class="header">
<a title="atlantis" href="/"><img src="/static/images/atlantis-icon.png"/></a>
<a title="atlantis" href="{{ .AtlantisURL }}"><img src="{{ .AtlantisURL }}/static/images/atlantis-icon.png"/></a>
<p class="title-heading">atlantis</p>
<p class="js-discard-success"><strong>Plan discarded and unlocked!</strong></p>
</section>
Expand All @@ -83,7 +85,7 @@ var indexTemplate = template.Must(template.New("index.html.tmpl").Parse(`
<p class="title-heading small"><strong>Locks</strong></p>
{{ if .Locks }}
{{ range .Locks }}
<a href="{{.LockURL}}">
<a href="{{ .AtlantisURL }}{{.LockURL.Path}}">
<div class="twelve columns button content lock-row">
<div class="list-title">{{.RepoFullName}} - <span class="heading-font-size">#{{.PullNum}}</span></div>
<div class="list-status"><code>Locked</code></div>
Expand All @@ -105,7 +107,6 @@ v{{ .AtlantisVersion }}

// LockDetailData holds the fields needed to display the lock detail view.
type LockDetailData struct {
UnlockURL string
LockKeyEncoded string
LockKey string
RepoOwner string
Expand All @@ -115,6 +116,7 @@ type LockDetailData struct {
Workspace string
Time time.Time
AtlantisVersion string
AtlantisURL url.URL
}

var lockTemplate = template.Must(template.New("lock.html.tmpl").Parse(`
Expand All @@ -126,16 +128,16 @@ var lockTemplate = template.Must(template.New("lock.html.tmpl").Parse(`
<meta name="description" content="">
<meta name="author" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/static/css/normalize.css">
<link rel="stylesheet" href="/static/css/skeleton.css">
<link rel="stylesheet" href="/static/css/custom.css">
<link rel="icon" type="image/png" href="/static/images/atlantis-icon.png">
<script src="/static/js/jquery-3.2.1.min.js"></script>
<link rel="stylesheet" href="{{ .AtlantisURL }}/static/css/normalize.css">
<link rel="stylesheet" href="{{ .AtlantisURL }}/static/css/skeleton.css">
<link rel="stylesheet" href="{{ .AtlantisURL }}/static/css/custom.css">
<link rel="icon" type="image/png" href="{{ .AtlantisURL }}/static/images/atlantis-icon.png">
<script src="{{ .AtlantisURL }}/static/js/jquery-3.2.1.min.js"></script>
</head>
<body>
<div class="container">
<section class="header">
<a title="atlantis" href="/"><img src="/static/images/atlantis-icon.png"/></a>
<a title="atlantis" href="{{ .AtlantisURL }}"><img src="{{ .AtlantisURL }}/static/images/atlantis-icon.png"/></a>
<p class="title-heading">atlantis</p>
<p class="title-heading"><strong>{{.LockKey}}</strong> <code>Locked</code></p>
</section>
Expand Down Expand Up @@ -200,10 +202,10 @@ v{{ .AtlantisVersion }}

btnDiscard.click(function() {
$.ajax({
url: '/locks?id='+lockId,
url: '{{ .AtlantisURL }}/locks?id='+lockId,
type: 'DELETE',
success: function(result) {
window.location.replace("/?discard=true");
window.location.replace("{{ .AtlantisURL }}/?discard=true");
}
});
});
Expand Down