Merge "changelog-webapp: Added find build controllers"
diff --git a/src/cmd/changelog-webapp/README.md b/src/cmd/changelog-webapp/README.md
index 5917052..4b1a7d1 100644
--- a/src/cmd/changelog-webapp/README.md
+++ b/src/cmd/changelog-webapp/README.md
@@ -6,7 +6,7 @@
 Create a new App Engine project to host this application. Detailed instructions are located [here](https://cloud.google.com/appengine/docs/standard/nodejs/building-app/creating-project).
 
 ### Secret Manager
-This application queries secret manager for any information that should not be publicly accessible, such as client secrets and internal instance URLs. Ensure this service is enabled in Google Cloud, and that the App Engine service account has the `Secret Manager Secret Accessor` role in Google Cloud IAM.
+This application queries secret manager for any information that should not be publicly accessible, such as client secrets and internal instance URLs. Ensure this service is enabled in Google Cloud, and that the App Engine service account has the `Secret Manager Secret Accessor` role in Google Cloud IAM. See [here](https://cloud.google.com/secret-manager/docs/configuring-secret-manager) for information on enabling the API and managing permissions.
 
 ## Configuration
 `app.yaml` stores public environment variables used to run the service. For private environment variables, the variable name is stored in Secret Manager instead. This is indicated by the `_NAME` suffix for any environment variable.
diff --git a/src/cmd/changelog-webapp/app.yaml b/src/cmd/changelog-webapp/app.yaml
index 4f005fd..5044ddb 100644
--- a/src/cmd/changelog-webapp/app.yaml
+++ b/src/cmd/changelog-webapp/app.yaml
@@ -11,13 +11,16 @@
   # Webpage configuration
   STATIC_BASE_PATH: "src/cmd/changelog-webapp/static/"
   CHANGELOG_QUERY_SIZE: "50"
+  COS_FALLBACK_REPO_PREFIX: "mirrors/cros/"
 
   # External sources
-  COS_EXTERNAL_GERRIT_INSTANCE: "cos-review.googlesource.com"
+  COS_EXTERNAL_GERRIT_INSTANCE: "https://cos-review.googlesource.com"
+  COS_EXTERNAL_FALLBACK_GERRIT_INSTANCE: "https://chromium-review.googlesource.com"
   COS_EXTERNAL_GOB_INSTANCE: "cos.googlesource.com"
   COS_EXTERNAL_MANIFEST_REPO: "cos/manifest-snapshots"
 
   # Internal source names (values are retrieved from secret manager)
   COS_INTERNAL_GERRIT_INSTANCE_NAME: "cos-internal-gerrit-instance"
+  COS_INTERNAL_FALLBACK_GERRIT_INSTANCE_NAME: "cos-internal-fallback-gerrit-instance"
   COS_INTERNAL_GOB_INSTANCE_NAME: "cos-internal-gob-instance"
-  COS_INTERNAL_MANIFEST_REPO_NAME: "cos-internal-manifest-repo"
\ No newline at end of file
+  COS_INTERNAL_MANIFEST_REPO_NAME: "cos-internal-manifest-repo"
diff --git a/src/cmd/changelog-webapp/controllers/authHandlers.go b/src/cmd/changelog-webapp/controllers/authHandlers.go
index deb71ac..aa159ee 100644
--- a/src/cmd/changelog-webapp/controllers/authHandlers.go
+++ b/src/cmd/changelog-webapp/controllers/authHandlers.go
@@ -171,7 +171,7 @@
 		return
 	}
 	if val, ok := session.Values["oauthState"]; !ok || val == nil {
-		http.Redirect(w, r, "/", http.StatusPermanentRedirect)
+		http.Error(w, err.Error(), http.StatusBadRequest)
 		return
 	}
 	sessionState := session.Values["oauthState"].(string)
diff --git a/src/cmd/changelog-webapp/controllers/pageHandlers.go b/src/cmd/changelog-webapp/controllers/pageHandlers.go
index c2d24f6..edcec32 100644
--- a/src/cmd/changelog-webapp/controllers/pageHandlers.go
+++ b/src/cmd/changelog-webapp/controllers/pageHandlers.go
@@ -20,12 +20,14 @@
 	"fmt"
 	"net/http"
 	"os"
+	"regexp"
 	"strconv"
 	"strings"
 	"text/template"
 
 	secretmanager "cloud.google.com/go/secretmanager/apiv1"
 	"cos.googlesource.com/cos/tools/src/pkg/changelog"
+	"cos.googlesource.com/cos/tools/src/pkg/findbuild"
 	"google.golang.org/grpc/codes"
 	"google.golang.org/grpc/status"
 
@@ -37,17 +39,22 @@
 )
 
 var (
-	internalInstance     string
-	internalManifestRepo string
-	externalInstance     string
-	externalManifestRepo string
-	envQuerySize         string
+	internalGerritInstance         string
+	internalFallbackGerritInstance string
+	internalGoBInstance            string
+	internalManifestRepo           string
+	externalGerritInstance         string
+	externalFallbackGerritInstance string
+	externalGoBInstance            string
+	externalManifestRepo           string
+	fallbackRepoPrefix             string
+	envQuerySize                   string
 
 	staticBasePath          string
 	indexTemplate           *template.Template
 	changelogTemplate       *template.Template
 	promptLoginTemplate     *template.Template
-	locateCLTemplate        *template.Template
+	locateBuildTemplate     *template.Template
 	statusForbiddenTemplate *template.Template
 	basicTextTemplate       *template.Template
 
@@ -68,7 +75,8 @@
 		codes.Unavailable.String():        "503 Service Unavailable",
 		codes.DataLoss.String():           "500 Internal Server Error",
 	}
