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

fix jira error parsing #39204

Merged
merged 11 commits into from
Mar 14, 2024
2 changes: 1 addition & 1 deletion integrations/access/jira/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ func NewJiraClient(conf JiraConfig, clusterName, teleportProxyAddr string, statu
if resp.IsError() {
switch result := resp.Error().(type) {
case *ErrorResult:
return trace.Errorf("http error code=%v, errors=[%v]", resp.StatusCode(), strings.Join(result.ErrorMessages, ", "))
return trace.Errorf("http error code=%v, errors=[%s]", resp.StatusCode(), result)
case nil:
return nil
default:
Copy link
Contributor

Choose a reason for hiding this comment

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

If the error is >399 (resp.IsError()==true), does it make sense to return nil if no error is defined?

Copy link
Contributor Author

@hugoShaka hugoShaka Mar 13, 2024

Choose a reason for hiding this comment

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

I think this should be unreachable, but I'm quite afraid of breaking something 😓 . It feels like this was added on purpose. Maybe for the "request cancelled" kind of issues.

Expand Down
79 changes: 77 additions & 2 deletions integrations/access/jira/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,86 @@

package jira

import (
"encoding/json"
"fmt"
"strings"

"github.com/gravitational/trace"
)

// Jira REST API resources

// ErrorResult is used to parse the errors from Jira.
// The JSON Schema is specified here:
// https://docs.atlassian.com/software/jira/docs/api/REST/1000.1223.0/#error-responses
// However JIRA does not consistently respect the schema (especially for old instances).
// We need to support legacy errors as well (array of strings).
type ErrorResult struct {
ErrorMessages []string `url:"errorMessages"`
Errors []string `url:"errors"`
ErrorMessages []string `url:"errorMessages"`
Details ErrorDetails `url:"errors"`
}

// Error implements the error interface and returns a string describing the
// error returned by Jira.
func (e ErrorResult) Error() string {
sb := strings.Builder{}
if len(e.ErrorMessages) > 0 {
sb.WriteString(fmt.Sprintf("error messages: %s ", e.ErrorMessages))
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit: Should we maybe string.Join(e.ErrorMessages, ";") or something like this here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I always find those hard to read when multiple error happen, I'd prefer JSON-style array for those, which is done natively by Sprintf %s.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure about the error messages format (do they include some punctuation?), but adding a join char should help with readability.
Otherwise we might end up with a long string without knowing where a particular ErrorMessage starts and where it ends
Eg

V=[issue invalid format error]
using join=issue invalid; format error

}
if details := e.Details.String(); details != "" {
sb.WriteString(fmt.Sprintf("error details: %s", details))
}
result := sb.String()
if result == "" {
return "Unknown Jira error"
}
return result
}

// ErrorDetails are used to unmarshall inconsistently formatted Jira errors
// details.
type ErrorDetails struct {
// errors contain object-formatted Jira errors. Usually Jira returns
// errors in an object where keys are single word representing what is
// broken, and values containing text descrption of the issue.
// This is the official return format, according to Jira's docs.
errors map[string]string
// legacyErrors ensures backward compatibility with Jira errors returned as
// a list. It's unclear which Jira version and which part of jira can return
// such errors, but they existed at some point, and we might still get them.
legacyErrors []string
}

func (e *ErrorDetails) UnmarshalJSON(data []byte) error {
// Try to parse as a new error
var errors map[string]string
if err := json.Unmarshal(data, &errors); err == nil {
e.errors = errors
return nil
}

// Try to parse as a legacy error
var legacyErrors []string
if err := json.Unmarshal(data, &legacyErrors); err == nil {
e.legacyErrors = legacyErrors
return nil
}

// Everything failed, we return an unrmarshalling error that contains the data.
// This way, even if everything failed, the user still has the original response in the logs.
return trace.Errorf("Failed to unmarshal Jira error: %q", string(data))
}

func (e ErrorDetails) String() string {
switch {
case len(e.errors) > 0:
return fmt.Sprintf("%s", e.errors)
case len(e.legacyErrors) > 0:
return fmt.Sprintf("%s", e.legacyErrors)
default:
return ""
}
}

type GetMyPermissionsQueryOptions struct {
Expand Down
83 changes: 83 additions & 0 deletions integrations/access/jira/types_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Teleport
* Copyright (C) 2023 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package jira

import (
"encoding/json"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/stretchr/testify/require"
)

func TestErrorResultUnmarshal(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input string
expectedOutput ErrorResult
assertErr require.ErrorAssertionFunc
}{
{
name: "new error",
input: `{"errorMessages":[], "errors": {"project": "project is required"}}`,
expectedOutput: ErrorResult{
Details: ErrorDetails{errors: map[string]string{"project": "project is required"}},
},
assertErr: require.NoError,
},
{
name: "legacy error",
input: `{"errorMessages":["foo"],"errors":["bar", "baz"]}`,
expectedOutput: ErrorResult{
ErrorMessages: []string{"foo"},
Details: ErrorDetails{legacyErrors: []string{"bar", "baz"}},
},
assertErr: require.NoError,
},
{
name: "empty error",
input: `{"errorMessages":["There was an error parsing JSON. Check that your request body is valid."]}`,
expectedOutput: ErrorResult{
ErrorMessages: []string{"There was an error parsing JSON. Check that your request body is valid."},
},
assertErr: require.NoError,
},
{
name: "malformed error",
input: `{"errorMessages":["Foo"],"errors":"This is a single string"}`,
expectedOutput: ErrorResult{ErrorMessages: []string{"Foo"}},
assertErr: func(t require.TestingT, err error, i ...interface{}) {
require.ErrorContains(t, err, "This is a single string")
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt := tt
t.Parallel()
data := []byte(tt.input)
var result ErrorResult
tt.assertErr(t, json.Unmarshal(data, &result))
diff := cmp.Diff(tt.expectedOutput, result, cmpopts.EquateEmpty())
require.Empty(t, diff)
})
}
}