| // 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. | |
| // This package contains the error interface returned by changelog and | |
| // findbuild packages. It includes functions to retrieve HTTP status codes | |
| // from Gerrit and Gitiles errors, and functions to create ChangelogErrors | |
| // relevant to the changelog and findbuild features. | |
| package utils | |
| import ( | |
| "errors" | |
| "fmt" | |
| "regexp" | |
| "strings" | |
| "google.golang.org/grpc/codes" | |
| "google.golang.org/grpc/status" | |
| ) | |
| var ( | |
| grpcCodeToHTTP = map[string]string{ | |
| codes.Unknown.String(): "500", | |
| codes.InvalidArgument.String(): "400", | |
| codes.NotFound.String(): "404", | |
| codes.PermissionDenied.String(): "403", | |
| codes.Unauthenticated.String(): "401", | |
| codes.ResourceExhausted.String(): "429", | |
| codes.FailedPrecondition.String(): "400", | |
| codes.OutOfRange.String(): "400", | |
| codes.Internal.String(): "500", | |
| codes.Unavailable.String(): "503", | |
| codes.DataLoss.String(): "500", | |
| } | |
| // ForbiddenError is a ChangelogError object indicating the user does not have | |
| // permission to access a resource | |
| ForbiddenError = &UtilChangelogError{ | |
| httpCode: "403", | |
| header: "No Access", | |
| err: "This account does not have access to internal repositories. Please retry with an authorized account, or select the external button to query from publically accessible sources.", | |
| } | |
| // InternalServerError is a ChangelogError object indicating an internal error | |
| InternalServerError = &UtilChangelogError{ | |
| httpCode: "500", | |
| header: "Internal Server Error", | |
| err: "An unexpected error occurred while retrieving the requested information.", | |
| } | |
| gitiles403ErrMsg = "unexpected HTTP 403 from Gitiles" | |
| gerritErrCodeRe = regexp.MustCompile("status code\\s*(\\d+)") | |
| ) | |
| // ChangelogError is the error type used by the changelog and findbuild package | |
| type ChangelogError interface { | |
| error | |
| HTTPCode() string | |
| Header() string | |
| HTMLError() string | |
| Retryable() bool | |
| } | |
| // UtilChangelogError implements the ChangelogError interface | |
| type UtilChangelogError struct { | |
| httpCode string | |
| header string | |
| err string | |
| htmlErr string | |
| retryable bool | |
| } | |
| // HTTPCode retrieves the HTTP error code associated with the error | |
| // ex. 400 | |
| func (e *UtilChangelogError) HTTPCode() string { | |
| return e.httpCode | |
| } | |
| // Header retrieves the full HTTP status associated with the error | |
| // ex. 400 Bad Request | |
| func (e *UtilChangelogError) Header() string { | |
| return e.header | |
| } | |
| // Error returns the error string | |
| func (e *UtilChangelogError) Error() string { | |
| return e.err | |
| } | |
| // HTMLError returns an HTML version of the error string | |
| func (e *UtilChangelogError) HTMLError() string { | |
| if e.htmlErr != "" { | |
| return e.htmlErr | |
| } | |
| return e.Error() | |
| } | |
| // Retryable indicates whether increasing the search range | |
| // could resolve this error | |
| func (e *UtilChangelogError) Retryable() bool { | |
| return e.retryable | |
| } | |
| func unwrapError(err error) error { | |
| innerErr := err | |
| for errors.Unwrap(innerErr) != nil { | |
| innerErr = errors.Unwrap(innerErr) | |
| } | |
| return innerErr | |
| } | |
| func croslandLink(croslandURL, source, target string) string { | |
| return fmt.Sprintf("%s/log/%s..%s", croslandURL, source, target) | |
| } | |
| // BothBuildsNotFound indicates that neither build was not found | |
| func BothBuildsNotFound(croslandURL, source, target, sourceBuildNum, targetBuildNum string) *UtilChangelogError { | |
| return &UtilChangelogError{ | |
| httpCode: "404", | |
| header: "Build Not Found", | |
| err: strings.Join([]string{ | |
| "The builds associated with input", | |
| source, | |
| "and", | |
| target, | |
| "cannot be found. It may be possible that the inputs are either invalid or both belong", | |
| "to pre-Cusky builds. If both of the inputs belong to pre-Cusky builds, note that this tool only supports changelogs", | |
| "between Cusky builds. Otherwise, please input valid build numbers (example: 13310.1035.0) or valid image names", | |
| "(example: cos-rc-85-13310-1034-0).", | |
| }, " "), | |
| htmlErr: fmt.Sprintf("%s %s and %s %s<br><br>%s %s <a href=%s target=\"_blank\">%s</a>. %s %s", | |
| "The builds associated with input", | |
| source, | |
| target, | |
| "could not be found.", | |
| "It may be possible that the inputs are either invalid or both belong to pre-Cusky builds.", | |
| "If both of the inputs belong to pre-Cusky builds, please check", | |
| croslandLink(croslandURL, sourceBuildNum, targetBuildNum), | |
| croslandLink(croslandURL, sourceBuildNum, targetBuildNum), | |
| "Otherwise, please input valid build numbers", | |
| "(example: 13310.1035.0) or valid image names (example: cos-rc-85-13310-1034-0).", | |
| ), | |
| } | |
| } | |
| // BuildNotFound returns a ChangelogError object for changelog indicating | |
| // the desired build could not be found | |
| func BuildNotFound(buildNumber string) *UtilChangelogError { | |
| return &UtilChangelogError{ | |
| httpCode: "404", | |
| header: "Build Not Found", | |
| err: strings.Join([]string{ | |
| "The build associated with input", | |
| buildNumber, | |
| "cannot be found. It may be possible that the input is either invalid or belongs to a", | |
| "pre-Cusky build. If you entered a pre-Cusky build number or image name, note that changelog between", | |
| "pre-Cusky and Cusky builds are not supported. Otherwise, please input a valid build number", | |
| "(example: 13310.1035.0) or a valid image name (example: cos-rc-85-13310-1034-0).", | |
| }, " "), | |
| htmlErr: fmt.Sprintf("%s %s %s<br><br>%s %s %s %s", | |
| "The build associated with input", | |
| buildNumber, | |
| "cannot be found.", | |
| "It may be possible that either the input is either invalid or belongs to a", | |
| "pre-Cusky build. If you entered a pre-Cusky build number or image name, note that changelog between", | |
| "pre-Cusky and Cusky builds are not supported. Otherwise, please input a valid build number", | |
| "(example: 13310.1035.0) or a valid image name (example: cos-rc-85-13310-1034-0).", | |
| ), | |
| } | |
| } | |
| func clLink(clID, instanceURL string) string { | |
| return fmt.Sprintf("<a href=\"%s/c/%s\" target=\"_blank\">CL %s</a>", instanceURL, clID, clID) | |
| } | |
| // CLNotFound returns a ChangelogError object for findbuild indicating the provided | |
| // CL could not be found | |
| func CLNotFound(clID string) *UtilChangelogError { | |
| return &UtilChangelogError{ | |
| httpCode: "404", | |
| header: "CL Not Found", | |
| err: fmt.Sprintf("No CL was found matching the identifier: %s. Please enter either the CL-number (example: 3206) or a Commit-SHA (example: I7e549d7753cc7acec2b44bb5a305347a97719ab9) of a submitted CL.", clID), | |
| } | |
| } | |
| // CLLandingNotFound returns a ChangelogError object for findbuild indicating | |
| // no build was found containing a CL | |
| func CLLandingNotFound(clID, instanceURL string) *UtilChangelogError { | |
| errStrFmt := "No build was found containing %s." | |
| link := clLink(clID, instanceURL) | |
| return &UtilChangelogError{ | |
| httpCode: "406", | |
| header: "No Build Found", | |
| err: fmt.Sprintf(errStrFmt, "CL "+clID), | |
| htmlErr: fmt.Sprintf(errStrFmt, link), | |
| retryable: true, | |
| } | |
| } | |
| // CLNotUsed returns a ChangelogError object for findbuild indicating | |
| // that the repository and branch associated with a CL was not found in any | |
| // manifest files | |
| func CLNotUsed(clID, repo, branch, instanceURL string) *UtilChangelogError { | |
| errStrFmt := "%s modifies the %s repository on the %s branch, which has not been used in COS builds since the CL's submission." | |
| link := clLink(clID, instanceURL) | |
| return &UtilChangelogError{ | |
| httpCode: "406", | |
| header: "CL Not Used", | |
| err: fmt.Sprintf(errStrFmt, "CL "+clID, repo, branch), | |
| htmlErr: fmt.Sprintf(errStrFmt, link, repo, branch), | |
| } | |
| } | |
| // CLTooRecent returns a ChangelogError object for findbuild indicating the provided | |
| // CL could not be found | |
| func CLTooRecent(clID, instanceURL string) *UtilChangelogError { | |
| errStrFmt := "%s was submitted too recently to be included in any builds. Please wait a couple hours and try again." | |
| link := clLink(clID, instanceURL) | |
| return &UtilChangelogError{ | |
| httpCode: "406", | |
| header: "CL Too Recent", | |
| err: fmt.Sprintf(errStrFmt, "CL "+clID), | |
| htmlErr: fmt.Sprintf(errStrFmt, link), | |
| } | |
| } | |
| // CLNotSubmitted returns a ChangelogError object for findbuild indicating | |
| // that the provided CL has not been submitted | |
| func CLNotSubmitted(clID, instanceURL string) *UtilChangelogError { | |
| errStrFmt := "%s has not been submitted yet. A CL will not enter any build until it is successfully submitted." | |
| link := clLink(clID, instanceURL) | |
| return &UtilChangelogError{ | |
| httpCode: "406", | |
| header: "CL Not Submitted", | |
| err: fmt.Sprintf(errStrFmt, "CL "+clID), | |
| htmlErr: fmt.Sprintf(errStrFmt, link), | |
| } | |
| } | |
| // CLInvalidRelease returns a ChangelogError object for findbuild indicating | |
| // that the branch a CL was submitted in was not recognized as a release branch | |
| func CLInvalidRelease(clID, release, instanceURL string) *UtilChangelogError { | |
| errStrFmt := "%s maps to release %s, which is not a valid release" | |
| link := clLink(clID, instanceURL) | |
| return &UtilChangelogError{ | |
| httpCode: "406", | |
| header: "Invalid Release Branch", | |
| err: fmt.Sprintf(errStrFmt, "CL "+clID, release), | |
| htmlErr: fmt.Sprintf(errStrFmt, link, release), | |
| } | |
| } | |
| // GitilesErrCode parses a Gitiles error message and returns an HTTP error code | |
| // associated with the error. Returns 500 if no error code is found. | |
| func GitilesErrCode(err error) string { | |
| err = unwrapError(err) | |
| rpcStatus, ok := status.FromError(err) | |
| if !ok { | |
| return "500" | |
| } | |
| code, text := rpcStatus.Code(), rpcStatus.Message() | |
| // RPC status code misclassifies 403 error as 500 error for Gitiles requests | |
| if code == codes.Internal && text == gitiles403ErrMsg { | |
| code = codes.PermissionDenied | |
| } | |
| if httpCode, ok := grpcCodeToHTTP[code.String()]; ok { | |
| return httpCode | |
| } | |
| return "500" | |
| } | |
| // GerritErrCode parse a Gerrit error and returns an HTTP error code associated | |
| // with the error. Returns 500 if no error code is found. | |
| func GerritErrCode(err error) string { | |
| err = unwrapError(err) | |
| matches := gerritErrCodeRe.FindStringSubmatch(err.Error()) | |
| if len(matches) != 2 { | |
| return "500" | |
| } | |
| return matches[1] | |
| } |