-	gitiles403Desc = "unexpected HTTP 403 from Gitiles"
+	gitiles403Desc  = "unexpected HTTP 403 from Gitiles"
+	gerritErrCodeRe = regexp.MustCompile("status code\\s*(\\d+)")
 )
 
 func init() {
@@ -77,7 +85,15 @@
 	if err != nil {
 		log.Fatalf("Failed to setup client: %v", err)
 	}
-	internalInstance, err = getSecret(client, os.Getenv("COS_INTERNAL_GOB_INSTANCE_NAME"))
+	internalGerritInstance, err = getSecret(client, os.Getenv("COS_INTERNAL_GERRIT_INSTANCE_NAME"))
+	if err != nil {
+		log.Fatalf("Failed to retrieve secret for COS_INTERNAL_GERRIT_INSTANCE_NAME with key name %s\n%v", os.Getenv("COS_INTERNAL_GERRIT_INSTANCE_NAME"), err)
+	}
+	internalFallbackGerritInstance, err = getSecret(client, os.Getenv("COS_INTERNAL_FALLBACK_GERRIT_INSTANCE_NAME"))
+	if err != nil {
+		log.Fatalf("Failed to retrieve secret for COS_INTERNAL_FALLBACK_GERRIT_INSTANCE_NAME with key name %s\n%v", os.Getenv("COS_INTERNAL_FALLBACK_GERRIT_INSTANCE_NAME"), err)
+	}
+	internalGoBInstance, err = getSecret(client, os.Getenv("COS_INTERNAL_GOB_INSTANCE_NAME"))
 	if err != nil {
 		log.Fatalf("Failed to retrieve secret for COS_INTERNAL_GOB_INSTANCE_NAME with key name %s\n%v", os.Getenv("COS_INTERNAL_GOB_INSTANCE_NAME"), err)
 	}
@@ -85,12 +101,16 @@
 	if err != nil {
 		log.Fatalf("Failed to retrieve secret for COS_INTERNAL_MANIFEST_REPO_NAME with key name %s\n%v", os.Getenv("COS_INTERNAL_MANIFEST_REPO_NAME"), err)
 	}
-	externalInstance = os.Getenv("COS_EXTERNAL_GOB_INSTANCE")
+	externalGerritInstance = os.Getenv("COS_EXTERNAL_GERRIT_INSTANCE")
+	externalFallbackGerritInstance = os.Getenv("COS_EXTERNAL_FALLBACK_GERRIT_INSTANCE")
+	externalGoBInstance = os.Getenv("COS_EXTERNAL_GOB_INSTANCE")
 	externalManifestRepo = os.Getenv("COS_EXTERNAL_MANIFEST_REPO")
+	fallbackRepoPrefix = os.Getenv("COS_FALLBACK_REPO_PREFIX")
 	envQuerySize = getIntVerifiedEnv("CHANGELOG_QUERY_SIZE")
 	staticBasePath = os.Getenv("STATIC_BASE_PATH")
 	indexTemplate = template.Must(template.ParseFiles(staticBasePath + "templates/index.html"))
 	changelogTemplate = template.Must(template.ParseFiles(staticBasePath + "templates/changelog.html"))
+	locateBuildTemplate = template.Must(template.ParseFiles(staticBasePath + "templates/locateBuild.html"))
 	promptLoginTemplate = template.Must(template.ParseFiles(staticBasePath + "templates/promptLogin.html"))
 	basicTextTemplate = template.Must(template.ParseFiles(staticBasePath + "templates/error.html"))
 }
