Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support user-facing BuildPlanner errors #2719

Merged
merged 10 commits into from
Aug 23, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
198 changes: 114 additions & 84 deletions pkg/platform/api/buildplanner/model/buildplan.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,6 @@ const (
ArtifactSkipped = "SKIPPED"
ArtifactSucceeded = "SUCCEEDED"

// Types
NotFound = "NotFound"
BuildResultPlanningError = "PlanningError"

// Tag types
TagSource = "src"
TagDependency = "dep"
Expand Down Expand Up @@ -64,7 +60,16 @@ const (
XActiveStateBuilderMimeType = "application/x-activestate-builder"

// Error types
RemediableSolveErrorType = "RemediableSolveError"
ErrorType = "Error"
NotFoundErrorType = "NotFound"
BuildResultPlanningErrorType = "PlanningError"
ParseErrorType = "ParseError"
AlreadyExistsErrorType = "AlreadyExists"
NoChangeSinceLastCommitErrorType = "NoChangeSinceLastCommit"
HeadOnBranchMovedErrorType = "HeadOnBranchMoved"
ForbiddenErrorType = "Forbidden"
RemediableSolveErrorType = "RemediableSolveError"
PlanningErrorType = "PlanningError"
)

func IsStateToolArtifact(mimeType string) bool {
Expand Down Expand Up @@ -149,62 +154,31 @@ func NewBuildPlanResponse(owner, project string) BuildPlan {

type BuildPlanByProject struct {
Project *Project `json:"project"`
*Error
}

func (b *BuildPlanByProject) Build() (*Build, error) {
if b.Project == nil {
return nil, errs.New("BuildPlanByProject.Build: Project is nil")
}

if b.Project.Error != nil {
if b.Project.Error.Message != "" {
return nil, errs.New("BuildPlanByProject.Build: Could not get build, API returned project error message: %s", b.Project.Message)
}
return nil, errs.New("BuildPlanByProject.Build: Could not retrieve project")
if IsErrorResponse(b.Project.Type) {
return nil, ProcessProjectError(b.Project, "Could not get build from project response")
}

if b.Project.Commit == nil {
return nil, errs.New("BuildPlanByProject.Build: Commit is nil")
}

if b.Project.Commit.Error != nil {
if b.Project.Commit.Error.Message != "" {
return nil, errs.New("Could not get build, API returned commit error message: %s", b.Project.Commit.Message)
}
return nil, errs.New("BuildPlanByProject.Build: Could not retrieve commit")
}

if b.Project.Commit.Type == NotFound {
return nil, locale.NewError("err_buildplanner_commit_not_found", "Build plan does not contain commit")
if IsErrorResponse(b.Project.Commit.Type) {
return nil, ProcessCommitError(b.Project.Commit, "Could not get build from commit from project response")
}

if b.Project.Commit.Build == nil {
return nil, errs.New("BuildPlanByProject.Build: Commit does not contain build")
}

if b.Project.Commit.Build.PlanningError != nil {
var errs []string
var isTransient bool
for _, se := range b.Project.Commit.Build.SubErrors {
if se.Type != RemediableSolveErrorType {
continue
}

if se.Message != "" {
errs = append(errs, se.Message)
isTransient = se.IsTransient
}
for _, ve := range se.ValidationErrors {
if ve.Error != "" {
errs = append(errs, ve.Error)
}
}
}
return nil, &BuildPlannerError{
ValidationErrors: errs,
IsTransient: isTransient,
}
if IsErrorResponse(b.Project.Commit.Build.Type) {
return nil, ProcessBuildError(b.Project.Commit.Build, "Could not get build from project commit response")
}

return b.Project.Commit.Build, nil
Expand All @@ -215,59 +189,93 @@ func (b *BuildPlanByProject) CommitID() (strfmt.UUID, error) {
return "", errs.New("BuildPlanByProject.CommitID: Project is nil")
}

if b.Project.Error != nil {
if b.Project.Error.Message != "" {
return "", errs.New("BuildPlanByProject.CommitID: Could not get commit ID, API returned project error message: %s", b.Project.Message)
}
return "", errs.New("BuildPlanByProject.CommitID: Could not retrieve project")
if IsErrorResponse(b.Project.Type) {
return "", ProcessProjectError(b.Project, "Could not get commit ID from project response")
}

if b.Project.Commit == nil {
return "", errs.New("BuildPlanByProject.CommitID: Commit is nil")
}

if b.Project.Commit.Error != nil {
if b.Project.Commit.Error.Message != "" {
return "", errs.New("BuildPlanByProject.CommitID: Could not get commit ID. API returned commit error message: %s", b.Project.Commit.Message)
}
return "", errs.New("BuildPlanByProject.CommitID: Could not retrieve commit")
if IsErrorResponse(b.Project.Commit.Type) {
return "", ProcessCommitError(b.Project.Commit, "Could not get commit ID from project commit response")
}

return b.Project.Commit.CommitID, nil
}

type BuildPlanByCommit struct {
Commit *Commit `json:"commit"`
*Error
}

func (b *BuildPlanByCommit) Build() (*Build, error) {
if b.Commit == nil {
return nil, errs.New("BuildPlanByCommit.Build: Commit is nil")
}

if b.Commit.Error != nil {
if b.Commit.Error.Message != "" {
return nil, errs.New("BuildPlanByCommit.Build: Could not get build via commit ID, API returned commit error message: %s", b.Commit.Message)
}
return nil, errs.New("BuildPlanByCommit.Build: Could not retrieve commit")
}

if b.Commit.Type == NotFound {
return nil, locale.NewError("err_buildplanner_commit_not_found", "Build plan does not contain commit")
if IsErrorResponse(b.Commit.Type) {
return nil, ProcessCommitError(b.Commit, "Could not get build from commit response")
}

if b.Commit.Build == nil {
if b.Commit.Error != nil {
return nil, errs.New("BuildPlanByCommit.Build: Commit not found: %s", b.Commit.Error.Message)
}
return nil, errs.New("BuildPlanByCommit.Build: Commit does not contain build")
}

if b.Commit.Build.PlanningError != nil {
if IsErrorResponse(b.Commit.Build.Type) {
return nil, ProcessBuildError(b.Commit.Build, "Could not get build from commit response")
}

return b.Commit.Build, nil
}

func (b *BuildPlanByCommit) CommitID() (strfmt.UUID, error) {
if b.Commit == nil {
return "", errs.New("BuildPlanByCommit.CommitID: Commit is nil")
}

if IsErrorResponse(b.Commit.Type) {
return "", ProcessCommitError(b.Commit, "Could not get commit ID from commit response")
}

return b.Commit.CommitID, nil
}

func IsErrorResponse(errorType string) bool {
return errorType == ErrorType ||
errorType == AlreadyExistsErrorType ||
errorType == NoChangeSinceLastCommitErrorType ||
errorType == HeadOnBranchMovedErrorType ||
errorType == ForbiddenErrorType ||
errorType == RemediableSolveErrorType ||
errorType == PlanningErrorType
}

func ProcessCommitError(commit *Commit, fallbackMessage string) error {
if commit.Error == nil {
return errs.New(fallbackMessage)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a scenario that we expect to happen? We might want to present something localized if that's the case.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, if this happens we've encountered an error type but the message wasn't set which would be an invalid according to the build planner's schema.

}

switch commit.Type {
case NotFoundErrorType:
return locale.NewInputError("err_buildplanner_commit_not_found", "Could not find commit, recieved message: {{.V0}}", commit.Message)
MDrakos marked this conversation as resolved.
Show resolved Hide resolved
case ParseErrorType:
return locale.NewInputError("err_buildplanner_parse_error", "The platform failed to parse the build expression, received the following message: {{.V0}}. Path: {{.V1}}", commit.Message, commit.ParseError.Path)
MDrakos marked this conversation as resolved.
Show resolved Hide resolved
case ForbiddenErrorType:
return locale.NewInputError("err_buildplanner_forbidden", "Unable to complete the operation: {{.V0}}, recieved message: {{.V1}}", commit.Operation, commit.Message)
MDrakos marked this conversation as resolved.
Show resolved Hide resolved
case HeadOnBranchMovedErrorType:
return locale.NewInputError("err_buildplanner_head_on_branch_moved", "Head on branch has moved, recieved message: {{.V0}}", commit.Error.Message)
MDrakos marked this conversation as resolved.
Show resolved Hide resolved
case NoChangeSinceLastCommitErrorType:
return locale.NewInputError("err_buildplanner_no_change_since_last_commit", "No change since last commit, recieved message: {{.V0}}", commit.Error.Message)
MDrakos marked this conversation as resolved.
Show resolved Hide resolved
default:
return errs.New(fallbackMessage)
}
}

func ProcessBuildError(build *Build, fallbackMessage string) error {
MDrakos marked this conversation as resolved.
Show resolved Hide resolved
if build.Type == PlanningErrorType {
var errs []string
var isTransient bool
for _, se := range b.Commit.Build.SubErrors {
for _, se := range build.SubErrors {
if se.Type != RemediableSolveErrorType {
continue
}
Expand All @@ -282,31 +290,29 @@ func (b *BuildPlanByCommit) Build() (*Build, error) {
}
}
}
return nil, &BuildPlannerError{
return &BuildPlannerError{
ValidationErrors: errs,
IsTransient: isTransient,
}
}

return b.Commit.Build, nil
return locale.NewInputError("err_buildplanner_build", "Encountered error processing build response")
}

func (b *BuildPlanByCommit) CommitID() (strfmt.UUID, error) {
if b.Commit == nil {
return "", errs.New("BuildPlanByCommit.CommitID: Commit is nil")
func ProcessProjectError(project *Project, fallbackMessage string) error {
if project.Error == nil {
return errs.New(fallbackMessage)
}

if b.Commit.Error != nil {
if b.Commit.Error.Message != "" {
return "", errs.New("BuildPlanByCommit.CommitID: Could not get commit ID, API returned commit error message: %s", b.Commit.Message)
}
return "", errs.New("BuildPlanByCommit.CommitID: Could not retrieve commit")
if project.Type == NotFoundErrorType {
return locale.NewInputError("err_buildplanner_project_not_found", "Unable to find project, recieved message: {{.V0}}", project.Message)
}

return b.Commit.CommitID, nil
return errs.New(fallbackMessage)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't want to silence this one (input error)?

Copy link
Member Author

@MDrakos MDrakos Aug 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case, if we have to use the fallback message we've encountered an error type that we aren't handling so we should update these functions.

}

type BuildExpression struct {
Type string `json:"__typename"`
Commit *Commit `json:"commit"`
*Error
}
Expand All @@ -315,6 +321,7 @@ type BuildExpression struct {
// It contains the resulting commit from the operation and any errors.
// The resulting commit is pushed to the platform automatically.
type PushCommitResult struct {
Type string `json:"__typename"`
Commit *Commit `json:"pushCommit"`
*Error
}
Expand All @@ -324,13 +331,10 @@ type PushCommitResult struct {
// The resulting commit is NOT pushed to the platform automatically.
type StageCommitResult struct {
Commit *Commit `json:"stageCommit"`
*Error
*ParseError
}

// Error contains an error message.
type Error struct {
Type string `json:"__typename"`
Message string `json:"message"`
}

Expand All @@ -348,6 +352,10 @@ type Commit struct {
CommitID strfmt.UUID `json:"commitId"`
Build *Build `json:"build"`
*Error
*ParseError
*ForbiddenError
*HeadOnBranchMovedError
*NoChangeSinceLastCommitError
}

// Build is a directed acyclic graph. It begins with a set of terminal nodes
Expand Down Expand Up @@ -440,6 +448,7 @@ type Build struct {
Steps []*Step `json:"steps"`
Sources []*Source `json:"sources"`
BuildLogIDs []*BuildLogID `json:"buildLogIds"`
*Error
*PlanningError
}

Expand Down Expand Up @@ -492,6 +501,13 @@ type Source struct {
Version string `json:"version"`
}

// NotFoundError represents an error that occurred because a resource was not found.
type NotFoundError struct {
Type string `json:"type"`
Resource string `json:"resource"`
MayNeedAuthentication bool `json:"mayNeedAuthentication"`
}

// PlanningError represents an error that occurred during planning.
type PlanningError struct {
Message string `json:"message"`
Expand All @@ -500,9 +516,23 @@ type PlanningError struct {

// ParseError is an error that occurred while parsing the build expression.
type ParseError struct {
Type string `json:"__typename"`
Message string `json:"message"`
Path string `json:"path"`
Path string `json:"path"`
}

type ForbiddenError struct {
Operation string `json:"operation"`
}

// HeadOnBranchMovedError represents an error that occurred because the head on
// a remote branch has moved.
type HeadOnBranchMovedError struct {
HeadBranchID strfmt.UUID `json:"branchId"`
}

// NoChangeSinceLastCommitError represents an error that occurred because there
// were no changes since the last commit.
type NoChangeSinceLastCommitError struct {
NoChangeCommitID strfmt.UUID `json:"commitId"`
}

// BuildExprLocation represents a location in the build script where an error occurred.
Expand Down
1 change: 1 addition & 0 deletions pkg/platform/api/buildplanner/request/buildexpression.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ query ($commitID: ID!) {
expr
}
... on Error{
__typename
message
}
... on NotFound {
Expand Down
7 changes: 7 additions & 0 deletions pkg/platform/api/buildplanner/request/commit.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ query ($commitID: ID!) {
}
}
... on PlanningError {
__typename
message
subErrors {
__typename
Expand Down Expand Up @@ -155,15 +156,21 @@ query ($commitID: ID!) {
}
}
... on Error {
__typename
message
}
}
}
... on Error {
__typename
message
}
... on NotFound {
__typename
message
type
resource
mayNeedAuthentication
}
}
}
Expand Down
Loading
Loading