| // 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" |
| "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 |
| } |
| if buildData == nil { |
| log.Errorf("CL %s not found", cl) |
| 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) |
| } |
| } |