@@ -112,6 +132,14 @@
 	Internal   bool
 }
 
+type locateBuildPage struct {
+	CL            string
+	CLNum         string
+	BuildNum      string
+	GerritLink    string
+	Internal      bool
+}
+
 type statusPage struct {
 	ActivePage string
 }
@@ -162,6 +190,14 @@
 	return output
 }
 
+func unwrappedError(err error) error {
+	innerErr := err
+	for errors.Unwrap(innerErr) != nil {
+		innerErr = errors.Unwrap(innerErr)
+	}
+	return innerErr
+}
+
 func gobCommitLink(instance, repo, SHA string) string {
 	return fmt.Sprintf("https://%s/%s/+/%s", instance, repo, SHA)
 }
@@ -230,6 +266,34 @@
 	return page
 }
 
+func findBuildWithFallback(httpClient *http.Client, gerrit, fallbackGerrit, gob, repo, cl string, internal bool) (*findbuild.BuildResponse, bool, error) {
+	didFallback := false
+	request := &findbuild.BuildRequest{
+		HTTPClient:   httpClient,
+		GerritHost:   gerrit,
+		GitilesHost:  gob,
+		ManifestRepo: repo,
+		RepoPrefix:   "",
+		CL:           cl,
+	}
+	buildData, err := findbuild.FindBuild(request)
+	innerErr := unwrappedError(err)
+	if innerErr == findbuild.ErrorCLNotFound {
+		log.Debugf("Cl %s not found in Gerrit instance, using fallback", cl)
+		fallbackRequest := &findbuild.BuildRequest{
+			HTTPClient:   httpClient,
+			GerritHost:   fallbackGerrit,
+			GitilesHost:  gob,
+			ManifestRepo: repo,
+			RepoPrefix:   fallbackRepoPrefix,
+			CL:           cl,
+		}
+		buildData, err = findbuild.FindBuild(fallbackRequest)
+		didFallback = true
+	}
+	return buildData, didFallback, err
+}
+
 // handleError creates the error page for a given error
 func handleError(w http.ResponseWriter, inputErr error, currPage string) {
 	var header, text string
@@ -280,28 +344,34 @@
 		return
 	}
 	if err := r.ParseForm(); err != nil {
-		changelogTemplate.Execute(w, &changelogPage{QuerySize: envQuerySize})
+		err = changelogTemplate.Execute(w, &changelogPage{QuerySize: envQuerySize})
+		if err != nil {
+			log.Errorf("HandleChangelog: error executing locatebuild template: %v", err)
+		}
 		return
 	}
 	source := r.FormValue("source")
 	target := r.FormValue("target")
 	// If no source/target values specified in request, display empty changelog page
 	if source == "" || target == "" {
-		changelogTemplate.Execute(w, &changelogPage{QuerySize: envQuerySize, Internal: true})
+		err = changelogTemplate.Execute(w, &changelogPage{QuerySize: envQuerySize, Internal: true})
+		if err != nil {
+			log.Errorf("HandleChangelog: error executing locatebuild template: %v", err)
+		}
 		return
 	}
 	querySize, err := strconv.Atoi(r.FormValue("n"))
 	if err != nil {
 		querySize, _ = strconv.Atoi(envQuerySize)
 	}
