Merge "changelog-webapp: Added error pages"
diff --git a/go.mod b/go.mod
index a147d73..b915599 100644
--- a/go.mod
+++ b/go.mod
@@ -21,5 +21,6 @@
 	google.golang.org/api v0.28.0
 	google.golang.org/grpc v1.29.1
 	google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790
+	google.golang.org/grpc v1.29.1
 	google.golang.org/protobuf v1.25.0
 )
diff --git a/src/cmd/changelog-webapp/controllers/pageHandlers.go b/src/cmd/changelog-webapp/controllers/pageHandlers.go
index abc425e..c2d24f6 100644
--- a/src/cmd/changelog-webapp/controllers/pageHandlers.go
+++ b/src/cmd/changelog-webapp/controllers/pageHandlers.go
@@ -16,6 +16,7 @@
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"net/http"
 	"os"
@@ -25,6 +26,8 @@
 
 	secretmanager "cloud.google.com/go/secretmanager/apiv1"
 	"cos.googlesource.com/cos/tools/src/pkg/changelog"
+	"google.golang.org/grpc/codes"
+	"google.golang.org/grpc/status"
 
 	log "github.com/sirupsen/logrus"
 )
@@ -39,10 +42,33 @@
 	externalInstance     string
 	externalManifestRepo string
 	envQuerySize         string
-	staticBasePath       string
-	indexTemplate        *template.Template
-	changelogTemplate    *template.Template
-	promptLoginTemplate  *template.Template
+
+	staticBasePath          string
+	indexTemplate           *template.Template
+	changelogTemplate       *template.Template
+	promptLoginTemplate     *template.Template
+	locateCLTemplate        *template.Template
+	statusForbiddenTemplate *template.Template
+	basicTextTemplate       *template.Template
+
+	grpcCodeToHeader = map[string]string{
+		codes.Canceled.String():           "499 Client Closed Request",
+		codes.Unknown.String():            "500 Internal Server Error",
+		codes.InvalidArgument.String():    "400 Bad Request",
+		codes.DeadlineExceeded.String():   "504 Gateway Timeout",
+		codes.NotFound.String():           "404 Not Found",
+		codes.PermissionDenied.String():   "403 Forbidden",
+		codes.Unauthenticated.String():    "401 Unauthorized",
+		codes.ResourceExhausted.String():  "429 Too Many Requests",
+		codes.FailedPrecondition.String(): "400 Bad Request",
+		codes.Aborted.String():            "409 Conflict",
+		codes.OutOfRange.String():         "400 Bad Request",
+		codes.Unimplemented.String():      "501 Not Implemented",
+		codes.Internal.String():           "500 Internal Server Error",
+		codes.Unavailable.String():        "503 Service Unavailable",
+		codes.DataLoss.String():           "500 Internal Server Error",
+	}
+	gitiles403Desc = "unexpected HTTP 403 from Gitiles"
 )
 
 func init() {
@@ -66,6 +92,7 @@
 	indexTemplate = template.Must(template.ParseFiles(staticBasePath + "templates/index.html"))
 	changelogTemplate = template.Must(template.ParseFiles(staticBasePath + "templates/changelog.html"))
 	promptLoginTemplate = template.Must(template.ParseFiles(staticBasePath + "templates/promptLogin.html"))
+	basicTextTemplate = template.Must(template.ParseFiles(staticBasePath + "templates/error.html"))
 }
 
 type changelogData struct {
@@ -85,6 +112,16 @@
 	Internal   bool
 }
 
+type statusPage struct {
+	ActivePage string
+}
+
+type basicTextPage struct {
+	Header     string
+	Body       string
+	ActivePage string
+}
+
 type repoTable struct {
 	Name          string
 	Additions     []*repoTableEntry
@@ -114,10 +151,6 @@
 	URL  string
 }
 
-type promptLoginPage struct {
-	ActivePage string
-}
-
 // getIntVerifiedEnv retrieves an environment variable but checks that it can be
 // converted to int first
 func getIntVerifiedEnv(envName string) string {
@@ -197,6 +230,39 @@
 	return page
 }
 
