blob: 470ca15e8cb0f78f61cbfe3f8d41b13c7c676c02 [file] [log] [blame]
// Copyright 2020 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import (
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"strconv"
"strings"
"text/template"
secretmanager "cloud.google.com/go/secretmanager/apiv1"
"cos.googlesource.com/cos/tools.git/src/pkg/changelog"
"cos.googlesource.com/cos/tools.git/src/pkg/findbuild"
"cos.googlesource.com/cos/tools.git/src/pkg/utils"
log "github.com/sirupsen/logrus"
)
const (
subjectLen int = 100
)
var (
internalGerritInstance string
internalFallbackGerritInstance string
internalGoBInstance string
internalManifestRepo string
croslandURL string
externalGerritInstance string
externalFallbackGerritInstance string
externalGoBInstance string
externalManifestRepo string
envQuerySize string
envBoard string
artifactsBucket string
staticBasePath string
indexTemplate *template.Template
readme *template.Template
changelogTemplate *template.Template
promptLoginTemplate *template.Template
findBuildTemplate *template.Template
findReleasedBuildTemplate *template.Template
statusForbiddenTemplate *template.Template
basicTextTemplate *template.Template
)
func init() {
var err error
client, err := secretmanager.NewClient(context.Background())
if err != nil {
log.Fatalf("Failed to setup client: %v", err)
}
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)
}
internalManifestRepo, err = getSecret(client, os.Getenv("COS_INTERNAL_MANIFEST_REPO_NAME"))
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)
}
croslandURL, err = getSecret(client, os.Getenv("CROSLAND_NAME"))
if err != nil {
log.Fatalf("Failed to retrieve secret for CROSLAND_NAME with key name %s\n%v", os.Getenv("CROSLAND_NAME"), err)
}
artifactsBucket, err = getSecret(client, os.Getenv("COS_CHANGELOG_ARTIFACTS_BUCKET_NAME"))
if err != nil {
log.Fatalf("Failed to retrieve secret for COS_CHANGELOG_ARTIFACTS_BUCKET_NAME with key name %s\n%v", os.Getenv("COS_CHANGELOG_ARTIFACTS_BUCKET_NAME"), err)
}
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")
envBoard = os.Getenv("BOARD_NAME")
envQuerySize = getIntVerifiedEnv("CHANGELOG_QUERY_SIZE")
staticBasePath = os.Getenv("STATIC_BASE_PATH")
indexTemplate = template.Must(template.ParseFiles(staticBasePath + "templates/index.html"))
readme = template.Must(template.ParseFiles(staticBasePath + "templates/readme.html"))
changelogTemplate = template.Must(template.ParseFiles(staticBasePath + "templates/changelog.html"))
findBuildTemplate = template.Must(template.ParseFiles(staticBasePath + "templates/findBuild.html"))
findReleasedBuildTemplate = template.Must(template.ParseFiles(staticBasePath + "templates/findReleasedBuild.html"))
promptLoginTemplate = template.Must(template.ParseFiles(staticBasePath + "templates/promptLogin.html"))
basicTextTemplate = template.Must(template.ParseFiles(staticBasePath + "templates/error.html"))
}
type changelogData struct {
Source string
Target string
Additions map[string]*changelog.RepoLog
Removals map[string]*changelog.RepoLog
Internal bool
}
type changelogPage struct {
Source string
Target string
SourceBoard string
SourceMilestone string
TargetBoard string
TargetMilestone string
QuerySize string
RepoTables []*repoTable
Internal bool
Sysctl sysctlChanges
}
type sysctlChanges struct {
Changes [][]string
NotFound string
NotEmpty bool
}
type findBuildPage struct {
CL string
CLNum string
BuildNum string
GerritLink string
Internal bool
}
type statusPage struct {
ActivePage string
SignedIn bool
}
type basicTextPage struct {
Header string
Body string
ActivePage string
SignedIn bool
}
type repoTable struct {
Name string
Additions []*repoTableEntry
Removals []*repoTableEntry
AdditionsLink string
RemovalsLink string
}
type repoTableEntry struct {
IsAddition bool
SHA *shaAttr
Subject string
Bugs []*bugAttr
AuthorName string
CommitterName string
CommitTime string
ReleaseNote string
}
type shaAttr struct {
Name string
URL string
}
type bugAttr struct {
Name string
URL string
}
// getIntVerifiedEnv retrieves an environment variable but checks that it can be
// converted to int first
func getIntVerifiedEnv(envName string) string {
output := os.Getenv(envName)
if _, err := strconv.Atoi(output); err != nil {
log.Errorf("failed to parse env variable %s with value %s: %v",
envName, os.Getenv(output), err)
}
return output
}
func gobCommitLink(instance, repo, SHA string) string {
return fmt.Sprintf("https://%s/%s/+/%s", instance, repo, SHA)
}
func gobDiffLink(instance, repo, sourceSHA, targetSHA string, diffLink bool) string {
if !diffLink {
return fmt.Sprintf("https://%s/%s/+log/%s?n=10000", instance, repo, targetSHA)
}
return fmt.Sprintf("https://%s/%s/+log/%s..%s?n=10000", instance, repo, sourceSHA, targetSHA)
}
func createRepoTableEntry(instance, repo string, commit *changelog.Commit, isAddition bool) *repoTableEntry {
entry := new(repoTableEntry)
entry.IsAddition = isAddition
entry.SHA = &shaAttr{Name: commit.SHA[:8], URL: gobCommitLink(instance, repo, commit.SHA)}
entry.Subject = commit.Subject
if len(entry.Subject) > subjectLen {
entry.Subject = entry.Subject[:subjectLen]
}
entry.Bugs = make([]*bugAttr, len(commit.Bugs))
for i, bugURL := range commit.Bugs {
name := bugURL[strings.Index(bugURL, "/")+1:]
entry.Bugs[i] = &bugAttr{Name: name, URL: "http://" + bugURL}
}
entry.AuthorName = commit.AuthorName
entry.CommitterName = commit.CommitterName
entry.CommitTime = commit.CommitTime
entry.ReleaseNote = commit.ReleaseNote
return entry
}
func createChangelogPage(data changelogData) *changelogPage {
page := &changelogPage{Source: data.Source, Target: data.Target, QuerySize: envQuerySize, Internal: data.Internal}
for repoPath, addLog := range data.Additions {
diffLink := false
table := &repoTable{Name: repoPath}
for _, commit := range addLog.Commits {
tableEntry := createRepoTableEntry(addLog.InstanceURL, addLog.Repo, commit, true)
table.Additions = append(table.Additions, tableEntry)
}
if rmLog, ok := data.Removals[repoPath]; ok {
for _, commit := range data.Removals[repoPath].Commits {
tableEntry := createRepoTableEntry(rmLog.InstanceURL, rmLog.Repo, commit, false)
table.Removals = append(table.Removals, tableEntry)
}
if data.Removals[repoPath].HasMoreCommits {
diffLink = addLog.Repo == rmLog.Repo
table.RemovalsLink = gobDiffLink(rmLog.InstanceURL, rmLog.Repo, addLog.TargetSHA, rmLog.TargetSHA, diffLink)
}
}
if addLog.HasMoreCommits {
table.AdditionsLink = gobDiffLink(addLog.InstanceURL, addLog.Repo, addLog.SourceSHA, addLog.TargetSHA, diffLink)
}
page.RepoTables = append(page.RepoTables, table)
}
// Add remaining repos that had removals but no additions
for repoPath, repoLog := range data.Removals {
if _, ok := data.Additions[repoPath]; ok {
continue
}
table := &repoTable{Name: repoPath}
for _, commit := range repoLog.Commits {
tableEntry := createRepoTableEntry(repoLog.InstanceURL, repoLog.Repo, commit, false)
table.Removals = append(table.Removals, tableEntry)
}
page.RepoTables = append(page.RepoTables, table)
if repoLog.HasMoreCommits {
table.RemovalsLink = gobDiffLink(repoLog.InstanceURL, repoLog.Repo, repoLog.SourceSHA, repoLog.TargetSHA, false)
}
}
return page
}
func findBuildWithFallback(httpClient *http.Client, gerrit, fallbackGerrit, gob, repo, cl string, internal bool) (*findbuild.BuildResponse, bool, utils.ChangelogError) {
didFallback := false
request := &findbuild.BuildRequest{
HTTPClient: httpClient,
GerritHost: gerrit,
GitilesHost: gob,
ManifestRepo: repo,
CL: cl,
}
buildData, err := findbuild.FindBuild(request)
if err != nil && err.HTTPCode() == "404" {
log.Debugf("Cl %s not found in Gerrit instance, using fallback", cl)
fallbackRequest := &findbuild.BuildRequest{
HTTPClient: httpClient,
GerritHost: fallbackGerrit,
GitilesHost: gob,
ManifestRepo: repo,
CL: cl,
}
buildData, err = findbuild.FindBuild(fallbackRequest)
didFallback = true
}
return buildData, didFallback, err
}
func findReleaseBuild(httpClient *http.Client, gerrit, gob, repo, cl string, internal bool) (*findbuild.BuildResponse, utils.ChangelogError) {
request := &findbuild.BuildRequest{
HTTPClient: httpClient,
GerritHost: gerrit,
GitilesHost: gob,
ManifestRepo: repo,
CL: cl,
}
buildData, err := findbuild.FindReleasedBuild(request)
if err != nil && err.HTTPCode() == "404" {
log.Debugf("Cl %s not found in Gerrit instance", cl)
}
return buildData, err
}
// handleError creates the error page for a given error
func handleError(w http.ResponseWriter, r *http.Request, displayErr utils.ChangelogError, currPage string) {
err := basicTextTemplate.Execute(w, &basicTextPage{
Header: displayErr.Header(),
Body: displayErr.HTMLError(),
ActivePage: currPage,
SignedIn: SignedIn(r),
})
if err != nil {
log.Error(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
// HandleIndex serves the home page
func HandleIndex(w http.ResponseWriter, r *http.Request) {
err := indexTemplate.Execute(w, &statusPage{SignedIn: SignedIn(r)})
if err != nil {
log.Error(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
// HandleReadme serves the about page
func HandleReadme(w http.ResponseWriter, r *http.Request) {
err := readme.Execute(w, &statusPage{SignedIn: SignedIn(r)})
if err != nil {
log.Error(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
// HandleChangelog serves the changelog page
func HandleChangelog(w http.ResponseWriter, r *http.Request) {
if RequireToken(w, r, "/changelog/") {
return
}
var err error
if err := r.ParseForm(); err != nil {
err = changelogTemplate.Execute(w, &changelogPage{QuerySize: envQuerySize})
if err != nil {
log.Errorf("error executing findbuild template: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
return
}
source := r.FormValue("source")
target := r.FormValue("target")
sourceMilestone := r.FormValue("source-milestone")
targetMilestone := r.FormValue("target-milestone")
sourceBoard := r.FormValue("source-board")
if sourceBoard == "" {
sourceBoard = envBoard
}
targetBoard := r.FormValue("target-board")
if targetBoard == "" {
targetBoard = envBoard
}
// If no source/target values specified in request, display empty changelog page
if source == "" || target == "" {
err = changelogTemplate.Execute(w, &changelogPage{QuerySize: envQuerySize, Internal: true})
if err != nil {
log.Errorf("error executing findbuild template: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
return
}
querySize, err := strconv.Atoi(r.FormValue("n"))
if err != nil {
querySize, _ = strconv.Atoi(envQuerySize)
}
internal, instance, manifestRepo := false, externalGoBInstance, externalManifestRepo
if r.FormValue("internal") == "true" {
internal, instance, manifestRepo = true, internalGoBInstance, internalManifestRepo
}
httpClient, err := HTTPClient(w, r)
if err != nil {
loginURL := GetLoginURL("/changelog/", false)
http.Redirect(w, r, loginURL, http.StatusTemporaryRedirect)
return
}
added, removed, utilErr := changelog.Changelog(httpClient, source, target, instance, manifestRepo, croslandURL, querySize)
if utilErr != nil {
log.Errorf("error retrieving changelog between builds %s and %s on GoB instance: %s with manifest repository: %s\n%v\n",
source, target, externalGoBInstance, externalManifestRepo, utilErr)
handleError(w, r, utilErr, "/changelog/")
return
}
page := createChangelogPage(changelogData{
Source: source,
Target: target,
Additions: added,
Removals: removed,
Internal: internal,
})
page.SourceMilestone = sourceMilestone
page.SourceBoard = sourceBoard
page.TargetMilestone = targetMilestone
page.TargetBoard = targetBoard
var foundSource, foundTarget bool
page.Sysctl.Changes, foundSource, foundTarget = changelog.GetSysctlDiff(artifactsBucket, sourceBoard,
sourceMilestone, source, targetBoard, targetMilestone, target)
page.Sysctl.NotEmpty = false
if !foundSource {
page.Sysctl.NotFound += fmt.Sprintf("sysctl file for %s-%s-%s not found.<br>", sourceBoard, sourceMilestone, source)
page.Sysctl.NotEmpty = true
}
if !foundTarget {
page.Sysctl.NotFound += fmt.Sprintf("sysctl file for %s-%s-%s not found.<br>", targetBoard, targetMilestone, target)
page.Sysctl.NotEmpty = true
}
if len(page.Sysctl.Changes) > 0 {
page.Sysctl.NotEmpty = true
}
err = changelogTemplate.Execute(w, page)
if err != nil {
log.Errorf("error executing changelog template: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
// HandleFindBuild serves the Locate CL page
func HandleFindBuild(w http.ResponseWriter, r *http.Request) {
if RequireToken(w, r, "/findbuild/") {
return
}
var err error
if err = r.ParseForm(); err != nil {
err = findBuildTemplate.Execute(w, &findBuildPage{Internal: true})
if err != nil {
log.Errorf("error executing findbuild template: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
return
}
cl := r.FormValue("cl")
// If no CL value specified in request, display empty CL form
if cl == "" {
err = findBuildTemplate.Execute(w, &findBuildPage{Internal: true})
if err != nil {
log.Errorf("error executing findbuild template: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
return
}
internal, gerrit, fallbackGerrit, gob, repo := false, externalGerritInstance, externalFallbackGerritInstance, externalGoBInstance, externalManifestRepo
if r.FormValue("cos-internal") == "true" {
internal, gerrit, fallbackGerrit, gob, repo = true, internalGerritInstance, internalFallbackGerritInstance, internalGoBInstance, internalManifestRepo
}
httpClient, err := HTTPClient(w, r)
if err != nil {
loginURL := GetLoginURL("/findbuild/", false)
http.Redirect(w, r, loginURL, http.StatusTemporaryRedirect)
return
}
buildData, didFallback, utilErr := findBuildWithFallback(httpClient, gerrit, fallbackGerrit, gob, repo, cl, internal)
if utilErr != nil {
log.Errorf("error retrieving build for CL %s with internal set to %t\n%v", cl, internal, utilErr)
handleError(w, r, utilErr, "/findbuild/")
return
}
var gerritLink string
if didFallback {
gerritLink = fallbackGerrit + "/c/" + buildData.CLNum
} else {
gerritLink = gerrit + "/c/" + buildData.CLNum
}
page := &findBuildPage{
CL: cl,
CLNum: buildData.CLNum,
BuildNum: buildData.BuildNum,
Internal: internal,
GerritLink: gerritLink,
}
err = findBuildTemplate.Execute(w, page)
if err != nil {
log.Errorf("error executing findbuild template: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
// HandleFindReleasedBuild serves the Locate CL page
func HandleFindReleasedBuild(w http.ResponseWriter, r *http.Request) {
if RequireToken(w, r, "/findreleasedbuild/") { // TODO add findreleasebuild.html
return
}
var err error
if err = r.ParseForm(); err != nil {
err = findReleasedBuildTemplate.Execute(w, &findBuildPage{Internal: true})
if err != nil {
log.Errorf("error executing findbuild template: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
return
}
cl := r.FormValue("cl")
// If no CL value specified in request, display empty CL form
if cl == "" {
err = findReleasedBuildTemplate.Execute(w, &findBuildPage{Internal: true})
if err != nil {
log.Errorf("error executing findreleasedbuild template: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
return
}
internal, gerrit, gob, repo := false, externalGerritInstance, externalGoBInstance, externalManifestRepo
if r.FormValue("cos-internal") == "true" {
internal, gerrit, gob, repo = true, internalGerritInstance, internalGoBInstance, internalManifestRepo
}
httpClient, err := HTTPClient(w, r)
if err != nil {
loginURL := GetLoginURL("/findreleasedbuild/", false)
http.Redirect(w, r, loginURL, http.StatusTemporaryRedirect)
return
}
buildData, utilErr := findReleaseBuild(httpClient, gerrit, gob, repo, cl, internal)
if utilErr != nil {
log.Errorf("error retrieving build for CL %s with internal set to %t\n%v", cl, internal, utilErr)
handleError(w, r, utilErr, "/findreleasedbuild/")
return
}
gerritLink := gerrit + "/c/" + buildData.CLNum
page := &findBuildPage{
CL: cl,
CLNum: buildData.CLNum,
BuildNum: buildData.BuildNum,
Internal: internal,
GerritLink: gerritLink,
}
err = findReleasedBuildTemplate.Execute(w, page)
if err != nil {
log.Errorf("error executing findreleasebuild template: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
// HandleFindReleasedBuildGerrit returns the released build number in JSON
func HandleFindReleasedBuildGerrit(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if origin, ok := allowedOrigin(r.Header.Get("Origin")); ok {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Access-Control-Allow-Credentials", "true")
}
var err error
if err = r.ParseForm(); err != nil {
log.Errorf("error parsing form: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
cl := r.FormValue("cl")
// If no CL value specified in request, display empty CL form
if cl == "" {
http.Error(w, "empty cl provided", http.StatusInternalServerError)
return
}
internal, gerrit, gob, repo := false, externalGerritInstance, externalGoBInstance, externalManifestRepo
if r.FormValue("cos-internal") == "true" {
internal, gerrit, gob, repo = true, internalGerritInstance, internalGoBInstance, internalManifestRepo
}
buildData, utilErr := findReleaseBuild(nil, gerrit, gob, repo, cl, internal)
if utilErr != nil {
log.Errorf("error retrieving build for CL %s with internal set to %t\n%v", cl, internal, utilErr)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
versions := []string{}
if buildData.BuildNum == "0.000.0" {
versions = append(versions, "no build")
} else {
versions = append(versions, buildData.BuildNum)
}
json.NewEncoder(w).Encode(map[string][]string{"versions": versions})
}
func allowedOrigin(origin string) (string, bool) {
if origin == "https://cos-review.googlesource.com" || origin == "https://cos-internal-review.googlesource.com" {
return origin, true
}
return "", false
}