-	internal, instance, manifestRepo := false, externalInstance, externalManifestRepo
+	internal, instance, manifestRepo := false, externalGoBInstance, externalManifestRepo
 	if r.FormValue("internal") == "true" {
-		internal, instance, manifestRepo = true, internalInstance, internalManifestRepo
+		internal, instance, manifestRepo = true, internalGoBInstance, internalManifestRepo
 	}
 	added, removed, err := changelog.Changelog(httpClient, source, target, instance, manifestRepo, querySize)
 	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)
+			source, target, externalGoBInstance, externalManifestRepo, err)
 		handleError(w, err, "changelog")
 		return
 	}
@@ -318,3 +388,60 @@
 		log.Errorf("HandleChangelog: error executing changelog template: %v", err)
 	}
 }
+
+// HandleLocateBuild serves the Locate CL page
+func HandleLocateBuild(w http.ResponseWriter, r *http.Request) {
+	httpClient, err := HTTPClient(w, r, "/locatebuild/")
+	// Require login to access if no session found
+	if err != nil {
+		log.Debug(err)
+		err = promptLoginTemplate.Execute(w, &statusPage{ActivePage: "locatebuild"})
+		if err != nil {
+			log.Errorf("HandleLocateBuild: error executing promptLogin template: %v", err)
+		}
+		return
+	}
+	if err := r.ParseForm(); err != nil {
+		err = locateBuildTemplate.Execute(w, &locateBuildPage{Internal: true})
+		if err != nil {
+			log.Errorf("HandleLocateBuild: error executing locatebuild template: %v", err)
+		}
+		return
+	}
+	cl := r.FormValue("cl")
+	// If no CL value specified in request, display empty CL form
+	if cl == "" {
+		err = locateBuildTemplate.Execute(w, &locateBuildPage{Internal: true})
+		if err != nil {
+			log.Errorf("HandleLocateBuild: error executing locatebuild template: %v", err)
+		}
+		return
+	}
+	internal, gerrit, fallbackGerrit, gob, repo := false, externalGerritInstance, externalFallbackGerritInstance, externalGoBInstance, externalManifestRepo
+	if r.FormValue("internal") == "true" {
+		internal, gerrit, fallbackGerrit, gob, repo = true, internalGerritInstance, internalFallbackGerritInstance, internalGoBInstance, internalManifestRepo
+	}
+	buildData, didFallback, err := findBuildWithFallback(httpClient, gerrit, fallbackGerrit, gob, repo, cl, internal)
+	if err != nil {
+		log.Errorf("HandleLocateBuild: error retrieving build for CL %s with internal set to %t\n%v", cl, internal, err)
+		handleError(w, err, "locatebuild")
+		return
+	}
+	var gerritLink string
+	if didFallback {
+		gerritLink = fallbackGerrit + "/q/" + buildData.CLNum
+	} else {
+		gerritLink = gerrit + "/q/" + buildData.CLNum
+	}
+	page := &locateBuildPage{
+		CL:         cl,
+		CLNum:      buildData.CLNum,
+		BuildNum:   buildData.BuildNum,
+		Internal:   internal,
+		GerritLink: gerritLink,
+	}
+	err = locateBuildTemplate.Execute(w, page)
+	if err != nil {
+		log.Errorf("HandleLocateBuild: error executing locatebuild template: %v", err)
+	}
+}
diff --git a/src/cmd/changelog-webapp/main.go b/src/cmd/changelog-webapp/main.go
index a0f6fff..3813591 100644
--- a/src/cmd/changelog-webapp/main.go
+++ b/src/cmd/changelog-webapp/main.go
@@ -38,10 +38,11 @@
 
 	http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir(staticBasePath))))
 	http.HandleFunc("/", controllers.HandleIndex)
+	http.HandleFunc("/changelog/", controllers.HandleChangelog)
+	http.HandleFunc("/locatebuild/", controllers.HandleLocateBuild)
 	http.HandleFunc("/login/", func(w http.ResponseWriter, r *http.Request) {
 		controllers.HandleLogin(w, r, "/")
 	})
-	http.HandleFunc("/changelog/", controllers.HandleChangelog)
 	http.HandleFunc("/oauth2callback/", controllers.HandleCallback)
 
 	if port == "" {