mirror of
https://gitlab.com/psuapp/psu.git
synced 2024-08-30 18:12:34 +00:00
Enhance Portainer API error handling
This commit is contained in:
parent
63c82efd33
commit
eaf7d2e5cf
@ -125,6 +125,9 @@ func (n *portainerClientImp) do(uri, method string, requestBody io.Reader, heade
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for HTTP error status codes
|
||||||
|
err = getResponseHTTPError(resp)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -164,11 +167,6 @@ func (n *portainerClientImp) doJSON(uri, method string, headers http.Header, req
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = checkResponseForErrors(resp)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode response body, if any
|
// Decode response body, if any
|
||||||
if responseBody != nil {
|
if responseBody != nil {
|
||||||
d := json.NewDecoder(resp.Body)
|
d := json.NewDecoder(resp.Body)
|
||||||
|
@ -1,16 +1,73 @@
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
import "fmt"
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
// GenericError represents the body of a generic error returned by the Portainer API
|
// GenericError represents the body of a generic error returned by the Portainer API
|
||||||
type GenericError struct {
|
type GenericError struct {
|
||||||
|
Code int
|
||||||
Err string
|
Err string
|
||||||
Details string
|
Details string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *GenericError) Error() string {
|
func (e GenericError) Error() string {
|
||||||
if e.Details != "" {
|
if e.Details != "" {
|
||||||
return fmt.Sprintf("%s: %s", e.Err, e.Details)
|
return fmt.Sprintf("%s: %s", e.Err, e.Details)
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%s", e.Err)
|
return fmt.Sprintf("%s", e.Err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get an http.Response's error (if any)
|
||||||
|
func getResponseHTTPError(resp *http.Response) error {
|
||||||
|
if resp.StatusCode < 300 {
|
||||||
|
// There is no error
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch resp.StatusCode {
|
||||||
|
// Error codes found in the Portainer API 1.22.0 Swagger spec
|
||||||
|
case http.StatusBadRequest, http.StatusForbidden, http.StatusNotFound, http.StatusConflict, http.StatusInternalServerError, http.StatusServiceUnavailable:
|
||||||
|
// Guess it's a GenericError
|
||||||
|
genericError, err := getResponseGenericHTTPError(resp)
|
||||||
|
if err != nil {
|
||||||
|
// It's not a GenericError
|
||||||
|
return getResponseNonGenericHTTPError(resp)
|
||||||
|
}
|
||||||
|
return &genericError
|
||||||
|
default:
|
||||||
|
return getResponseNonGenericHTTPError(resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getResponseGenericHTTPError(resp *http.Response) (genericError GenericError, err error) {
|
||||||
|
genericError = GenericError{
|
||||||
|
Code: resp.StatusCode,
|
||||||
|
}
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&genericError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func getResponseNonGenericHTTPError(resp *http.Response) error {
|
||||||
|
bodyString, err := getResponseBodyAsString(resp)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return errors.New(bodyString)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getResponseBodyAsString(resp *http.Response) (bodyString string, err error) {
|
||||||
|
bodyBytes, err := ioutil.ReadAll(resp.Body)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bodyString = string(bodyBytes)
|
||||||
|
resp.Body = ioutil.NopCloser(bytes.NewReader(bodyBytes))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@ -54,3 +59,159 @@ func TestGenericError_Error(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_getResponseHTTPError(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
resp *http.Response
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "bad request (generic) error",
|
||||||
|
args: args{
|
||||||
|
resp: func() (resp *http.Response) {
|
||||||
|
resp = &http.Response{
|
||||||
|
StatusCode: http.StatusBadRequest,
|
||||||
|
}
|
||||||
|
bodyBytes, _ := json.Marshal(map[string]interface{}{
|
||||||
|
"Err": "Error",
|
||||||
|
"Details": "Bad request",
|
||||||
|
})
|
||||||
|
resp.Body = ioutil.NopCloser(bytes.NewReader(bodyBytes))
|
||||||
|
return
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
wantErr: &GenericError{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
Err: "Error",
|
||||||
|
Details: "Bad request",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "forbidden (generic) error",
|
||||||
|
args: args{
|
||||||
|
resp: func() (resp *http.Response) {
|
||||||
|
resp = &http.Response{
|
||||||
|
StatusCode: http.StatusForbidden,
|
||||||
|
}
|
||||||
|
bodyBytes, _ := json.Marshal(map[string]interface{}{
|
||||||
|
"Err": "Error",
|
||||||
|
"Details": "Forbidden",
|
||||||
|
})
|
||||||
|
resp.Body = ioutil.NopCloser(bytes.NewReader(bodyBytes))
|
||||||
|
return
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
wantErr: &GenericError{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
Err: "Error",
|
||||||
|
Details: "Forbidden",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "not found (generic) error",
|
||||||
|
args: args{
|
||||||
|
resp: func() (resp *http.Response) {
|
||||||
|
resp = &http.Response{
|
||||||
|
StatusCode: http.StatusNotFound,
|
||||||
|
}
|
||||||
|
bodyBytes, _ := json.Marshal(map[string]interface{}{
|
||||||
|
"Err": "Error",
|
||||||
|
"Details": "Not found",
|
||||||
|
})
|
||||||
|
resp.Body = ioutil.NopCloser(bytes.NewReader(bodyBytes))
|
||||||
|
return
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
wantErr: &GenericError{
|
||||||
|
Code: http.StatusNotFound,
|
||||||
|
Err: "Error",
|
||||||
|
Details: "Not found",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "conflict (generic) error",
|
||||||
|
args: args{
|
||||||
|
resp: func() (resp *http.Response) {
|
||||||
|
resp = &http.Response{
|
||||||
|
StatusCode: http.StatusConflict,
|
||||||
|
}
|
||||||
|
bodyBytes, _ := json.Marshal(map[string]interface{}{
|
||||||
|
"Err": "Error",
|
||||||
|
"Details": "Conflict",
|
||||||
|
})
|
||||||
|
resp.Body = ioutil.NopCloser(bytes.NewReader(bodyBytes))
|
||||||
|
return
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
wantErr: &GenericError{
|
||||||
|
Code: http.StatusConflict,
|
||||||
|
Err: "Error",
|
||||||
|
Details: "Conflict",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "internal server error (generic) error",
|
||||||
|
args: args{
|
||||||
|
resp: func() (resp *http.Response) {
|
||||||
|
resp = &http.Response{
|
||||||
|
StatusCode: http.StatusInternalServerError,
|
||||||
|
}
|
||||||
|
bodyBytes, _ := json.Marshal(map[string]interface{}{
|
||||||
|
"Err": "Error",
|
||||||
|
"Details": "Internal server error",
|
||||||
|
})
|
||||||
|
resp.Body = ioutil.NopCloser(bytes.NewReader(bodyBytes))
|
||||||
|
return
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
wantErr: &GenericError{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
Err: "Error",
|
||||||
|
Details: "Internal server error",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "service unavailable (generic) error",
|
||||||
|
args: args{
|
||||||
|
resp: func() (resp *http.Response) {
|
||||||
|
resp = &http.Response{
|
||||||
|
StatusCode: http.StatusServiceUnavailable,
|
||||||
|
}
|
||||||
|
bodyBytes, _ := json.Marshal(map[string]interface{}{
|
||||||
|
"Err": "Error",
|
||||||
|
"Details": "Service unavailable",
|
||||||
|
})
|
||||||
|
resp.Body = ioutil.NopCloser(bytes.NewReader(bodyBytes))
|
||||||
|
return
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
wantErr: &GenericError{
|
||||||
|
Code: http.StatusServiceUnavailable,
|
||||||
|
Err: "Error",
|
||||||
|
Details: "Service unavailable",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "method not allowed (non generic) error",
|
||||||
|
args: args{
|
||||||
|
resp: func() (resp *http.Response) {
|
||||||
|
resp = &http.Response{
|
||||||
|
StatusCode: http.StatusMethodNotAllowed,
|
||||||
|
Body: ioutil.NopCloser(bytes.NewReader([]byte("Err"))),
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
wantErr: errors.New("Err"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
assert.Equal(t, tt.wantErr, getResponseHTTPError(tt.args.resp))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,12 +1,6 @@
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -21,24 +15,3 @@ func GetTranslatedStackType(t portainer.StackType) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if an http.Response object has errors
|
|
||||||
func checkResponseForErrors(resp *http.Response) error {
|
|
||||||
if 300 <= resp.StatusCode {
|
|
||||||
// Guess it's a GenericError
|
|
||||||
respBody := GenericError{}
|
|
||||||
err := json.NewDecoder(resp.Body).Decode(&respBody)
|
|
||||||
if err != nil {
|
|
||||||
// It's not a GenericError
|
|
||||||
bodyBytes, err := ioutil.ReadAll(resp.Body)
|
|
||||||
defer resp.Body.Close()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
resp.Body = ioutil.NopCloser(bytes.NewReader(bodyBytes))
|
|
||||||
return errors.New(string(bodyBytes))
|
|
||||||
}
|
|
||||||
return &respBody
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
@ -1,10 +1,6 @@
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
@ -48,50 +44,3 @@ func TestGetTranslatedStackType(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_checkResponseForErrors(t *testing.T) {
|
|
||||||
type args struct {
|
|
||||||
resp *http.Response
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "generic error",
|
|
||||||
args: args{
|
|
||||||
resp: func() (resp *http.Response) {
|
|
||||||
resp = &http.Response{
|
|
||||||
StatusCode: http.StatusNotFound,
|
|
||||||
}
|
|
||||||
bodyBytes, _ := json.Marshal(map[string]interface{}{
|
|
||||||
"Err": "Error",
|
|
||||||
"Details": "Not found",
|
|
||||||
})
|
|
||||||
resp.Body = ioutil.NopCloser(bytes.NewReader(bodyBytes))
|
|
||||||
return
|
|
||||||
}(),
|
|
||||||
},
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "non generic error",
|
|
||||||
args: args{
|
|
||||||
resp: func() (resp *http.Response) {
|
|
||||||
resp = &http.Response{
|
|
||||||
StatusCode: http.StatusNotFound,
|
|
||||||
Body: ioutil.NopCloser(bytes.NewReader([]byte("Err"))),
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}(),
|
|
||||||
},
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
assert.Equal(t, tt.wantErr, checkResponseForErrors(tt.args.resp) != nil)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Loading…
Reference in New Issue
Block a user