+// handleError creates the error page for a given error
+func handleError(w http.ResponseWriter, inputErr error, currPage string) {
+	var header, text string
+	innerErr := inputErr
+	for errors.Unwrap(innerErr) != nil {
+		innerErr = errors.Unwrap(innerErr)
+	}
+	rpcStatus, ok := status.FromError(innerErr)
+	// Error is not a status code, display generic header
+	if !ok {
+		basicTextTemplate.Execute(w, &basicTextPage{
+			Header:     "An error occurred while fulfilling your request",
+			Body:       innerErr.Error(),
+			ActivePage: currPage,
+		})
+		return
+	}
+	code, text := rpcStatus.Code(), rpcStatus.Message()
+	// RPC status code misclassifies 403 error as internal for Gitiles requests
+	if text == gitiles403Desc {
+		code = codes.PermissionDenied
+	}
+	if _, ok := grpcCodeToHeader[code.String()]; !ok {
+		header = "An error occurred while fulfilling your request"
+	}
+	header = grpcCodeToHeader[code.String()]
+	basicTextTemplate.Execute(w, &basicTextPage{
+		Header:     header,
+		Body:       text,
+		ActivePage: currPage,
+	})
+}
+
 // HandleIndex serves the home page
 func HandleIndex(w http.ResponseWriter, r *http.Request) {
 	indexTemplate.Execute(w, nil)
@@ -204,19 +270,19 @@
 
 // HandleChangelog serves the changelog page
 func HandleChangelog(w http.ResponseWriter, r *http.Request) {
-	if err := r.ParseForm(); err != nil {
-		changelogTemplate.Execute(w, &changelogPage{QuerySize: envQuerySize})
-		return
-	}
 	httpClient, err := HTTPClient(w, r, "/changelog/")
 	if err != nil {
 		log.Debug(err)
-		err = promptLoginTemplate.Execute(w, &promptLoginPage{ActivePage: "changelog"})
+		err = promptLoginTemplate.Execute(w, &statusPage{ActivePage: "changelog"})
 		if err != nil {
 			log.Errorf("HandleChangelog: error executing promptLogin template: %v", err)
 		}
 		return
 	}
+	if err := r.ParseForm(); err != nil {
+		changelogTemplate.Execute(w, &changelogPage{QuerySize: envQuerySize})
+		return
+	}
 	source := r.FormValue("source")
 	target := r.FormValue("target")
 	// If no source/target values specified in request, display empty changelog page
@@ -236,7 +302,7 @@
 	if err != nil {
 		log.Errorf("HandleChangelog: error retrieving changelog between builds %s and %s on GoB instance: %s with manifest repository: %s\n%v\n",
 			source, target, externalInstance, externalManifestRepo, err)
-		http.Error(w, err.Error(), http.StatusInternalServerError)
+		handleError(w, err, "changelog")
 		return
 	}
 	page := createChangelogPage(changelogData{
diff --git a/src/cmd/changelog-webapp/static/templates/error.html b/src/cmd/changelog-webapp/static/templates/error.html
new file mode 100644
index 0000000..b57a938
--- /dev/null
+++ b/src/cmd/changelog-webapp/static/templates/error.html
@@ -0,0 +1,35 @@
+<html>
+<head>
+  <meta name="description" content="Google COS build information">
+  <link rel="stylesheet" href="/static/css/base.css">
+</head>
+<body>
+  <div class="navbar">
+    <p class="navbar-title">Container Optimized OS</p>
+  </div>
+  <div class="sidenav">
+    {{if (eq .ActivePage "home")}}
+      <a class="active" href="/">Home</a>
+    {{else}}
+      <a href="/">Home</a>
+    {{end}}
+    {{if (eq .ActivePage "changelog")}}
+      <a class="active" href="/changelog/">Changelog</a>
+    {{else}}
+      <a href="/changelog/">Changelog</a>
+    {{end}}
+    {{if (eq .ActivePage "locateCl")}}
+      <a class="active" href="/locatecl/">Locate CL</a>
+    {{else}}
+      <a href="/locatecl/">Locate CL</a>
+    {{end}}
+    <a href="/login/">Login</a>
+  </div>
+  <div class="main">
+    <h1>{{ .Header }}</h1>
+    <p>{{ .Body }}</a>
+    </p>
+  </div>
+</body>
+
+</html>
\ No newline at end of file
diff --git a/src/pkg/changelog/changelog.go b/src/pkg/changelog/changelog.go
index 1f26545..098138a 100644
--- a/src/pkg/changelog/changelog.go
+++ b/src/pkg/changelog/changelog.go
@@ -85,7 +85,7 @@
 		}
 		client, err := gerritClient(httpClient, remoteURL)
 		if err != nil {
-			return fmt.Errorf("createClients: error creating client mapping:\n%v", err)
+			return fmt.Errorf("createClients: error creating client mapping:\n%w", err)
 		}
 		clients[remoteURL] = client
 	}
@@ -102,7 +102,7 @@
 	}
 	doc := etree.NewDocument()
 	if err := doc.ReadFromString(manifest); err != nil {
-		return nil, fmt.Errorf("repoMap: error parsing manifest xml:\n%v", err)
+		return nil, fmt.Errorf("repoMap: error parsing manifest xml:\n%w", err)
 	}
 	root := doc.SelectElement("manifest")
 
@@ -136,12 +136,12 @@
 	log.Debugf("Retrieving manifest file for build %s\n", buildNum)
 	response, err := utils.DownloadManifest(client, repo, buildNum)
 	if err != nil {
-		return nil, fmt.Errorf("mappedManifest: error downloading manifest file from repo %s:\n%v",
+		return nil, fmt.Errorf("mappedManifest: error downloading manifest file from repo %s:\n%w",
 			repo, err)
 	}
 	mappedManifest, err := repoMap(response.Contents)
 	if err != nil {
-		return nil, fmt.Errorf("mappedManifest: error parsing manifest contents from repo %s:\n%v",
+		return nil, fmt.Errorf("mappedManifest: error parsing manifest contents from repo %s:\n%w",
 			repo, err)
 	}
 	return mappedManifest, nil
@@ -239,27 +239,27 @@
 	// so that client knows what URL to use
 	manifestClient, err := gerritClient(httpClient, host)
 	if err != nil {
-		return nil, nil, fmt.Errorf("Changelog: error creating client for GoB instance: %s:\n%v", host, err)
+		return nil, nil, fmt.Errorf("Changelog: error creating client for GoB instance: %s:\n%w", host, err)
 	}
 	sourceRepos, err := mappedManifest(manifestClient, repo, sourceBuildNum)
 	if err != nil {
-		return nil, nil, fmt.Errorf("Changelog: error retrieving mapped manifest for source build number: %s using manifest repository: %s:\n%v",
+		return nil, nil, fmt.Errorf("Changelog: error retrieving mapped manifest for source build number: %s using manifest repository: %s:\n%w",
 			sourceBuildNum, repo, err)
 	}
 	targetRepos, err := mappedManifest(manifestClient, repo, targetBuildNum)
 	if err != nil {
-		return nil, nil, fmt.Errorf("Changelog: error retrieving mapped manifest for target build number: %s using manifest repository: %s:\n%v",
+		return nil, nil, fmt.Errorf("Changelog: error retrieving mapped manifest for target build number: %s using manifest repository: %s:\n%w",
 			targetBuildNum, repo, err)
 	}
 
 	clients[host] = manifestClient
 	err = createGerritClients(clients, httpClient, sourceRepos)
 	if err != nil {
-		return nil, nil, fmt.Errorf("Changelog: error creating source clients:\n%v", err)
+		return nil, nil, fmt.Errorf("Changelog: error creating source clients:\n%w", err)
 	}
 	err = createGerritClients(clients, httpClient, targetRepos)
 	if err != nil {
-		return nil, nil, fmt.Errorf("Changelog: error creating target clients:\n%v", err)
+		return nil, nil, fmt.Errorf("Changelog: error creating target clients:\n%w", err)
 	}
 
 	addChan := make(chan additionsResult, 1)
@@ -268,11 +268,11 @@
 	go additions(clients, targetRepos, sourceRepos, querySize, missChan)
 	missRes := <-missChan
 	if missRes.Err != nil {
-		return nil, nil, fmt.Errorf("Changelog: failure when retrieving missed commits:\n%v", missRes.Err)
+		return nil, nil, fmt.Errorf("Changelog: failure when retrieving missed commits:\n%w", missRes.Err)
 	}
 	addRes := <-addChan
 	if addRes.Err != nil {
-		return nil, nil, fmt.Errorf("Changelog: failure when retrieving commit additions:\n%v", addRes.Err)
+		return nil, nil, fmt.Errorf("Changelog: failure when retrieving commit additions:\n%w", addRes.Err)
 	}
 
 	return addRes.Additions, missRes.Additions, nil